# План доработки модуля 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 #}
```
---
## Этап 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 ✨ НОВЫЙ
```