Добавление проектов
Добавили возможность создавания разных проектов.
This commit is contained in:
86
backend/api/project.php
Normal file
86
backend/api/project.php
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
241
backend/app/class/enity/class_project.php
Normal file
241
backend/app/class/enity/class_project.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем в БД
|
// Обновляем в БД
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
137
front_vue/package-lock.json
generated
137
front_vue/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
BIN
front_vue/public/favicon.ico
Normal file
BIN
front_vue/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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: () => []
|
||||||
|
|||||||
137
front_vue/src/components/ProjectSelector.vue
Normal file
137
front_vue/src/components/ProjectSelector.vue
Normal 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>
|
||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
145
front_vue/src/stores/projects.js
Normal file
145
front_vue/src/stores/projects.js
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user