diff --git a/app/Database/Migrations/2026-02-08-110006_CreateTaskChecklistsTable.php b/app/Database/Migrations/2026-02-08-110006_CreateTaskChecklistsTable.php new file mode 100644 index 0000000..ecaafc8 --- /dev/null +++ b/app/Database/Migrations/2026-02-08-110006_CreateTaskChecklistsTable.php @@ -0,0 +1,59 @@ +forge->addField([ + 'id' => [ + 'type' => 'INT', + 'constraint' => 11, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'task_id' => [ + 'type' => 'INT', + 'constraint' => 11, + 'unsigned' => true, + 'null' => false, + ], + 'title' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + 'null' => false, + ], + 'is_completed' => [ + 'type' => 'BOOLEAN', + 'default' => false, + ], + 'order_index' => [ + 'type' => 'INT', + 'constraint' => 11, + 'default' => 0, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + + $this->forge->addKey('id', true); + $this->forge->addKey('task_id'); + $this->forge->addForeignKey('task_id', 'tasks', 'id', '', 'CASCADE'); + + $this->forge->createTable('task_checklists'); + } + + public function down() + { + $this->forge->dropTable('task_checklists'); + } +} \ No newline at end of file diff --git a/app/Modules/Tasks/Config/Routes.php b/app/Modules/Tasks/Config/Routes.php index 55d31c4..6a66c16 100644 --- a/app/Modules/Tasks/Config/Routes.php +++ b/app/Modules/Tasks/Config/Routes.php @@ -32,6 +32,11 @@ $routes->group('tasks', ['filter' => ['org', 'subscription:tasks'], 'namespace' $routes->post('(:num)/subtasks', 'TasksController::addSubtask/$1'); $routes->post('(:num)/subtasks/(:num)/toggle', 'TasksController::toggleSubtask/$1/$2'); $routes->post('(:num)/subtasks/(:num)/delete', 'TasksController::deleteSubtask/$1/$2'); + + // Checklists API + $routes->post('(:num)/checklists', 'TasksController::addChecklistItem/$1'); + $routes->post('(:num)/checklists/(:num)/toggle', 'TasksController::toggleChecklistItem/$1/$2'); + $routes->post('(:num)/checklists/(:num)/delete', 'TasksController::deleteChecklistItem/$1/$2'); }); // API Routes для Tasks diff --git a/app/Modules/Tasks/Controllers/TasksController.php b/app/Modules/Tasks/Controllers/TasksController.php index 39af6bc..af68289 100644 --- a/app/Modules/Tasks/Controllers/TasksController.php +++ b/app/Modules/Tasks/Controllers/TasksController.php @@ -614,4 +614,108 @@ class TasksController extends BaseController 'success' => $result, ]); } + + // ========== Чек-листы (Checklists) ========== + + /** + * API: добавить элемент чек-листа + */ + public function addChecklistItem(int $taskId) + { + if (!$this->access->canEdit('tasks')) { + return $this->response->setJSON([ + 'success' => false, + 'error' => 'Нет прав для изменения задач' + ]); + } + + $organizationId = $this->requireActiveOrg(); + $userId = $this->getCurrentUserId(); + + $title = trim($this->request->getPost('title')); + if (empty($title)) { + return $this->response->setJSON([ + 'success' => false, + 'error' => 'Название элемента чек-листа обязательно' + ]); + } + + // Проверяем что задача принадлежит организации + $task = $this->taskService->getTask($taskId, $organizationId); + if (!$task) { + return $this->response->setJSON([ + 'success' => false, + 'error' => 'Задача не найдена' + ]); + } + + $checklistId = $this->taskService->addChecklistItem($taskId, $title, $userId); + + return $this->response->setJSON([ + 'success' => (bool) $checklistId, + 'checklist_id' => $checklistId, + ]); + } + + /** + * API: переключить статус элемента чек-листа + */ + public function toggleChecklistItem(int $taskId, int $checklistId) + { + if (!$this->access->canEdit('tasks')) { + return $this->response->setJSON([ + 'success' => false, + 'error' => 'Нет прав для изменения задач' + ]); + } + + $organizationId = $this->requireActiveOrg(); + $userId = $this->getCurrentUserId(); + + // Проверяем что задача принадлежит организации + $task = $this->taskService->getTask($taskId, $organizationId); + if (!$task) { + return $this->response->setJSON([ + 'success' => false, + 'error' => 'Задача не найдена' + ]); + } + + $result = $this->taskService->toggleChecklistItem($taskId, $checklistId, $userId); + + return $this->response->setJSON([ + 'success' => $result, + ]); + } + + /** + * API: удалить элемент чек-листа + */ + public function deleteChecklistItem(int $taskId, int $checklistId) + { + if (!$this->access->canEdit('tasks')) { + return $this->response->setJSON([ + 'success' => false, + 'error' => 'Нет прав для изменения задач' + ]); + } + + $organizationId = $this->requireActiveOrg(); + $userId = $this->getCurrentUserId(); + + // Проверяем что задача принадлежит организации + $task = $this->taskService->getTask($taskId, $organizationId); + if (!$task) { + return $this->response->setJSON([ + 'success' => false, + 'error' => 'Задача не найдена' + ]); + } + + $result = $this->taskService->deleteChecklistItem($taskId, $checklistId, $userId); + + return $this->response->setJSON([ + 'success' => $result, + ]); + } } diff --git a/app/Modules/Tasks/Models/TaskChecklistModel.php b/app/Modules/Tasks/Models/TaskChecklistModel.php new file mode 100644 index 0000000..c1b3f4c --- /dev/null +++ b/app/Modules/Tasks/Models/TaskChecklistModel.php @@ -0,0 +1,74 @@ +where('task_id', $taskId) + ->orderBy('order_index', 'ASC') + ->findAll(); + } + + /** + * Получить следующий порядковый номер для элемента чек-листа + */ + public function getNextOrder(int $taskId): int + { + $max = $this->selectMax('order_index') + ->where('task_id', $taskId) + ->first(); + + return ($max['order_index'] ?? 0) + 1; + } + + /** + * Переключить статус элемента чек-листа + */ + public function toggle(int $checklistId): bool + { + $checklist = $this->find($checklistId); + if (!$checklist) { + return false; + } + + return $this->update($checklistId, [ + 'is_completed' => !$checklist['is_completed'], + ]); + } + + /** + * Получить количество незавершённых элементов чек-листа + */ + public function getIncompleteCount(int $taskId): int + { + return $this->where('task_id', $taskId) + ->where('is_completed', false) + ->countAllResults(); + } +} \ No newline at end of file diff --git a/app/Modules/Tasks/Services/TaskService.php b/app/Modules/Tasks/Services/TaskService.php index 6699429..b5f5c2b 100644 --- a/app/Modules/Tasks/Services/TaskService.php +++ b/app/Modules/Tasks/Services/TaskService.php @@ -6,6 +6,7 @@ use App\Modules\Tasks\Models\TaskModel; use App\Modules\Tasks\Models\TaskAssigneeModel; use App\Modules\Tasks\Models\TaskColumnModel; use App\Modules\Tasks\Models\TaskSubtaskModel; +use App\Modules\Tasks\Models\TaskChecklistModel; use CodeIgniter\Events\Events; class TaskService @@ -14,6 +15,7 @@ class TaskService protected TaskAssigneeModel $assigneeModel; protected TaskColumnModel $columnModel; protected TaskSubtaskModel $subtaskModel; + protected TaskChecklistModel $checklistModel; public function __construct() { @@ -21,6 +23,7 @@ class TaskService $this->assigneeModel = new TaskAssigneeModel(); $this->columnModel = new TaskColumnModel(); $this->subtaskModel = new TaskSubtaskModel(); + $this->checklistModel = new TaskChecklistModel(); } /** @@ -180,6 +183,9 @@ class TaskService $task['subtasks'] = $this->subtaskModel->getByTask($taskId); $task['subtasks_count'] = count($task['subtasks']); $task['subtasks_completed'] = count(array_filter($task['subtasks'], fn($s) => $s['is_completed'])); + $task['checklists'] = $this->checklistModel->getByTask($taskId); + $task['checklists_count'] = count($task['checklists']); + $task['checklists_completed'] = count(array_filter($task['checklists'], fn($c) => $c['is_completed'])); return $task; } @@ -349,4 +355,83 @@ class TaskService return $result; } + + // ========== Чек-листы (Checklists) ========== + + /** + * Добавить элемент чек-листа + */ + public function addChecklistItem(int $taskId, string $title, int $userId): int + { + $orderIndex = $this->checklistModel->getNextOrder($taskId); + + $checklistId = $this->checklistModel->insert([ + 'task_id' => $taskId, + 'title' => trim($title), + 'order_index' => $orderIndex, + ]); + + if ($checklistId) { + Events::trigger('task.checklist_item_created', $taskId, $checklistId, $userId); + } + + return $checklistId; + } + + /** + * Переключить статус элемента чек-листа + */ + public function toggleChecklistItem(int $taskId, int $checklistId, int $userId): bool + { + $checklist = $this->checklistModel->find($checklistId); + if (!$checklist || $checklist['task_id'] != $taskId) { + return false; + } + + $result = $this->checklistModel->toggle($checklistId); + + if ($result) { + Events::trigger('task.checklist_item_toggled', $taskId, $checklistId, !$checklist['is_completed'], $userId); + } + + return $result; + } + + /** + * Удалить элемент чек-листа + */ + public function deleteChecklistItem(int $taskId, int $checklistId, int $userId): bool + { + $checklist = $this->checklistModel->find($checklistId); + if (!$checklist || $checklist['task_id'] != $taskId) { + return false; + } + + $result = $this->checklistModel->delete($checklistId); + + if ($result) { + Events::trigger('task.checklist_item_deleted', $taskId, $checklistId, $userId); + } + + return $result; + } + + /** + * Обновить элемент чек-листа + */ + public function updateChecklistItem(int $taskId, int $checklistId, array $data, int $userId): bool + { + $checklist = $this->checklistModel->find($checklistId); + if (!$checklist || $checklist['task_id'] != $taskId) { + return false; + } + + $result = $this->checklistModel->update($checklistId, $data); + + if ($result) { + Events::trigger('task.checklist_item_updated', $taskId, $checklistId, $data, $userId); + } + + return $result; + } } diff --git a/app/Modules/Tasks/Views/tasks/show.twig b/app/Modules/Tasks/Views/tasks/show.twig index d2a16aa..ca9fa08 100644 --- a/app/Modules/Tasks/Views/tasks/show.twig +++ b/app/Modules/Tasks/Views/tasks/show.twig @@ -140,6 +140,51 @@ + {# Чек-лист #} +
Чек-лист пуст
+ {% endif %} + + +