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 @@
-
+
+
-
+
+
+
+
+
+
@@ -32,7 +58,7 @@
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;
+}