1
0

Добавление проектов

Добавили возможность создавания разных проектов.
This commit is contained in:
2026-01-14 10:46:38 +07:00
parent 04e88cb7fa
commit 719aecd09e
22 changed files with 996 additions and 233 deletions

86
backend/api/project.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'POST') {
$data = RestApi::getInput();
$action = $data['action'] ?? null;
$project = new Project();
// Получение данных проекта (проект + колонки + отделы)
if ($action === 'get_project_data') {
$project_id = $data['id_project'] ?? null;
$result = Project::getProjectData($project_id);
if ($result) {
RestApi::response(['success' => true, 'data' => $result]);
} else {
RestApi::response(['success' => false, 'errors' => ['project' => 'Проект не найден']], 404);
}
}
// Установка колонки "Готово" для проекта
if ($action === 'set_ready_column') {
$project_id = $data['id_project'] ?? null;
$column_id = $data['column_id'] ?? null;
$result = Project::setReadyColumn($project_id, $column_id);
RestApi::response($result);
}
// Создание проекта
if ($action === 'create') {
$project->name = $data['name'] ?? '';
$project->id_ready = $data['id_ready'] ?? null;
$result = $project->create();
RestApi::response($result);
}
// Обновление проекта
if ($action === 'update') {
$project->id = $data['id'] ?? null;
$project->name = $data['name'] ?? '';
$project->id_ready = $data['id_ready'] ?? null;
$project->id_order = $data['id_order'] ?? null;
$result = $project->update();
RestApi::response($result);
}
// Удаление проекта
if ($action === 'delete') {
$id = $data['id'] ?? null;
$result = Project::delete($id);
RestApi::response($result);
}
// Метод не указан
if (!$action) {
RestApi::response(['success' => false, 'error' => 'Укажите метод'], 400);
}
}
if ($method === 'GET') {
// Получение всех проектов
// ?active=ID — дополнительно вернуть данные активного проекта
$project = new Project();
$projects = $project->getAll();
$active_id = $_GET['active'] ?? null;
if ($active_id) {
// Возвращаем список проектов + данные активного
$activeData = Project::getProjectData((int)$active_id);
RestApi::response([
'success' => true,
'data' => [
'projects' => $projects,
'active' => $activeData
]
]);
} else {
// Только список проектов
RestApi::response(['success' => true, 'data' => $projects]);
}
}
?>

View File

@@ -7,24 +7,6 @@ if ($method === 'POST') {
$action = $data['action'] ?? null; $action = $data['action'] ?? null;
$task = new Task(); $task = new Task();
// Получение колонок
if ($action === 'get_columns') {
$result = $task->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') { if ($action === 'upload_image') {
$task_id = $data['task_id'] ?? null; $task_id = $data['task_id'] ?? null;
@@ -73,6 +55,7 @@ if ($method === 'POST') {
// Создание задачи // Создание задачи
if ($action === 'create') { if ($action === 'create') {
$task->id_project = $data['id_project'] ?? null;
$task->id_department = $data['id_department'] ?? null; $task->id_department = $data['id_department'] ?? null;
$task->id_label = $data['id_label'] ?? null; $task->id_label = $data['id_label'] ?? null;
$task->id_account = $data['id_account'] ?? null; $task->id_account = $data['id_account'] ?? null;
@@ -110,8 +93,15 @@ if ($method === 'POST') {
} }
if ($method === 'GET') { if ($method === 'GET') {
// Получение всех задач // Получение задач проекта
// ?id_project=1 (обязательный)
// ?archive=0 (неархивные, по умолчанию), ?archive=1 (архивные), ?archive=all (все) // ?archive=0 (неархивные, по умолчанию), ?archive=1 (архивные), ?archive=all (все)
$id_project = $_GET['id_project'] ?? null;
if (!$id_project) {
RestApi::response(['success' => false, 'errors' => ['id_project' => 'Проект не указан']], 400);
}
$archive = $_GET['archive'] ?? 0; $archive = $_GET['archive'] ?? 0;
if ($archive === 'all') { if ($archive === 'all') {
$archive = null; $archive = null;
@@ -120,7 +110,7 @@ if ($method === 'GET') {
} }
$task = new Task(); $task = new Task();
$tasks = $task->getAll($archive); $tasks = $task->getAll($id_project, $archive);
RestApi::response(['success' => true, 'data' => $tasks]); RestApi::response(['success' => true, 'data' => $tasks]);
} }

View File

@@ -0,0 +1,241 @@
<?php
class Project extends BaseEntity {
protected $db_name = 'project';
// Свойства проекта
public $id;
public $id_order;
public $name;
public $id_ready;
// Валидация данных
protected function validate() {
static::$error_message = [];
if (!$this->id) {
$this->addError('id', 'ID проекта не указан');
}
if (!$this->name) {
$this->addError('name', 'Название проекта не может быть пустым');
}
return $this->getErrors();
}
// Валидация данных (для create)
protected function validateCreate() {
static::$error_message = [];
if (!$this->name) {
$this->addError('name', 'Название проекта не может быть пустым');
}
return $this->getErrors();
}
// Создание проекта
public function create() {
// Валидация
if ($errors = $this->validateCreate()) {
return $errors;
}
// Получаем максимальный order
$maxOrder = Database::max($this->db_name, 'id_order') ?? 0;
// Вставляем в базу
Database::insert($this->db_name, [
'name' => $this->name,
'id_order' => $maxOrder + 1,
'id_ready' => $this->id_ready
]);
$this->id = Database::id();
return [
'success' => true,
'id' => $this->id
];
}
// Обновление проекта
public function update() {
// Валидация
if ($errors = $this->validate()) {
return $errors;
}
// Проверка что проект существует
$project = Database::get($this->db_name, ['id'], ['id' => $this->id]);
if (!$project) {
$this->addError('project', 'Проект не найден');
return $this->getErrors();
}
// Формируем данные для обновления
$update_data = [
'name' => $this->name
];
// id_ready обновляем только если передан
if ($this->id_ready !== null) {
$update_data['id_ready'] = $this->id_ready;
}
// id_order обновляем только если передан
if ($this->id_order !== null) {
$update_data['id_order'] = $this->id_order;
}
// Обновляем в БД
Database::update($this->db_name, $update_data, [
'id' => $this->id
]);
return ['success' => true];
}
// Удаление проекта
public static function delete($id) {
// Проверка что проект существует
$project = self::checkProject($id);
if (!$project) {
return ['success' => false, 'errors' => ['project' => 'Проект не найден']];
}
// Удаляем все связанные данные
// Удаляем задачи проекта (и их файлы)
$tasks = Database::select('cards_task', ['id'], ['id_project' => $id]);
foreach ($tasks as $task) {
Task::delete($task['id']);
}
// Удаляем колонки проекта
Database::delete('columns', ['id_project' => $id]);
// Удаляем отделы проекта
Database::delete('departments', ['id_project' => $id]);
// Удаляем сам проект
Database::delete('project', ['id' => $id]);
return ['success' => true];
}
// Получение всех проектов
public function getAll() {
return Database::select($this->db_name, [
'id',
'id_order',
'name',
'id_ready'
], [
'ORDER' => ['id_order' => 'ASC']
]);
}
// Получение одного проекта
public static function get($id) {
return Database::get('project', [
'id',
'id_order',
'name',
'id_ready'
], ['id' => $id]);
}
// Получение id_ready (ID колонки "Готово") по ID проекта
public static function getReadyColumnId($project_id) {
$project = Database::get('project', ['id_ready'], ['id' => $project_id]);
return $project ? (int)$project['id_ready'] : null;
}
// Установка id_ready для проекта
public static function setReadyColumn($project_id, $column_id) {
// Проверка что проект существует
$project = self::checkProject($project_id);
if (!$project) {
return ['success' => false, 'errors' => ['project' => 'Проект не найден']];
}
// Проверка что колонка существует и принадлежит этому проекту
$column = Database::get('columns', ['id', 'id_project'], ['id' => $column_id]);
if (!$column || (int)$column['id_project'] !== (int)$project_id) {
return ['success' => false, 'errors' => ['column' => 'Колонка не найдена или не принадлежит проекту']];
}
// Обновляем id_ready
Database::update('project', [
'id_ready' => $column_id
], [
'id' => $project_id
]);
return ['success' => true, 'id_ready' => $column_id];
}
// Получение колонок проекта
public static function getColumns($project_id) {
return Database::select('columns', [
'id',
'name_columns',
'color',
'id_order'
], [
'id_project' => $project_id,
'ORDER' => ['id_order' => 'ASC']
]);
}
// Получение отделов проекта
public static function getDepartments($project_id) {
return Database::select('departments', [
'id',
'name_departments',
'color'
], [
'id_project' => $project_id
]);
}
// Получение всех данных проекта (проект + колонки + отделы + метки)
public static function getProjectData($project_id) {
$project = self::get($project_id);
if (!$project) {
return null;
}
// Получаем метки (глобальные)
$labels = Database::select('labels', [
'id',
'name_labels',
'icon',
'color'
]);
return [
'project' => $project,
'columns' => self::getColumns($project_id),
'departments' => self::getDepartments($project_id),
'labels' => $labels
];
}
// Проверка и получение проекта
public static function checkProject($project_id) {
return Database::get('project', '*', ['id' => $project_id]);
}
// Проверка проекта с ответом (при ошибке — сразу ответ и exit)
public static function check($project_id) {
$project = self::checkProject($project_id);
if (!$project_id || !$project) {
RestApi::response(['success' => false, 'errors' => ['project' => 'Проект не найден']], 400);
}
return $project;
}
}
?>

View File

@@ -6,6 +6,7 @@ class Task extends BaseEntity {
// Свойства задачи // Свойства задачи
public $id; public $id;
public $id_project;
public $id_department; public $id_department;
public $id_label; public $id_label;
public $order; public $order;
@@ -45,6 +46,9 @@ class Task extends BaseEntity {
if (!$this->id_department) { if (!$this->id_department) {
$this->addError('id_department', 'Департамент не указан'); $this->addError('id_department', 'Департамент не указан');
} }
if (!$this->id_project) {
$this->addError('id_project', 'Проект не указан');
}
return $this->getErrors(); return $this->getErrors();
} }
@@ -58,6 +62,7 @@ class Task extends BaseEntity {
// Вставляем в базу // Вставляем в базу
Database::insert($this->db_name, [ Database::insert($this->db_name, [
'id_project' => $this->id_project,
'id_department' => $this->id_department, 'id_department' => $this->id_department,
'id_label' => $this->id_label, 'id_label' => $this->id_label,
'order' => $this->order ?? 0, 'order' => $this->order ?? 0,
@@ -101,7 +106,7 @@ class Task extends BaseEntity {
} }
// Проверка что задача существует и получаем текущие данные // Проверка что задача существует и получаем текущие данные
$task = Database::get($this->db_name, ['id', 'column_id', 'order'], ['id' => $this->id]); $task = Database::get($this->db_name, ['id', 'column_id', 'order', 'id_project'], ['id' => $this->id]);
if (!$task) { if (!$task) {
$this->addError('task', 'Задача не найдена'); $this->addError('task', 'Задача не найдена');
return $this->getErrors(); return $this->getErrors();
@@ -113,6 +118,9 @@ class Task extends BaseEntity {
// Если column_id не передан — оставляем текущий // Если column_id не передан — оставляем текущий
$new_column_id = $this->column_id !== null ? (int)$this->column_id : $old_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 = [ $update_data = [
'id_department' => $this->id_department, 'id_department' => $this->id_department,
@@ -127,9 +135,9 @@ class Task extends BaseEntity {
]; ];
// Обновляем date_closed при смене колонки // Обновляем date_closed при смене колонки
if ($new_column_id === COLUMN_DONE_ID && $old_column_id !== COLUMN_DONE_ID) { 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'); $update_data['date_closed'] = date('Y-m-d H:i:s');
} elseif ($old_column_id === COLUMN_DONE_ID && $new_column_id !== COLUMN_DONE_ID) { } elseif ($done_column_id && $old_column_id === $done_column_id && $new_column_id !== $done_column_id) {
$update_data['date_closed'] = null; $update_data['date_closed'] = null;
} }
@@ -173,6 +181,9 @@ class Task extends BaseEntity {
$new_column_id = (int)$column_id; $new_column_id = (int)$column_id;
$archive = (int)$task['archive']; $archive = (int)$task['archive'];
// Получаем id_ready (колонка "Готово") из проекта
$done_column_id = Project::getReadyColumnId($task['id_project']);
// Получаем все карточки целевой колонки с тем же статусом архивации (кроме перемещаемой) // Получаем все карточки целевой колонки с тем же статусом архивации (кроме перемещаемой)
$cards = Database::select('cards_task', ['id', 'order'], [ $cards = Database::select('cards_task', ['id', 'order'], [
'column_id' => $column_id, 'column_id' => $column_id,
@@ -192,13 +203,13 @@ class Task extends BaseEntity {
]; ];
// Только для перемещаемой карточки обновляем date_closed // Только для перемещаемой карточки обновляем date_closed
if ($card['id'] == $id) { if ($card['id'] == $id && $done_column_id) {
// Перемещаем В колонку "Готово" — устанавливаем дату закрытия // Перемещаем В колонку "Готово" — устанавливаем дату закрытия
if ($new_column_id === COLUMN_DONE_ID && $old_column_id !== COLUMN_DONE_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'); $update_data['date_closed'] = date('Y-m-d H:i:s');
} }
// Перемещаем ИЗ колонки "Готово" — обнуляем дату // Перемещаем ИЗ колонки "Готово" — обнуляем дату
elseif ($old_column_id === COLUMN_DONE_ID && $new_column_id !== COLUMN_DONE_ID) { elseif ($old_column_id === $done_column_id && $new_column_id !== $done_column_id) {
$update_data['date_closed'] = null; $update_data['date_closed'] = null;
} }
} }
@@ -211,16 +222,20 @@ class Task extends BaseEntity {
return ['success' => true]; return ['success' => true];
} }
// Получение всех задач // Получение всех задач проекта
// $id_project: ID проекта (обязательный)
// $archive: 0 = неархивные, 1 = архивные, null = все // $archive: 0 = неархивные, 1 = архивные, null = все
public function getAll($archive = 0) { public function getAll($id_project, $archive = 0) {
$where = []; $where = [
'id_project' => $id_project
];
if ($archive !== null) { if ($archive !== null) {
$where['archive'] = $archive ? 1 : 0; $where['archive'] = $archive ? 1 : 0;
} }
$tasks = Database::select($this->db_name, [ $tasks = Database::select($this->db_name, [
'id', 'id',
'id_project',
'id_department', 'id_department',
'id_label', 'id_label',
'id_account', 'id_account',
@@ -238,7 +253,7 @@ class Task extends BaseEntity {
// Декодируем JSON и получаем avatar_url из accounts // Декодируем JSON и получаем avatar_url из accounts
return array_map(function($task) { return array_map(function($task) {
$task['file_img'] = json_decode($task['file_img'], true) ?? []; $task['file_img'] = $task['file_img'] ? json_decode($task['file_img'], true) : [];
// Получаем avatar_url из accounts по id_account // Получаем avatar_url из accounts по id_account
if ($task['id_account']) { if ($task['id_account']) {
@@ -252,21 +267,27 @@ class Task extends BaseEntity {
}, $tasks); }, $tasks);
} }
// Получение всех колонок // Получение колонок проекта
public function getColumns() { public function getColumns($id_project) {
return Database::select('columns', [ return Database::select('columns', [
'id', 'id',
'name_columns', 'name_columns',
'color' 'color',
'id_order'
], [
'id_project' => $id_project,
'ORDER' => ['id_order' => 'ASC']
]); ]);
} }
// Получение всех департаментов // Получение отделов проекта
public function getDepartments() { public function getDepartments($id_project) {
return Database::select('departments', [ return Database::select('departments', [
'id', 'id',
'name_departments', 'name_departments',
'color' 'color'
], [
'id_project' => $id_project
]); ]);
} }
@@ -294,8 +315,11 @@ class Task extends BaseEntity {
// Проверка что задача существует // Проверка что задача существует
$task = self::check_task($id); $task = self::check_task($id);
// Получаем id_ready (колонка "Готово") из проекта
$done_column_id = Project::getReadyColumnId($task['id_project']);
// Архивировать можно только задачи в колонке "Готово" // Архивировать можно только задачи в колонке "Готово"
if ($archive && (int)$task['column_id'] !== COLUMN_DONE_ID) { if ($archive && $done_column_id && (int)$task['column_id'] !== $done_column_id) {
RestApi::response([ RestApi::response([
'success' => false, 'success' => false,
'errors' => ['column' => 'Архивировать можно только задачи из колонки "Готово"'] 'errors' => ['column' => 'Архивировать можно только задачи из колонки "Готово"']
@@ -308,8 +332,8 @@ class Task extends BaseEntity {
]; ];
// При разархивировании — возвращаем в колонку "Готово" // При разархивировании — возвращаем в колонку "Готово"
if (!$archive) { if (!$archive && $done_column_id) {
$update_data['column_id'] = COLUMN_DONE_ID; $update_data['column_id'] = $done_column_id;
} }
// Обновляем в БД // Обновляем в БД

View File

@@ -81,7 +81,7 @@ class TaskImage {
$file_size = strlen($file_data); $file_size = strlen($file_data);
// Обновляем file_img в базе // Обновляем file_img в базе
$current_files = json_decode($task['file_img'], true) ?? []; $current_files = $task['file_img'] ? json_decode($task['file_img'], true) : [];
$current_files[] = [ $current_files[] = [
'url' => $file_url, 'url' => $file_url,
'name' => $final_name, 'name' => $final_name,
@@ -115,7 +115,7 @@ class TaskImage {
} }
// Получаем текущие файлы // Получаем текущие файлы
$current_files = json_decode($task['file_img'], true) ?? []; $current_files = $task['file_img'] ? json_decode($task['file_img'], true) : [];
$upload_dir = __DIR__ . '/../../../public/task/' . $task_id; $upload_dir = __DIR__ . '/../../../public/task/' . $task_id;
$deleted = []; $deleted = [];

View File

@@ -12,6 +12,7 @@
require_once __DIR__ . '/restAPI/class_restApi.php'; require_once __DIR__ . '/restAPI/class_restApi.php';
require_once __DIR__ . '/class/enity/class_base.php'; require_once __DIR__ . '/class/enity/class_base.php';
require_once __DIR__ . '/class/enity/class_user.php'; require_once __DIR__ . '/class/enity/class_user.php';
require_once __DIR__ . '/class/enity/class_project.php';
require_once __DIR__ . '/class/enity/class_task.php'; require_once __DIR__ . '/class/enity/class_task.php';
require_once __DIR__ . '/class/enity/class_taskImage.php'; require_once __DIR__ . '/class/enity/class_taskImage.php';
@@ -23,15 +24,13 @@
define('DB_PORT', 3306); define('DB_PORT', 3306);
define('DB_CHARSET', 'utf8mb4'); define('DB_CHARSET', 'utf8mb4');
// ID колонки "Готово" (для фиксации date_closed и архивации)
define('COLUMN_DONE_ID', 4);
// Инициализация подключения к БД // Инициализация подключения к БД
Database::init(); Database::init();
$routes = [ $routes = [
'/api/user' => __DIR__ . '/../api/user.php', '/api/user' => __DIR__ . '/../api/user.php',
'/api/task' => __DIR__ . '/../api/task.php', '/api/task' => __DIR__ . '/../api/task.php',
'/api/project' => __DIR__ . '/../api/project.php',
]; ];
$publicActions = ['auth_login', 'check_session']; $publicActions = ['auth_login', 'check_session'];

View File

@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<title>TaskBoard</title> <title>TaskBoard</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">

View File

@@ -7,9 +7,9 @@
"": { "": {
"name": "taskboard", "name": "taskboard",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC",
"dependencies": { "dependencies": {
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.3",
"pinia": "^3.0.4",
"vite": "^7.3.1", "vite": "^7.3.1",
"vue": "^3.5.26", "vue": "^3.5.26",
"vue-router": "^4.6.4" "vue-router": "^4.6.4"
@@ -892,6 +892,30 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/devtools-kit": {
"version": "7.7.9",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^7.7.9",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.9",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.26", "version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz",
@@ -942,6 +966,30 @@
"integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/birpc": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/copy-anything": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
"license": "MIT",
"dependencies": {
"is-what": "^5.2.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -1038,6 +1086,24 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/is-what": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1047,6 +1113,12 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -1065,6 +1137,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1084,6 +1162,36 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pinia": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.7"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.5.0",
"vue": "^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/@vue/devtools-api": {
"version": "7.7.9",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
"integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.9"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -1112,6 +1220,12 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.55.1", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
@@ -1165,6 +1279,27 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/superjson": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
"license": "MIT",
"dependencies": {
"copy-anything": "^4"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",

View File

@@ -11,6 +11,7 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.3",
"pinia": "^3.0.4",
"vite": "^7.3.1", "vite": "^7.3.1",
"vue": "^3.5.26", "vue": "^3.5.26",
"vue-router": "^4.6.4" "vue-router": "^4.6.4"

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -38,40 +38,56 @@ export const authApi = {
}) })
} }
// ==================== DEPARTMENTS ==================== // ==================== PROJECTS ====================
export const departmentsApi = { export const projectsApi = {
getAll: () => request('/api/task', { // active: ID проекта для загрузки данных (опционально)
getAll: (active = null) => {
let url = '/api/project'
if (active) url += `?active=${active}`
return request(url, { credentials: 'include' })
},
// Получение данных проекта (проект + колонки + отделы) — один запрос вместо трёх
getData: (id_project) => request('/api/project', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'get_departments' }) body: JSON.stringify({ action: 'get_project_data', id_project })
}) }),
} create: (data) => request('/api/project', {
// ==================== LABELS ====================
export const labelsApi = {
getAll: () => request('/api/task', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'get_labels' }) body: JSON.stringify({ action: 'create', ...data })
}) }),
} update: (data) => request('/api/project', {
// ==================== COLUMNS ====================
export const columnsApi = {
getAll: () => request('/api/task', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'get_columns' }) body: JSON.stringify({ action: 'update', ...data })
}),
delete: (id) => request('/api/project', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'delete', id })
}),
setReadyColumn: (id_project, column_id) => request('/api/project', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'set_ready_column', id_project, column_id })
}) })
} }
// ==================== CARDS ==================== // ==================== CARDS ====================
export const cardsApi = { export const cardsApi = {
// id_project: ID проекта (обязательный)
// archive: 0 = неархивные (по умолчанию), 1 = архивные, 'all' = все // archive: 0 = неархивные (по умолчанию), 1 = архивные, 'all' = все
getAll: (archive = 0) => request(`/api/task${archive !== 0 ? `?archive=${archive}` : ''}`, { credentials: 'include' }), getAll: (id_project, archive = 0) => {
let url = `/api/task?id_project=${id_project}`
if (archive !== 0) url += `&archive=${archive}`
return request(url, { credentials: 'include' })
},
updateOrder: (id, column_id, to_index) => request('/api/task', { updateOrder: (id, column_id, to_index) => request('/api/task', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
@@ -125,25 +141,3 @@ export const taskImageApi = {
export const usersApi = { export const usersApi = {
getAll: () => request('/api/user', { credentials: 'include' }) getAll: () => request('/api/user', { credentials: 'include' })
} }
// ==================== CONFIG ====================
export const configApi = {
get: () => request('/api/user', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'get_config' })
})
}
// Загрузка конфига с сервера и мерж с window.APP_CONFIG
export const loadServerConfig = async () => {
try {
const result = await configApi.get()
if (result.success && result.data) {
window.APP_CONFIG = { ...window.APP_CONFIG, ...result.data }
}
} catch (error) {
console.error('Ошибка загрузки конфига:', error)
}
}

View File

@@ -7,6 +7,7 @@
:column="column" :column="column"
:departments="departments" :departments="departments"
:labels="labels" :labels="labels"
:done-column-id="doneColumnId"
@drop-card="handleDropCard" @drop-card="handleDropCard"
@open-task="(card) => emit('open-task', { card, columnId: column.id })" @open-task="(card) => emit('open-task', { card, columnId: column.id })"
@create-task="emit('create-task', column.id)" @create-task="emit('create-task', column.id)"
@@ -23,6 +24,7 @@ import { cardsApi } from '../api'
const props = defineProps({ const props = defineProps({
activeDepartment: Number, activeDepartment: Number,
doneColumnId: Number,
departments: { departments: {
type: Array, type: Array,
default: () => [] default: () => []
@@ -55,15 +57,13 @@ onUpdated(refreshIcons)
// Локальная копия карточек для optimistic UI // Локальная копия карточек для optimistic UI
const localCards = ref([]) const localCards = ref([])
// Синхронизируем с props при загрузке данных // Синхронизируем с props при загрузке/смене проекта
watch(() => props.cards, (newCards) => { watch(() => props.cards, (newCards) => {
if (newCards.length > 0 && localCards.value.length === 0) { // Копируем данные и добавляем order если нет
// Первая загрузка - копируем данные и добавляем order если нет
localCards.value = JSON.parse(JSON.stringify(newCards)).map((card, idx) => ({ localCards.value = JSON.parse(JSON.stringify(newCards)).map((card, idx) => ({
...card, ...card,
order: card.order ?? idx order: card.order ?? idx
})) }))
}
}, { immediate: true }) }, { immediate: true })
// Собираем колонки с карточками (используем localCards, сортируем по order) // Собираем колонки с карточками (используем localCards, сортируем по order)
@@ -117,7 +117,8 @@ const inProgressTasks = computed(() => {
}) })
const completedTasks = computed(() => { const completedTasks = computed(() => {
const col = filteredColumns.value.find(c => c.id === 4) // Готово if (!props.doneColumnId) return 0
const col = filteredColumns.value.find(c => c.id === props.doneColumnId)
return col ? col.cards.length : 0 return col ? col.cards.length : 0
}) })
@@ -134,15 +135,13 @@ const handleDropCard = async ({ cardId, fromColumnId, toColumnId, toIndex }) =>
const card = localCards.value.find(c => c.id === cardId) const card = localCards.value.find(c => c.id === cardId)
if (!card) return if (!card) return
const doneColumnId = window.APP_CONFIG.COLUMN_DONE_ID
// Локально обновляем для мгновенного отклика // Локально обновляем для мгновенного отклика
card.column_id = toColumnId card.column_id = toColumnId
// Обновляем date_closed при перемещении в/из колонки "Готово" // Обновляем date_closed при перемещении в/из колонки "Готово"
if (toColumnId === doneColumnId && fromColumnId !== doneColumnId) { if (props.doneColumnId && toColumnId === props.doneColumnId && fromColumnId !== props.doneColumnId) {
card.date_closed = new Date().toISOString() card.date_closed = new Date().toISOString()
} else if (fromColumnId === doneColumnId && toColumnId !== doneColumnId) { } else if (props.doneColumnId && fromColumnId === props.doneColumnId && toColumnId !== props.doneColumnId) {
card.date_closed = null card.date_closed = null
} }
@@ -203,6 +202,7 @@ const saveTask = async (taskData, columnId) => {
// Отправляем на сервер // Отправляем на сервер
const result = await cardsApi.create({ const result = await cardsApi.create({
id_project: taskData.id_project,
id_department: taskData.departmentId, id_department: taskData.departmentId,
id_label: taskData.labelId, id_label: taskData.labelId,
id_account: taskData.accountId, id_account: taskData.accountId,

View File

@@ -55,7 +55,7 @@
<span v-if="card.dueDate && Number(columnId) !== doneColumnId" class="due-date" :class="dueDateStatus"> <span v-if="card.dueDate && Number(columnId) !== doneColumnId" class="due-date" :class="dueDateStatus">
{{ daysLeftText }} {{ daysLeftText }}
</span> </span>
<span v-if="Number(columnId) === doneColumnId && card.dateClosed" class="date-closed"> <span v-if="doneColumnId && Number(columnId) === doneColumnId && card.dateClosed" class="date-closed">
Закрыто: {{ closedDateText }} Закрыто: {{ closedDateText }}
</span> </span>
</div> </div>
@@ -69,6 +69,7 @@ import { getFullUrl } from '../api'
const props = defineProps({ const props = defineProps({
card: Object, card: Object,
columnId: [String, Number], columnId: [String, Number],
doneColumnId: Number,
index: Number, index: Number,
departments: { departments: {
type: Array, type: Array,
@@ -174,12 +175,9 @@ const closedDateText = computed(() => {
return formatDateWithYear(props.card.dateClosed) return formatDateWithYear(props.card.dateClosed)
}) })
// ID колонки "Готово" из конфига
const doneColumnId = window.APP_CONFIG.COLUMN_DONE_ID
// Можно ли архивировать (только если колонка "Готово") // Можно ли архивировать (только если колонка "Готово")
const canArchive = computed(() => { const canArchive = computed(() => {
return Number(props.columnId) === doneColumnId return props.doneColumnId && Number(props.columnId) === props.doneColumnId
}) })
const handleArchive = () => { const handleArchive = () => {

View File

@@ -26,6 +26,7 @@
<Card <Card
:card="card" :card="card"
:column-id="column.id" :column-id="column.id"
:done-column-id="doneColumnId"
:index="index" :index="index"
:departments="departments" :departments="departments"
:labels="labels" :labels="labels"
@@ -47,6 +48,7 @@ import Card from './Card.vue'
const props = defineProps({ const props = defineProps({
column: Object, column: Object,
doneColumnId: Number,
departments: { departments: {
type: Array, type: Array,
default: () => [] default: () => []

View File

@@ -0,0 +1,137 @@
<template>
<div class="project-select" v-if="store.currentProject" @click.stop>
<button class="project-btn" @click="dropdownOpen = !dropdownOpen">
<i data-lucide="folder" class="folder-icon"></i>
{{ store.currentProject?.name || 'Выберите проект' }}
<i data-lucide="chevron-down" class="chevron" :class="{ open: dropdownOpen }"></i>
</button>
<div class="project-dropdown" v-if="dropdownOpen">
<button
v-for="project in store.projects"
:key="project.id"
class="project-option"
:class="{ active: store.currentProjectId === project.id }"
@click="handleSelect(project.id)"
>
{{ project.name }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useProjectsStore } from '../stores/projects'
const store = useProjectsStore()
const dropdownOpen = ref(false)
const emit = defineEmits(['change'])
const handleSelect = async (projectId) => {
dropdownOpen.value = false
await store.selectProject(projectId)
emit('change', projectId)
}
// Закрытие дропдауна при клике вне
const closeDropdown = (e) => {
if (!e.target.closest('.project-select')) {
dropdownOpen.value = false
}
}
onMounted(() => {
document.addEventListener('click', closeDropdown)
if (window.lucide) window.lucide.createIcons()
})
onUnmounted(() => {
document.removeEventListener('click', closeDropdown)
})
</script>
<style scoped>
.project-select {
position: relative;
}
.project-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 11px;
background: var(--accent-soft);
border: 1px solid var(--accent);
border-radius: 6px;
color: var(--accent);
font-family: inherit;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.3px;
cursor: pointer;
transition: all 0.15s;
}
.project-btn:hover {
background: var(--accent);
color: #000;
}
.project-btn .folder-icon {
width: 12px;
height: 12px;
margin-right: 2px;
}
.project-btn .chevron {
width: 10px;
height: 10px;
margin-left: 4px;
opacity: 0.7;
transition: transform 0.2s;
}
.project-btn .chevron.open {
transform: rotate(180deg);
}
.project-dropdown {
position: absolute;
top: calc(100% + 6px);
left: 0;
min-width: 200px;
background: var(--bg-secondary);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 6px;
z-index: 100;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.project-option {
display: block;
width: 100%;
padding: 10px 12px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary);
font-family: inherit;
font-size: 13px;
text-align: left;
cursor: pointer;
transition: all 0.15s;
}
.project-option:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--text-primary);
}
.project-option.active {
background: var(--accent-soft);
color: var(--accent);
}
</style>

View File

@@ -199,6 +199,7 @@ const props = defineProps({
show: Boolean, show: Boolean,
card: Object, card: Object,
columnId: [String, Number], columnId: [String, Number],
doneColumnId: Number,
isArchived: { isArchived: {
type: Boolean, type: Boolean,
default: false default: false
@@ -461,7 +462,7 @@ const handleDelete = () => {
// Можно ли архивировать (только если колонка "Готово") // Можно ли архивировать (только если колонка "Готово")
const canArchive = computed(() => { const canArchive = computed(() => {
return Number(props.columnId) === window.APP_CONFIG.COLUMN_DONE_ID return props.doneColumnId && Number(props.columnId) === props.doneColumnId
}) })
const handleArchive = () => { const handleArchive = () => {

View File

@@ -1,5 +1,9 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
createApp(App).use(router).mount('#app') const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -3,10 +3,7 @@ import MainApp from './views/MainApp.vue'
import LoginPage from './views/LoginPage.vue' import LoginPage from './views/LoginPage.vue'
import TeamPage from './views/TeamPage.vue' import TeamPage from './views/TeamPage.vue'
import ArchivePage from './views/ArchivePage.vue' import ArchivePage from './views/ArchivePage.vue'
import { authApi, loadServerConfig } from './api' import { authApi } from './api'
// Флаг загрузки конфига (один раз за сессию)
let configLoaded = false
// Проверка авторизации // Проверка авторизации
const checkAuth = async () => { const checkAuth = async () => {
@@ -60,11 +57,6 @@ router.beforeEach(async (to, from, next) => {
// Уже авторизован — на главную // Уже авторизован — на главную
next('/') next('/')
} else { } else {
// Загружаем конфиг с сервера один раз для защищённых страниц
if (to.meta.requiresAuth && isAuth && !configLoaded) {
await loadServerConfig()
configLoaded = true
}
next() next()
} }
}) })

View File

@@ -0,0 +1,145 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { projectsApi, usersApi } from '../api'
export const useProjectsStore = defineStore('projects', () => {
// ==================== СОСТОЯНИЕ ====================
const projects = ref([])
const departments = ref([])
const labels = ref([])
const columns = ref([])
const users = ref([])
const loading = ref(false)
const initialized = ref(false)
// Текущий проект (из localStorage)
const savedProjectId = localStorage.getItem('currentProjectId')
const savedProjectName = localStorage.getItem('currentProjectName')
const currentProjectId = ref(savedProjectId ? parseInt(savedProjectId) : null)
// ==================== ГЕТТЕРЫ ====================
// Текущий проект (объект)
const currentProject = computed(() =>
projects.value.find(p => p.id === currentProjectId.value) ||
(savedProjectName && currentProjectId.value ? { id: currentProjectId.value, name: savedProjectName } : null)
)
// ID колонки "Готово" текущего проекта
const doneColumnId = computed(() => {
const project = projects.value.find(p => p.id === currentProjectId.value)
return project ? Number(project.id_ready) : null
})
// ==================== ДЕЙСТВИЯ ====================
// Инициализация (загрузка проектов + данных активного)
const init = async () => {
if (initialized.value) return
loading.value = true
try {
// Загружаем проекты и данные активного одним запросом
const result = await projectsApi.getAll(currentProjectId.value || undefined)
if (result.success) {
if (result.data.projects) {
projects.value = result.data.projects
// Применяем данные активного проекта
if (result.data.active) {
columns.value = result.data.active.columns
departments.value = result.data.active.departments
labels.value = result.data.active.labels
}
} else {
projects.value = result.data
}
// Если нет выбранного проекта — выбираем первый
if (!currentProjectId.value || !projects.value.find(p => p.id === currentProjectId.value)) {
if (projects.value.length > 0) {
await selectProject(projects.value[0].id, false)
}
} else {
// Обновляем название в localStorage
const project = projects.value.find(p => p.id === currentProjectId.value)
if (project) localStorage.setItem('currentProjectName', project.name)
}
}
// Загружаем пользователей
const usersData = await usersApi.getAll()
if (usersData.success) users.value = usersData.data
initialized.value = true
} catch (error) {
console.error('Ошибка инициализации:', error)
} finally {
loading.value = false
}
}
// Выбор проекта
const selectProject = async (projectId, fetchData = true) => {
currentProjectId.value = projectId
localStorage.setItem('currentProjectId', projectId.toString())
// Сохраняем название
const project = projects.value.find(p => p.id === projectId)
if (project) localStorage.setItem('currentProjectName', project.name)
// Загружаем данные проекта
if (fetchData) {
await fetchProjectData()
}
}
// Загрузка данных текущего проекта
const fetchProjectData = async () => {
if (!currentProjectId.value) return
try {
const projectData = await projectsApi.getData(currentProjectId.value)
if (projectData.success) {
columns.value = projectData.data.columns
departments.value = projectData.data.departments
labels.value = projectData.data.labels
}
} catch (error) {
console.error('Ошибка загрузки данных проекта:', error)
}
}
// Сброс при выходе
const reset = () => {
projects.value = []
departments.value = []
labels.value = []
columns.value = []
users.value = []
currentProjectId.value = null
initialized.value = false
localStorage.removeItem('currentProjectId')
localStorage.removeItem('currentProjectName')
}
return {
// Состояние
projects,
departments,
labels,
columns,
users,
loading,
initialized,
currentProjectId,
// Геттеры
currentProject,
doneColumnId,
// Действия
init,
selectProject,
fetchProjectData,
reset
}
})

View File

@@ -9,6 +9,12 @@
<Header title="Архив задач"> <Header title="Архив задач">
<template #filters> <template #filters>
<div class="filters"> <div class="filters">
<!-- Выбор проекта -->
<ProjectSelector @change="onProjectChange" />
<div class="filter-divider"></div>
<!-- Фильтр по отделам -->
<button <button
class="filter-tag" class="filter-tag"
:class="{ active: activeDepartment === null }" :class="{ active: activeDepartment === null }"
@@ -17,7 +23,7 @@
Все Все
</button> </button>
<button <button
v-for="dept in departments" v-for="dept in store.departments"
:key="dept.id" :key="dept.id"
class="filter-tag" class="filter-tag"
:class="{ active: activeDepartment === dept.id }" :class="{ active: activeDepartment === dept.id }"
@@ -44,8 +50,8 @@
v-for="card in filteredCards" v-for="card in filteredCards"
:key="card.id" :key="card.id"
:card="card" :card="card"
:departments="departments" :departments="store.departments"
:labels="labels" :labels="store.labels"
@click="openTaskPanel(card)" @click="openTaskPanel(card)"
@restore="handleRestore" @restore="handleRestore"
@delete="confirmDelete" @delete="confirmDelete"
@@ -73,9 +79,9 @@
:card="editingCard" :card="editingCard"
:column-id="null" :column-id="null"
:is-archived="true" :is-archived="true"
:departments="departments" :departments="store.departments"
:labels="labels" :labels="store.labels"
:users="users" :users="store.users"
@close="closePanel" @close="closePanel"
@save="handleSaveTask" @save="handleSaveTask"
@delete="handleDeleteTask" @delete="handleDeleteTask"
@@ -101,26 +107,15 @@ import Header from '../components/Header.vue'
import ArchiveCard from '../components/ArchiveCard.vue' import ArchiveCard from '../components/ArchiveCard.vue'
import TaskPanel from '../components/TaskPanel.vue' import TaskPanel from '../components/TaskPanel.vue'
import ConfirmDialog from '../components/ConfirmDialog.vue' import ConfirmDialog from '../components/ConfirmDialog.vue'
import { departmentsApi, labelsApi, cardsApi, usersApi } from '../api' import ProjectSelector from '../components/ProjectSelector.vue'
import { useProjectsStore } from '../stores/projects'
import { cardsApi } from '../api'
// Активный фильтр по отделу (синхронизация с основной доской) // ==================== STORE ====================
const savedDepartment = localStorage.getItem('activeDepartment') const store = useProjectsStore()
const activeDepartment = ref(savedDepartment ? parseInt(savedDepartment) : null)
// Сохраняем в localStorage при изменении // ==================== КАРТОЧКИ ====================
watch(activeDepartment, (newVal) => {
if (newVal === null) {
localStorage.removeItem('activeDepartment')
} else {
localStorage.setItem('activeDepartment', newVal.toString())
}
})
// Данные
const departments = ref([])
const labels = ref([])
const cards = ref([]) const cards = ref([])
const users = ref([])
const loading = ref(true) const loading = ref(true)
// Отфильтрованные карточки (сортируем по дате завершения, новые сверху) // Отфильтрованные карточки (сортируем по дате завершения, новые сверху)
@@ -130,30 +125,24 @@ const filteredCards = computed(() => {
result = result.filter(card => card.departmentId === activeDepartment.value) result = result.filter(card => card.departmentId === activeDepartment.value)
} }
return result.sort((a, b) => { return result.sort((a, b) => {
// Сортировка по дате завершения (новые сверху)
const dateA = a.dateClosed ? new Date(a.dateClosed).getTime() : 0 const dateA = a.dateClosed ? new Date(a.dateClosed).getTime() : 0
const dateB = b.dateClosed ? new Date(b.dateClosed).getTime() : 0 const dateB = b.dateClosed ? new Date(b.dateClosed).getTime() : 0
return dateB - dateA return dateB - dateA
}) })
}) })
// Загрузка данных // Загрузка архивных карточек
const fetchData = async () => { const fetchCards = async () => {
if (!store.currentProjectId) {
loading.value = false
return
}
loading.value = true loading.value = true
try { try {
const [departmentsData, labelsData, cardsData, usersData] = await Promise.all([ const result = await cardsApi.getAll(store.currentProjectId, 1) // archive = 1
departmentsApi.getAll(), if (result.success) {
labelsApi.getAll(), cards.value = result.data.map(card => ({
cardsApi.getAll(1), // archive = 1
usersApi.getAll()
])
if (departmentsData.success) departments.value = departmentsData.data
if (labelsData.success) labels.value = labelsData.data
if (usersData.success) users.value = usersData.data
if (cardsData.success) {
cards.value = cardsData.data.map(card => ({
id: card.id, id: card.id,
title: card.title, title: card.title,
description: card.descript, description: card.descript,
@@ -175,14 +164,30 @@ const fetchData = async () => {
})) }))
})) }))
} }
} catch (error) {
console.error('Ошибка загрузки данных:', error)
} finally { } finally {
loading.value = false loading.value = false
} }
} }
// Панель редактирования // При смене проекта
const onProjectChange = async () => {
activeDepartment.value = null
await fetchCards()
}
// ==================== ФИЛЬТР ПО ОТДЕЛАМ ====================
const savedDepartment = localStorage.getItem('activeDepartment')
const activeDepartment = ref(savedDepartment ? parseInt(savedDepartment) : null)
watch(activeDepartment, (newVal) => {
if (newVal === null) {
localStorage.removeItem('activeDepartment')
} else {
localStorage.setItem('activeDepartment', newVal.toString())
}
})
// ==================== ПАНЕЛЬ РЕДАКТИРОВАНИЯ ====================
const panelOpen = ref(false) const panelOpen = ref(false)
const editingCard = ref(null) const editingCard = ref(null)
@@ -196,10 +201,8 @@ const closePanel = () => {
editingCard.value = null editingCard.value = null
} }
// Сохранение задачи
const handleSaveTask = async (taskData) => { const handleSaveTask = async (taskData) => {
if (taskData.id) { if (taskData.id) {
// Обновляем на сервере
await cardsApi.update({ await cardsApi.update({
id: taskData.id, id: taskData.id,
id_department: taskData.departmentId, id_department: taskData.departmentId,
@@ -211,7 +214,6 @@ const handleSaveTask = async (taskData) => {
descript_full: taskData.details descript_full: taskData.details
}) })
// Обновляем локально
const card = cards.value.find(c => c.id === taskData.id) const card = cards.value.find(c => c.id === taskData.id)
if (card) { if (card) {
card.title = taskData.title card.title = taskData.title
@@ -228,7 +230,6 @@ const handleSaveTask = async (taskData) => {
closePanel() closePanel()
} }
// Удаление через панель
const handleDeleteTask = async (cardId) => { const handleDeleteTask = async (cardId) => {
const result = await cardsApi.delete(cardId) const result = await cardsApi.delete(cardId)
if (result.success) { if (result.success) {
@@ -237,7 +238,7 @@ const handleDeleteTask = async (cardId) => {
closePanel() closePanel()
} }
// Диалог подтверждения удаления // ==================== УДАЛЕНИЕ С ПОДТВЕРЖДЕНИЕМ ====================
const confirmDialogOpen = ref(false) const confirmDialogOpen = ref(false)
const cardToDelete = ref(null) const cardToDelete = ref(null)
@@ -257,7 +258,7 @@ const handleConfirmDelete = async () => {
cardToDelete.value = null cardToDelete.value = null
} }
// Восстановление из архива // ==================== ВОССТАНОВЛЕНИЕ ====================
const handleRestore = async (cardId) => { const handleRestore = async (cardId) => {
const result = await cardsApi.setArchive(cardId, 0) const result = await cardsApi.setArchive(cardId, 0)
if (result.success) { if (result.success) {
@@ -265,18 +266,17 @@ const handleRestore = async (cardId) => {
} }
} }
// Восстановление через панель редактирования
const handleRestoreFromPanel = async (cardId) => { const handleRestoreFromPanel = async (cardId) => {
await handleRestore(cardId) await handleRestore(cardId)
closePanel() closePanel()
} }
// Инициализация // ==================== ИНИЦИАЛИЗАЦИЯ ====================
onMounted(() => { onMounted(async () => {
fetchData() await store.init()
if (window.lucide) { await fetchCards()
window.lucide.createIcons()
} if (window.lucide) window.lucide.createIcons()
}) })
</script> </script>
@@ -304,6 +304,14 @@ onMounted(() => {
flex-wrap: wrap; flex-wrap: wrap;
} }
/* Разделитель между проектом и отделами */
.filter-divider {
width: 1px;
height: 24px;
background: rgba(255, 255, 255, 0.1);
margin: 0 8px;
}
/* Кнопка фильтра */ /* Кнопка фильтра */
.filter-tag { .filter-tag {
padding: 6px 12px; padding: 6px 12px;

View File

@@ -9,6 +9,12 @@
<Header title="Доска задач"> <Header title="Доска задач">
<template #filters> <template #filters>
<div class="filters"> <div class="filters">
<!-- Выбор проекта -->
<ProjectSelector @change="onProjectChange" />
<div class="filter-divider"></div>
<!-- Фильтр по отделам -->
<button <button
class="filter-tag" class="filter-tag"
:class="{ active: activeDepartment === null }" :class="{ active: activeDepartment === null }"
@@ -17,7 +23,7 @@
Все Все
</button> </button>
<button <button
v-for="dept in departments" v-for="dept in store.departments"
:key="dept.id" :key="dept.id"
class="filter-tag" class="filter-tag"
:class="{ active: activeDepartment === dept.id }" :class="{ active: activeDepartment === dept.id }"
@@ -52,10 +58,11 @@
<Board <Board
ref="boardRef" ref="boardRef"
:active-department="activeDepartment" :active-department="activeDepartment"
:departments="departments" :departments="store.departments"
:labels="labels" :labels="store.labels"
:columns="columns" :columns="store.columns"
:cards="cards" :cards="cards"
:done-column-id="store.doneColumnId"
@stats-updated="stats = $event" @stats-updated="stats = $event"
@open-task="openTaskPanel" @open-task="openTaskPanel"
@create-task="openNewTaskPanel" @create-task="openNewTaskPanel"
@@ -68,9 +75,10 @@
:show="panelOpen" :show="panelOpen"
:card="editingCard" :card="editingCard"
:column-id="editingColumnId" :column-id="editingColumnId"
:departments="departments" :done-column-id="store.doneColumnId"
:labels="labels" :departments="store.departments"
:users="users" :labels="store.labels"
:users="store.users"
@close="closePanel" @close="closePanel"
@save="handleSaveTask" @save="handleSaveTask"
@delete="handleDeleteTask" @delete="handleDeleteTask"
@@ -85,14 +93,34 @@ import Sidebar from '../components/Sidebar.vue'
import Header from '../components/Header.vue' import Header from '../components/Header.vue'
import Board from '../components/Board.vue' import Board from '../components/Board.vue'
import TaskPanel from '../components/TaskPanel.vue' import TaskPanel from '../components/TaskPanel.vue'
import { departmentsApi, labelsApi, columnsApi, cardsApi, usersApi } from '../api' import ProjectSelector from '../components/ProjectSelector.vue'
import { useProjectsStore } from '../stores/projects'
import { cardsApi } from '../api'
// Активный фильтр по отделу (null = все) // ==================== STORE ====================
// Восстанавливаем из localStorage const store = useProjectsStore()
// ==================== КАРТОЧКИ ====================
const cards = ref([])
// Загрузка карточек текущего проекта
const fetchCards = async () => {
if (!store.currentProjectId) return
const result = await cardsApi.getAll(store.currentProjectId)
if (result.success) cards.value = result.data
}
// При смене проекта — перезагружаем карточки
const onProjectChange = async () => {
activeDepartment.value = null
await fetchCards()
}
// ==================== ФИЛЬТР ПО ОТДЕЛАМ ====================
const savedDepartment = localStorage.getItem('activeDepartment') const savedDepartment = localStorage.getItem('activeDepartment')
const activeDepartment = ref(savedDepartment ? parseInt(savedDepartment) : null) const activeDepartment = ref(savedDepartment ? parseInt(savedDepartment) : null)
// Сохраняем в localStorage при изменении
watch(activeDepartment, (newVal) => { watch(activeDepartment, (newVal) => {
if (newVal === null) { if (newVal === null) {
localStorage.removeItem('activeDepartment') localStorage.removeItem('activeDepartment')
@@ -100,91 +128,60 @@ watch(activeDepartment, (newVal) => {
localStorage.setItem('activeDepartment', newVal.toString()) localStorage.setItem('activeDepartment', newVal.toString())
} }
}) })
// Статистика для шапки
// ==================== СТАТИСТИКА ====================
const stats = ref({ total: 0, inProgress: 0, done: 0 }) const stats = ref({ total: 0, inProgress: 0, done: 0 })
// Данные из API
const departments = ref([])
const labels = ref([])
const columns = ref([])
const cards = ref([])
const users = ref([])
// Загрузка всех данных из API параллельно // ==================== ПАНЕЛЬ РЕДАКТИРОВАНИЯ ====================
const fetchData = async () => {
try {
const [departmentsData, labelsData, columnsData, cardsData, usersData] = await Promise.all([
departmentsApi.getAll(),
labelsApi.getAll(),
columnsApi.getAll(),
cardsApi.getAll(),
usersApi.getAll()
])
if (departmentsData.success) departments.value = departmentsData.data
if (labelsData.success) labels.value = labelsData.data
if (columnsData.success) columns.value = columnsData.data
if (cardsData.success) cards.value = cardsData.data
if (usersData.success) users.value = usersData.data
} catch (error) {
console.error('Ошибка загрузки данных:', error)
}
}
// Выход из системы
// Ссылка на компонент Board для вызова его методов
const boardRef = ref(null) const boardRef = ref(null)
// Состояние панели редактирования
const panelOpen = ref(false) const panelOpen = ref(false)
// Редактируемая карточка (null = создание новой)
const editingCard = ref(null) const editingCard = ref(null)
// ID колонки для новой/редактируемой карточки
const editingColumnId = ref(null) const editingColumnId = ref(null)
// Открыть панель для редактирования существующей задачи
const openTaskPanel = ({ card, columnId }) => { const openTaskPanel = ({ card, columnId }) => {
editingCard.value = card editingCard.value = card
editingColumnId.value = columnId editingColumnId.value = columnId
panelOpen.value = true panelOpen.value = true
} }
// Открыть панель для создания новой задачи
const openNewTaskPanel = (columnId) => { const openNewTaskPanel = (columnId) => {
editingCard.value = null editingCard.value = null
editingColumnId.value = columnId editingColumnId.value = columnId
panelOpen.value = true panelOpen.value = true
} }
// Закрыть панель и сбросить состояние
const closePanel = () => { const closePanel = () => {
panelOpen.value = false panelOpen.value = false
editingCard.value = null editingCard.value = null
editingColumnId.value = null editingColumnId.value = null
} }
// Сохранить задачу через Board компонент
const handleSaveTask = async (taskData) => { const handleSaveTask = async (taskData) => {
if (!taskData.id && store.currentProjectId) {
taskData.id_project = store.currentProjectId
}
await boardRef.value?.saveTask(taskData, editingColumnId.value) await boardRef.value?.saveTask(taskData, editingColumnId.value)
closePanel() closePanel()
} }
// Удалить задачу через Board компонент
const handleDeleteTask = async (cardId) => { const handleDeleteTask = async (cardId) => {
await boardRef.value?.deleteTask(cardId, editingColumnId.value) await boardRef.value?.deleteTask(cardId, editingColumnId.value)
closePanel() closePanel()
} }
// Архивировать задачу через Board компонент
const handleArchiveTask = async (cardId) => { const handleArchiveTask = async (cardId) => {
await boardRef.value?.archiveTask(cardId) await boardRef.value?.archiveTask(cardId)
closePanel() closePanel()
} }
// Инициализация при монтировании // ==================== ИНИЦИАЛИЗАЦИЯ ====================
onMounted(() => { onMounted(async () => {
fetchData() // Инициализируем store (загрузит проекты, departments, labels, users)
if (window.lucide) { await store.init()
window.lucide.createIcons() // Загружаем карточки
} await fetchCards()
if (window.lucide) window.lucide.createIcons()
}) })
</script> </script>
@@ -195,7 +192,6 @@ onMounted(() => {
min-height: 100vh; min-height: 100vh;
} }
/* Основная область контента */ /* Основная область контента */
.main-wrapper { .main-wrapper {
flex: 1; flex: 1;
@@ -213,6 +209,14 @@ onMounted(() => {
flex-wrap: wrap; flex-wrap: wrap;
} }
/* Разделитель между проектом и отделами */
.filter-divider {
width: 1px;
height: 24px;
background: rgba(255, 255, 255, 0.1);
margin: 0 8px;
}
/* Кнопка фильтра */ /* Кнопка фильтра */
.filter-tag { .filter-tag {
padding: 6px 12px; padding: 6px 12px;

View File

@@ -11,7 +11,7 @@
Target Server Version : 90200 (9.2.0) Target Server Version : 90200 (9.2.0)
File Encoding : 65001 File Encoding : 65001
Date: 14/01/2026 08:21:46 Date: 14/01/2026 10:46:09
*/ */
SET NAMES utf8mb4; SET NAMES utf8mb4;
@@ -45,7 +45,7 @@ CREATE TABLE `accounts_session` (
`ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 40 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; ) ENGINE = InnoDB AUTO_INCREMENT = 46 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ---------------------------- -- ----------------------------
-- Table structure for cards_task -- Table structure for cards_task
@@ -68,7 +68,7 @@ CREATE TABLE `cards_task` (
`descript_full` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, `descript_full` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`id_project` int NULL DEFAULT NULL, `id_project` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; ) ENGINE = InnoDB AUTO_INCREMENT = 46 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ---------------------------- -- ----------------------------
-- Table structure for columns -- Table structure for columns
@@ -78,7 +78,8 @@ CREATE TABLE `columns` (
`id` int NOT NULL AUTO_INCREMENT, `id` int NOT NULL AUTO_INCREMENT,
`name_columns` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `name_columns` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`color` varchar(7) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `color` varchar(7) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`id_project` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `id_project` int NULL DEFAULT NULL,
`id_order` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 56 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; ) ENGINE = InnoDB AUTO_INCREMENT = 56 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
@@ -92,7 +93,7 @@ CREATE TABLE `departments` (
`color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`id_project` int NULL DEFAULT NULL, `id_project` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ---------------------------- -- ----------------------------
-- Table structure for labels -- Table structure for labels
@@ -114,8 +115,8 @@ CREATE TABLE `project` (
`id` int NOT NULL AUTO_INCREMENT, `id` int NOT NULL AUTO_INCREMENT,
`id_order` int NULL DEFAULT NULL, `id_order` int NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`id_ready` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `id_ready` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; ) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;