diff --git a/backend/api/task.php b/backend/api/task.php new file mode 100644 index 0000000..fc2f8b1 --- /dev/null +++ b/backend/api/task.php @@ -0,0 +1,112 @@ +getColumns(); + RestApi::response(['success' => true, 'data' => $result]); + } + + // Получение департаментов + if ($action === 'get_departments') { + $result = $task->getDepartments(); + RestApi::response(['success' => true, 'data' => $result]); + } + + // Получение меток + if ($action === 'get_labels') { + $result = $task->getLabels(); + RestApi::response(['success' => true, 'data' => $result]); + } + + // Загрузка изображения + if ($action === 'upload_image') { + $task_id = $data['task_id'] ?? null; + $file_base64 = $data['file_data'] ?? null; + $file_name = $data['file_name'] ?? null; + + $result = TaskImage::upload($task_id, $file_base64, $file_name); + RestApi::response($result); + } + + // Удаление изображений (принимает file_names массив или file_name строку) + if ($action === 'delete_image') { + $task_id = $data['task_id'] ?? null; + $file_names = $data['file_names'] ?? $data['file_name'] ?? null; + + $result = TaskImage::delete($task_id, $file_names); + RestApi::response($result); + } + + // Изменение порядка и колонки задачи + if ($action === 'update_order') { + $id = $data['id'] ?? null; + $column_id = $data['column_id'] ?? null; + $to_index = $data['to_index'] ?? 0; + + $result = Task::updateOrder($id, $column_id, $to_index); + RestApi::response($result); + } + + // Обновление задачи + if ($action === 'update') { + $task->id = $data['id'] ?? null; + $task->id_department = $data['id_department'] ?? null; + $task->id_label = $data['id_label'] ?? null; + $task->id_account = $data['id_account'] ?? null; + $task->column_id = $data['column_id'] ?? null; + $task->order = $data['order'] ?? null; + $task->date = $data['date'] ?? null; + $task->title = $data['title'] ?? ''; + $task->descript = $data['descript'] ?? ''; + $task->descript_full = $data['descript_full'] ?? ''; + + $result = $task->update(); + RestApi::response($result); + } + + // Создание задачи + if ($action === 'create') { + $task->id_department = $data['id_department'] ?? null; + $task->id_label = $data['id_label'] ?? null; + $task->id_account = $data['id_account'] ?? null; + $task->column_id = $data['column_id'] ?? null; + $task->order = $data['order'] ?? 0; + $task->date = $data['date'] ?? null; + $task->title = $data['title'] ?? ''; + $task->descript = $data['descript'] ?? ''; + $task->descript_full = $data['descript_full'] ?? ''; + $files = $data['files'] ?? []; + + $result = $task->create($files); + RestApi::response($result); + } + + // Удаление задачи + if ($action === 'delete') { + $id = $data['id'] ?? null; + $result = Task::delete($id); + RestApi::response($result); + } + + // Метод не указан + if (!$action) { + RestApi::response(['success' => false, 'error' => 'Укажите метод'], 400); + } +} + +if ($method === 'GET') { + // Получение всех задач + $task = new Task(); + $tasks = $task->getAll(); + + RestApi::response(['success' => true, 'data' => $tasks]); +} + +?> diff --git a/backend/app/class/enity/class_task.php b/backend/app/class/enity/class_task.php new file mode 100644 index 0000000..760b774 --- /dev/null +++ b/backend/app/class/enity/class_task.php @@ -0,0 +1,248 @@ +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', 'Департамент не указан'); + } + + return $this->getErrors(); + } + + // Создание задачи (с файлами) + public function create($files = []) { + // Валидация + if ($errors = $this->validateCreate()) { + return $errors; + } + + // Вставляем в базу + Database::insert($this->db_name, [ + '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, + '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 = TaskImage::upload($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'], ['id' => $this->id]); + if (!$task) { + $this->addError('task', 'Задача не найдена'); + return $this->getErrors(); + } + + // Обновляем в БД + Database::update($this->db_name, [ + 'id_department' => $this->id_department, + 'id_label' => $this->id_label, + 'order' => $this->order, + '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 + ], [ + 'id' => $this->id + ]); + + return ['success' => true]; + } + + // Удаление задачи + public static function delete($id) { + // Проверка что задача существует + self::check_task($id); + + // Удаляем папку с файлами если есть + $upload_dir = __DIR__ . '/../../../public/img/task/' . $id; + if (is_dir($upload_dir)) { + $files = glob($upload_dir . '/*'); + foreach ($files as $file) { + if (is_file($file)) { + unlink($file); + } + } + rmdir($upload_dir); + } + + // Удаляем из базы + Database::delete('cards_task', ['id' => $id]); + + return ['success' => true]; + } + + // Изменение порядка и колонки задачи (с пересчётом order) + public static function updateOrder($id, $column_id, $to_index) { + + // Проверка что задача существует + self::check_task($id); + + // Получаем все карточки целевой колонки (кроме перемещаемой) + $cards = Database::select('cards_task', ['id', 'order'], [ + 'column_id' => $column_id, + 'id[!]' => $id, + 'ORDER' => ['order' => 'ASC'] + ]) ?? []; + + // Вставляем перемещаемую карточку в нужную позицию + array_splice($cards, $to_index, 0, [['id' => $id]]); + + // Пересчитываем order для всех карточек + foreach ($cards as $index => $card) { + Database::update('cards_task', [ + 'order' => $index, + 'column_id' => $column_id + ], [ + 'id' => $card['id'] + ]); + } + + return ['success' => true]; + } + + // Получение всех задач + public function getAll() { + $tasks = Database::select($this->db_name, [ + 'id', + 'id_department', + 'id_label', + 'id_account', + 'order', + 'column_id', + 'date', + 'date_create', + 'file_img', + 'title', + 'descript', + 'descript_full' + ]); + + // Декодируем JSON и получаем avatar_url из accounts + return array_map(function($task) { + $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; + } + + return $task; + }, $tasks); + } + + // Получение всех колонок + public function getColumns() { + return Database::select('columns', [ + 'id', + 'name_columns', + 'color' + ]); + } + + // Получение всех департаментов + public function getDepartments() { + return Database::select('departments', [ + 'id', + 'name_departments', + 'color' + ]); + } + + // Получение всех меток + 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; + } +} + +?> \ No newline at end of file diff --git a/backend/app/class/enity/class_taskImage.php b/backend/app/class/enity/class_taskImage.php new file mode 100644 index 0000000..796bf30 --- /dev/null +++ b/backend/app/class/enity/class_taskImage.php @@ -0,0 +1,163 @@ + false, 'errors' => ['file' => 'Ошибка декодирования файла']]; + } + + // Проверка расширения + $allowed_ext = ['png', 'jpg', 'jpeg']; + $ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION)); + if (!in_array($ext, $allowed_ext)) { + return ['success' => false, 'errors' => ['file' => 'Разрешены только PNG, JPG, JPEG']]; + } + + // Проверка размера (10 МБ) + $max_size = 10 * 1024 * 1024; + if (strlen($file_data) > $max_size) { + return ['success' => false, 'errors' => ['file' => 'Файл слишком большой. Максимум 10 МБ']]; + } + + // Проверка MIME типа + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mime = $finfo->buffer($file_data); + $allowed_mime = ['image/png', 'image/jpeg']; + if (!in_array($mime, $allowed_mime)) { + return ['success' => false, 'errors' => ['file' => 'Недопустимый тип файла']]; + } + + // Всё ок — возвращаем данные + return [ + 'task' => $task, + 'file_data' => $file_data + ]; + } + + // Генерация уникального имени файла + protected static function getUniqueName($upload_dir, $file_name) { + $ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION)); + $base_name = pathinfo($file_name, PATHINFO_FILENAME); + $final_name = $file_name; + $counter = 1; + + while (file_exists($upload_dir . '/' . $final_name)) { + $final_name = $base_name . '_' . $counter . '.' . $ext; + $counter++; + } + + return $final_name; + } + + // Загрузка изображения + public static function upload($task_id, $file_base64, $file_name) { + // Валидация + $validation = self::validate($task_id, $file_base64, $file_name); + if (isset($validation['success'])) return $validation; + + $task = $validation['task']; + $file_data = $validation['file_data']; + + // Путь к папке + $upload_dir = __DIR__ . '/../../../public/img/task/' . $task_id; + if (!is_dir($upload_dir)) { + mkdir($upload_dir, 0755, true); + } + + // Уникальное имя + $final_name = self::getUniqueName($upload_dir, $file_name); + + // Сохранение файла + $file_path = $upload_dir . '/' . $final_name; + if (!file_put_contents($file_path, $file_data)) { + return ['success' => false, 'errors' => ['file' => 'Ошибка сохранения файла']]; + } + + // Формируем URL + $file_url = self::$upload_path . $task_id . '/' . $final_name; + $file_size = strlen($file_data); + + // Обновляем file_img в базе + $current_files = json_decode($task['file_img'], true) ?? []; + $current_files[] = [ + 'url' => $file_url, + 'name' => $final_name, + 'size' => $file_size + ]; + + Database::update(self::$db_name, [ + 'file_img' => json_encode($current_files, JSON_UNESCAPED_UNICODE) + ], [ + 'id' => $task_id + ]); + + return [ + 'success' => true, + 'file' => [ + 'url' => $file_url, + 'name' => $final_name, + 'size' => $file_size + ] + ]; + } + + // Удаление изображений (принимает строку или массив имён файлов) + public static function delete($task_id, $file_names) { + // Проверка и получение задачи + $task = Task::check_task($task_id); + + // Приводим к массиву если передана строка + if (!is_array($file_names)) { + $file_names = [$file_names]; + } + + // Получаем текущие файлы + $current_files = json_decode($task['file_img'], true) ?? []; + $upload_dir = __DIR__ . '/../../../public/img/task/' . $task_id; + $deleted = []; + + // Удаляем каждый файл + foreach ($file_names as $file_name) { + // Ищем файл в массиве + foreach ($current_files as $index => $file) { + if ($file['name'] === $file_name) { + // Удаляем файл с диска + $file_path = $upload_dir . '/' . $file_name; + if (file_exists($file_path)) { + unlink($file_path); + } + // Удаляем из массива + array_splice($current_files, $index, 1); + $deleted[] = $file_name; + break; + } + } + } + + // Обновляем в базе + Database::update(self::$db_name, [ + 'file_img' => json_encode($current_files, JSON_UNESCAPED_UNICODE) + ], [ + 'id' => $task_id + ]); + + // Удаляем папку если она пустая + if (is_dir($upload_dir) && count(scandir($upload_dir)) === 2) { + rmdir($upload_dir); + } + + return ['success' => true, 'deleted' => $deleted]; + } +} + +?> diff --git a/backend/app/class/enity/class_user.php b/backend/app/class/enity/class_user.php index b4441a6..6d1c5ba 100644 --- a/backend/app/class/enity/class_user.php +++ b/backend/app/class/enity/class_user.php @@ -58,6 +58,7 @@ class Account extends BaseEntity { // Получение всех пользователей public function getAll() { return Database::select($this->db_name, [ + 'id', 'id_department', 'name', 'username', diff --git a/backend/app/config.php b/backend/app/config.php index 168f1f5..6c0fc3a 100644 --- a/backend/app/config.php +++ b/backend/app/config.php @@ -12,6 +12,8 @@ require_once __DIR__ . '/restAPI/class_restApi.php'; require_once __DIR__ . '/class/enity/class_base.php'; require_once __DIR__ . '/class/enity/class_user.php'; + require_once __DIR__ . '/class/enity/class_task.php'; + require_once __DIR__ . '/class/enity/class_taskImage.php'; // Данные подключения к БД define('DB_HOST', '192.168.1.9'); @@ -26,6 +28,9 @@ $routes = [ '/api/user' => 'api/user.php', + '/api/task' => 'api/task.php', ]; + $publicActions = ['auth_login', 'check_session']; + ?> \ No newline at end of file diff --git a/backend/app/functions/routing.php b/backend/app/functions/routing.php index 90c5116..8433a5e 100644 --- a/backend/app/functions/routing.php +++ b/backend/app/functions/routing.php @@ -1,5 +1,34 @@ check_session($_COOKIE['session'] ?? null); + if (!$result['success']) { + RestApi::response($result, 403); + } + } + } +} + // Функция роутинга API function handleRouting($routes = []) { diff --git a/backend/index.php b/backend/index.php index dc9e4fb..2ba00a0 100644 --- a/backend/index.php +++ b/backend/index.php @@ -15,14 +15,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit; } -// Игнорируем запросы к favicon.ico и другим статическим файлам -$requestUri = $_SERVER['REQUEST_URI'] ?? ''; -if (preg_match('/\.(ico|png|jpg|jpeg|gif|css|js|svg|woff|woff2|ttf|eot)$/i', $requestUri)) { - http_response_code(404); - exit; -} - +ignore_favicon(); +check_ApiAuth($publicActions); handleRouting($routes); ?> diff --git a/backend/public/img/task/10/345.png b/backend/public/img/task/10/345.png new file mode 100644 index 0000000..c803071 Binary files /dev/null and b/backend/public/img/task/10/345.png differ diff --git a/backend/public/img/task/12/345.png b/backend/public/img/task/12/345.png new file mode 100644 index 0000000..c803071 Binary files /dev/null and b/backend/public/img/task/12/345.png differ diff --git a/backend/public/img/task/8/345.png b/backend/public/img/task/8/345.png new file mode 100644 index 0000000..c803071 Binary files /dev/null and b/backend/public/img/task/8/345.png differ diff --git a/front_vue/src/api.js b/front_vue/src/api.js index a6daecc..fad73e5 100644 --- a/front_vue/src/api.js +++ b/front_vue/src/api.js @@ -31,37 +31,77 @@ export const authApi = { // ==================== DEPARTMENTS ==================== export const departmentsApi = { - getAll: () => request('/departments', { credentials: 'include' }) + getAll: () => request('/api/task', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'get_departments' }) + }) } // ==================== LABELS ==================== export const labelsApi = { - getAll: () => request('/labels', { credentials: 'include' }) + getAll: () => request('/api/task', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'get_labels' }) + }) } // ==================== COLUMNS ==================== export const columnsApi = { - getAll: () => request('/columns', { credentials: 'include' }) + getAll: () => request('/api/task', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'get_columns' }) + }) } // ==================== CARDS ==================== export const cardsApi = { - getAll: () => request('/cards', { credentials: 'include' }), - create: (data) => request('/cards', { + getAll: () => request('/api/task', { credentials: 'include' }), + updateOrder: (id, column_id, to_index) => request('/api/task', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'update_order', id, column_id, to_index }) + }), + create: (data) => request('/api/task', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) + body: JSON.stringify({ action: 'create', ...data }) }), - update: (id, data) => request(`/cards/${id}`, { - method: 'PUT', + update: (data) => request('/api/task', { + method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) + body: JSON.stringify({ action: 'update', ...data }) }), - delete: (id) => request(`/cards/${id}`, { - method: 'DELETE', - credentials: 'include' + delete: (id) => request('/api/task', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'delete', id }) + }) +} + +// ==================== TASK IMAGES ==================== +export const taskImageApi = { + upload: (task_id, file_data, file_name) => request('/api/task', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'upload_image', task_id, file_data, file_name }) + }), + // Принимает строку (один файл) или массив (несколько файлов) + delete: (task_id, file_names) => request('/api/task', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'delete_image', task_id, file_names }) }) } diff --git a/front_vue/src/components/Board.vue b/front_vue/src/components/Board.vue index 14bff5c..b0a5491 100644 --- a/front_vue/src/components/Board.vue +++ b/front_vue/src/components/Board.vue @@ -18,6 +18,7 @@