1
0

Добавление проектов

Добавили возможность создавания разных проектов.
This commit is contained in:
2026-01-14 10:46:38 +07:00
parent 04e88cb7fa
commit 719aecd09e
22 changed files with 996 additions and 233 deletions

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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 = () => {

View File

@@ -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: () => []

View 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>

View File

@@ -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 = () => {

View File

@@ -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')

View File

@@ -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()
}
})

View 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
}
})

View File

@@ -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;

View File

@@ -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;