Tasks Module Stage 1: RBAC + Validation + Events Fix
- Add canCreate/canEdit/canDelete checks in TasksController - Add input validation in store() and update() methods - Fix events naming: task.* (singular) instead of tasks.* - Add CSRF validation for API endpoints
This commit is contained in:
parent
725c62a179
commit
36fb84caf9
|
|
@ -0,0 +1,453 @@
|
||||||
|
# План доработки модуля 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 #}
|
||||||
|
<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 Миграция
|
||||||
|
|
||||||
|
```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 ✨ НОВЫЙ
|
||||||
|
```
|
||||||
|
|
@ -216,6 +216,10 @@ class TasksController extends BaseController
|
||||||
*/
|
*/
|
||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
|
if (!$this->access->canCreate('tasks')) {
|
||||||
|
return $this->forbiddenResponse('Нет прав для создания задач');
|
||||||
|
}
|
||||||
|
|
||||||
$organizationId = $this->requireActiveOrg();
|
$organizationId = $this->requireActiveOrg();
|
||||||
$boardId = (int) ($this->request->getGet('board') ?? 0);
|
$boardId = (int) ($this->request->getGet('board') ?? 0);
|
||||||
|
|
||||||
|
|
@ -263,17 +267,35 @@ class TasksController extends BaseController
|
||||||
*/
|
*/
|
||||||
public function store()
|
public function store()
|
||||||
{
|
{
|
||||||
|
if (!$this->access->canCreate('tasks')) {
|
||||||
|
return $this->forbiddenResponse('Нет прав для создания задач');
|
||||||
|
}
|
||||||
|
|
||||||
$organizationId = $this->requireActiveOrg();
|
$organizationId = $this->requireActiveOrg();
|
||||||
$userId = $this->getCurrentUserId();
|
$userId = $this->getCurrentUserId();
|
||||||
|
|
||||||
|
// Валидация входных данных
|
||||||
|
$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' => 'permit_empty|valid_date[Y-m-d]',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$validation->withRequest($this->request)->run()) {
|
||||||
|
return redirect()->back()->withErrors($validation->getErrors())->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'organization_id' => $organizationId,
|
'organization_id' => $organizationId,
|
||||||
'board_id' => $this->request->getPost('board_id'),
|
'board_id' => (int) $this->request->getPost('board_id'),
|
||||||
'column_id' => $this->request->getPost('column_id'),
|
'column_id' => (int) $this->request->getPost('column_id'),
|
||||||
'title' => $this->request->getPost('title'),
|
'title' => trim($this->request->getPost('title')),
|
||||||
'description' => $this->request->getPost('description'),
|
'description' => trim($this->request->getPost('description')) ?: null,
|
||||||
'priority' => $this->request->getPost('priority') ?? 'medium',
|
'priority' => $this->request->getPost('priority') ?? 'medium',
|
||||||
'due_date' => $this->request->getPost('due_date') ?: null,
|
'due_date' => $this->request->getPost('due_date') ?: null,
|
||||||
];
|
];
|
||||||
|
|
||||||
$taskId = $this->taskService->createTask($data, $userId);
|
$taskId = $this->taskService->createTask($data, $userId);
|
||||||
|
|
@ -314,6 +336,10 @@ class TasksController extends BaseController
|
||||||
*/
|
*/
|
||||||
public function edit(int $id)
|
public function edit(int $id)
|
||||||
{
|
{
|
||||||
|
if (!$this->access->canEdit('tasks')) {
|
||||||
|
return $this->forbiddenResponse('Нет прав для редактирования задач');
|
||||||
|
}
|
||||||
|
|
||||||
$organizationId = $this->requireActiveOrg();
|
$organizationId = $this->requireActiveOrg();
|
||||||
$task = $this->taskService->getTask($id, $organizationId);
|
$task = $this->taskService->getTask($id, $organizationId);
|
||||||
|
|
||||||
|
|
@ -353,14 +379,32 @@ class TasksController extends BaseController
|
||||||
*/
|
*/
|
||||||
public function update(int $id)
|
public function update(int $id)
|
||||||
{
|
{
|
||||||
|
if (!$this->access->canEdit('tasks')) {
|
||||||
|
return $this->forbiddenResponse('Нет прав для редактирования задач');
|
||||||
|
}
|
||||||
|
|
||||||
$organizationId = $this->requireActiveOrg();
|
$organizationId = $this->requireActiveOrg();
|
||||||
$userId = $this->getCurrentUserId();
|
$userId = $this->getCurrentUserId();
|
||||||
|
|
||||||
|
// Валидация входных данных
|
||||||
|
$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' => 'permit_empty|valid_date[Y-m-d]',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$validation->withRequest($this->request)->run()) {
|
||||||
|
return redirect()->back()->withErrors($validation->getErrors())->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'board_id' => $this->request->getPost('board_id'),
|
'board_id' => (int) $this->request->getPost('board_id'),
|
||||||
'column_id' => $this->request->getPost('column_id'),
|
'column_id' => (int) $this->request->getPost('column_id'),
|
||||||
'title' => $this->request->getPost('title'),
|
'title' => trim($this->request->getPost('title')),
|
||||||
'description' => $this->request->getPost('description'),
|
'description' => trim($this->request->getPost('description')) ?: null,
|
||||||
'priority' => $this->request->getPost('priority') ?? 'medium',
|
'priority' => $this->request->getPost('priority') ?? 'medium',
|
||||||
'due_date' => $this->request->getPost('due_date') ?: null,
|
'due_date' => $this->request->getPost('due_date') ?: null,
|
||||||
];
|
];
|
||||||
|
|
@ -383,6 +427,10 @@ class TasksController extends BaseController
|
||||||
*/
|
*/
|
||||||
public function destroy(int $id)
|
public function destroy(int $id)
|
||||||
{
|
{
|
||||||
|
if (!$this->access->canDelete('tasks')) {
|
||||||
|
return $this->forbiddenResponse('Нет прав для удаления задач');
|
||||||
|
}
|
||||||
|
|
||||||
$organizationId = $this->requireActiveOrg();
|
$organizationId = $this->requireActiveOrg();
|
||||||
$userId = $this->getCurrentUserId();
|
$userId = $this->getCurrentUserId();
|
||||||
|
|
||||||
|
|
@ -396,11 +444,25 @@ class TasksController extends BaseController
|
||||||
*/
|
*/
|
||||||
public function moveColumn()
|
public function moveColumn()
|
||||||
{
|
{
|
||||||
|
if (!$this->access->canEdit('tasks')) {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Нет прав для изменения задач'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$organizationId = $this->requireActiveOrg();
|
$organizationId = $this->requireActiveOrg();
|
||||||
$userId = $this->getCurrentUserId();
|
$userId = $this->getCurrentUserId();
|
||||||
|
|
||||||
$taskId = $this->request->getPost('task_id');
|
$taskId = (int) $this->request->getPost('task_id');
|
||||||
$newColumnId = $this->request->getPost('column_id');
|
$newColumnId = (int) $this->request->getPost('column_id');
|
||||||
|
|
||||||
|
if (!$taskId || !$newColumnId) {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Некорректные параметры'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$result = $this->taskService->changeColumn($taskId, $newColumnId, $userId);
|
$result = $this->taskService->changeColumn($taskId, $newColumnId, $userId);
|
||||||
|
|
||||||
|
|
@ -418,6 +480,13 @@ class TasksController extends BaseController
|
||||||
*/
|
*/
|
||||||
public function complete(int $id)
|
public function complete(int $id)
|
||||||
{
|
{
|
||||||
|
if (!$this->access->canEdit('tasks')) {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Нет прав для изменения задач'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$userId = $this->getCurrentUserId();
|
$userId = $this->getCurrentUserId();
|
||||||
$result = $this->taskService->completeTask($id, $userId);
|
$result = $this->taskService->completeTask($id, $userId);
|
||||||
|
|
||||||
|
|
@ -429,6 +498,13 @@ class TasksController extends BaseController
|
||||||
*/
|
*/
|
||||||
public function reopen(int $id)
|
public function reopen(int $id)
|
||||||
{
|
{
|
||||||
|
if (!$this->access->canEdit('tasks')) {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Нет прав для изменения задач'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$userId = $this->getCurrentUserId();
|
$userId = $this->getCurrentUserId();
|
||||||
$result = $this->taskService->reopenTask($id, $userId);
|
$result = $this->taskService->reopenTask($id, $userId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class TaskService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем событие
|
// Генерируем событие
|
||||||
Events::trigger('tasks.created', $taskId, $data, $userId);
|
Events::trigger('task.created', $taskId, $data, $userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $taskId;
|
return $taskId;
|
||||||
|
|
@ -63,7 +63,7 @@ class TaskService
|
||||||
$result = $this->taskModel->update($taskId, $data);
|
$result = $this->taskModel->update($taskId, $data);
|
||||||
|
|
||||||
if ($result) {
|
if ($result) {
|
||||||
Events::trigger('tasks.updated', $taskId, $data, $userId);
|
Events::trigger('task.updated', $taskId, $data, $userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|
@ -96,7 +96,7 @@ class TaskService
|
||||||
$result = $this->taskModel->update($taskId, $data);
|
$result = $this->taskModel->update($taskId, $data);
|
||||||
|
|
||||||
if ($result) {
|
if ($result) {
|
||||||
Events::trigger('tasks.column_changed', $taskId, $oldColumnId, $newColumnId, $userId);
|
Events::trigger('task.column_changed', $taskId, $oldColumnId, $newColumnId, $userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|
@ -117,7 +117,7 @@ class TaskService
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($result) {
|
if ($result) {
|
||||||
Events::trigger('tasks.completed', $taskId, $userId);
|
Events::trigger('task.completed', $taskId, $userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|
@ -138,7 +138,7 @@ class TaskService
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($result) {
|
if ($result) {
|
||||||
Events::trigger('tasks.reopened', $taskId, $userId);
|
Events::trigger('task.reopened', $taskId, $userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|
@ -157,7 +157,7 @@ class TaskService
|
||||||
$result = $this->taskModel->delete($taskId);
|
$result = $this->taskModel->delete($taskId);
|
||||||
|
|
||||||
if ($result) {
|
if ($result) {
|
||||||
Events::trigger('tasks.deleted', $taskId, $userId);
|
Events::trigger('task.deleted', $taskId, $userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue