# План доработки модуля Tasks > На основе ТЗ п.3.6 и анализа текущего состояния кода --- ## Этап 1: Базовые улучшения (Critical) ### 1.1 RBAC — добавить проверку прав **Файлы:** `TasksController.php` ```php // Добавить в начало методов: public function store() { if (!$this->access->canCreate('tasks')) { return $this->forbiddenResponse('Нет прав для создания задач'); } } public function edit(int $id) { if (!$this->access->canEdit('tasks')) { return $this->forbiddenResponse('Нет прав для редактирования'); } } ``` **Аналогично:** `update()`, `destroy()`, `moveColumn()`, `complete()`, `reopen()` ### 1.2 Валидация входных данных **Файлы:** `TasksController.php` ```php public function store() { $validation = \Config\Services::validation(); $validation->setRules([ 'title' => 'required|min_length[3]|max_length[255]', 'board_id' => 'required|integer', 'column_id' => 'required|integer', 'priority' => 'in_list[low,medium,high,urgent]', 'due_date' => 'valid_date', ]); if (!$validation->withRequest($this->request)->run()) { return redirect()->back()->withErrors($validation->getErrors())->withInput(); } } ``` ### 1.3 Исправить Events нейминг **Файл:** `TaskService.php` ```php // Было: Events::trigger('tasks.created', $taskId, $data, $userId); // Стало (согласно ТЗ и документации): Events::trigger('task.created', $taskId, $data, $userId); ``` **Аналогично:** `task.updated`, `task.column_changed`, `task.completed`, `task.reopened` --- ## Этап 2: Подзадачи (Subtasks) ### 2.1 Миграция ```php // database/migrations/2026-02-08-CreateTaskSubtasksTable.php public function up() { $this->forge->addField([ 'id' => ['type' => 'INT', 'auto_increment' => true], 'task_id' => ['type' => 'INT'], 'title' => ['type' => 'VARCHAR', 'constraint' => 255], 'is_completed' => ['type' => 'BOOLEAN', 'default' => false], 'order_index' => ['type' => 'INT', 'default' => 0], 'created_at' => ['type' => 'DATETIME'], 'updated_at' => ['type' => 'DATETIME'], ]); $this->forge->addKey('id', true); $this->forge->addForeignKey('task_id', 'tasks', 'id', 'CASCADE', 'CASCADE'); $this->forge->createTable('task_subtasks'); } ``` ### 2.2 Модель ```php // app/Modules/Tasks/Models/TaskSubtaskModel.php class TaskSubtaskModel extends Model { use TenantScopedModel; protected $table = 'task_subtasks'; protected $tenantField = 'task.organization_id'; // через task relationship protected $allowedFields = ['task_id', 'title', 'is_completed', 'order_index']; } ``` ### 2.3 API ```php // TasksController.php public function addSubtask(int $taskId) { $data = [ 'task_id' => $taskId, 'title' => $this->request->getPost('title'), 'order_index' => $this->subtaskModel->getNextOrder($taskId), ]; $this->subtaskModel->insert($data); return redirect()->to("/tasks/{$taskId}"); } public function toggleSubtask(int $taskId, int $subtaskId) { $subtask = $this->subtaskModel->find($subtaskId); $this->subtaskModel->update($subtaskId, [ 'is_completed' => !$subtask['is_completed'] ]); } ``` ### 2.4 View ```twig {# tasks/show.twig #}
{% for subtask in task.subtasks %}
{{ subtask.title }}
{% endfor %}
``` --- ## Этап 3: Чек-листы (Checklists) > Аналогично подзадачам, но чек-листы — это произвольные списки внутри задачи ### 3.1 Структура ``` task_checklists ├── id ├── task_id ├── title (например "Подготовка к встрече") └── items (JSON array или связанная таблица) task_checklist_items ├── id ├── checklist_id ├── text ├── is_completed └── order_index ``` --- ## Этап 4: Вложения (Attachments) ### 4.1 Миграция ```php // database/migrations/2026-02-08-CreateTaskAttachmentsTable.php public function up() { $this->forge->addField([ 'id' => ['type' => 'INT', 'auto_increment' => true], 'task_id' => ['type' => 'INT'], 'file_name' => ['type' => 'VARCHAR', 'constraint' => 255], 'file_path' => ['type' => 'VARCHAR', 'constraint' => 500], 'file_size' => ['type' => 'INT'], 'file_type' => ['type' => 'VARCHAR', 'constraint' => 100], 'uploaded_by' => ['type' => 'INT'], 'created_at' => ['type' => 'DATETIME'], ]); $this->forge->addKey('id', true); $this->forge->addForeignKey('task_id', 'tasks', 'id', 'CASCADE', 'CASCADE'); $this->forge->createTable('task_attachments'); } ``` ### 4.2 Загрузка файлов ```php // TasksController.php public function uploadAttachment(int $taskId) { $file = $this->request->getFile('file'); if ($file->isValid()) { $uploadedPath = $this->uploadService->upload($file, "tasks/{$taskId}"); $this->attachmentModel->insert([ 'task_id' => $taskId, 'file_name' => $file->getName(), 'file_path' => $uploadedPath, 'file_size' => $file->getSize(), 'file_type' => $file->getMimeType(), 'uploaded_by' => $this->getCurrentUserId(), ]); } } ``` --- ## Этап 5: Комментарии ### 5.1 Миграция ```php // database/migrations/2026-02-08-CreateTaskCommentsTable.php public function up() { $this->forge->addField([ 'id' => ['type' => 'INT', 'auto_increment' => true], 'task_id' => ['type' => 'INT'], 'user_id' => ['type' => 'INT'], 'content' => ['type' => 'TEXT'], 'mentioned_users' => ['type' => 'JSON'], // массив user_id для @mentions 'created_at' => ['type' => 'DATETIME'], 'updated_at' => ['type' => 'DATETIME'], ]); $this->forge->addKey('id', true); $this->forge->addForeignKey('task_id', 'tasks', 'id', 'CASCADE', 'CASCADE'); $this->forge->createTable('task_comments'); } ``` ### 5.2 Обработка @mentions ```php // TaskCommentService.php public function parseMentions(string $content): array { preg_match_all('/@(\d+)/', $content, $matches); return $matches[1] ?? []; } public function createComment(int $taskId, int $userId, string $content): int { $mentionedUsers = $this->parseMentions($content); $commentId = $this->commentModel->insert([ 'task_id' => $taskId, 'user_id' => $userId, 'content' => $content, 'mentioned_users' => json_encode($mentionedUsers), ]); // Уведомить упомянутых пользователей foreach ($mentionedUsers as $mentionedUserId) { $this->notificationService->create([ 'user_id' => $mentionedUserId, 'type' => 'mention', 'message' => "Вас упомянули в задаче", 'link' => "/tasks/{$taskId}", ]); } return $commentId; } ``` --- ## Этап 6: Зависимости задач ### 6.1 Миграция ```php // database/migrations/2026-02-08-CreateTaskDependenciesTable.php public function up() { $this->forge->addField([ 'id' => ['type' => 'INT', 'auto_increment' => true], 'blocking_task_id' => ['type' => 'INT'], // задача, которая блокирует 'blocked_task_id' => ['type' => 'INT'], // задача, которая заблокирована 'dependency_type' => ['type' => 'ENUM', 'options' => ['blocks', 'depends_on']], 'created_at' => ['type' => 'DATETIME'], ]); $this->forge->addKey('id', true); $this->forge->addForeignKey('blocking_task_id', 'tasks', 'id', 'CASCADE', 'CASCADE'); $this->forge->addForeignKey('blocked_task_id', 'tasks', 'id', 'CASCADE', 'CASCADE'); $this->forge->createTable('task_dependencies'); } ``` ### 6.2 Проверка при изменении статуса ```php // TaskService.php public function completeTask(int $taskId, int $userId): bool { // Проверить зависимости $blockingTasks = $this->getBlockingTasks($taskId); if (!empty($blockingTasks)) { foreach ($blockingTasks as $blocking) { if (!$blocking['is_completed']) { throw new \Exception("Задача заблокирована: {$blocking['title']}"); } } } return parent::completeTask($taskId, $userId); } ``` --- ## Этап 7: Интеграция с CRM ### 7.1 События CRM → Tasks ```php // app/Config/Events.php // При создании сделки Events::on('deal.created', function ($deal, $userId) { service('eventManager') ->forModule('crm') ->moduleOn('deal.created', function ($deal, $userId) { if (service('moduleSubscription')->isModuleActive('tasks')) { $taskService = service('taskService'); $taskService->createTask([ 'title' => "Первичный контакт: {$deal['title']}", 'description' => 'Сделка создана, необходимо связаться с клиентом', 'priority' => 'high', 'due_date' => date('Y-m-d', strtotime('+1 day')), ], $userId); } }); }); // При переводе на этап "Коммерческое предложение" Events::on('deal.stage_changed', function ($deal, $oldStage, $newStage, $userId) { if ($newStage === 'proposal') { // Создать задачу на подготовку КП } }); // При завершении сделки Events::on('deal.won', function ($deal, $userId) { // Создать задачу "Благодарность клиенту" }); ``` ### 7.2 Задачи в карточке клиента ```php // CRM/Controllers/ClientsController.php public function view(int $id) { $client = $this->clientModel->find($id); $tasks = $this->taskService->getByClientId($id); // метод для получения задач клиента return $this->renderTwig('@CRM/Clients/view', [ 'client' => $client, 'tasks' => $tasks, ]); } ``` --- ## Этап 8: Интеграция с Booking ### 8.1 События Booking → Tasks ```php // При создании записи Events::on('booking.created', function ($booking, $userId) { // Создать задачу на подготовку к встрече }); ``` --- ## Приоритеты реализации | Приоритет | Задача | Оценка | |-----------|--------|--------| | P0 | RBAC (права доступа) | 2ч | | P0 | Валидация | 2ч | | P1 | Подзадачи (subtasks) | 8ч | | P1 | Чек-листы | 6ч | | P1 | Вложения | 8ч | | P2 | Комментарии + @mentions | 12ч | | P2 | Зависимости задач | 6ч | | P2 | Интеграция CRM → Tasks | 8ч | | P3 | Интеграция Booking → Tasks | 4ч | **Общая оценка:** ~56 часов --- ## Зависимости между этапами ``` Этап 1 (RBAC + валидация) → нужен перед всеми Этап 2 (subtasks) → зависит от 1 Этап 3 (checklists) → зависит от 1 Этап 4 (attachments) → зависит от 1 Этап 5 (comments) → зависит от 2 Этап 6 (dependencies) → зависит от 1 Этап 7 (CRM integration) → зависит от 5 Этап 8 (Booking integration) → зависит от 1 ``` --- ## Модели для создания/обновления ``` app/Modules/Tasks/Models/ ├── TaskModel.php (существует) ├── TaskColumnModel.php (существует) ├── TaskBoardModel.php (существует) ├── TaskAssigneeModel.php (существует) ├── TaskSubtaskModel.php ✨ НОВАЯ ├── TaskChecklistModel.php ✨ НОВАЯ ├── TaskChecklistItemModel.php ✨ НОВАЯ ├── TaskAttachmentModel.php ✨ НОВАЯ └── TaskCommentModel.php ✨ НОВАЯ ``` --- ## Контроллеры ``` app/Modules/Tasks/Controllers/ ├── TasksController.php (существует) ├── TaskApiController.php (существует) ├── TaskSubtasksController.php ✨ НОВЫЙ ├── TaskAttachmentsController.php ✨ НОВЫЙ └── TaskCommentsController.php ✨ НОВЫЙ ```