diff --git a/backend/api/comment.php b/backend/api/comment.php index 5169778..bfc0f3f 100644 --- a/backend/api/comment.php +++ b/backend/api/comment.php @@ -93,6 +93,14 @@ if ($method === 'POST') { $project_id = $taskData['id_project']; ProjectAccess::requireAccess($project_id, $current_user_id); + // Нельзя загружать файлы к комментариям архивных задач + if ((int)$taskData['archive'] === 1) { + RestApi::response([ + 'success' => false, + 'errors' => ['task' => 'Нельзя загружать файлы к комментариям архивных задач'] + ], 400); + } + // Проверяем право на загрузку картинок ProjectAccess::requirePermission($project_id, $current_user_id, 'upload_images'); @@ -119,6 +127,14 @@ if ($method === 'POST') { $project_id = $taskData['id_project']; ProjectAccess::requireAccess($project_id, $current_user_id); + // Нельзя удалять файлы из комментариев архивных задач + if ((int)$taskData['archive'] === 1) { + RestApi::response([ + 'success' => false, + 'errors' => ['task' => 'Нельзя удалять файлы из комментариев архивных задач'] + ], 400); + } + // Проверка прав: автор комментария ИЛИ админ проекта $isAuthor = (int)$commentData['id_accounts'] === (int)$current_user_id; $isAdmin = ProjectAccess::isAdmin($project_id, $current_user_id); diff --git a/backend/api/task.php b/backend/api/task.php index 5a63582..f1ca930 100644 --- a/backend/api/task.php +++ b/backend/api/task.php @@ -77,6 +77,12 @@ if ($method === 'POST') { // Проверяем право на создание задач ProjectAccess::requirePermission($project_id, $user_id, 'create_task'); + + // Проверяем что у проекта есть колонки + $columns = Project::getColumns($project_id); + if (empty($columns)) { + RestApi::response(['success' => false, 'errors' => ['project' => 'У проекта нет колонок. Создайте хотя бы одну колонку.']], 400); + } $task->id_project = $project_id; $task->id_department = $data['id_department'] ?? null; diff --git a/backend/app/class/enity/class_comment.php b/backend/app/class/enity/class_comment.php index ed2c754..9bb5fdd 100644 --- a/backend/app/class/enity/class_comment.php +++ b/backend/app/class/enity/class_comment.php @@ -30,8 +30,12 @@ class Comment extends BaseEntity { return $errors; } - // Проверяем что задача существует - Task::check_task($this->id_task); + // Проверяем что задача существует и не архивная + $task = Task::check_task($this->id_task); + if ((int)$task['archive'] === 1) { + $this->addError('task', 'Нельзя комментировать архивные задачи'); + return $this->getErrors(); + } // Если это ответ — проверяем что родительский комментарий существует if ($this->id_answer) { @@ -75,6 +79,13 @@ class Comment extends BaseEntity { // Проверяем что комментарий существует $comment = self::checkComment($this->id); + // Проверяем что задача не архивная + $task = Database::get('cards_task', ['archive'], ['id' => $comment['id_task']]); + if ($task && (int)$task['archive'] === 1) { + $this->addError('task', 'Нельзя редактировать комментарии архивных задач'); + return $this->getErrors(); + } + // Проверяем что пользователь — автор комментария if ((int)$comment['id_accounts'] !== (int)$this->id_accounts) { $this->addError('access', 'Вы можете редактировать только свои комментарии'); @@ -99,8 +110,16 @@ class Comment extends BaseEntity { // Проверяем что комментарий существует $comment = self::checkComment($id); - // Получаем задачу для проверки админа проекта - $task = Database::get('cards_task', ['id_project'], ['id' => $comment['id_task']]); + // Получаем задачу для проверки админа проекта и архивации + $task = Database::get('cards_task', ['id_project', 'archive'], ['id' => $comment['id_task']]); + + // Нельзя удалять комментарии архивных задач + if ($task && (int)$task['archive'] === 1) { + RestApi::response([ + 'success' => false, + 'errors' => ['task' => 'Нельзя удалять комментарии архивных задач'] + ], 400); + } // Проверяем права: автор комментария ИЛИ админ проекта $isAuthor = (int)$comment['id_accounts'] === (int)$id_accounts; diff --git a/backend/app/class/enity/class_project.php b/backend/app/class/enity/class_project.php index a1da35a..c9d981a 100644 --- a/backend/app/class/enity/class_project.php +++ b/backend/app/class/enity/class_project.php @@ -112,10 +112,10 @@ class Project extends BaseEntity { // ==================== CRUD ПРОЕКТОВ ==================== - // Создание проекта с дефолтными колонками + // Создание проекта БЕЗ колонок (колонки создаются на фронте) public static function create($name, $user_id) { // Получаем максимальный id_order - $maxOrder = Database::max('project', 'id_order') ?? 0; + $maxOrder = (int)(Database::max('project', 'id_order') ?? 0); // Создаём проект с создателем как владельцем (id_admin) Database::insert('project', [ @@ -129,34 +129,9 @@ class Project extends BaseEntity { 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, 'is_admin' => true ]; } @@ -237,7 +212,7 @@ class Project extends BaseEntity { } // Получаем максимальный id_order для проекта - $maxOrder = Database::max('columns', 'id_order', ['id_project' => $project_id]) ?? 0; + $maxOrder = (int)(Database::max('columns', 'id_order', ['id_project' => $project_id]) ?? 0); Database::insert('columns', [ 'name_columns' => $name, diff --git a/backend/app/class/enity/class_task.php b/backend/app/class/enity/class_task.php index d0a173b..d2128f6 100644 --- a/backend/app/class/enity/class_task.php +++ b/backend/app/class/enity/class_task.php @@ -61,6 +61,9 @@ class Task extends BaseEntity { return $errors; } + // Формируем дату создания (одна переменная для БД и ответа) + $date_create = date('Y-m-d H:i:s'); + // Вставляем в базу Database::insert($this->db_name, [ 'id_project' => $this->id_project, @@ -75,7 +78,7 @@ class Task extends BaseEntity { 'descript' => $this->descript ?: null, 'descript_full' => $this->descript_full ?: null, 'archive' => 0, - 'date_create' => date('Y-m-d H:i:s'), + 'date_create' => $date_create, 'file_img' => '[]' ]); @@ -96,6 +99,9 @@ class Task extends BaseEntity { return [ 'success' => true, 'id' => $this->id, + 'date' => $this->date ?: null, + 'date_create' => $date_create, + 'date_closed' => null, 'files' => $uploaded_files ]; } @@ -108,12 +114,18 @@ class Task extends BaseEntity { } // Проверка что задача существует и получаем текущие данные - $task = Database::get($this->db_name, ['id', 'column_id', 'order', 'id_project'], ['id' => $this->id]); + $task = Database::get($this->db_name, ['id', 'column_id', 'order', 'id_project', 'archive'], ['id' => $this->id]); if (!$task) { $this->addError('task', 'Задача не найдена'); return $this->getErrors(); } + // Архивные задачи нельзя редактировать + if ((int)$task['archive'] === 1) { + $this->addError('task', 'Архивные задачи нельзя редактировать'); + return $this->getErrors(); + } + // Получаем текущую колонку $old_column_id = (int)$task['column_id']; @@ -179,14 +191,32 @@ class Task extends BaseEntity { // Загрузка файла к задаче public static function uploadFile($task_id, $file_base64, $file_name) { // Проверка что задача существует - self::check_task($task_id); + $task = self::check_task($task_id); + + // Архивные задачи нельзя редактировать + if ((int)$task['archive'] === 1) { + RestApi::response([ + 'success' => false, + 'errors' => ['task' => 'Нельзя загружать файлы в архивную задачу'] + ], 400); + } + return FileUpload::upload('task', $task_id, $file_base64, $file_name); } // Удаление файлов задачи public static function deleteFile($task_id, $file_names) { // Проверка что задача существует - self::check_task($task_id); + $task = self::check_task($task_id); + + // Архивные задачи нельзя редактировать + if ((int)$task['archive'] === 1) { + RestApi::response([ + 'success' => false, + 'errors' => ['task' => 'Нельзя удалять файлы из архивной задачи'] + ], 400); + } + return FileUpload::delete('task', $task_id, $file_names); } @@ -199,6 +229,14 @@ class Task extends BaseEntity { $new_column_id = (int)$column_id; $archive = (int)$task['archive']; + // Архивные задачи нельзя перемещать + if ($archive === 1) { + RestApi::response([ + 'success' => false, + 'errors' => ['task' => 'Архивные задачи нельзя перемещать'] + ], 400); + } + // Получаем id_ready (колонка "Готово") из проекта $done_column_id = Project::getReadyColumnId($task['id_project']); diff --git a/front_vue/public/config.js b/front_vue/public/config.js index ceefce1..f94eb90 100644 --- a/front_vue/public/config.js +++ b/front_vue/public/config.js @@ -2,11 +2,11 @@ window.APP_CONFIG = { API_BASE: 'http://192.168.1.6', - // Интервалы автообновления данных (в секундах) + // Интервалы автообновления данных (в секундах, 0 = отключено) REFRESH_INTERVALS: { cards: 2, // Карточки на доске comments: 5, // Комментарии к задаче - invites: 10 // Приглашения на странице без проектов + invites: 5 // Приглашения (страница: задачи + страница без проектов) }, // Брейкпоинт для мобильной версии (px) diff --git a/front_vue/src/api.js b/front_vue/src/api.js index b18d9d6..44b98c7 100644 --- a/front_vue/src/api.js +++ b/front_vue/src/api.js @@ -396,11 +396,15 @@ export const serverSettings = { parseDate(dateStr) { if (!dateStr) return null // Добавляем таймзону сервера для корректного парсинга - const normalized = dateStr.replace(' ', 'T') + let normalized = dateStr.replace(' ', 'T') // Если уже есть таймзона — не добавляем if (normalized.includes('+') || normalized.includes('Z')) { return new Date(normalized) } + // Если нет времени (только дата YYYY-MM-DD) — добавляем 00:00:00 + if (normalized.length === 10) { + normalized += 'T00:00:00' + } return new Date(normalized + this.timezoneOffset) } } diff --git a/front_vue/src/components/Board.vue b/front_vue/src/components/Board.vue index d1ce2dc..a058063 100644 --- a/front_vue/src/components/Board.vue +++ b/front_vue/src/components/Board.vue @@ -321,7 +321,7 @@ const saveTask = async (taskData, columnId) => { }) if (result.success) { - // Добавляем локально с ID от сервера + // Добавляем локально с данными от сервера localCards.value.push({ id: parseInt(result.id), id_department: taskData.departmentId, @@ -332,8 +332,9 @@ const saveTask = async (taskData, columnId) => { descript_full: taskData.details, avatar_img: taskData.assignee, column_id: columnId, - date: taskData.dueDate, - date_create: new Date().toISOString().split('T')[0], + date: result.date, + date_create: result.date_create, + date_closed: result.date_closed, order: maxOrder, files: result.files || [] }) diff --git a/front_vue/src/components/ConfirmDialog.vue b/front_vue/src/components/ConfirmDialog.vue index 9ab4076..61c3758 100644 --- a/front_vue/src/components/ConfirmDialog.vue +++ b/front_vue/src/components/ConfirmDialog.vue @@ -84,7 +84,7 @@ const dialogShowDiscard = computed(() => props.showDiscard ?? config.value.showD const dialogVariant = computed(() => props.variant ?? config.value.variant ?? 'default') const dialogDiscardVariant = computed(() => props.discardVariant ?? config.value.discardVariant ?? 'default') -const emit = defineEmits(['confirm', 'cancel', 'discard']) +const emit = defineEmits(['confirm', 'cancel', 'discard', 'update:show']) // Внутреннее состояние загрузки: null | 'confirm' | 'discard' const loading = ref(null) @@ -104,8 +104,9 @@ const handleConfirm = async () => { loading.value = 'confirm' try { await props.action() - // Успех — эмитим confirm для закрытия + // Успех — эмитим confirm и закрываем диалог emit('confirm') + emit('update:show', false) } catch (e) { console.error('ConfirmDialog action failed:', e) // При ошибке — не закрываем диалог @@ -113,14 +114,16 @@ const handleConfirm = async () => { loading.value = null } } else { - // Простой режим — просто эмитим + // Простой режим — просто эмитим и закрываем emit('confirm') + emit('update:show', false) } } const handleCancel = () => { if (loading.value) return emit('cancel') + emit('update:show', false) } const handleDiscard = async () => { @@ -131,8 +134,9 @@ const handleDiscard = async () => { loading.value = 'discard' try { await props.discardAction() - // Успех — эмитим discard для закрытия + // Успех — эмитим discard и закрываем диалог emit('discard') + emit('update:show', false) } catch (e) { console.error('ConfirmDialog discardAction failed:', e) // При ошибке — не закрываем диалог @@ -140,8 +144,9 @@ const handleDiscard = async () => { loading.value = null } } else { - // Простой режим — просто эмитим + // Простой режим — просто эмитим и закрываем emit('discard') + emit('update:show', false) } } diff --git a/front_vue/src/components/ProjectPanel.vue b/front_vue/src/components/ProjectPanel.vue index 08048f2..c4e0d64 100644 --- a/front_vue/src/components/ProjectPanel.vue +++ b/front_vue/src/components/ProjectPanel.vue @@ -785,6 +785,25 @@ const handleSave = async () => { if (result.success) { const newProjectId = result.id + // Создаём колонки для нового проекта + const createdColumnIds = [] + for (const column of form.value.columns) { + if (column.tempId) { + const colResult = await store.addColumn(column.name_columns, column.color) + if (colResult.success) { + createdColumnIds.push(colResult.id) + } else { + toast.error('Ошибка создания колонки') + } + } + } + + // Устанавливаем последнюю колонку как финальную (id_ready) + if (createdColumnIds.length > 0) { + const lastColumnId = createdColumnIds[createdColumnIds.length - 1] + await store.reorderColumns(createdColumnIds) // Это автоматически установит id_ready + } + // Создаём отделы для нового проекта for (const department of form.value.departments) { if (department.tempId) { diff --git a/front_vue/src/components/TaskPanel/CommentItem.vue b/front_vue/src/components/TaskPanel/CommentItem.vue index e2ceb68..be1bd08 100644 --- a/front_vue/src/components/TaskPanel/CommentItem.vue +++ b/front_vue/src/components/TaskPanel/CommentItem.vue @@ -22,6 +22,7 @@