From b747b586b30975d583d243fc79b50ec54f549662 Mon Sep 17 00:00:00 2001 From: Veronika Minova Date: Sat, 22 Aug 2026 23:48:15 +0200 Subject: [PATCH] added CRUD for Projects, added set project for report --- backend/src/API.php | 204 +++++++++++++++++++++++--- backend/src/Init.php | 1 + backend/src/Maintenance.php | 123 ++++++++++++++++ backend/src/Models/Projects.php | 108 ++++++++++++++ backend/src/Models/Reports.php | 41 +++++- frontend/src/App.vue | 29 +++- frontend/src/assets/css/style.css | 156 +++++++++++++++++++- frontend/src/backend.js | 38 +++-- frontend/src/components/ReportBox.vue | 10 +- frontend/src/dragDropSettings.js | 58 ++++++++ frontend/src/projects.js | 77 ++++++++++ frontend/src/router.js | 2 + frontend/src/views/Archive.vue | 5 +- frontend/src/views/BugAdd.vue | 49 +++++-- frontend/src/views/Dashboard.vue | 44 ++++-- frontend/src/views/Projects.vue | 183 +++++++++++++++++++++++ frontend/src/views/Report.vue | 55 ++++++- 17 files changed, 1118 insertions(+), 65 deletions(-) create mode 100644 backend/src/Models/Projects.php create mode 100644 frontend/src/dragDropSettings.js create mode 100644 frontend/src/projects.js create mode 100644 frontend/src/views/Projects.vue diff --git a/backend/src/API.php b/backend/src/API.php index c5284ab..786db88 100644 --- a/backend/src/API.php +++ b/backend/src/API.php @@ -8,6 +8,7 @@ use TPsoft\APIlite\APIlite; use TPsoft\BugreportBackend\Models\Reports; use TPsoft\BugreportBackend\Models\Attachments; use TPsoft\BugreportBackend\Models\Options; +use TPsoft\BugreportBackend\Models\Projects; class API extends APIlite @@ -18,21 +19,22 @@ class API extends APIlite * @param string $title * @param string $description * @param int $status 0 = Uncategorized, 1 = Waiting, 2 = InProgress, 3 = Blocked, 4 = Archived - * @param string $group + * @param int $project_id * @param int $priority 0 = Low, 1 = Medium, 2 = High, 3 = Urgent * * @return int */ - public function add(string $title, string $description, int $status = 0, ?string $group = null, int $priority = 0): int + public function add(string $title, string $description, int $status = 0, ?int $project_id = null, int $priority = 0): int { $status = intval($status); $priority = intval($priority); + $project_id = $this->resolveProjectId($project_id); $reports = new Reports(); $report_id = $reports->report(null, [ 'report_title' => $title, 'report_description' => $description, 'report_status' => $status, - 'report_group' => $group, + 'project_id' => $project_id, 'report_priority' => $priority, ]); return $report_id; @@ -48,6 +50,12 @@ class API extends APIlite */ public function update(int $report_id, array $report_data): bool { + if (array_key_exists('report_group', $report_data)) { + throw new \InvalidArgumentException('Field "report_group" is no longer supported'); + } + if (array_key_exists('project_id', $report_data)) { + $report_data['project_id'] = $this->resolveProjectId(intval($report_data['project_id'])); + } $reports = new Reports(); $suc = $reports->report($report_id, $report_data); return $suc !== false; @@ -77,7 +85,9 @@ class API extends APIlite public function get(int $report_id): array { $reports = new Reports(); - return $reports->report($report_id); + $report = $reports->reportWithProject($report_id); + if ($report === false) throw new \InvalidArgumentException('Report not found'); + return $report; } /** @@ -85,20 +95,17 @@ class API extends APIlite * * @param array $status 0 = Uncategorized, 1 = Waiting, 2 = InProgress, 3 = Blocked, 4 = Archived * @param int $page Pagination from 0 + * @param int $project_id Optional project filter * * @return array */ - public function getAll(?array $status = null, int $page = 0): array + public function getAll(?array $status = null, int $page = 0, ?int $project_id = null): array { $page = intval($page); $reports = new Reports(); if ($status === null) $status = array(0, 1, 2, 3); - $ret = $reports->search('reports') - ->where(['report_status' => $status]) - ->order(array('report_priority' => 'DESC', 'ordnum' => 'ASC')) - ->limit($page * 10, 10) - ->toArray(); - return $ret; + if (!is_null($project_id)) $this->requireProject($project_id, false); + return $reports->getListWithProjects($status, $page, $project_id); } /** @@ -106,13 +113,14 @@ class API extends APIlite * * @param array $status 0 = Uncategorized, 1 = Waiting, 2 = InProgress, 3 = Blocked, 4 = Archived * @param int $page Pagination from 0 + * @param int $project_id Optional project filter * * @return array */ - public function getAllGrouped(?array $status = null, int $page = 0): array + public function getAllGrouped(?array $status = null, int $page = 0, ?int $project_id = null): array { $page = intval($page); - $all = $this->getAll($status, $page); + $all = $this->getAll($status, $page, $project_id); $groups = []; foreach ($all as $report) { $groups[$report['report_status']][] = $report; @@ -124,19 +132,16 @@ class API extends APIlite * Get archived reports * * @param int $page Pagination from 0 + * @param int $project_id Optional project filter * * @return array */ - public function getArchived(int $page = 0): array + public function getArchived(int $page = 0, ?int $project_id = null): array { $page = intval($page); + if (!is_null($project_id)) $this->requireProject($project_id, false); $reports = new Reports(); - $ret = $reports->search('reports') - ->where(['report_status' => 4]) - ->order(array('created_dt' => 'DESC')) - ->limit($page * 10, 10) - ->toArray(); - return $ret; + return $reports->getListWithProjects(array(4), $page, $project_id, true); } /** @@ -171,6 +176,165 @@ class API extends APIlite return $suc !== false; } + /** + * Get projects + * + * @param bool $include_archived Include archived projects + * + * @return array + */ + public function projectGetAll(bool $include_archived = false): array + { + $projects = new Projects(); + return $projects->getListWithReportCount($include_archived); + } + + /** + * Get project + * + * @param int $project_id + * + * @return array + */ + public function projectGet(int $project_id): array + { + return $this->requireProject($project_id, false); + } + + /** + * Add project + * + * @param string $name + * @param string $code + * @param string $color Hex color in #RRGGBB format + * + * @return int + */ + public function projectAdd(string $name, string $code, string $color): int + { + $name = $this->validateProjectName($name); + $code = $this->validateProjectCode($code); + $color = $this->validateProjectColor($color); + $projects = new Projects(); + $project_id = $projects->project(null, array( + 'project_name' => $name, + 'project_code' => $code, + 'project_color' => $color, + 'project_active' => 1, + 'project_system' => 0, + )); + if ($project_id === false) throw new \RuntimeException('Project was not created'); + return intval($project_id); + } + + /** + * Update project + * + * @param int $project_id + * @param string $name + * @param string $color Hex color in #RRGGBB format + * + * @return bool + */ + public function projectUpdate(int $project_id, string $name, string $color): bool + { + $project = $this->requireEditableProject($project_id); + $name = $this->validateProjectName($name, $project_id); + $color = $this->validateProjectColor($color); + $projects = new Projects(); + $suc = $projects->project($project['project_id'], array( + 'project_name' => $name, + 'project_color' => $color, + )); + return $suc !== false; + } + + /** + * Archive or restore project + * + * @param int $project_id + * @param bool $archived True to archive, false to restore + * + * @return bool + */ + public function projectArchive(int $project_id, bool $archived = true): bool + { + $project = $this->requireEditableProject($project_id); + $projects = new Projects(); + $suc = $projects->project($project['project_id'], array( + 'project_active' => $archived ? 0 : 1, + )); + return $suc !== false; + } + + private function resolveProjectId(?int $project_id = null): int + { + if (is_null($project_id)) { + $projects = new Projects(); + $project = $projects->projectBy('project_code', 'unassigned'); + if ($project === false) throw new \RuntimeException('Unassigned project not found'); + return intval($project['project_id']); + } + $project = $this->requireProject($project_id); + return intval($project['project_id']); + } + + private function requireProject(int $project_id, bool $require_active = true): array + { + $projects = new Projects(); + $project = $projects->project($project_id); + if ($project === false) throw new \InvalidArgumentException('Project not found'); + if ($require_active && intval($project['project_active']) !== 1) { + throw new \InvalidArgumentException('Archived project cannot be assigned'); + } + return $project; + } + + private function requireEditableProject(int $project_id): array + { + $project = $this->requireProject($project_id, false); + if (intval($project['project_system']) === 1) { + throw new \InvalidArgumentException('System project cannot be changed'); + } + return $project; + } + + private function validateProjectName(string $name, ?int $project_id = null): string + { + $name = trim($name); + if (strlen($name) <= 0 || strlen($name) > 255) { + throw new \InvalidArgumentException('Project name must contain 1 to 255 characters'); + } + $projects = new Projects(); + $existing = $projects->projectBy('project_name', $name); + if ($existing !== false && intval($existing['project_id']) !== intval($project_id)) { + throw new \InvalidArgumentException('Project name already exists'); + } + return $name; + } + + private function validateProjectCode(string $code): string + { + $code = strtolower(trim($code)); + if (!preg_match('/^[a-z0-9][a-z0-9_-]{0,63}$/', $code)) { + throw new \InvalidArgumentException('Project code has invalid format'); + } + $projects = new Projects(); + if ($projects->projectBy('project_code', $code) !== false) { + throw new \InvalidArgumentException('Project code already exists'); + } + return $code; + } + + private function validateProjectColor(string $color): string + { + $color = strtoupper(trim($color)); + if (!preg_match('/^#[0-9A-F]{6}$/', $color)) { + throw new \InvalidArgumentException('Project color must use #RRGGBB format'); + } + return $color; + } + /** * Add report attachment * diff --git a/backend/src/Init.php b/backend/src/Init.php index ee16619..be4ed36 100644 --- a/backend/src/Init.php +++ b/backend/src/Init.php @@ -11,6 +11,7 @@ if (Configuration::DB_TYPE == 'mysql') { $dbh = new DBmodel(sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', Configuration::DB_HOST, Configuration::DB_NAME), Configuration::DB_USER, Configuration::DB_PASS); } else if (Configuration::DB_TYPE == 'sqlite') { $dbh = new DBmodel(sprintf('sqlite:%s', Configuration::DB_FILEPATH)); + $dbh->query('PRAGMA foreign_keys = ON'); } else { throw new Exception('Unknown database type'); } diff --git a/backend/src/Maintenance.php b/backend/src/Maintenance.php index ed2dca1..34a6c79 100644 --- a/backend/src/Maintenance.php +++ b/backend/src/Maintenance.php @@ -41,6 +41,129 @@ class Maintenance extends \TPsoft\DBmodel\Maintenance $this->dbver(3); $dbver = 3; } + if ($dbver == 3) { + $this->migrateProjects(); + $dbver = 4; + } + } + + private function migrateProjects(): void + { + $db_type = $this->dbh->getDBtype(); + if ($db_type == 'sqlite') { + $this->migrateProjectsSqlite(); + return; + } + if ($db_type == 'mysql') { + $this->migrateProjectsMysql(); + return; + } + throw new \Exception('Unknown DB type: ' . $db_type); + } + + private function migrateProjectsSqlite(): void + { + $pdo = $this->dbh->dbh; + $pdo->beginTransaction(); + try { + $this->queryOrFail(' + CREATE TABLE `projects` ( + `project_id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + `project_name` VARCHAR(255) COLLATE NOCASE NOT NULL UNIQUE, + `project_code` VARCHAR(64) NOT NULL UNIQUE, + `project_color` VARCHAR(7) NOT NULL, + `project_active` INTEGER NOT NULL DEFAULT 1, + `project_system` INTEGER NOT NULL DEFAULT 0, + `created_dt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_dt` DATETIME DEFAULT NULL + ) + '); + $this->insertUnassignedProject(); + $unassigned_id = $this->unassignedProjectId(); + + $this->queryOrFail(' + CREATE TABLE `reports_v4` ( + `report_id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + `report_title` VARCHAR(255) DEFAULT NULL, + `report_description` TEXT DEFAULT NULL, + `report_status` INTEGER DEFAULT NULL, + `project_id` INTEGER NOT NULL, + `report_priority` INTEGER DEFAULT NULL, + `ordnum` INTEGER DEFAULT NULL, + `created_dt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`project_id`) REFERENCES `projects` (`project_id`) ON DELETE RESTRICT + ) + '); + $this->queryOrFail(sprintf( + 'INSERT INTO `reports_v4` (`report_id`, `report_title`, `report_description`, `report_status`, `project_id`, `report_priority`, `ordnum`, `created_dt`) + SELECT `report_id`, `report_title`, `report_description`, `report_status`, %d, `report_priority`, `ordnum`, `created_dt` FROM `reports`', + $unassigned_id + )); + $this->queryOrFail('DROP TABLE `reports`'); + $this->queryOrFail('ALTER TABLE `reports_v4` RENAME TO `reports`'); + $this->queryOrFail('CREATE INDEX `reports_project_id` ON `reports` (`project_id`)'); + $this->dbver(4); + $pdo->commit(); + } catch (\Throwable $error) { + if ($pdo->inTransaction()) $pdo->rollBack(); + throw $error; + } + } + + private function migrateProjectsMysql(): void + { + $this->queryOrFail(' + CREATE TABLE `projects` ( + `project_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `project_name` VARCHAR(255) NOT NULL UNIQUE, + `project_code` VARCHAR(64) NOT NULL UNIQUE, + `project_color` VARCHAR(7) NOT NULL, + `project_active` TINYINT(1) NOT NULL DEFAULT 1, + `project_system` TINYINT(1) NOT NULL DEFAULT 0, + `created_dt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_dt` DATETIME DEFAULT NULL + ) ENGINE=InnoDB + '); + $this->insertUnassignedProject(); + $unassigned_id = $this->unassignedProjectId(); + $this->queryOrFail('ALTER TABLE `reports` ADD `project_id` INT DEFAULT NULL AFTER `report_status`'); + $this->queryOrFail(sprintf('UPDATE `reports` SET `project_id` = %d', $unassigned_id)); + $this->queryOrFail('ALTER TABLE `reports` MODIFY `project_id` INT NOT NULL'); + $this->queryOrFail('ALTER TABLE `reports` DROP COLUMN `report_group`'); + $this->queryOrFail('CREATE INDEX `reports_project_id` ON `reports` (`project_id`)'); + $this->queryOrFail(' + ALTER TABLE `reports` + ADD CONSTRAINT `reports_project_id_fk` + FOREIGN KEY (`project_id`) REFERENCES `projects` (`project_id`) ON DELETE RESTRICT + '); + $this->dbver(4); + } + + private function insertUnassignedProject(): void + { + $this->queryOrFail(sprintf( + 'INSERT INTO `projects` (`project_name`, `project_code`, `project_color`, `project_active`, `project_system`, `created_dt`) + VALUES (%s, %s, %s, 1, 1, %s)', + $this->dbh->quote('Nezaradené'), + $this->dbh->quote('unassigned'), + $this->dbh->quote('#797979'), + $this->dbh->quote(date('Y-m-d H:i:s')) + )); + } + + private function unassignedProjectId(): int + { + return intval($this->dbh->getOne(sprintf( + 'SELECT `project_id` FROM `projects` WHERE `project_code` = %s', + $this->dbh->quote('unassigned') + ))); + } + + private function queryOrFail(string $query): void + { + if ($this->dbh->query($query) === false) { + throw new \RuntimeException($this->dbh->errorMessage()); + } } protected function settings(string $key, ?string $value = null): string|false diff --git a/backend/src/Models/Projects.php b/backend/src/Models/Projects.php new file mode 100644 index 0000000..7db8680 --- /dev/null +++ b/backend/src/Models/Projects.php @@ -0,0 +1,108 @@ + array( + 'name' => 'projects', + 'primary_key_name' => 'project_id', + 'allow_attributes' => array( + 'project_name' => 'VARCHAR(255)', + 'project_code' => 'VARCHAR(64)', + 'project_color' => 'VARCHAR(7)', + 'project_active' => 'INTEGER', + 'project_system' => 'INTEGER', + 'created_dt' => 'DATETIME', + 'updated_dt' => 'DATETIME' + ) + ), + ); + + public function exist($primary_key = null) { + return $this->existRecord('projects', $primary_key); + } + + public function project($primary_key = null, $data = array()) { + if (is_null($primary_key) + && !isset($data['created_dt'])) + { + $data['created_dt'] = date('Y-m-d H:i:s'); + } + if (!is_null($primary_key) + && is_array($data) + && count($data) > 0 + && !isset($data['updated_dt'])) + { + $data['updated_dt'] = date('Y-m-d H:i:s'); + } + return $this->record('projects', $primary_key, $data); + } + + public function projectBy($colname, $colvalue) { + return $this->recordBy('projects', $colname, $colvalue); + } + + public function projectSave($data = array()) { + return $this->project($this->exist($data) ? $data : null, $data); + } + + public function projectEmpty() { + return $this->recordEmpty('projects'); + } + + public function projectAttributes() { + return $this->typesAttributes('projects'); + } + + public function projectCount() { + return $this->count('projects'); + } + + public function getList($search = array(), $reverse = false, $concat_or = false) { + return $this->search('projects') + ->where($search, $concat_or) + ->order(array('project_id' => $reverse ? 'DESC' : 'ASC')) + ->toArray(); + } + + public function getListOrganize($cola_name, $search = array(), $reverse = false, $concat_or = false) { + $all = $this->getList($search, $reverse, $concat_or); + $ret = array(); + if (is_array($all)) foreach ($all as $key => $row) { + $ret[$row[$cola_name]] = $row; + } + return $ret; + } + + public function getListByID($search = array(), $reverse = false, $concat_or = false) { + return $this->getListOrganize('project_id', $search, $reverse, $concat_or); + } + + public function projectCombo($col_key, $col_value, $add_empty = false) { + return $this->search('projects') + ->toCombo($col_key, $col_value, $add_empty); + } + + public function getListWithReportCount($include_archived = false) { + $where = $include_archived ? '' : ' WHERE `project_active` = 1'; + return $this->getAll( + 'SELECT `projects`.*,' + . ' (SELECT COUNT(*) FROM `reports` WHERE `reports`.`project_id` = `projects`.`project_id`) AS `report_count`' + . ' FROM `projects`' + . $where + . ' ORDER BY `project_active` DESC, `project_system` DESC, `project_name` ASC' + ); + } + +} + +?> diff --git a/backend/src/Models/Reports.php b/backend/src/Models/Reports.php index ad3dc57..50ad120 100644 --- a/backend/src/Models/Reports.php +++ b/backend/src/Models/Reports.php @@ -19,7 +19,7 @@ class Reports extends \TPsoft\DBmodel\DBmodel { 'report_title' => 'VARCHAR(255)', 'report_description' => 'TEXT', 'report_status' => 'INTEGER', - 'report_group' => 'VARCHAR(255)', + 'project_id' => 'INTEGER', 'report_priority' => 'INTEGER', 'created_dt' => 'DATETIME', 'ordnum' => 'INTEGER' @@ -85,6 +85,45 @@ class Reports extends \TPsoft\DBmodel\DBmodel { ->toCombo($col_key, $col_value, $add_empty); } + public function reportWithProject($report_id) { + $this->import(new Projects()); + return $this->getRow(sprintf( + 'SELECT `reports`.*, `projects`.`project_name`, `projects`.`project_code`,' + . ' `projects`.`project_color`, `projects`.`project_active`' + . ' FROM %s' + . ' INNER JOIN %s ON `projects`.`project_id` = `reports`.`project_id`' + . ' WHERE `reports`.`report_id` = %d LIMIT 1', + $this->tables['reports']['name'], + $this->tables['projects']['name'], + intval($report_id) + )); + } + + public function getListWithProjects($status, $page = 0, $project_id = null, $archived = false) { + $status = array_map('intval', $status); + if (count($status) <= 0) return array(); + $where = '`reports`.`report_status` IN (' . implode(', ', $status) . ')'; + if (!is_null($project_id)) { + $where .= ' AND `reports`.`project_id` = ' . intval($project_id); + } + $order = $archived + ? '`reports`.`created_dt` DESC' + : '`reports`.`report_priority` DESC, `reports`.`ordnum` ASC'; + $this->import(new Projects()); + return $this->getAll(sprintf( + 'SELECT `reports`.*, `projects`.`project_name`, `projects`.`project_code`,' + . ' `projects`.`project_color`, `projects`.`project_active`' + . ' FROM %s' + . ' INNER JOIN %s ON `projects`.`project_id` = `reports`.`project_id`' + . ' WHERE %s ORDER BY %s LIMIT %d, 10', + $this->tables['reports']['name'], + $this->tables['projects']['name'], + $where, + $order, + intval($page) * 10 + )); + } + } ?> diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 0840034..4c24a06 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -15,7 +15,20 @@ v-model="short_bug" @keyup.enter="onShortBugEnter" /> - @@ -44,6 +57,10 @@ > Archív + + Projekty API @@ -66,6 +83,12 @@ import { getDragDropPreference, setDragDropEnabled, } from "./dragDropSettings"; +import { + activeProjects, + loadProjects, + selectedProjectId, + setSelectedProject, +} from "./projects"; const short_bug = ref(""); const dragMediaQuery = getDragDropMediaQuery(); @@ -88,14 +111,16 @@ function onShortBugEnter(event) { } function shortBugAdd() { + if (selectedProjectId.value == null) return; let content = short_bug.value; short_bug.value = ""; - backend.add(content, "", "0", "0", "1").then(() => { + backend.add(content, "", "0", selectedProjectId.value, "1").then(() => { events.emit("reports-changed"); }); } onMounted(() => { + loadProjects().catch((error) => console.log(error)); if (dragMediaQuery != null) { dragMediaQuery.addEventListener("change", updateAutomaticDragDropState); } diff --git a/frontend/src/assets/css/style.css b/frontend/src/assets/css/style.css index fd3ae26..91798d8 100644 --- a/frontend/src/assets/css/style.css +++ b/frontend/src/assets/css/style.css @@ -75,7 +75,8 @@ h1 { } button, -.button { +.button, +select { border-radius: 8px; border: 1px solid var(--color-bg0); padding: 0.6em 1.2em; @@ -88,16 +89,27 @@ button, transition: all 0.3s; } button:hover, -.button:hover { +.button:hover, +select:hover { border-color: var(--color-bg1); background-color: var(--color-bg0); } button:focus, .button:focus, +select:focus, button:focus-visible, -.button:focus-visible { +.button:focus-visible, +select:focus-visible { outline: 4px auto -webkit-focus-ring-color; } +select:disabled { + opacity: 0.6; + cursor: not-allowed; +} +select option { + background-color: var(--color-bg); + color: var(--color-text0); +} .card { padding: 2em; @@ -141,13 +153,21 @@ button:focus-visible, justify-content: center } #header .short-bug input { - width: 80%; + width: auto; + flex-grow: 1; padding: 5px; border: 1px solid #ccc; border-radius: 3px; background-color: var(--color-bg2); color: var(--color-text0); } +#header .short-bug select { + max-width: 180px; + margin-left: 5px; + padding: 5px 10px; + border-color: var(--color-text0); + border-radius: 5px; +} #header .menu { display: flex; flex-direction: row; @@ -190,7 +210,7 @@ button:focus-visible, background-color: var(--color-bg1); } #header .short-bug button { - margin: 0px; + margin: 0 0 0 5px; /* height: 30px; */ } @@ -237,11 +257,27 @@ button:focus-visible, #header .short-bug { flex-direction: column; } + #header .short-bug input, + #header .short-bug select, + #header .short-bug button { + width: 100%; + max-width: none; + margin: 3px 0; + box-sizing: border-box; + } } /* ---------------------------------------------------- 03 - DASHBOARD */ +.dashboard-toolbar { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + padding: 10px 20px; + background-color: var(--color-bg2); +} #dashboard { display: flex; flex-direction: row; @@ -307,6 +343,15 @@ button:focus-visible, text-align: left; padding: 5px; } +.project-color { + display: inline-block; + width: 12px; + height: 12px; + margin-right: 5px; + border: 1px solid var(--color-text0); + border-radius: 50%; + vertical-align: middle; +} #dashboard .report .report-id { text-align: center; padding: 5px; @@ -571,12 +616,109 @@ button:focus-visible, /* margin-right: 20px; */ align-items: center; } +#report .report-header select { + max-width: 180px; +} +.project-label { + display: inline-flex; + align-items: center; +} + +/* ---------------------------------------------------- + 09 - PROJECTS + */ +#projects { + max-width: 1100px; + margin: 0 auto; + padding: 20px; +} +#projects .project-new { + display: flex; + align-items: end; + gap: 15px; + padding: 15px; + background-color: var(--color-bg2); + border-radius: 5px; +} +#projects .project-new .form-group { + flex: 1; +} +#projects .project-new .project-color-field { + flex: 0 0 80px; +} +#projects .project-new input[type="color"] { + min-height: 34px; + padding: 2px; +} +#projects .error-message { + padding: 10px; + background-color: var(--color-bgRed); + border-radius: 5px; +} +#projects .project-list { + margin-top: 20px; +} +#projects .project-row { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 10px; + padding: 12px; + background-color: var(--color-bg2); + border-left: 5px solid var(--color-bg1); + border-radius: 5px; +} +#projects .project-row.archived { + opacity: 0.65; + border-left-color: var(--color-bgGray); +} +#projects .project-row > .project-color { + width: 20px; + height: 20px; + flex: 0 0 20px; +} +#projects .project-data { + display: flex; + flex-direction: column; + flex: 1; +} +#projects .project-data input[type="text"] { + padding: 5px; +} +#projects .project-data input[type="color"] { + margin-top: 5px; +} +#projects .project-meta { + display: flex; + flex-direction: column; + min-width: 110px; +} +#projects .project-actions { + display: flex; + gap: 5px; +} +#projects .project-system { + font-style: italic; +} +@media (max-width: 800px) { + #projects .project-new, + #projects .project-row { + flex-direction: column; + align-items: stretch; + } + #projects .project-actions { + flex-wrap: wrap; + } +} #report .report-header div span { background-color: var(--color-bg0); color: var(--color-text0); padding: 2px 10px; align-items: center; } +#report .report-header div span.project-color { + padding: 0; +} #report .report-header div strong { background-color: var(--color-bg1); color: var(--color-text0); @@ -644,6 +786,10 @@ button:focus-visible, background-color: var(--color-bg2); color: var(--color-text0); } +.form-group select { + width: 99%; + box-sizing: border-box; +} .form-actions { margin-top: 10px; text-align: right; diff --git a/frontend/src/backend.js b/frontend/src/backend.js index e0ef05c..0112efb 100644 --- a/frontend/src/backend.js +++ b/frontend/src/backend.js @@ -2,7 +2,7 @@ * Generated by APIlite * https://gitea.tpsoft.org/TPsoft.org/APIlite * - * 2026-07-07 22:15:38 */ + * 2026-08-22 20:42:16 */ class backend { endpoint = import.meta.env.VITE_BACKENDAPI_URL; @@ -154,8 +154,8 @@ class backend { return this.callPromise('__HELP__', {}); } - add(title, description, status, group, priority) { - return this.callPromise('add', {title: title, description: description, status: status, group: group, priority: priority}); + add(title, description, status, project_id, priority) { + return this.callPromise('add', {title: title, description: description, status: status, project_id: project_id, priority: priority}); } update(report_id, report_data) { @@ -170,16 +170,16 @@ class backend { return this.callPromise('get', {report_id: report_id}); } - getAll(status, page) { - return this.callPromise('getAll', {status: status, page: page}); + getAll(status, page, project_id) { + return this.callPromise('getAll', {status: status, page: page, project_id: project_id}); } - getAllGrouped(status, page) { - return this.callPromise('getAllGrouped', {status: status, page: page}); + getAllGrouped(status, page, project_id) { + return this.callPromise('getAllGrouped', {status: status, page: page, project_id: project_id}); } - getArchived(page) { - return this.callPromise('getArchived', {page: page}); + getArchived(page, project_id) { + return this.callPromise('getArchived', {page: page, project_id: project_id}); } updateOrdNum(ordnums) { @@ -190,6 +190,26 @@ class backend { return this.callPromise('updateStatus', {report_id: report_id, status: status}); } + projectGetAll(include_archived) { + return this.callPromise('projectGetAll', {include_archived: include_archived}); + } + + projectGet(project_id) { + return this.callPromise('projectGet', {project_id: project_id}); + } + + projectAdd(name, code, color) { + return this.callPromise('projectAdd', {name: name, code: code, color: color}); + } + + projectUpdate(project_id, name, color) { + return this.callPromise('projectUpdate', {project_id: project_id, name: name, color: color}); + } + + projectArchive(project_id, archived) { + return this.callPromise('projectArchive', {project_id: project_id, archived: archived}); + } + attachmentAdd(report_id, attachment_type, attachment_content) { return this.callPromise('attachmentAdd', {report_id: report_id, attachment_type: attachment_type, attachment_content: attachment_content}); } diff --git a/frontend/src/components/ReportBox.vue b/frontend/src/components/ReportBox.vue index 5b449e4..5513572 100644 --- a/frontend/src/components/ReportBox.vue +++ b/frontend/src/components/ReportBox.vue @@ -5,7 +5,8 @@ defineProps({ description: String, date: String, priority: Number, - group: String, + projectName: String, + projectColor: String, });