From 250eac70a7e179e89ec9926c262c07ba6e4b2563 Mon Sep 17 00:00:00 2001 From: Falknat Date: Sun, 18 Jan 2026 10:19:34 +0700 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавил возможность удаления, создание и редактирование проектов. --- backend/api/project.php | 112 +++ backend/app/class/enity/class_project.php | 259 ++++++ front_vue/src/api.js | 71 ++ front_vue/src/components/DepartmentTags.vue | 49 +- front_vue/src/components/ProjectPanel.vue | 856 +++++++++++++++++++ front_vue/src/components/ProjectSelector.vue | 212 ++++- front_vue/src/components/ui/ColorPicker.vue | 448 ++++++++++ front_vue/src/components/ui/SlidePanel.vue | 2 +- front_vue/src/stores/dialogs.js | 24 + front_vue/src/stores/projects.js | 164 +++- front_vue/src/views/MainApp.vue | 92 +- 11 files changed, 2273 insertions(+), 16 deletions(-) create mode 100644 front_vue/src/components/ProjectPanel.vue create mode 100644 front_vue/src/components/ui/ColorPicker.vue diff --git a/backend/api/project.php b/backend/api/project.php index 644f285..d1a460e 100644 --- a/backend/api/project.php +++ b/backend/api/project.php @@ -5,6 +5,7 @@ $method = $_SERVER['REQUEST_METHOD']; if ($method === 'POST') { $data = RestApi::getInput(); $action = $data['action'] ?? null; + $user_id = RestApi::getCurrentUserId(); // Получение данных проекта (проект + колонки + отделы) if ($action === 'get_project_data') { @@ -17,6 +18,117 @@ if ($method === 'POST') { } } + // ==================== CRUD ПРОЕКТОВ ==================== + + // Создание проекта + if ($action === 'create') { + $name = trim($data['name'] ?? ''); + if (!$name) { + RestApi::response(['success' => false, 'errors' => ['name' => 'Укажите название проекта']], 400); + } + $result = Project::create($name, $user_id); + RestApi::response($result, $result['success'] ? 200 : 400); + } + + // Обновление проекта + if ($action === 'update') { + $id = $data['id'] ?? null; + $name = trim($data['name'] ?? ''); + if (!$id || !$name) { + RestApi::response(['success' => false, 'errors' => ['data' => 'Укажите ID и название']], 400); + } + $result = Project::update($id, $name, $user_id); + RestApi::response($result, $result['success'] ? 200 : 403); + } + + // Удаление проекта + if ($action === 'delete') { + $id = $data['id'] ?? null; + if (!$id) { + RestApi::response(['success' => false, 'errors' => ['id' => 'Укажите ID проекта']], 400); + } + $result = Project::delete($id, $user_id); + RestApi::response($result, $result['success'] ? 200 : 403); + } + + // Обновление порядка проектов + if ($action === 'update_order') { + $ids = $data['ids'] ?? []; + if (empty($ids)) { + RestApi::response(['success' => false, 'errors' => ['ids' => 'Укажите массив ID']], 400); + } + $result = Project::updateOrder($ids, $user_id); + RestApi::response($result); + } + + // ==================== CRUD КОЛОНОК ==================== + + // Добавление колонки + if ($action === 'add_column') { + $project_id = $data['project_id'] ?? null; + $name = trim($data['name'] ?? ''); + $color = $data['color'] ?? '#6366f1'; + if (!$project_id || !$name) { + RestApi::response(['success' => false, 'errors' => ['data' => 'Укажите project_id и name']], 400); + } + $result = Project::addColumn($project_id, $name, $color, $user_id); + RestApi::response($result, $result['success'] ? 200 : 403); + } + + // Обновление колонки + if ($action === 'update_column') { + $id = $data['id'] ?? null; + $name = isset($data['name']) ? trim($data['name']) : null; + $color = $data['color'] ?? null; + if (!$id) { + RestApi::response(['success' => false, 'errors' => ['id' => 'Укажите ID колонки']], 400); + } + $result = Project::updateColumn($id, $name, $color, $user_id); + RestApi::response($result, $result['success'] ? 200 : 403); + } + + // Получение количества задач в колонке (для подтверждения удаления) + if ($action === 'get_column_tasks_count') { + $id = $data['id'] ?? null; + if (!$id) { + RestApi::response(['success' => false, 'errors' => ['id' => 'Укажите ID колонки']], 400); + } + $count = Project::getColumnTasksCount($id); + RestApi::response(['success' => true, 'count' => $count]); + } + + // Удаление колонки + if ($action === 'delete_column') { + $id = $data['id'] ?? null; + if (!$id) { + RestApi::response(['success' => false, 'errors' => ['id' => 'Укажите ID колонки']], 400); + } + $result = Project::deleteColumn($id, $user_id); + RestApi::response($result, $result['success'] ? 200 : 403); + } + + // Обновление порядка колонок + if ($action === 'update_columns_order') { + $project_id = $data['project_id'] ?? null; + $ids = $data['ids'] ?? []; + if (!$project_id || empty($ids)) { + RestApi::response(['success' => false, 'errors' => ['data' => 'Укажите project_id и ids']], 400); + } + $result = Project::updateColumnsOrder($project_id, $ids, $user_id); + RestApi::response($result, $result['success'] ? 200 : 403); + } + + // Установка финальной колонки + if ($action === 'set_ready_column') { + $project_id = $data['project_id'] ?? null; + $column_id = $data['column_id'] ?? null; + if (!$project_id || !$column_id) { + RestApi::response(['success' => false, 'errors' => ['data' => 'Укажите project_id и column_id']], 400); + } + $result = Project::setReadyColumn($project_id, $column_id, $user_id); + RestApi::response($result, $result['success'] ? 200 : 403); + } + // Метод не указан if (!$action) { RestApi::response(['success' => false, 'error' => 'Укажите метод'], 400); diff --git a/backend/app/class/enity/class_project.php b/backend/app/class/enity/class_project.php index c89c2d8..2273ccc 100644 --- a/backend/app/class/enity/class_project.php +++ b/backend/app/class/enity/class_project.php @@ -124,6 +124,265 @@ class Project extends BaseEntity { $admins = json_decode($project['id_admin'], true) ?: []; return in_array((int)$user_id, $admins, true); } + + // ==================== CRUD ПРОЕКТОВ ==================== + + // Создание проекта с дефолтными колонками + public static function create($name, $user_id) { + // Получаем максимальный id_order + $maxOrder = Database::max('project', 'id_order') ?? 0; + + // Создаём проект + Database::insert('project', [ + 'name' => $name, + 'id_order' => $maxOrder + 1, + 'id_admin' => json_encode([(int)$user_id]), + 'id_member' => json_encode([]) + ]); + + $projectId = Database::id(); + if (!$projectId) { + return ['success' => false, 'errors' => ['project' => 'Ошибка создания проекта']]; + } + + // Создаём дефолтные колонки + Database::insert('columns', [ + 'name_columns' => 'К выполнению', + 'color' => '#6366f1', + 'id_project' => $projectId, + 'id_order' => 1 + ]); + $firstColumnId = Database::id(); + + Database::insert('columns', [ + 'name_columns' => 'Готово', + 'color' => '#22c55e', + 'id_project' => $projectId, + 'id_order' => 2 + ]); + $readyColumnId = Database::id(); + + // Устанавливаем id_ready + Database::update('project', ['id_ready' => $readyColumnId], ['id' => $projectId]); + + return [ + 'success' => true, + 'id' => $projectId, + 'columns' => [ + ['id' => $firstColumnId, 'name_columns' => 'К выполнению', 'color' => '#6366f1', 'id_order' => 1], + ['id' => $readyColumnId, 'name_columns' => 'Готово', 'color' => '#22c55e', 'id_order' => 2] + ], + 'id_ready' => $readyColumnId + ]; + } + + // Обновление проекта + public static function update($id, $name, $user_id) { + // Проверяем права + if (!self::isAdmin($id, $user_id)) { + return ['success' => false, 'errors' => ['access' => 'Нет прав на редактирование']]; + } + + Database::update('project', ['name' => $name], ['id' => $id]); + return ['success' => true]; + } + + // Удаление проекта (каскадно: колонки, задачи, комментарии, файлы) + public static function delete($id, $user_id) { + // Проверяем права + if (!self::isAdmin($id, $user_id)) { + return ['success' => false, 'errors' => ['access' => 'Нет прав на удаление']]; + } + + // Получаем все задачи проекта + $tasks = Database::select('cards_task', ['id'], ['id_project' => $id]); + $taskIds = array_column($tasks, 'id'); + + if (!empty($taskIds)) { + // Получаем все комментарии к задачам + $comments = Database::select('comments', ['id'], ['id_task' => $taskIds]); + + // Удаляем файлы комментариев с диска + foreach ($comments as $comment) { + FileUpload::deleteFolder('comment', $comment['id']); + } + + // Удаляем комментарии из БД + Database::delete('comments', ['id_task' => $taskIds]); + } + + // Удаляем файлы задач с диска + foreach ($tasks as $task) { + FileUpload::deleteFolder('task', $task['id']); + } + + // Удаляем задачи из БД + Database::delete('cards_task', ['id_project' => $id]); + + // Удаляем колонки + Database::delete('columns', ['id_project' => $id]); + + // Удаляем отделы проекта + Database::delete('departments', ['id_project' => $id]); + + // Удаляем проект + Database::delete('project', ['id' => $id]); + + return ['success' => true]; + } + + // Обновление порядка проектов + public static function updateOrder($ids, $user_id) { + foreach ($ids as $order => $id) { + Database::update('project', ['id_order' => $order + 1], ['id' => $id]); + } + return ['success' => true]; + } + + // ==================== CRUD КОЛОНОК ==================== + + // Добавление колонки + public static function addColumn($project_id, $name, $color, $user_id) { + // Проверяем права + if (!self::isAdmin($project_id, $user_id)) { + return ['success' => false, 'errors' => ['access' => 'Нет прав на редактирование']]; + } + + // Получаем максимальный id_order для проекта + $maxOrder = Database::max('columns', 'id_order', ['id_project' => $project_id]) ?? 0; + + Database::insert('columns', [ + 'name_columns' => $name, + 'color' => $color ?: '#6366f1', + 'id_project' => $project_id, + 'id_order' => $maxOrder + 1 + ]); + + $columnId = Database::id(); + return [ + 'success' => true, + 'id' => $columnId, + 'column' => [ + 'id' => $columnId, + 'name_columns' => $name, + 'color' => $color ?: '#6366f1', + 'id_order' => $maxOrder + 1 + ] + ]; + } + + // Обновление колонки + public static function updateColumn($id, $name, $color, $user_id) { + // Получаем колонку для проверки проекта + $column = Database::get('columns', ['id_project'], ['id' => $id]); + if (!$column) { + return ['success' => false, 'errors' => ['column' => 'Колонка не найдена']]; + } + + // Проверяем права + if (!self::isAdmin($column['id_project'], $user_id)) { + return ['success' => false, 'errors' => ['access' => 'Нет прав на редактирование']]; + } + + $updateData = []; + if ($name !== null) $updateData['name_columns'] = $name; + if ($color !== null) $updateData['color'] = $color; + + if (!empty($updateData)) { + Database::update('columns', $updateData, ['id' => $id]); + } + + return ['success' => true]; + } + + // Удаление колонки (возвращает количество задач для подтверждения) + public static function getColumnTasksCount($column_id) { + return Database::count('cards_task', ['column_id' => $column_id]); + } + + // Удаление колонки с задачами + public static function deleteColumn($id, $user_id) { + // Получаем колонку для проверки проекта + $column = Database::get('columns', ['id_project', 'id_order'], ['id' => $id]); + if (!$column) { + return ['success' => false, 'errors' => ['column' => 'Колонка не найдена']]; + } + + // Проверяем права + if (!self::isAdmin($column['id_project'], $user_id)) { + return ['success' => false, 'errors' => ['access' => 'Нет прав на удаление']]; + } + + // Проверяем, не является ли это последней колонкой перед "Готово" + // Минимум должно быть 3 колонки (2 обычные + готово), чтобы можно было удалить + $columnsCount = Database::count('columns', ['id_project' => $column['id_project']]); + if ($columnsCount <= 2) { + return ['success' => false, 'errors' => ['column' => 'Нельзя удалить последнюю колонку перед финальной']]; + } + + // Проверяем, не является ли это последней (финальной) колонкой + $lastColumn = Database::get('columns', ['id'], [ + 'id_project' => $column['id_project'], + 'ORDER' => ['id_order' => 'DESC'], + 'LIMIT' => 1 + ]); + if ($lastColumn && (int)$lastColumn['id'] === (int)$id) { + return ['success' => false, 'errors' => ['column' => 'Нельзя удалить финальную колонку (последнюю)']]; + } + + // Получаем задачи колонки + $tasks = Database::select('cards_task', ['id'], ['column_id' => $id]); + $taskIds = array_column($tasks, 'id'); + + // Удаляем комментарии к задачам + if (!empty($taskIds)) { + Database::delete('comments', ['id_task' => $taskIds]); + } + + // Удаляем задачи + Database::delete('cards_task', ['column_id' => $id]); + + // Удаляем колонку + Database::delete('columns', ['id' => $id]); + + // Обновляем id_ready на новую последнюю колонку + $newLastColumn = Database::get('columns', ['id'], [ + 'id_project' => $column['id_project'], + 'ORDER' => ['id_order' => 'DESC'], + 'LIMIT' => 1 + ]); + if ($newLastColumn) { + Database::update('project', ['id_ready' => $newLastColumn['id']], ['id' => $column['id_project']]); + } + + return ['success' => true, 'deleted_tasks' => count($taskIds)]; + } + + // Обновление порядка колонок + public static function updateColumnsOrder($project_id, $ids, $user_id) { + // Проверяем права + if (!self::isAdmin($project_id, $user_id)) { + return ['success' => false, 'errors' => ['access' => 'Нет прав на редактирование']]; + } + + foreach ($ids as $order => $id) { + Database::update('columns', ['id_order' => $order + 1], ['id' => $id, 'id_project' => $project_id]); + } + + // Автоматически устанавливаем id_ready на последнюю колонку + $lastColumnId = end($ids); + if ($lastColumnId) { + Database::update('project', ['id_ready' => $lastColumnId], ['id' => $project_id]); + } + + return ['success' => true]; + } + + // Установка финальной колонки (id_ready) - ЗАБЛОКИРОВАНО + // Финальная колонка всегда последняя, изменение запрещено + public static function setReadyColumn($project_id, $column_id, $user_id) { + return ['success' => false, 'errors' => ['column' => 'Изменение финальной колонки запрещено. Финальная колонка всегда последняя.']]; + } } ?> diff --git a/front_vue/src/api.js b/front_vue/src/api.js index 34b3fb8..000b004 100644 --- a/front_vue/src/api.js +++ b/front_vue/src/api.js @@ -64,6 +64,77 @@ export const projectsApi = { credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'get_project_data', id_project }) + }), + // Создание проекта + create: (name) => request('/api/project', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'create', name }) + }), + // Обновление проекта + update: (id, name) => request('/api/project', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'update', id, name }) + }), + // Удаление проекта + delete: (id) => request('/api/project', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'delete', id }) + }), + // Обновление порядка проектов + updateOrder: (ids) => request('/api/project', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'update_order', ids }) + }), + // ==================== КОЛОНКИ ==================== + // Добавление колонки + addColumn: (project_id, name, color) => request('/api/project', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'add_column', project_id, name, color }) + }), + // Обновление колонки + updateColumn: (id, name, color) => request('/api/project', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'update_column', id, name, color }) + }), + // Получение количества задач в колонке + getColumnTasksCount: (id) => request('/api/project', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'get_column_tasks_count', id }) + }), + // Удаление колонки + deleteColumn: (id) => request('/api/project', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'delete_column', id }) + }), + // Обновление порядка колонок + updateColumnsOrder: (project_id, ids) => request('/api/project', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'update_columns_order', project_id, ids }) + }), + // Установка финальной колонки + setReadyColumn: (project_id, column_id) => request('/api/project', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'set_ready_column', project_id, column_id }) }) } diff --git a/front_vue/src/components/DepartmentTags.vue b/front_vue/src/components/DepartmentTags.vue index 7e94a0c..233675d 100644 --- a/front_vue/src/components/DepartmentTags.vue +++ b/front_vue/src/components/DepartmentTags.vue @@ -1,6 +1,16 @@ diff --git a/front_vue/src/components/ui/ColorPicker.vue b/front_vue/src/components/ui/ColorPicker.vue new file mode 100644 index 0000000..7e7e87a --- /dev/null +++ b/front_vue/src/components/ui/ColorPicker.vue @@ -0,0 +1,448 @@ + + + + + diff --git a/front_vue/src/components/ui/SlidePanel.vue b/front_vue/src/components/ui/SlidePanel.vue index 5e7f9ed..9abe855 100644 --- a/front_vue/src/components/ui/SlidePanel.vue +++ b/front_vue/src/components/ui/SlidePanel.vue @@ -368,7 +368,7 @@ onUnmounted(() => { .panel.mobile .panel-body { padding: 16px; min-height: 0; - gap: 0; + gap: 20px; overflow-y: auto; } diff --git a/front_vue/src/stores/dialogs.js b/front_vue/src/stores/dialogs.js index 4a21a35..558ab2a 100644 --- a/front_vue/src/stores/dialogs.js +++ b/front_vue/src/stores/dialogs.js @@ -50,5 +50,29 @@ export const DIALOGS = { confirmText: 'Сохранить', showDiscard: true, variant: 'default' + }, + + // Удаление проекта + deleteProject: { + title: 'Удалить проект?', + message: 'Все задачи, колонки и комментарии
будут удалены навсегда.', + confirmText: 'Удалить', + variant: 'danger' + }, + + // Удаление колонки + deleteColumn: { + title: 'Удалить колонку?', + message: 'Колонка и все задачи в ней
будут удалены навсегда.', + confirmText: 'Удалить', + variant: 'danger' + }, + + // Удаление колонки с задачами (динамический message) + deleteColumnWithTasks: { + title: 'Удалить колонку?', + message: '', // Будет задан динамически + confirmText: 'Удалить', + variant: 'danger' } } diff --git a/front_vue/src/stores/projects.js b/front_vue/src/stores/projects.js index afd2fd3..f571159 100644 --- a/front_vue/src/stores/projects.js +++ b/front_vue/src/stores/projects.js @@ -226,6 +226,156 @@ export const useProjectsStore = defineStore('projects', () => { localStorage.removeItem('currentProjectName') } + // ==================== CRUD ПРОЕКТОВ ==================== + + // Создание проекта + const createProject = async (name) => { + const result = await projectsApi.create(name) + if (result.success) { + // Добавляем проект в список + projects.value.push({ + id: result.id, + name, + id_order: projects.value.length + 1, + id_ready: result.id_ready, + id_admin: true // Создатель = админ + }) + // Переключаемся на новый проект + await selectProject(result.id) + } + return result + } + + // Обновление проекта + const updateProject = async (id, name) => { + const result = await projectsApi.update(id, name) + if (result.success) { + const project = projects.value.find(p => p.id === id) + if (project) { + project.name = name + // Обновляем localStorage если это текущий проект + if (id === currentProjectId.value) { + localStorage.setItem('currentProjectName', name) + } + } + } + return result + } + + // Удаление проекта + const deleteProject = async (id) => { + const result = await projectsApi.delete(id) + if (result.success) { + const index = projects.value.findIndex(p => p.id === id) + if (index !== -1) { + projects.value.splice(index, 1) + } + // Если удалили текущий проект — переключаемся на первый + if (id === currentProjectId.value) { + if (projects.value.length > 0) { + await selectProject(projects.value[0].id) + } else { + currentProjectId.value = null + columns.value = [] + departments.value = [] + cards.value = [] + localStorage.removeItem('currentProjectId') + localStorage.removeItem('currentProjectName') + } + } + } + return result + } + + // Обновление порядка проектов + const reorderProjects = async (ids) => { + // Оптимистичное обновление + const reordered = ids.map((id, index) => { + const project = projects.value.find(p => p.id === id) + return { ...project, id_order: index + 1 } + }) + projects.value = reordered + + // Отправляем на сервер + await projectsApi.updateOrder(ids) + } + + // ==================== CRUD КОЛОНОК ==================== + + // Добавление колонки + const addColumn = async (name, color = '#6366f1') => { + if (!currentProjectId.value) return { success: false } + + const result = await projectsApi.addColumn(currentProjectId.value, name, color) + if (result.success) { + columns.value.push(result.column) + } + return result + } + + // Обновление колонки + const updateColumn = async (id, name, color) => { + const result = await projectsApi.updateColumn(id, name, color) + if (result.success) { + const column = columns.value.find(c => c.id === id) + if (column) { + if (name !== null && name !== undefined) column.name_columns = name + if (color !== null && color !== undefined) column.color = color + } + } + return result + } + + // Получение количества задач в колонке + const getColumnTasksCount = async (id) => { + const result = await projectsApi.getColumnTasksCount(id) + return result.success ? result.count : 0 + } + + // Удаление колонки + const deleteColumn = async (id) => { + const result = await projectsApi.deleteColumn(id) + if (result.success) { + const index = columns.value.findIndex(c => c.id === id) + if (index !== -1) { + columns.value.splice(index, 1) + } + // Удаляем карточки этой колонки из локального состояния + cards.value = cards.value.filter(c => c.column_id !== id) + } + return result + } + + // Обновление порядка колонок + const reorderColumns = async (ids) => { + if (!currentProjectId.value) return + + // Оптимистичное обновление + const reordered = ids.map((id, index) => { + const column = columns.value.find(c => c.id === id) + return { ...column, id_order: index + 1 } + }) + columns.value = reordered + + // Отправляем на сервер + await projectsApi.updateColumnsOrder(currentProjectId.value, ids) + } + + // Установка финальной колонки + const setReadyColumn = async (columnId) => { + if (!currentProjectId.value) return { success: false } + + const result = await projectsApi.setReadyColumn(currentProjectId.value, columnId) + if (result.success) { + // Обновляем id_ready в проекте + const project = projects.value.find(p => p.id === currentProjectId.value) + if (project) { + project.id_ready = columnId + } + } + return result + } + return { // Состояние projects, @@ -254,6 +404,18 @@ export const useProjectsStore = defineStore('projects', () => { fetchCards, fetchArchivedCards, clearCards, - reset + reset, + // CRUD проектов + createProject, + updateProject, + deleteProject, + reorderProjects, + // CRUD колонок + addColumn, + updateColumn, + getColumnTasksCount, + deleteColumn, + reorderColumns, + setReadyColumn } }) diff --git a/front_vue/src/views/MainApp.vue b/front_vue/src/views/MainApp.vue index 5705dee..e472f93 100644 --- a/front_vue/src/views/MainApp.vue +++ b/front_vue/src/views/MainApp.vue @@ -7,12 +7,35 @@ @@ -79,6 +109,7 @@ import PageLayout from '../components/PageLayout.vue' import Header from '../components/Header.vue' import Board from '../components/Board.vue' import TaskPanel from '../components/TaskPanel' +import ProjectPanel from '../components/ProjectPanel.vue' import DepartmentTags from '../components/DepartmentTags.vue' import ProjectSelector from '../components/ProjectSelector.vue' import MobileSelect from '../components/ui/MobileSelect.vue' @@ -172,6 +203,33 @@ const handleArchiveTask = async (cardId) => { closePanel() } +// ==================== ПАНЕЛЬ ПРОЕКТА ==================== +const projectPanelOpen = ref(false) +const editingProject = ref(null) + +const openCreateProjectPanel = () => { + editingProject.value = null + projectPanelOpen.value = true +} + +const openEditProjectPanel = (project) => { + editingProject.value = project + projectPanelOpen.value = true +} + +const closeProjectPanel = () => { + projectPanelOpen.value = false + editingProject.value = null +} + +const onProjectSaved = async (projectId) => { + // Перезагружаем данные если изменился текущий проект + if (projectId === store.currentProjectId) { + await store.fetchProjectData() + await fetchCards() + } +} + // ==================== АВТООБНОВЛЕНИЕ (POLLING) ==================== const REFRESH_INTERVAL = (window.APP_CONFIG?.IDLE_REFRESH_SECONDS ?? 30) * 1000 let pollTimer = null @@ -231,4 +289,36 @@ onUnmounted(() => { .main.mobile { padding: 0; } + +/* Мобильные кнопки управления проектами */ +.mobile-project-actions { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; +} + +.mobile-project-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: none; + border: none; + border-radius: 6px; + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s; +} + +.mobile-project-btn:active { + color: var(--accent); + background: rgba(255, 255, 255, 0.06); +} + +.mobile-project-btn i { + width: 18px; + height: 18px; +}