bp/TASKS_MODULE_ROADMAP.md

13 KiB
Raw Blame History

План доработки модуля Tasks

На основе ТЗ п.3.6 и анализа текущего состояния кода


Этап 1: Базовые улучшения (Critical)

1.1 RBAC — добавить проверку прав

Файлы: TasksController.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

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

// Было:
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 Миграция

// 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 Модель

// 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

// 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

{# tasks/show.twig #}
<div class="subtasks">
    {% for subtask in task.subtasks %}
    <div class="subtask {{ subtask.is_completed ? 'completed' }}">
        <input type="checkbox" {{ subtask.is_completed ? 'checked' }}
               onchange="toggleSubtask({{ subtask.id }})">
        {{ subtask.title }}
    </div>
    {% endfor %}
    <input type="text" placeholder="Добавить подзадачу...">
</div>

Этап 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 Миграция

// 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 Загрузка файлов

// 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 Миграция

// 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

// 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 Миграция

// 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 Проверка при изменении статуса

// 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

// 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 Задачи в карточке клиента

// 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

// При создании записи
Events::on('booking.created', function ($booking, $userId) {
    // Создать задачу на подготовку к встрече
});

Приоритеты реализации

Приоритет Задача Оценка
P0 RBAC (права доступа)
P0 Валидация
P1 Подзадачи (subtasks)
P1 Чек-листы
P1 Вложения
P2 Комментарии + @mentions 12ч
P2 Зависимости задач
P2 Интеграция CRM → Tasks
P3 Интеграция Booking → Tasks

Общая оценка: ~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 ✨ НОВЫЙ