bp/app/Modules/Tasks/Services/TaskService.php

438 lines
12 KiB
PHP

<?php
namespace App\Modules\Tasks\Services;
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
{
protected TaskModel $taskModel;
protected TaskAssigneeModel $assigneeModel;
protected TaskColumnModel $columnModel;
protected TaskSubtaskModel $subtaskModel;
protected TaskChecklistModel $checklistModel;
public function __construct()
{
$this->taskModel = new TaskModel();
$this->assigneeModel = new TaskAssigneeModel();
$this->columnModel = new TaskColumnModel();
$this->subtaskModel = new TaskSubtaskModel();
$this->checklistModel = new TaskChecklistModel();
}
/**
* Получить модель задач для использования в DataTable
*/
public function getModel(): TaskModel
{
return $this->taskModel;
}
/**
* Создать новую задачу
*/
public function createTask(array $data, int $userId, array $assigneeIds = []): int
{
$data['created_by'] = $userId;
$taskId = $this->taskModel->insert($data);
if ($taskId) {
// Добавляем исполнителей
foreach ($assigneeIds as $userId) {
$this->assigneeModel->addAssignee($taskId, (int)$userId);
}
// Генерируем событие
Events::trigger('task.created', $taskId, $data, $userId);
}
return $taskId;
}
/**
* Обновить задачу
*/
public function updateTask(int $taskId, array $data, int $userId): bool
{
$oldTask = $this->taskModel->find($taskId);
if (!$oldTask) {
return false;
}
$result = $this->taskModel->update($taskId, $data);
if ($result) {
Events::trigger('task.updated', $taskId, $data, $userId);
}
return $result;
}
/**
* Изменить колонку задачи (drag-and-drop)
*/
public function changeColumn(int $taskId, int $newColumnId, int $userId): bool
{
$task = $this->taskModel->find($taskId);
if (!$task) {
return false;
}
$newColumn = $this->columnModel->find($newColumnId);
if (!$newColumn) {
return false;
}
$oldColumnId = $task['column_id'];
$data = ['column_id' => $newColumnId];
// Если задача завершена (новая колонка не "Завершено"), очищаем completed_at
if ($newColumn['name'] !== 'Завершено') {
$data['completed_at'] = null;
}
$result = $this->taskModel->update($taskId, $data);
if ($result) {
Events::trigger('task.column_changed', $taskId, $oldColumnId, $newColumnId, $userId);
}
return $result;
}
/**
* Завершить задачу
*/
public function completeTask(int $taskId, int $userId): bool
{
$task = $this->taskModel->find($taskId);
if (!$task) {
return false;
}
$result = $this->taskModel->update($taskId, [
'completed_at' => date('Y-m-d H:i:s'),
]);
if ($result) {
Events::trigger('task.completed', $taskId, $userId);
}
return $result;
}
/**
* Возобновить задачу
*/
public function reopenTask(int $taskId, int $userId): bool
{
$task = $this->taskModel->find($taskId);
if (!$task) {
return false;
}
$result = $this->taskModel->update($taskId, [
'completed_at' => null,
]);
if ($result) {
Events::trigger('task.reopened', $taskId, $userId);
}
return $result;
}
/**
* Удалить задачу
*/
public function deleteTask(int $taskId, int $userId): bool
{
$task = $this->taskModel->find($taskId);
if (!$task) {
return false;
}
$result = $this->taskModel->delete($taskId);
if ($result) {
Events::trigger('task.deleted', $taskId, $userId);
}
return $result;
}
/**
* Получить задачу по ID
*/
public function getTask(int $taskId, int $organizationId): ?array
{
$task = $this->taskModel->getTask($taskId, $organizationId);
if (!$task) {
return null;
}
$task['assignees'] = $this->assigneeModel->getAssigneesByTask($taskId);
$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;
}
/**
* Получить задачи для Канбана
*/
public function getTasksForKanban(int $boardId): array
{
return $this->taskModel->getTasksGroupedByColumn($boardId);
}
/**
* Получить задачи для календаря
*/
public function getTasksForCalendar(int $organizationId, string $month): array
{
return $this->taskModel->getTasksForCalendar($organizationId, $month);
}
/**
* Получить статистику
*/
public function getStats(int $organizationId): array
{
return $this->taskModel->getTaskStats($organizationId);
}
/**
* Обновить исполнителей задачи
*/
public function updateAssignees(int $taskId, array $userIds): bool
{
// Удаляем старых исполнителей
$this->assigneeModel->where('task_id', $taskId)->delete();
// Добавляем новых
foreach ($userIds as $userId) {
$this->assigneeModel->addAssignee($taskId, (int)$userId);
}
return true;
}
/**
* Создать задачу из события (например, из сделки CRM)
*/
public function createFromEvent(string $eventType, array $eventData, int $organizationId): ?int
{
$taskData = [
'organization_id' => $organizationId,
'board_id' => $this->getDefaultBoardId($organizationId),
'column_id' => $this->getFirstColumnId($organizationId),
'title' => $eventData['title'] ?? 'Задача',
'description' => $eventData['description'] ?? '',
'priority' => $eventData['priority'] ?? 'medium',
'due_date' => $eventData['due_date'] ?? null,
];
$assignees = $eventData['assignees'] ?? [];
return $this->createTask($taskData, $eventData['created_by'] ?? 1, $assignees);
}
/**
* Получить ID дефолтной доски
*/
protected function getDefaultBoardId(int $organizationId): int
{
$boardModel = new TaskBoardModel();
$board = $boardModel->getDefaultBoard($organizationId);
if (!$board) {
// Создаём дефолтную доску
return $boardModel->createDefaultBoard($organizationId);
}
return $board['id'];
}
/**
* Получить ID первой колонки
*/
protected function getFirstColumnId(int $organizationId): int
{
$boardId = $this->getDefaultBoardId($organizationId);
$columns = $this->columnModel->getColumnsByBoard($boardId);
return $columns[0]['id'] ?? 1;
}
// ========== Подзадачи (Subtasks) ==========
/**
* Добавить подзадачу
*/
public function addSubtask(int $taskId, string $title, int $userId): int
{
$orderIndex = $this->subtaskModel->getNextOrder($taskId);
$subtaskId = $this->subtaskModel->insert([
'task_id' => $taskId,
'title' => trim($title),
'order_index' => $orderIndex,
]);
if ($subtaskId) {
Events::trigger('task.subtask_created', $taskId, $subtaskId, $userId);
}
return $subtaskId;
}
/**
* Переключить статус подзадачи
*/
public function toggleSubtask(int $taskId, int $subtaskId, int $userId): bool
{
$subtask = $this->subtaskModel->find($subtaskId);
if (!$subtask || $subtask['task_id'] != $taskId) {
return false;
}
$result = $this->subtaskModel->toggle($subtaskId);
if ($result) {
Events::trigger('task.subtask_toggled', $taskId, $subtaskId, !$subtask['is_completed'], $userId);
}
return $result;
}
/**
* Удалить подзадачу
*/
public function deleteSubtask(int $taskId, int $subtaskId, int $userId): bool
{
$subtask = $this->subtaskModel->find($subtaskId);
if (!$subtask || $subtask['task_id'] != $taskId) {
return false;
}
$result = $this->subtaskModel->delete($subtaskId);
if ($result) {
Events::trigger('task.subtask_deleted', $taskId, $subtaskId, $userId);
}
return $result;
}
/**
* Обновить подзадачу
*/
public function updateSubtask(int $taskId, int $subtaskId, array $data, int $userId): bool
{
$subtask = $this->subtaskModel->find($subtaskId);
if (!$subtask || $subtask['task_id'] != $taskId) {
return false;
}
$result = $this->subtaskModel->update($subtaskId, $data);
if ($result) {
Events::trigger('task.subtask_updated', $taskId, $subtaskId, $data, $userId);
}
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;
}
}