Добавление проектов
Добавили возможность создавания разных проектов.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<title>TaskBoard</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
137
front_vue/package-lock.json
generated
137
front_vue/package-lock.json
generated
@@ -7,9 +7,9 @@
|
||||
"": {
|
||||
"name": "taskboard",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"pinia": "^3.0.4",
|
||||
"vite": "^7.3.1",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^4.6.4"
|
||||
@@ -892,6 +892,30 @@
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/devtools-kit": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
|
||||
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-shared": "^7.7.9",
|
||||
"birpc": "^2.3.0",
|
||||
"hookable": "^5.5.3",
|
||||
"mitt": "^3.0.1",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"speakingurl": "^14.0.1",
|
||||
"superjson": "^2.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-shared": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
|
||||
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rfdc": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz",
|
||||
@@ -942,6 +966,30 @@
|
||||
"integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-anything": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-what": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -1038,6 +1086,24 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hookable": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-what": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
|
||||
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -1047,6 +1113,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -1065,6 +1137,12 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1084,6 +1162,36 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.5.0",
|
||||
"vue": "^3.5.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/@vue/devtools-api": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
|
||||
"integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-kit": "^7.7.9"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -1112,6 +1220,12 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
||||
@@ -1165,6 +1279,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/speakingurl": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/superjson": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
|
||||
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"copy-anything": "^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"pinia": "^3.0.4",
|
||||
"vite": "^7.3.1",
|
||||
"vue": "^3.5.26",
|
||||
"vue-router": "^4.6.4"
|
||||
|
||||
BIN
front_vue/public/favicon.ico
Normal file
BIN
front_vue/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
@@ -38,40 +38,56 @@ export const authApi = {
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== DEPARTMENTS ====================
|
||||
export const departmentsApi = {
|
||||
getAll: () => request('/api/task', {
|
||||
// ==================== PROJECTS ====================
|
||||
export const projectsApi = {
|
||||
// active: ID проекта для загрузки данных (опционально)
|
||||
getAll: (active = null) => {
|
||||
let url = '/api/project'
|
||||
if (active) url += `?active=${active}`
|
||||
return request(url, { credentials: 'include' })
|
||||
},
|
||||
// Получение данных проекта (проект + колонки + отделы) — один запрос вместо трёх
|
||||
getData: (id_project) => request('/api/project', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'get_departments' })
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== LABELS ====================
|
||||
export const labelsApi = {
|
||||
getAll: () => request('/api/task', {
|
||||
body: JSON.stringify({ action: 'get_project_data', id_project })
|
||||
}),
|
||||
create: (data) => request('/api/project', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'get_labels' })
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== COLUMNS ====================
|
||||
export const columnsApi = {
|
||||
getAll: () => request('/api/task', {
|
||||
body: JSON.stringify({ action: 'create', ...data })
|
||||
}),
|
||||
update: (data) => request('/api/project', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'get_columns' })
|
||||
body: JSON.stringify({ action: 'update', ...data })
|
||||
}),
|
||||
delete: (id) => request('/api/project', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'delete', id })
|
||||
}),
|
||||
setReadyColumn: (id_project, column_id) => request('/api/project', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'set_ready_column', id_project, column_id })
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== CARDS ====================
|
||||
export const cardsApi = {
|
||||
// id_project: ID проекта (обязательный)
|
||||
// archive: 0 = неархивные (по умолчанию), 1 = архивные, 'all' = все
|
||||
getAll: (archive = 0) => request(`/api/task${archive !== 0 ? `?archive=${archive}` : ''}`, { credentials: 'include' }),
|
||||
getAll: (id_project, archive = 0) => {
|
||||
let url = `/api/task?id_project=${id_project}`
|
||||
if (archive !== 0) url += `&archive=${archive}`
|
||||
return request(url, { credentials: 'include' })
|
||||
},
|
||||
updateOrder: (id, column_id, to_index) => request('/api/task', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
@@ -125,25 +141,3 @@ export const taskImageApi = {
|
||||
export const usersApi = {
|
||||
getAll: () => request('/api/user', { credentials: 'include' })
|
||||
}
|
||||
|
||||
// ==================== CONFIG ====================
|
||||
export const configApi = {
|
||||
get: () => request('/api/user', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'get_config' })
|
||||
})
|
||||
}
|
||||
|
||||
// Загрузка конфига с сервера и мерж с window.APP_CONFIG
|
||||
export const loadServerConfig = async () => {
|
||||
try {
|
||||
const result = await configApi.get()
|
||||
if (result.success && result.data) {
|
||||
window.APP_CONFIG = { ...window.APP_CONFIG, ...result.data }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки конфига:', error)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
:column="column"
|
||||
:departments="departments"
|
||||
:labels="labels"
|
||||
:done-column-id="doneColumnId"
|
||||
@drop-card="handleDropCard"
|
||||
@open-task="(card) => emit('open-task', { card, columnId: column.id })"
|
||||
@create-task="emit('create-task', column.id)"
|
||||
@@ -23,6 +24,7 @@ import { cardsApi } from '../api'
|
||||
|
||||
const props = defineProps({
|
||||
activeDepartment: Number,
|
||||
doneColumnId: Number,
|
||||
departments: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -55,15 +57,13 @@ onUpdated(refreshIcons)
|
||||
// Локальная копия карточек для optimistic UI
|
||||
const localCards = ref([])
|
||||
|
||||
// Синхронизируем с props при загрузке данных
|
||||
// Синхронизируем с props при загрузке/смене проекта
|
||||
watch(() => props.cards, (newCards) => {
|
||||
if (newCards.length > 0 && localCards.value.length === 0) {
|
||||
// Первая загрузка - копируем данные и добавляем order если нет
|
||||
localCards.value = JSON.parse(JSON.stringify(newCards)).map((card, idx) => ({
|
||||
...card,
|
||||
order: card.order ?? idx
|
||||
}))
|
||||
}
|
||||
// Копируем данные и добавляем order если нет
|
||||
localCards.value = JSON.parse(JSON.stringify(newCards)).map((card, idx) => ({
|
||||
...card,
|
||||
order: card.order ?? idx
|
||||
}))
|
||||
}, { immediate: true })
|
||||
|
||||
// Собираем колонки с карточками (используем localCards, сортируем по order)
|
||||
@@ -117,7 +117,8 @@ const inProgressTasks = computed(() => {
|
||||
})
|
||||
|
||||
const completedTasks = computed(() => {
|
||||
const col = filteredColumns.value.find(c => c.id === 4) // Готово
|
||||
if (!props.doneColumnId) return 0
|
||||
const col = filteredColumns.value.find(c => c.id === props.doneColumnId)
|
||||
return col ? col.cards.length : 0
|
||||
})
|
||||
|
||||
@@ -134,15 +135,13 @@ const handleDropCard = async ({ cardId, fromColumnId, toColumnId, toIndex }) =>
|
||||
const card = localCards.value.find(c => c.id === cardId)
|
||||
if (!card) return
|
||||
|
||||
const doneColumnId = window.APP_CONFIG.COLUMN_DONE_ID
|
||||
|
||||
// Локально обновляем для мгновенного отклика
|
||||
card.column_id = toColumnId
|
||||
|
||||
// Обновляем date_closed при перемещении в/из колонки "Готово"
|
||||
if (toColumnId === doneColumnId && fromColumnId !== doneColumnId) {
|
||||
if (props.doneColumnId && toColumnId === props.doneColumnId && fromColumnId !== props.doneColumnId) {
|
||||
card.date_closed = new Date().toISOString()
|
||||
} else if (fromColumnId === doneColumnId && toColumnId !== doneColumnId) {
|
||||
} else if (props.doneColumnId && fromColumnId === props.doneColumnId && toColumnId !== props.doneColumnId) {
|
||||
card.date_closed = null
|
||||
}
|
||||
|
||||
@@ -203,6 +202,7 @@ const saveTask = async (taskData, columnId) => {
|
||||
|
||||
// Отправляем на сервер
|
||||
const result = await cardsApi.create({
|
||||
id_project: taskData.id_project,
|
||||
id_department: taskData.departmentId,
|
||||
id_label: taskData.labelId,
|
||||
id_account: taskData.accountId,
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<span v-if="card.dueDate && Number(columnId) !== doneColumnId" class="due-date" :class="dueDateStatus">
|
||||
{{ daysLeftText }}
|
||||
</span>
|
||||
<span v-if="Number(columnId) === doneColumnId && card.dateClosed" class="date-closed">
|
||||
<span v-if="doneColumnId && Number(columnId) === doneColumnId && card.dateClosed" class="date-closed">
|
||||
Закрыто: {{ closedDateText }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -69,6 +69,7 @@ import { getFullUrl } from '../api'
|
||||
const props = defineProps({
|
||||
card: Object,
|
||||
columnId: [String, Number],
|
||||
doneColumnId: Number,
|
||||
index: Number,
|
||||
departments: {
|
||||
type: Array,
|
||||
@@ -174,12 +175,9 @@ const closedDateText = computed(() => {
|
||||
return formatDateWithYear(props.card.dateClosed)
|
||||
})
|
||||
|
||||
// ID колонки "Готово" из конфига
|
||||
const doneColumnId = window.APP_CONFIG.COLUMN_DONE_ID
|
||||
|
||||
// Можно ли архивировать (только если колонка "Готово")
|
||||
const canArchive = computed(() => {
|
||||
return Number(props.columnId) === doneColumnId
|
||||
return props.doneColumnId && Number(props.columnId) === props.doneColumnId
|
||||
})
|
||||
|
||||
const handleArchive = () => {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<Card
|
||||
:card="card"
|
||||
:column-id="column.id"
|
||||
:done-column-id="doneColumnId"
|
||||
:index="index"
|
||||
:departments="departments"
|
||||
:labels="labels"
|
||||
@@ -47,6 +48,7 @@ import Card from './Card.vue'
|
||||
|
||||
const props = defineProps({
|
||||
column: Object,
|
||||
doneColumnId: Number,
|
||||
departments: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
|
||||
137
front_vue/src/components/ProjectSelector.vue
Normal file
137
front_vue/src/components/ProjectSelector.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="project-select" v-if="store.currentProject" @click.stop>
|
||||
<button class="project-btn" @click="dropdownOpen = !dropdownOpen">
|
||||
<i data-lucide="folder" class="folder-icon"></i>
|
||||
{{ store.currentProject?.name || 'Выберите проект' }}
|
||||
<i data-lucide="chevron-down" class="chevron" :class="{ open: dropdownOpen }"></i>
|
||||
</button>
|
||||
<div class="project-dropdown" v-if="dropdownOpen">
|
||||
<button
|
||||
v-for="project in store.projects"
|
||||
:key="project.id"
|
||||
class="project-option"
|
||||
:class="{ active: store.currentProjectId === project.id }"
|
||||
@click="handleSelect(project.id)"
|
||||
>
|
||||
{{ project.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useProjectsStore } from '../stores/projects'
|
||||
|
||||
const store = useProjectsStore()
|
||||
const dropdownOpen = ref(false)
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const handleSelect = async (projectId) => {
|
||||
dropdownOpen.value = false
|
||||
await store.selectProject(projectId)
|
||||
emit('change', projectId)
|
||||
}
|
||||
|
||||
// Закрытие дропдауна при клике вне
|
||||
const closeDropdown = (e) => {
|
||||
if (!e.target.closest('.project-select')) {
|
||||
dropdownOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeDropdown)
|
||||
if (window.lucide) window.lucide.createIcons()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', closeDropdown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.project-select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.project-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 11px;
|
||||
background: var(--accent-soft);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 6px;
|
||||
color: var(--accent);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.project-btn:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.project-btn .folder-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.project-btn .chevron {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-left: 4px;
|
||||
opacity: 0.7;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.project-btn .chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.project-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
min-width: 200px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 6px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.project-option {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.project-option:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.project-option.active {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
@@ -199,6 +199,7 @@ const props = defineProps({
|
||||
show: Boolean,
|
||||
card: Object,
|
||||
columnId: [String, Number],
|
||||
doneColumnId: Number,
|
||||
isArchived: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -461,7 +462,7 @@ const handleDelete = () => {
|
||||
|
||||
// Можно ли архивировать (только если колонка "Готово")
|
||||
const canArchive = computed(() => {
|
||||
return Number(props.columnId) === window.APP_CONFIG.COLUMN_DONE_ID
|
||||
return props.doneColumnId && Number(props.columnId) === props.doneColumnId
|
||||
})
|
||||
|
||||
const handleArchive = () => {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
|
||||
@@ -3,10 +3,7 @@ import MainApp from './views/MainApp.vue'
|
||||
import LoginPage from './views/LoginPage.vue'
|
||||
import TeamPage from './views/TeamPage.vue'
|
||||
import ArchivePage from './views/ArchivePage.vue'
|
||||
import { authApi, loadServerConfig } from './api'
|
||||
|
||||
// Флаг загрузки конфига (один раз за сессию)
|
||||
let configLoaded = false
|
||||
import { authApi } from './api'
|
||||
|
||||
// Проверка авторизации
|
||||
const checkAuth = async () => {
|
||||
@@ -60,11 +57,6 @@ router.beforeEach(async (to, from, next) => {
|
||||
// Уже авторизован — на главную
|
||||
next('/')
|
||||
} else {
|
||||
// Загружаем конфиг с сервера один раз для защищённых страниц
|
||||
if (to.meta.requiresAuth && isAuth && !configLoaded) {
|
||||
await loadServerConfig()
|
||||
configLoaded = true
|
||||
}
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
145
front_vue/src/stores/projects.js
Normal file
145
front_vue/src/stores/projects.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { projectsApi, usersApi } from '../api'
|
||||
|
||||
export const useProjectsStore = defineStore('projects', () => {
|
||||
// ==================== СОСТОЯНИЕ ====================
|
||||
const projects = ref([])
|
||||
const departments = ref([])
|
||||
const labels = ref([])
|
||||
const columns = ref([])
|
||||
const users = ref([])
|
||||
const loading = ref(false)
|
||||
const initialized = ref(false)
|
||||
|
||||
// Текущий проект (из localStorage)
|
||||
const savedProjectId = localStorage.getItem('currentProjectId')
|
||||
const savedProjectName = localStorage.getItem('currentProjectName')
|
||||
const currentProjectId = ref(savedProjectId ? parseInt(savedProjectId) : null)
|
||||
|
||||
// ==================== ГЕТТЕРЫ ====================
|
||||
// Текущий проект (объект)
|
||||
const currentProject = computed(() =>
|
||||
projects.value.find(p => p.id === currentProjectId.value) ||
|
||||
(savedProjectName && currentProjectId.value ? { id: currentProjectId.value, name: savedProjectName } : null)
|
||||
)
|
||||
|
||||
// ID колонки "Готово" текущего проекта
|
||||
const doneColumnId = computed(() => {
|
||||
const project = projects.value.find(p => p.id === currentProjectId.value)
|
||||
return project ? Number(project.id_ready) : null
|
||||
})
|
||||
|
||||
// ==================== ДЕЙСТВИЯ ====================
|
||||
// Инициализация (загрузка проектов + данных активного)
|
||||
const init = async () => {
|
||||
if (initialized.value) return
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// Загружаем проекты и данные активного одним запросом
|
||||
const result = await projectsApi.getAll(currentProjectId.value || undefined)
|
||||
|
||||
if (result.success) {
|
||||
if (result.data.projects) {
|
||||
projects.value = result.data.projects
|
||||
|
||||
// Применяем данные активного проекта
|
||||
if (result.data.active) {
|
||||
columns.value = result.data.active.columns
|
||||
departments.value = result.data.active.departments
|
||||
labels.value = result.data.active.labels
|
||||
}
|
||||
} else {
|
||||
projects.value = result.data
|
||||
}
|
||||
|
||||
// Если нет выбранного проекта — выбираем первый
|
||||
if (!currentProjectId.value || !projects.value.find(p => p.id === currentProjectId.value)) {
|
||||
if (projects.value.length > 0) {
|
||||
await selectProject(projects.value[0].id, false)
|
||||
}
|
||||
} else {
|
||||
// Обновляем название в localStorage
|
||||
const project = projects.value.find(p => p.id === currentProjectId.value)
|
||||
if (project) localStorage.setItem('currentProjectName', project.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Загружаем пользователей
|
||||
const usersData = await usersApi.getAll()
|
||||
if (usersData.success) users.value = usersData.data
|
||||
|
||||
initialized.value = true
|
||||
} catch (error) {
|
||||
console.error('Ошибка инициализации:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Выбор проекта
|
||||
const selectProject = async (projectId, fetchData = true) => {
|
||||
currentProjectId.value = projectId
|
||||
localStorage.setItem('currentProjectId', projectId.toString())
|
||||
|
||||
// Сохраняем название
|
||||
const project = projects.value.find(p => p.id === projectId)
|
||||
if (project) localStorage.setItem('currentProjectName', project.name)
|
||||
|
||||
// Загружаем данные проекта
|
||||
if (fetchData) {
|
||||
await fetchProjectData()
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка данных текущего проекта
|
||||
const fetchProjectData = async () => {
|
||||
if (!currentProjectId.value) return
|
||||
|
||||
try {
|
||||
const projectData = await projectsApi.getData(currentProjectId.value)
|
||||
|
||||
if (projectData.success) {
|
||||
columns.value = projectData.data.columns
|
||||
departments.value = projectData.data.departments
|
||||
labels.value = projectData.data.labels
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных проекта:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Сброс при выходе
|
||||
const reset = () => {
|
||||
projects.value = []
|
||||
departments.value = []
|
||||
labels.value = []
|
||||
columns.value = []
|
||||
users.value = []
|
||||
currentProjectId.value = null
|
||||
initialized.value = false
|
||||
localStorage.removeItem('currentProjectId')
|
||||
localStorage.removeItem('currentProjectName')
|
||||
}
|
||||
|
||||
return {
|
||||
// Состояние
|
||||
projects,
|
||||
departments,
|
||||
labels,
|
||||
columns,
|
||||
users,
|
||||
loading,
|
||||
initialized,
|
||||
currentProjectId,
|
||||
// Геттеры
|
||||
currentProject,
|
||||
doneColumnId,
|
||||
// Действия
|
||||
init,
|
||||
selectProject,
|
||||
fetchProjectData,
|
||||
reset
|
||||
}
|
||||
})
|
||||
@@ -9,6 +9,12 @@
|
||||
<Header title="Архив задач">
|
||||
<template #filters>
|
||||
<div class="filters">
|
||||
<!-- Выбор проекта -->
|
||||
<ProjectSelector @change="onProjectChange" />
|
||||
|
||||
<div class="filter-divider"></div>
|
||||
|
||||
<!-- Фильтр по отделам -->
|
||||
<button
|
||||
class="filter-tag"
|
||||
:class="{ active: activeDepartment === null }"
|
||||
@@ -17,7 +23,7 @@
|
||||
Все
|
||||
</button>
|
||||
<button
|
||||
v-for="dept in departments"
|
||||
v-for="dept in store.departments"
|
||||
:key="dept.id"
|
||||
class="filter-tag"
|
||||
:class="{ active: activeDepartment === dept.id }"
|
||||
@@ -44,8 +50,8 @@
|
||||
v-for="card in filteredCards"
|
||||
:key="card.id"
|
||||
:card="card"
|
||||
:departments="departments"
|
||||
:labels="labels"
|
||||
:departments="store.departments"
|
||||
:labels="store.labels"
|
||||
@click="openTaskPanel(card)"
|
||||
@restore="handleRestore"
|
||||
@delete="confirmDelete"
|
||||
@@ -73,9 +79,9 @@
|
||||
:card="editingCard"
|
||||
:column-id="null"
|
||||
:is-archived="true"
|
||||
:departments="departments"
|
||||
:labels="labels"
|
||||
:users="users"
|
||||
:departments="store.departments"
|
||||
:labels="store.labels"
|
||||
:users="store.users"
|
||||
@close="closePanel"
|
||||
@save="handleSaveTask"
|
||||
@delete="handleDeleteTask"
|
||||
@@ -101,26 +107,15 @@ import Header from '../components/Header.vue'
|
||||
import ArchiveCard from '../components/ArchiveCard.vue'
|
||||
import TaskPanel from '../components/TaskPanel.vue'
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
||||
import { departmentsApi, labelsApi, cardsApi, usersApi } from '../api'
|
||||
import ProjectSelector from '../components/ProjectSelector.vue'
|
||||
import { useProjectsStore } from '../stores/projects'
|
||||
import { cardsApi } from '../api'
|
||||
|
||||
// Активный фильтр по отделу (синхронизация с основной доской)
|
||||
const savedDepartment = localStorage.getItem('activeDepartment')
|
||||
const activeDepartment = ref(savedDepartment ? parseInt(savedDepartment) : null)
|
||||
// ==================== STORE ====================
|
||||
const store = useProjectsStore()
|
||||
|
||||
// Сохраняем в localStorage при изменении
|
||||
watch(activeDepartment, (newVal) => {
|
||||
if (newVal === null) {
|
||||
localStorage.removeItem('activeDepartment')
|
||||
} else {
|
||||
localStorage.setItem('activeDepartment', newVal.toString())
|
||||
}
|
||||
})
|
||||
|
||||
// Данные
|
||||
const departments = ref([])
|
||||
const labels = ref([])
|
||||
// ==================== КАРТОЧКИ ====================
|
||||
const cards = ref([])
|
||||
const users = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
// Отфильтрованные карточки (сортируем по дате завершения, новые сверху)
|
||||
@@ -130,30 +125,24 @@ const filteredCards = computed(() => {
|
||||
result = result.filter(card => card.departmentId === activeDepartment.value)
|
||||
}
|
||||
return result.sort((a, b) => {
|
||||
// Сортировка по дате завершения (новые сверху)
|
||||
const dateA = a.dateClosed ? new Date(a.dateClosed).getTime() : 0
|
||||
const dateB = b.dateClosed ? new Date(b.dateClosed).getTime() : 0
|
||||
return dateB - dateA
|
||||
})
|
||||
})
|
||||
|
||||
// Загрузка данных
|
||||
const fetchData = async () => {
|
||||
// Загрузка архивных карточек
|
||||
const fetchCards = async () => {
|
||||
if (!store.currentProjectId) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const [departmentsData, labelsData, cardsData, usersData] = await Promise.all([
|
||||
departmentsApi.getAll(),
|
||||
labelsApi.getAll(),
|
||||
cardsApi.getAll(1), // archive = 1
|
||||
usersApi.getAll()
|
||||
])
|
||||
|
||||
if (departmentsData.success) departments.value = departmentsData.data
|
||||
if (labelsData.success) labels.value = labelsData.data
|
||||
if (usersData.success) users.value = usersData.data
|
||||
|
||||
if (cardsData.success) {
|
||||
cards.value = cardsData.data.map(card => ({
|
||||
const result = await cardsApi.getAll(store.currentProjectId, 1) // archive = 1
|
||||
if (result.success) {
|
||||
cards.value = result.data.map(card => ({
|
||||
id: card.id,
|
||||
title: card.title,
|
||||
description: card.descript,
|
||||
@@ -175,14 +164,30 @@ const fetchData = async () => {
|
||||
}))
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Панель редактирования
|
||||
// При смене проекта
|
||||
const onProjectChange = async () => {
|
||||
activeDepartment.value = null
|
||||
await fetchCards()
|
||||
}
|
||||
|
||||
// ==================== ФИЛЬТР ПО ОТДЕЛАМ ====================
|
||||
const savedDepartment = localStorage.getItem('activeDepartment')
|
||||
const activeDepartment = ref(savedDepartment ? parseInt(savedDepartment) : null)
|
||||
|
||||
watch(activeDepartment, (newVal) => {
|
||||
if (newVal === null) {
|
||||
localStorage.removeItem('activeDepartment')
|
||||
} else {
|
||||
localStorage.setItem('activeDepartment', newVal.toString())
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== ПАНЕЛЬ РЕДАКТИРОВАНИЯ ====================
|
||||
const panelOpen = ref(false)
|
||||
const editingCard = ref(null)
|
||||
|
||||
@@ -196,10 +201,8 @@ const closePanel = () => {
|
||||
editingCard.value = null
|
||||
}
|
||||
|
||||
// Сохранение задачи
|
||||
const handleSaveTask = async (taskData) => {
|
||||
if (taskData.id) {
|
||||
// Обновляем на сервере
|
||||
await cardsApi.update({
|
||||
id: taskData.id,
|
||||
id_department: taskData.departmentId,
|
||||
@@ -211,7 +214,6 @@ const handleSaveTask = async (taskData) => {
|
||||
descript_full: taskData.details
|
||||
})
|
||||
|
||||
// Обновляем локально
|
||||
const card = cards.value.find(c => c.id === taskData.id)
|
||||
if (card) {
|
||||
card.title = taskData.title
|
||||
@@ -228,7 +230,6 @@ const handleSaveTask = async (taskData) => {
|
||||
closePanel()
|
||||
}
|
||||
|
||||
// Удаление через панель
|
||||
const handleDeleteTask = async (cardId) => {
|
||||
const result = await cardsApi.delete(cardId)
|
||||
if (result.success) {
|
||||
@@ -237,7 +238,7 @@ const handleDeleteTask = async (cardId) => {
|
||||
closePanel()
|
||||
}
|
||||
|
||||
// Диалог подтверждения удаления
|
||||
// ==================== УДАЛЕНИЕ С ПОДТВЕРЖДЕНИЕМ ====================
|
||||
const confirmDialogOpen = ref(false)
|
||||
const cardToDelete = ref(null)
|
||||
|
||||
@@ -257,7 +258,7 @@ const handleConfirmDelete = async () => {
|
||||
cardToDelete.value = null
|
||||
}
|
||||
|
||||
// Восстановление из архива
|
||||
// ==================== ВОССТАНОВЛЕНИЕ ====================
|
||||
const handleRestore = async (cardId) => {
|
||||
const result = await cardsApi.setArchive(cardId, 0)
|
||||
if (result.success) {
|
||||
@@ -265,18 +266,17 @@ const handleRestore = async (cardId) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Восстановление через панель редактирования
|
||||
const handleRestoreFromPanel = async (cardId) => {
|
||||
await handleRestore(cardId)
|
||||
closePanel()
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons()
|
||||
}
|
||||
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
|
||||
onMounted(async () => {
|
||||
await store.init()
|
||||
await fetchCards()
|
||||
|
||||
if (window.lucide) window.lucide.createIcons()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -304,6 +304,14 @@ onMounted(() => {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Разделитель между проектом и отделами */
|
||||
.filter-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* Кнопка фильтра */
|
||||
.filter-tag {
|
||||
padding: 6px 12px;
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
<Header title="Доска задач">
|
||||
<template #filters>
|
||||
<div class="filters">
|
||||
<!-- Выбор проекта -->
|
||||
<ProjectSelector @change="onProjectChange" />
|
||||
|
||||
<div class="filter-divider"></div>
|
||||
|
||||
<!-- Фильтр по отделам -->
|
||||
<button
|
||||
class="filter-tag"
|
||||
:class="{ active: activeDepartment === null }"
|
||||
@@ -17,7 +23,7 @@
|
||||
Все
|
||||
</button>
|
||||
<button
|
||||
v-for="dept in departments"
|
||||
v-for="dept in store.departments"
|
||||
:key="dept.id"
|
||||
class="filter-tag"
|
||||
:class="{ active: activeDepartment === dept.id }"
|
||||
@@ -52,10 +58,11 @@
|
||||
<Board
|
||||
ref="boardRef"
|
||||
:active-department="activeDepartment"
|
||||
:departments="departments"
|
||||
:labels="labels"
|
||||
:columns="columns"
|
||||
:departments="store.departments"
|
||||
:labels="store.labels"
|
||||
:columns="store.columns"
|
||||
:cards="cards"
|
||||
:done-column-id="store.doneColumnId"
|
||||
@stats-updated="stats = $event"
|
||||
@open-task="openTaskPanel"
|
||||
@create-task="openNewTaskPanel"
|
||||
@@ -68,9 +75,10 @@
|
||||
:show="panelOpen"
|
||||
:card="editingCard"
|
||||
:column-id="editingColumnId"
|
||||
:departments="departments"
|
||||
:labels="labels"
|
||||
:users="users"
|
||||
:done-column-id="store.doneColumnId"
|
||||
:departments="store.departments"
|
||||
:labels="store.labels"
|
||||
:users="store.users"
|
||||
@close="closePanel"
|
||||
@save="handleSaveTask"
|
||||
@delete="handleDeleteTask"
|
||||
@@ -85,14 +93,34 @@ import Sidebar from '../components/Sidebar.vue'
|
||||
import Header from '../components/Header.vue'
|
||||
import Board from '../components/Board.vue'
|
||||
import TaskPanel from '../components/TaskPanel.vue'
|
||||
import { departmentsApi, labelsApi, columnsApi, cardsApi, usersApi } from '../api'
|
||||
import ProjectSelector from '../components/ProjectSelector.vue'
|
||||
import { useProjectsStore } from '../stores/projects'
|
||||
import { cardsApi } from '../api'
|
||||
|
||||
// Активный фильтр по отделу (null = все)
|
||||
// Восстанавливаем из localStorage
|
||||
// ==================== STORE ====================
|
||||
const store = useProjectsStore()
|
||||
|
||||
// ==================== КАРТОЧКИ ====================
|
||||
const cards = ref([])
|
||||
|
||||
// Загрузка карточек текущего проекта
|
||||
const fetchCards = async () => {
|
||||
if (!store.currentProjectId) return
|
||||
|
||||
const result = await cardsApi.getAll(store.currentProjectId)
|
||||
if (result.success) cards.value = result.data
|
||||
}
|
||||
|
||||
// При смене проекта — перезагружаем карточки
|
||||
const onProjectChange = async () => {
|
||||
activeDepartment.value = null
|
||||
await fetchCards()
|
||||
}
|
||||
|
||||
// ==================== ФИЛЬТР ПО ОТДЕЛАМ ====================
|
||||
const savedDepartment = localStorage.getItem('activeDepartment')
|
||||
const activeDepartment = ref(savedDepartment ? parseInt(savedDepartment) : null)
|
||||
|
||||
// Сохраняем в localStorage при изменении
|
||||
watch(activeDepartment, (newVal) => {
|
||||
if (newVal === null) {
|
||||
localStorage.removeItem('activeDepartment')
|
||||
@@ -100,91 +128,60 @@ watch(activeDepartment, (newVal) => {
|
||||
localStorage.setItem('activeDepartment', newVal.toString())
|
||||
}
|
||||
})
|
||||
// Статистика для шапки
|
||||
|
||||
// ==================== СТАТИСТИКА ====================
|
||||
const stats = ref({ total: 0, inProgress: 0, done: 0 })
|
||||
// Данные из API
|
||||
const departments = ref([])
|
||||
const labels = ref([])
|
||||
const columns = ref([])
|
||||
const cards = ref([])
|
||||
const users = ref([])
|
||||
|
||||
// Загрузка всех данных из API параллельно
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [departmentsData, labelsData, columnsData, cardsData, usersData] = await Promise.all([
|
||||
departmentsApi.getAll(),
|
||||
labelsApi.getAll(),
|
||||
columnsApi.getAll(),
|
||||
cardsApi.getAll(),
|
||||
usersApi.getAll()
|
||||
])
|
||||
|
||||
if (departmentsData.success) departments.value = departmentsData.data
|
||||
if (labelsData.success) labels.value = labelsData.data
|
||||
if (columnsData.success) columns.value = columnsData.data
|
||||
if (cardsData.success) cards.value = cardsData.data
|
||||
if (usersData.success) users.value = usersData.data
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Выход из системы
|
||||
// Ссылка на компонент Board для вызова его методов
|
||||
// ==================== ПАНЕЛЬ РЕДАКТИРОВАНИЯ ====================
|
||||
const boardRef = ref(null)
|
||||
// Состояние панели редактирования
|
||||
const panelOpen = ref(false)
|
||||
// Редактируемая карточка (null = создание новой)
|
||||
const editingCard = ref(null)
|
||||
// ID колонки для новой/редактируемой карточки
|
||||
const editingColumnId = ref(null)
|
||||
|
||||
// Открыть панель для редактирования существующей задачи
|
||||
const openTaskPanel = ({ card, columnId }) => {
|
||||
editingCard.value = card
|
||||
editingColumnId.value = columnId
|
||||
panelOpen.value = true
|
||||
}
|
||||
|
||||
// Открыть панель для создания новой задачи
|
||||
const openNewTaskPanel = (columnId) => {
|
||||
editingCard.value = null
|
||||
editingColumnId.value = columnId
|
||||
panelOpen.value = true
|
||||
}
|
||||
|
||||
// Закрыть панель и сбросить состояние
|
||||
const closePanel = () => {
|
||||
panelOpen.value = false
|
||||
editingCard.value = null
|
||||
editingColumnId.value = null
|
||||
}
|
||||
|
||||
// Сохранить задачу через Board компонент
|
||||
const handleSaveTask = async (taskData) => {
|
||||
if (!taskData.id && store.currentProjectId) {
|
||||
taskData.id_project = store.currentProjectId
|
||||
}
|
||||
await boardRef.value?.saveTask(taskData, editingColumnId.value)
|
||||
closePanel()
|
||||
}
|
||||
|
||||
// Удалить задачу через Board компонент
|
||||
const handleDeleteTask = async (cardId) => {
|
||||
await boardRef.value?.deleteTask(cardId, editingColumnId.value)
|
||||
closePanel()
|
||||
}
|
||||
|
||||
// Архивировать задачу через Board компонент
|
||||
const handleArchiveTask = async (cardId) => {
|
||||
await boardRef.value?.archiveTask(cardId)
|
||||
closePanel()
|
||||
}
|
||||
|
||||
// Инициализация при монтировании
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons()
|
||||
}
|
||||
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
|
||||
onMounted(async () => {
|
||||
// Инициализируем store (загрузит проекты, departments, labels, users)
|
||||
await store.init()
|
||||
// Загружаем карточки
|
||||
await fetchCards()
|
||||
|
||||
if (window.lucide) window.lucide.createIcons()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -195,7 +192,6 @@ onMounted(() => {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
/* Основная область контента */
|
||||
.main-wrapper {
|
||||
flex: 1;
|
||||
@@ -213,6 +209,14 @@ onMounted(() => {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Разделитель между проектом и отделами */
|
||||
.filter-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* Кнопка фильтра */
|
||||
.filter-tag {
|
||||
padding: 6px 12px;
|
||||
|
||||
Reference in New Issue
Block a user