438 lines
16 KiB
PHP
438 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Modules\Tasks\Controllers;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Modules\Tasks\Services\TaskService;
|
|
use App\Modules\Tasks\Services\TaskBoardService;
|
|
use App\Models\OrganizationUserModel;
|
|
|
|
class TasksController extends BaseController
|
|
{
|
|
protected TaskService $taskService;
|
|
protected TaskBoardService $boardService;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->taskService = new TaskService();
|
|
$this->boardService = new TaskBoardService();
|
|
}
|
|
|
|
/**
|
|
* Главная страница - список задач (использует DataTable)
|
|
*/
|
|
public function index()
|
|
{
|
|
$organizationId = $this->requireActiveOrg();
|
|
|
|
return $this->renderTwig('@Tasks/tasks/index', [
|
|
'title' => 'Задачи',
|
|
'tableHtml' => $this->renderTable($this->getTableConfig()),
|
|
'stats' => $this->taskService->getStats($organizationId),
|
|
'boards' => $this->boardService->getOrganizationBoards($organizationId),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX endpoint для таблицы задач
|
|
*/
|
|
public function table(?array $config = null, ?string $pageUrl = null)
|
|
{
|
|
return parent::table($this->getTableConfig(), '/tasks');
|
|
}
|
|
|
|
/**
|
|
* Конфигурация таблицы задач для DataTable
|
|
*/
|
|
protected function getTableConfig(): array
|
|
{
|
|
$organizationId = $this->getActiveOrgId();
|
|
|
|
return [
|
|
'id' => 'tasks-table',
|
|
'url' => '/tasks/table',
|
|
'model' => $this->taskService->getModel(),
|
|
'columns' => [
|
|
'title' => [
|
|
'label' => 'Задача',
|
|
'width' => '35%',
|
|
],
|
|
'column_name' => [
|
|
'label' => 'Статус',
|
|
'width' => '15%',
|
|
],
|
|
'priority' => [
|
|
'label' => 'Приоритет',
|
|
'width' => '10%',
|
|
],
|
|
'due_date' => [
|
|
'label' => 'Срок',
|
|
'width' => '10%',
|
|
],
|
|
'created_by_name' => [
|
|
'label' => 'Автор',
|
|
'width' => '15%',
|
|
],
|
|
],
|
|
'searchable' => ['title', 'column_name', 'created_by_name'],
|
|
'sortable' => ['title', 'priority', 'due_date', 'created_at', 'column_name'],
|
|
'defaultSort' => 'created_at',
|
|
'order' => 'desc',
|
|
'actions' => ['label' => '', 'width' => '15%'],
|
|
'actionsConfig' => [
|
|
[
|
|
'label' => '',
|
|
'url' => '/tasks/{id}',
|
|
'icon' => 'fa-solid fa-eye',
|
|
'class' => 'btn-outline-primary btn-sm',
|
|
'title' => 'Просмотр',
|
|
],
|
|
[
|
|
'label' => '',
|
|
'url' => '/tasks/{id}/edit',
|
|
'icon' => 'fa-solid fa-pen',
|
|
'class' => 'btn-outline-primary btn-sm',
|
|
'title' => 'Редактировать',
|
|
'type' => 'edit',
|
|
],
|
|
[
|
|
'label' => '',
|
|
'url' => '/tasks/{id}/delete',
|
|
'icon' => 'fa-solid fa-trash',
|
|
'class' => 'btn-outline-danger btn-sm',
|
|
'title' => 'Удалить',
|
|
'type' => 'delete',
|
|
],
|
|
],
|
|
'emptyMessage' => 'Задач пока нет',
|
|
'emptyIcon' => 'fa-solid fa-check-square',
|
|
'emptyActionUrl' => '/tasks/new',
|
|
'emptyActionLabel' => 'Создать задачу',
|
|
'emptyActionIcon' => 'fa-solid fa-plus',
|
|
'can_edit' => true,
|
|
'can_delete' => true,
|
|
'fieldMap' => [
|
|
'column_name' => 'tc.name',
|
|
'created_by_name' => 'u.name',
|
|
],
|
|
'scope' => function($builder) use ($organizationId) {
|
|
$builder->from('tasks')
|
|
->select('tasks.id, tasks.title, tasks.description, tasks.priority, tasks.due_date, tasks.completed_at, tasks.created_at, tc.name as column_name, tc.color as column_color, u.name as created_by_name')
|
|
->join('task_columns tc', 'tasks.column_id = tc.id', 'left')
|
|
->join('users u', 'tasks.created_by = u.id', 'left')
|
|
->where('tasks.organization_id', $organizationId);
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Канбан-доска задач
|
|
*/
|
|
public function kanban()
|
|
{
|
|
$organizationId = $this->requireActiveOrg();
|
|
$boardId = (int) ($this->request->getGet('board') ?? 0);
|
|
|
|
// Получаем доску
|
|
if (!$boardId) {
|
|
$boards = $this->boardService->getOrganizationBoards($organizationId);
|
|
$boardId = $boards[0]['id'] ?? 0;
|
|
}
|
|
|
|
$board = $this->boardService->getBoardWithColumns($boardId, $organizationId);
|
|
|
|
if (!$board) {
|
|
return redirect()->to('/tasks')->with('error', 'Доска не найдена');
|
|
}
|
|
|
|
$kanbanData = $this->taskService->getTasksForKanban($boardId);
|
|
|
|
// Формируем колонки для компонента
|
|
$kanbanColumns = [];
|
|
foreach ($board['columns'] as $column) {
|
|
$columnTasks = $kanbanData[$column['id']]['tasks'] ?? [];
|
|
$kanbanColumns[] = [
|
|
'id' => $column['id'],
|
|
'name' => $column['name'],
|
|
'color' => $column['color'],
|
|
'items' => $columnTasks,
|
|
];
|
|
}
|
|
|
|
return $this->renderTwig('@Tasks/tasks/kanban', [
|
|
'title' => 'Задачи — Канбан',
|
|
'kanbanColumns' => $kanbanColumns,
|
|
'board' => $board,
|
|
'boards' => $this->boardService->getOrganizationBoards($organizationId),
|
|
'stats' => $this->taskService->getStats($organizationId),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Календарь задач
|
|
*/
|
|
public function calendar()
|
|
{
|
|
$organizationId = $this->requireActiveOrg();
|
|
$month = $this->request->getGet('month') ?? date('Y-m');
|
|
|
|
$currentTimestamp = strtotime($month . '-01');
|
|
$daysInMonth = date('t', $currentTimestamp);
|
|
$firstDayOfWeek = date('N', $currentTimestamp) - 1;
|
|
|
|
$tasks = $this->taskService->getTasksForCalendar($organizationId, $month);
|
|
$eventsByDate = [];
|
|
|
|
foreach ($tasks as $task) {
|
|
if ($task['due_date']) {
|
|
$dateKey = date('Y-m-d', strtotime($task['due_date']));
|
|
$eventsByDate[$dateKey][] = [
|
|
'id' => $task['id'],
|
|
'title' => $task['title'],
|
|
'date' => $task['due_date'],
|
|
'column_color' => $task['column_color'] ?? '#6B7280',
|
|
'priority' => $task['priority'],
|
|
'url' => '/tasks/' . $task['id'],
|
|
];
|
|
}
|
|
}
|
|
|
|
return $this->renderTwig('@Tasks/tasks/calendar', [
|
|
'title' => 'Задачи — Календарь',
|
|
'eventsByDate' => $eventsByDate,
|
|
'currentMonth' => $month,
|
|
'monthName' => date('F Y', $currentTimestamp),
|
|
'daysInMonth' => $daysInMonth,
|
|
'firstDayOfWeek' => $firstDayOfWeek,
|
|
'prevMonth' => date('Y-m', strtotime('-1 month', $currentTimestamp)),
|
|
'nextMonth' => date('Y-m', strtotime('+1 month', $currentTimestamp)),
|
|
'today' => date('Y-m-d'),
|
|
'stats' => $this->taskService->getStats($organizationId),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Страница создания задачи
|
|
*/
|
|
public function create()
|
|
{
|
|
$organizationId = $this->requireActiveOrg();
|
|
$boardId = (int) ($this->request->getGet('board') ?? 0);
|
|
|
|
// Получаем доски организации, если нет - создаём дефолтную
|
|
$boards = $this->boardService->getOrganizationBoards($organizationId);
|
|
if (empty($boards)) {
|
|
$boardId = $this->boardService->createBoard([
|
|
'organization_id' => $organizationId,
|
|
'name' => 'Мои задачи',
|
|
'description' => 'Основная доска задач',
|
|
], $this->getCurrentUserId());
|
|
$boards = $this->boardService->getOrganizationBoards($organizationId);
|
|
}
|
|
|
|
if (!$boardId && !empty($boards)) {
|
|
$boardId = $boards[0]['id'];
|
|
}
|
|
|
|
// Получаем пользователей организации
|
|
$orgUserModel = new OrganizationUserModel();
|
|
$orgUsers = $orgUserModel->getOrganizationUsers($organizationId);
|
|
$users = [];
|
|
foreach ($orgUsers as $user) {
|
|
$users[$user['user_id']] = $user['user_name'] ?: $user['user_email'];
|
|
}
|
|
|
|
return $this->renderTwig('@Tasks/tasks/form', [
|
|
'title' => 'Новая задача',
|
|
'actionUrl' => '/tasks',
|
|
'boards' => $boards,
|
|
'selectedBoard' => $boardId,
|
|
'users' => $users,
|
|
'currentUserId' => $this->getCurrentUserId(),
|
|
'priorities' => [
|
|
'low' => 'Низкий',
|
|
'medium' => 'Средний',
|
|
'high' => 'Высокий',
|
|
'urgent' => 'Срочный',
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Сохранить новую задачу
|
|
*/
|
|
public function store()
|
|
{
|
|
$organizationId = $this->requireActiveOrg();
|
|
$userId = $this->getCurrentUserId();
|
|
|
|
$data = [
|
|
'organization_id' => $organizationId,
|
|
'board_id' => $this->request->getPost('board_id'),
|
|
'column_id' => $this->request->getPost('column_id'),
|
|
'title' => $this->request->getPost('title'),
|
|
'description' => $this->request->getPost('description'),
|
|
'priority' => $this->request->getPost('priority') ?? 'medium',
|
|
'due_date' => $this->request->getPost('due_date') ?: null,
|
|
];
|
|
|
|
$taskId = $this->taskService->createTask($data, $userId);
|
|
|
|
if ($taskId) {
|
|
// Добавляем исполнителей
|
|
$assignees = $this->request->getPost('assignees') ?? [];
|
|
if (!empty($assignees)) {
|
|
$this->taskService->updateAssignees($taskId, $assignees);
|
|
}
|
|
|
|
return redirect()->to('/tasks')->with('success', 'Задача успешно создана');
|
|
}
|
|
|
|
return redirect()->back()->with('error', 'Ошибка при создании задачи')->withInput();
|
|
}
|
|
|
|
/**
|
|
* Просмотр задачи
|
|
*/
|
|
public function show(int $id)
|
|
{
|
|
$organizationId = $this->requireActiveOrg();
|
|
$task = $this->taskService->getTask($id, $organizationId);
|
|
|
|
if (!$task) {
|
|
return redirect()->to('/tasks')->with('error', 'Задача не найдена');
|
|
}
|
|
|
|
return $this->renderTwig('@Tasks/tasks/show', [
|
|
'title' => $task['title'],
|
|
'task' => (object) $task,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Страница редактирования задачи
|
|
*/
|
|
public function edit(int $id)
|
|
{
|
|
$organizationId = $this->requireActiveOrg();
|
|
$task = $this->taskService->getTask($id, $organizationId);
|
|
|
|
if (!$task) {
|
|
return redirect()->to('/tasks')->with('error', 'Задача не найдена');
|
|
}
|
|
|
|
// Получаем пользователей организации
|
|
$orgUserModel = new OrganizationUserModel();
|
|
$orgUsers = $orgUserModel->getOrganizationUsers($organizationId);
|
|
$users = [];
|
|
foreach ($orgUsers as $user) {
|
|
$users[$user['user_id']] = $user['user_name'] ?: $user['user_email'];
|
|
}
|
|
|
|
// Получаем доски
|
|
$boards = $this->boardService->getOrganizationBoards($organizationId);
|
|
|
|
return $this->renderTwig('@Tasks/tasks/form', [
|
|
'title' => 'Редактирование задачи',
|
|
'actionUrl' => "/tasks/{$id}",
|
|
'task' => (object) $task,
|
|
'boards' => $boards,
|
|
'users' => $users,
|
|
'currentUserId' => $this->getCurrentUserId(),
|
|
'priorities' => [
|
|
'low' => 'Низкий',
|
|
'medium' => 'Средний',
|
|
'high' => 'Высокий',
|
|
'urgent' => 'Срочный',
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Обновить задачу
|
|
*/
|
|
public function update(int $id)
|
|
{
|
|
$organizationId = $this->requireActiveOrg();
|
|
$userId = $this->getCurrentUserId();
|
|
|
|
$data = [
|
|
'board_id' => $this->request->getPost('board_id'),
|
|
'column_id' => $this->request->getPost('column_id'),
|
|
'title' => $this->request->getPost('title'),
|
|
'description' => $this->request->getPost('description'),
|
|
'priority' => $this->request->getPost('priority') ?? 'medium',
|
|
'due_date' => $this->request->getPost('due_date') ?: null,
|
|
];
|
|
|
|
$result = $this->taskService->updateTask($id, $data, $userId);
|
|
|
|
if ($result) {
|
|
// Обновляем исполнителей
|
|
$assignees = $this->request->getPost('assignees') ?? [];
|
|
$this->taskService->updateAssignees($id, $assignees);
|
|
|
|
return redirect()->to("/tasks/{$id}")->with('success', 'Задача обновлена');
|
|
}
|
|
|
|
return redirect()->back()->with('error', 'Ошибка при обновлении задачи')->withInput();
|
|
}
|
|
|
|
/**
|
|
* Удалить задачу
|
|
*/
|
|
public function destroy(int $id)
|
|
{
|
|
$organizationId = $this->requireActiveOrg();
|
|
$userId = $this->getCurrentUserId();
|
|
|
|
$this->taskService->deleteTask($id, $userId);
|
|
|
|
return redirect()->to('/tasks')->with('success', 'Задача удалена');
|
|
}
|
|
|
|
/**
|
|
* API: перемещение задачи между колонками (drag-and-drop)
|
|
*/
|
|
public function moveColumn()
|
|
{
|
|
$organizationId = $this->requireActiveOrg();
|
|
$userId = $this->getCurrentUserId();
|
|
|
|
$taskId = $this->request->getPost('task_id');
|
|
$newColumnId = $this->request->getPost('column_id');
|
|
|
|
$result = $this->taskService->changeColumn($taskId, $newColumnId, $userId);
|
|
|
|
$csrfToken = csrf_hash();
|
|
$csrfHash = csrf_token();
|
|
|
|
return $this->response
|
|
->setHeader('X-CSRF-TOKEN', $csrfToken)
|
|
->setHeader('X-CSRF-HASH', $csrfHash)
|
|
->setJSON(['success' => $result]);
|
|
}
|
|
|
|
/**
|
|
* API: завершить задачу
|
|
*/
|
|
public function complete(int $id)
|
|
{
|
|
$userId = $this->getCurrentUserId();
|
|
$result = $this->taskService->completeTask($id, $userId);
|
|
|
|
return $this->response->setJSON(['success' => $result]);
|
|
}
|
|
|
|
/**
|
|
* API: возобновить задачу
|
|
*/
|
|
public function reopen(int $id)
|
|
{
|
|
$userId = $this->getCurrentUserId();
|
|
$result = $this->taskService->reopenTask($id, $userId);
|
|
|
|
return $this->response->setJSON(['success' => $result]);
|
|
}
|
|
}
|