- Система комментариев к задачам с вложенными ответами - Редактирование и удаление комментариев - Прикрепление файлов к задачам и комментариям (картинки, архивы до 10 МБ) - Система прав проекта: админ проекта может удалять чужие комментарии и файлы - Универсальный класс FileUpload для загрузки файлов - Защита загрузки: только автор комментария может добавлять файлы - Каскадное удаление: задача → комментарии → файлы - Автообновление комментариев в реальном времени
379 lines
14 KiB
PHP
379 lines
14 KiB
PHP
<?php
|
||
|
||
class Task extends BaseEntity {
|
||
|
||
protected $db_name = 'cards_task';
|
||
|
||
// Свойства задачи
|
||
public $id;
|
||
public $id_project;
|
||
public $id_department;
|
||
public $id_label;
|
||
public $order;
|
||
public $column_id;
|
||
public $date;
|
||
public $date_closed;
|
||
public $id_account;
|
||
public $title;
|
||
public $descript;
|
||
public $descript_full;
|
||
public $archive;
|
||
|
||
// Валидация данных
|
||
protected function validate() {
|
||
static::$error_message = [];
|
||
|
||
if (!$this->id) {
|
||
$this->addError('id', 'ID задачи не указан');
|
||
}
|
||
if (!$this->title) {
|
||
$this->addError('title', 'Название не может быть пустым');
|
||
}
|
||
|
||
return $this->getErrors();
|
||
}
|
||
|
||
// Валидация данных (для create)
|
||
protected function validateCreate() {
|
||
static::$error_message = [];
|
||
|
||
if (!$this->title) {
|
||
$this->addError('title', 'Название не может быть пустым');
|
||
}
|
||
if (!$this->column_id) {
|
||
$this->addError('column_id', 'Колонка не указана');
|
||
}
|
||
if (!$this->id_department) {
|
||
$this->addError('id_department', 'Департамент не указан');
|
||
}
|
||
if (!$this->id_project) {
|
||
$this->addError('id_project', 'Проект не указан');
|
||
}
|
||
|
||
return $this->getErrors();
|
||
}
|
||
|
||
// Создание задачи (с файлами)
|
||
public function create($files = []) {
|
||
// Валидация
|
||
if ($errors = $this->validateCreate()) {
|
||
return $errors;
|
||
}
|
||
|
||
// Вставляем в базу
|
||
Database::insert($this->db_name, [
|
||
'id_project' => $this->id_project,
|
||
'id_department' => $this->id_department,
|
||
'id_label' => $this->id_label,
|
||
'order' => $this->order ?? 0,
|
||
'column_id' => $this->column_id,
|
||
'date' => $this->date ?: null,
|
||
'id_account' => $this->id_account,
|
||
'title' => $this->title,
|
||
'descript' => $this->descript ?: null,
|
||
'descript_full' => $this->descript_full ?: null,
|
||
'archive' => 0,
|
||
'date_create' => date('Y-m-d H:i:s'),
|
||
'file_img' => '[]'
|
||
]);
|
||
|
||
// Получаем ID созданной задачи
|
||
$this->id = Database::id();
|
||
|
||
// Загружаем файлы если есть
|
||
$uploaded_files = [];
|
||
if (!empty($files)) {
|
||
foreach ($files as $file) {
|
||
$result = FileUpload::upload('task', $this->id, $file['data'], $file['name']);
|
||
if ($result['success']) {
|
||
$uploaded_files[] = $result['file'];
|
||
}
|
||
}
|
||
}
|
||
|
||
return [
|
||
'success' => true,
|
||
'id' => $this->id,
|
||
'files' => $uploaded_files
|
||
];
|
||
}
|
||
|
||
// Обновление задачи
|
||
public function update() {
|
||
// Валидация
|
||
if ($errors = $this->validate()) {
|
||
return $errors;
|
||
}
|
||
|
||
// Проверка что задача существует и получаем текущие данные
|
||
$task = Database::get($this->db_name, ['id', 'column_id', 'order', 'id_project'], ['id' => $this->id]);
|
||
if (!$task) {
|
||
$this->addError('task', 'Задача не найдена');
|
||
return $this->getErrors();
|
||
}
|
||
|
||
// Получаем текущую колонку
|
||
$old_column_id = (int)$task['column_id'];
|
||
|
||
// Если column_id не передан — оставляем текущий
|
||
$new_column_id = $this->column_id !== null ? (int)$this->column_id : $old_column_id;
|
||
|
||
// Получаем id_ready (колонка "Готово") из проекта
|
||
$done_column_id = Project::getReadyColumnId($task['id_project']);
|
||
|
||
// Формируем данные для обновления
|
||
$update_data = [
|
||
'id_department' => $this->id_department,
|
||
'id_label' => $this->id_label,
|
||
'order' => $this->order ?? $task['order'],
|
||
'column_id' => $new_column_id,
|
||
'date' => $this->date ?: null,
|
||
'id_account' => $this->id_account,
|
||
'title' => $this->title,
|
||
'descript' => $this->descript ?: null,
|
||
'descript_full' => $this->descript_full ?: null
|
||
];
|
||
|
||
// Обновляем date_closed при смене колонки
|
||
if ($done_column_id && $new_column_id === $done_column_id && $old_column_id !== $done_column_id) {
|
||
$update_data['date_closed'] = date('Y-m-d H:i:s');
|
||
} elseif ($done_column_id && $old_column_id === $done_column_id && $new_column_id !== $done_column_id) {
|
||
$update_data['date_closed'] = null;
|
||
}
|
||
|
||
// Обновляем в БД
|
||
Database::update($this->db_name, $update_data, [
|
||
'id' => $this->id
|
||
]);
|
||
|
||
return ['success' => true];
|
||
}
|
||
|
||
// Удаление задачи
|
||
public static function delete($id) {
|
||
// Проверка что задача существует
|
||
self::check_task($id);
|
||
|
||
// Удаляем папку с файлами задачи
|
||
FileUpload::deleteFolder('task', $id);
|
||
|
||
// Удаляем все комментарии задачи и их файлы
|
||
$comments = Database::select('comments', ['id'], ['id_task' => $id]);
|
||
if ($comments) {
|
||
foreach ($comments as $comment) {
|
||
FileUpload::deleteFolder('comment', $comment['id']);
|
||
}
|
||
Database::delete('comments', ['id_task' => $id]);
|
||
}
|
||
|
||
// Удаляем задачу из базы
|
||
Database::delete('cards_task', ['id' => $id]);
|
||
|
||
return ['success' => true];
|
||
}
|
||
|
||
// === МЕТОДЫ ДЛЯ РАБОТЫ С ФАЙЛАМИ ===
|
||
|
||
// Загрузка файла к задаче
|
||
public static function uploadFile($task_id, $file_base64, $file_name) {
|
||
// Проверка что задача существует
|
||
self::check_task($task_id);
|
||
return FileUpload::upload('task', $task_id, $file_base64, $file_name);
|
||
}
|
||
|
||
// Удаление файлов задачи
|
||
public static function deleteFile($task_id, $file_names) {
|
||
// Проверка что задача существует
|
||
self::check_task($task_id);
|
||
return FileUpload::delete('task', $task_id, $file_names);
|
||
}
|
||
|
||
// Изменение порядка и колонки задачи (с пересчётом order)
|
||
public static function updateOrder($id, $column_id, $to_index) {
|
||
|
||
// Проверка что задача существует
|
||
$task = self::check_task($id);
|
||
$old_column_id = (int)$task['column_id'];
|
||
$new_column_id = (int)$column_id;
|
||
$archive = (int)$task['archive'];
|
||
|
||
// Получаем id_ready (колонка "Готово") из проекта
|
||
$done_column_id = Project::getReadyColumnId($task['id_project']);
|
||
|
||
// Получаем все карточки целевой колонки с тем же статусом архивации (кроме перемещаемой)
|
||
$cards = Database::select('cards_task', ['id', 'order'], [
|
||
'column_id' => $column_id,
|
||
'archive' => $archive,
|
||
'id[!]' => $id,
|
||
'ORDER' => ['order' => 'ASC']
|
||
]) ?? [];
|
||
|
||
// Вставляем перемещаемую карточку в нужную позицию
|
||
array_splice($cards, $to_index, 0, [['id' => $id]]);
|
||
|
||
// Пересчитываем order для всех карточек
|
||
foreach ($cards as $index => $card) {
|
||
$update_data = [
|
||
'order' => $index,
|
||
'column_id' => $column_id
|
||
];
|
||
|
||
// Только для перемещаемой карточки обновляем date_closed
|
||
if ($card['id'] == $id && $done_column_id) {
|
||
// Перемещаем В колонку "Готово" — устанавливаем дату закрытия
|
||
if ($new_column_id === $done_column_id && $old_column_id !== $done_column_id) {
|
||
$update_data['date_closed'] = date('Y-m-d H:i:s');
|
||
}
|
||
// Перемещаем ИЗ колонки "Готово" — обнуляем дату
|
||
elseif ($old_column_id === $done_column_id && $new_column_id !== $done_column_id) {
|
||
$update_data['date_closed'] = null;
|
||
}
|
||
}
|
||
|
||
Database::update('cards_task', $update_data, [
|
||
'id' => $card['id']
|
||
]);
|
||
}
|
||
|
||
return ['success' => true];
|
||
}
|
||
|
||
// Получение всех задач проекта
|
||
// $id_project: ID проекта (обязательный)
|
||
// $archive: 0 = неархивные, 1 = архивные, null = все
|
||
public function getAll($id_project, $archive = 0) {
|
||
$where = [
|
||
'id_project' => $id_project
|
||
];
|
||
if ($archive !== null) {
|
||
$where['archive'] = $archive ? 1 : 0;
|
||
}
|
||
|
||
$tasks = Database::select($this->db_name, [
|
||
'id',
|
||
'id_project',
|
||
'id_department',
|
||
'id_label',
|
||
'id_account',
|
||
'order',
|
||
'column_id',
|
||
'date',
|
||
'date_create',
|
||
'date_closed',
|
||
'file_img',
|
||
'title',
|
||
'descript',
|
||
'descript_full',
|
||
'archive'
|
||
], $where);
|
||
|
||
// Получаем количество комментариев для всех задач одним запросом
|
||
$task_ids = array_column($tasks, 'id');
|
||
$comments_counts = [];
|
||
if (!empty($task_ids)) {
|
||
$counts = Database::query(
|
||
"SELECT id_task, COUNT(*) as cnt FROM comments WHERE id_task IN (" . implode(',', $task_ids) . ") GROUP BY id_task"
|
||
)->fetchAll(\PDO::FETCH_ASSOC);
|
||
foreach ($counts as $row) {
|
||
$comments_counts[$row['id_task']] = (int)$row['cnt'];
|
||
}
|
||
}
|
||
|
||
// Декодируем JSON и получаем avatar_url из accounts
|
||
return array_map(function($task) use ($comments_counts) {
|
||
$task['file_img'] = $task['file_img'] ? json_decode($task['file_img'], true) : [];
|
||
|
||
// Получаем avatar_url из accounts по id_account
|
||
if ($task['id_account']) {
|
||
$account = Database::get('accounts', ['avatar_url'], ['id' => $task['id_account']]);
|
||
$task['avatar_img'] = $account['avatar_url'] ?? null;
|
||
} else {
|
||
$task['avatar_img'] = null;
|
||
}
|
||
|
||
// Количество комментариев
|
||
$task['comments_count'] = $comments_counts[$task['id']] ?? 0;
|
||
|
||
return $task;
|
||
}, $tasks);
|
||
}
|
||
|
||
// Получение колонок проекта
|
||
public function getColumns($id_project) {
|
||
return Database::select('columns', [
|
||
'id',
|
||
'name_columns',
|
||
'color',
|
||
'id_order'
|
||
], [
|
||
'id_project' => $id_project,
|
||
'ORDER' => ['id_order' => 'ASC']
|
||
]);
|
||
}
|
||
|
||
// Получение отделов проекта
|
||
public function getDepartments($id_project) {
|
||
return Database::select('departments', [
|
||
'id',
|
||
'name_departments',
|
||
'color'
|
||
], [
|
||
'id_project' => $id_project
|
||
]);
|
||
}
|
||
|
||
// Получение всех меток
|
||
public function getLabels() {
|
||
return Database::select('labels', [
|
||
'id',
|
||
'name_labels',
|
||
'icon',
|
||
'color'
|
||
]);
|
||
}
|
||
|
||
// Проверка и получение задачи (при ошибке — сразу ответ и exit)
|
||
public static function check_task($task_id) {
|
||
$task = Database::get('cards_task', '*', ['id' => $task_id]);
|
||
if (!$task_id || !$task) {
|
||
RestApi::response(['success' => false, 'errors' => ['task' => 'Задача не найдена']], 400);
|
||
}
|
||
return $task;
|
||
}
|
||
|
||
// Установка статуса архивации задачи (только для задач в колонке "Готово")
|
||
public static function setArchive($id, $archive = 1) {
|
||
// Проверка что задача существует
|
||
$task = self::check_task($id);
|
||
|
||
// Получаем id_ready (колонка "Готово") из проекта
|
||
$done_column_id = Project::getReadyColumnId($task['id_project']);
|
||
|
||
// Архивировать можно только задачи в колонке "Готово"
|
||
if ($archive && $done_column_id && (int)$task['column_id'] !== $done_column_id) {
|
||
RestApi::response([
|
||
'success' => false,
|
||
'errors' => ['column' => 'Архивировать можно только задачи из колонки "Готово"']
|
||
], 400);
|
||
}
|
||
|
||
// Данные для обновления
|
||
$update_data = [
|
||
'archive' => $archive ? 1 : 0
|
||
];
|
||
|
||
// При разархивировании — возвращаем в колонку "Готово"
|
||
if (!$archive && $done_column_id) {
|
||
$update_data['column_id'] = $done_column_id;
|
||
}
|
||
|
||
// Обновляем в БД
|
||
Database::update('cards_task', $update_data, [
|
||
'id' => $id
|
||
]);
|
||
|
||
return ['success' => true, 'archive' => $archive ? 1 : 0];
|
||
}
|
||
}
|
||
|
||
?>
|