diff --git a/app/Database/Migrations/2026-02-08-120006_CreateTaskAttachmentsTable.php b/app/Database/Migrations/2026-02-08-120000_CreateAttachmentsTable.php similarity index 75% rename from app/Database/Migrations/2026-02-08-120006_CreateTaskAttachmentsTable.php rename to app/Database/Migrations/2026-02-08-120000_CreateAttachmentsTable.php index 71b560d..b452523 100644 --- a/app/Database/Migrations/2026-02-08-120006_CreateTaskAttachmentsTable.php +++ b/app/Database/Migrations/2026-02-08-120000_CreateAttachmentsTable.php @@ -4,7 +4,7 @@ namespace App\Database\Migrations; use CodeIgniter\Database\Migration; -class CreateTaskAttachmentsTable extends Migration +class CreateAttachmentsTable extends Migration { public function up() { @@ -15,10 +15,15 @@ class CreateTaskAttachmentsTable extends Migration 'unsigned' => true, 'auto_increment' => true, ], - 'task_id' => [ + 'entity_type' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => false, + 'comment' => 'Тип сущности: task, deal, contact, etc.', + ], + 'entity_id' => [ 'type' => 'INT', 'constraint' => 11, - 'unsigned' => true, 'null' => false, ], 'file_name' => [ @@ -57,14 +62,14 @@ class CreateTaskAttachmentsTable extends Migration ]); $this->forge->addKey('id', true); - $this->forge->addKey('task_id'); - $this->forge->addForeignKey('task_id', 'tasks', 'id', '', 'CASCADE'); + $this->forge->addKey(['entity_type', 'entity_id']); + $this->forge->addKey('uploaded_by'); - $this->forge->createTable('task_attachments'); + $this->forge->createTable('attachments'); } public function down() { - $this->forge->dropTable('task_attachments'); + $this->forge->dropTable('attachments'); } } \ No newline at end of file diff --git a/app/Libraries/Twig/TwigGlobalsExtension.php b/app/Libraries/Twig/TwigGlobalsExtension.php index fdc77f2..ffcad55 100644 --- a/app/Libraries/Twig/TwigGlobalsExtension.php +++ b/app/Libraries/Twig/TwigGlobalsExtension.php @@ -52,6 +52,10 @@ class TwigGlobalsExtension extends AbstractExtension new TwigFunction('is_module_active', [$this, 'isModuleActive'], ['is_safe' => ['html']]), new TwigFunction('is_module_available', [$this, 'isModuleAvailable'], ['is_safe' => ['html']]), new TwigFunction('csrf_meta', [$this, 'csrf_meta'], ['is_safe' => ['html']]), + + // Attachment functions + new TwigFunction('format_filesize', [$this, 'formatFilesize'], ['is_safe' => ['html']]), + new TwigFunction('get_file_icon', [$this, 'getFileIcon'], ['is_safe' => ['html']]), ]; } @@ -59,6 +63,42 @@ class TwigGlobalsExtension extends AbstractExtension { return csrf_meta(); } + + public function formatFilesize(int $bytes): string + { + if ($bytes >= 1073741824) { + return number_format($bytes / 1073741824, 2) . ' GB'; + } elseif ($bytes >= 1048576) { + return number_format($bytes / 1048576, 2) . ' MB'; + } elseif ($bytes >= 1024) { + return number_format($bytes / 1024, 2) . ' KB'; + } + + return $bytes . ' B'; + } + + public function getFileIcon(string $fileType): string + { + $icons = [ + 'image' => 'fa-file-image', + 'pdf' => 'fa-file-pdf', + 'word' => 'fa-file-word', + 'excel' => 'fa-file-excel', + 'powerpoint' => 'fa-file-powerpoint', + 'zip' => 'fa-file-zipper', + 'text' => 'fa-file-lines', + 'video' => 'fa-file-video', + 'audio' => 'fa-file-audio', + ]; + + foreach ($icons as $key => $icon) { + if (stripos($fileType, $key) !== false) { + return $icon; + } + } + + return 'fa-file'; + } // ======================================== // Access Functions для Twig // ======================================== diff --git a/app/Modules/Tasks/Models/TaskAttachmentModel.php b/app/Models/AttachmentModel.php similarity index 60% rename from app/Modules/Tasks/Models/TaskAttachmentModel.php rename to app/Models/AttachmentModel.php index e7edf90..9ad2ab9 100644 --- a/app/Modules/Tasks/Models/TaskAttachmentModel.php +++ b/app/Models/AttachmentModel.php @@ -1,22 +1,19 @@ where('task_id', $taskId) + return $this->where('entity_type', $entityType) + ->where('entity_id', $entityId) ->orderBy('created_at', 'DESC') ->findAll(); } @@ -45,10 +43,19 @@ class TaskAttachmentModel extends Model return $this->find($attachmentId); } + /** + * Проверить принадлежность вложения сущности + */ + public function belongsToEntity(string $entityType, int $entityId, int $attachmentId): bool + { + $attachment = $this->find($attachmentId); + return $attachment && $attachment['entity_type'] === $entityType && $attachment['entity_id'] == $entityId; + } + /** * Удалить вложение */ - public function deleteAttachment(int $attachmentId, int $userId): bool + public function deleteAttachment(int $attachmentId): bool { $attachment = $this->find($attachmentId); if (!$attachment) { @@ -64,6 +71,32 @@ class TaskAttachmentModel extends Model return $this->delete($attachmentId); } + /** + * Удалить все вложения сущности + */ + public function deleteByEntity(string $entityType, int $entityId): bool + { + $attachments = $this->where('entity_type', $entityType) + ->where('entity_id', $entityId) + ->findAll(); + + foreach ($attachments as $attachment) { + $this->deleteAttachment($attachment['id']); + } + + return true; + } + + /** + * Получить количество вложений сущности + */ + public function countByEntity(string $entityType, int $entityId): int + { + return $this->where('entity_type', $entityType) + ->where('entity_id', $entityId) + ->countAllResults(); + } + /** * Получить размер файла в человекочитаемом формате */ diff --git a/app/Modules/Tasks/Controllers/TasksController.php b/app/Modules/Tasks/Controllers/TasksController.php index 288f5bd..46aa136 100644 --- a/app/Modules/Tasks/Controllers/TasksController.php +++ b/app/Modules/Tasks/Controllers/TasksController.php @@ -706,10 +706,10 @@ class TasksController extends BaseController throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Задача не найдена'); } - $attachmentModel = new \App\Modules\Tasks\Models\TaskAttachmentModel(); - $attachment = $attachmentModel->find($attachmentId); + $attachmentModel = new \App\Models\AttachmentModel(); + $attachment = $attachmentModel->getById($attachmentId); - if (!$attachment || $attachment['task_id'] != $taskId) { + if (!$attachment || $attachment['entity_type'] !== 'task' || $attachment['entity_id'] != $taskId) { throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Вложение не найдено'); } diff --git a/app/Modules/Tasks/Services/TaskService.php b/app/Modules/Tasks/Services/TaskService.php index 48b140a..483b197 100644 --- a/app/Modules/Tasks/Services/TaskService.php +++ b/app/Modules/Tasks/Services/TaskService.php @@ -6,7 +6,7 @@ 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\TaskAttachmentModel; +use App\Services\AttachmentService; use CodeIgniter\Events\Events; class TaskService @@ -15,7 +15,7 @@ class TaskService protected TaskAssigneeModel $assigneeModel; protected TaskColumnModel $columnModel; protected TaskSubtaskModel $subtaskModel; - protected TaskAttachmentModel $attachmentModel; + protected AttachmentService $attachmentService; public function __construct() { @@ -23,7 +23,7 @@ class TaskService $this->assigneeModel = new TaskAssigneeModel(); $this->columnModel = new TaskColumnModel(); $this->subtaskModel = new TaskSubtaskModel(); - $this->attachmentModel = new TaskAttachmentModel(); + $this->attachmentService = new AttachmentService(); } /** @@ -183,8 +183,8 @@ class TaskService $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['attachments'] = $this->attachmentModel->getByTask($taskId); - $task['attachments_count'] = count($task['attachments']); + $task['attachments'] = $this->attachmentService->getByEntity('task', $taskId); + $task['attachments_count'] = $this->attachmentService->count('task', $taskId); return $task; } @@ -355,45 +355,14 @@ class TaskService return $result; } - // ========== Вложения (Attachments) ========== + // ========== Вложения (Attachments) - используем общий AttachmentService ========== /** * Загрузить вложение */ public function uploadAttachment(int $taskId, array $file, int $userId): ?int { - $uploadPath = WRITEPATH . 'uploads/tasks/' . $taskId . '/'; - - // Создаём директорию если не существует - if (!is_dir($uploadPath)) { - mkdir($uploadPath, 0755, true); - } - - // Генерируем уникальное имя файла - $originalName = $file['name']; - $extension = pathinfo($originalName, PATHINFO_EXTENSION); - $newFileName = uniqid() . '_' . time() . '.' . $extension; - $filePath = $uploadPath . $newFileName; - - // Перемещаем загруженный файл - if (!move_uploaded_file($file['tmp_name'], $filePath)) { - return null; - } - - $attachmentId = $this->attachmentModel->insert([ - 'task_id' => $taskId, - 'file_name' => $originalName, - 'file_path' => $filePath, - 'file_size' => $file['size'], - 'file_type' => $file['type'] ?? '', - 'uploaded_by' => $userId, - ]); - - if ($attachmentId) { - Events::trigger('task.attachment_uploaded', $taskId, $attachmentId, $userId); - } - - return $attachmentId; + return $this->attachmentService->upload('task', $taskId, $file, $userId); } /** @@ -401,17 +370,6 @@ class TaskService */ public function deleteAttachment(int $taskId, int $attachmentId, int $userId): bool { - $attachment = $this->attachmentModel->find($attachmentId); - if (!$attachment || $attachment['task_id'] != $taskId) { - return false; - } - - $result = $this->attachmentModel->deleteAttachment($attachmentId, $userId); - - if ($result) { - Events::trigger('task.attachment_deleted', $taskId, $attachmentId, $userId); - } - - return $result; + return $this->attachmentService->delete('task', $taskId, $attachmentId, $userId); } } diff --git a/app/Modules/Tasks/Views/tasks/show.twig b/app/Modules/Tasks/Views/tasks/show.twig index fbd9440..29e10e7 100644 --- a/app/Modules/Tasks/Views/tasks/show.twig +++ b/app/Modules/Tasks/Views/tasks/show.twig @@ -155,13 +155,13 @@