diff --git a/backend/app/class/enity/class_project.php b/backend/app/class/enity/class_project.php index fd166db..a1da35a 100644 --- a/backend/app/class/enity/class_project.php +++ b/backend/app/class/enity/class_project.php @@ -377,6 +377,11 @@ class Project extends BaseEntity { return ['success' => false, 'errors' => ['access' => 'Нет прав на управление отделами']]; } + // Валидация имени + if (!$name || trim($name) === '') { + return ['success' => false, 'errors' => ['name' => 'Укажите название отдела']]; + } + // Получаем максимальный order_id для проекта $maxOrder = (int)(Database::max('departments', 'order_id', ['id_project' => $project_id]) ?? 0); $newOrderId = $maxOrder + 1; @@ -413,6 +418,11 @@ class Project extends BaseEntity { return ['success' => false, 'errors' => ['access' => 'Нет прав на управление отделами']]; } + // Валидация имени (если передано) + if ($name !== null && trim($name) === '') { + return ['success' => false, 'errors' => ['name' => 'Укажите название отдела']]; + } + $updateData = []; if ($name !== null) $updateData['name_departments'] = $name; if ($color !== null) $updateData['color'] = $color; diff --git a/front_vue/src/components/ProjectPanel.vue b/front_vue/src/components/ProjectPanel.vue index 76f4b0a..08048f2 100644 --- a/front_vue/src/components/ProjectPanel.vue +++ b/front_vue/src/components/ProjectPanel.vue @@ -307,6 +307,9 @@ const deleteColumnMessage = ref('') const departmentToDelete = ref(null) const deleteDepartmentMessage = ref('') +// Отделы к удалению (ID существующих отделов для удаления при сохранении) +const departmentsToDelete = ref([]) + // Drag state for columns const dragIndex = ref(null) const dragOverIndex = ref(null) @@ -495,16 +498,20 @@ const addDepartment = () => { const confirmDeleteDepartment = async (index) => { const department = form.value.departments[index] + + // Для новых отделов (ещё не на сервере) — удаляем сразу без диалога + if (department.tempId && !department.id) { + form.value.departments.splice(index, 1) + nextTick(refreshIcons) + return + } + + // Для существующих отделов — показываем диалог с предупреждением departmentToDelete.value = index - // Для существующих отделов проверяем количество задач - if (department.id) { - const count = await store.getDepartmentTasksCount(department.id) - if (count > 0) { - deleteDepartmentMessage.value = `В отделе ${count} задач.
Задачи не удалятся, но потеряют привязку к отделу.` - } else { - deleteDepartmentMessage.value = 'Отдел будет удалён.' - } + const count = await store.getDepartmentTasksCount(department.id) + if (count > 0) { + deleteDepartmentMessage.value = `В отделе ${count} задач.
Задачи не удалятся, но потеряют привязку к отделу.` } else { deleteDepartmentMessage.value = 'Отдел будет удалён.' } @@ -516,15 +523,12 @@ const confirmDeleteDepartmentAction = async () => { const index = departmentToDelete.value const department = form.value.departments[index] - // Если отдел уже существует на сервере — удаляем через API + // Если отдел существует на сервере — добавляем в список для удаления при сохранении if (department.id) { - const result = await store.deleteDepartment(department.id) - if (!result.success) { - throw new Error(result.errors?.department || 'Ошибка удаления') - } + departmentsToDelete.value.push(department.id) } - // Удаляем из формы + // Удаляем из формы (локально) form.value.departments.splice(index, 1) departmentToDelete.value = null @@ -768,15 +772,35 @@ const handleSave = async () => { try { if (isNew.value) { + // Проверяем что у всех отделов есть имена (для нового проекта тоже) + const emptyDeptNew = form.value.departments.find(d => !d.name_departments?.trim()) + if (emptyDeptNew) { + toast.error('Укажите название для всех отделов') + return + } + // Создание проекта const result = await store.createProject(form.value.name) if (result.success) { - // Обновляем колонки если есть изменения - // Дефолтные колонки уже созданы сервером - // Здесь можно добавить логику для кастомизации колонок при создании + const newProjectId = result.id + + // Создаём отделы для нового проекта + for (const department of form.value.departments) { + if (department.tempId) { + const deptResult = await store.addDepartmentToProject( + newProjectId, + department.name_departments, + department.color + ) + if (!deptResult.success) { + toast.error(deptResult.errors?.name || 'Ошибка создания отдела') + } + } + } + toast.success('Проект создан') - emit('saved', result.id) + emit('saved', newProjectId) emit('close') } else { toast.error('Ошибка создания проекта') @@ -816,6 +840,23 @@ const handleSave = async () => { await store.reorderColumns(ids) } + // Удаляем отделы, помеченные на удаление + for (const deptId of departmentsToDelete.value) { + const result = await store.deleteDepartment(deptId) + if (!result.success) { + toast.error(result.errors?.department || result.errors?.access || 'Ошибка удаления отдела') + return + } + } + departmentsToDelete.value = [] + + // Проверяем что у всех отделов есть имена + const emptyDepartment = form.value.departments.find(d => !d.name_departments?.trim()) + if (emptyDepartment) { + toast.error('Укажите название для всех отделов') + return + } + // Обрабатываем отделы for (const department of form.value.departments) { if (department.tempId && !department.id) { @@ -824,6 +865,9 @@ const handleSave = async () => { if (result.success) { department.id = result.id delete department.tempId + } else { + toast.error(result.errors?.name || result.errors?.access || 'Ошибка создания отдела') + return } } else if (department.id) { // Проверяем изменения в существующем отделе @@ -832,7 +876,11 @@ const handleSave = async () => { const nameChanged = department.name_departments !== original.name_departments const colorChanged = department.color !== original.color if (nameChanged || colorChanged) { - await store.updateDepartment(department.id, department.name_departments, department.color) + const result = await store.updateDepartment(department.id, department.name_departments, department.color) + if (!result.success) { + toast.error(result.errors?.name || result.errors?.access || 'Ошибка обновления отдела') + return + } } } } @@ -903,6 +951,7 @@ watch(() => props.show, async (newVal) => { if (newVal) { isSaving.value = false tempIdCounter = 0 + departmentsToDelete.value = [] if (props.project) { // Редактирование — загружаем данные diff --git a/front_vue/src/stores/projects.js b/front_vue/src/stores/projects.js index 9909f68..22ca3a3 100644 --- a/front_vue/src/stores/projects.js +++ b/front_vue/src/stores/projects.js @@ -661,6 +661,13 @@ export const useProjectsStore = defineStore('projects', () => { await projectsApi.updateDepartmentsOrder(currentProjectId.value, ids) } + // Добавление отдела в конкретный проект (для создания нового проекта) + const addDepartmentToProject = async (projectId, name, color = '#6366f1') => { + const result = await projectsApi.addDepartment(projectId, name, color) + // Не добавляем в локальный state — при переключении на проект данные загрузятся + return result + } + return { // Состояние projects, @@ -714,6 +721,7 @@ export const useProjectsStore = defineStore('projects', () => { setReadyColumn, // CRUD отделов addDepartment, + addDepartmentToProject, updateDepartment, getDepartmentTasksCount, deleteDepartment,