added CRUD for Projects,

added set project for report
This commit is contained in:
2026-08-22 23:48:15 +02:00
parent 369b3b7728
commit b747b586b3
17 changed files with 1118 additions and 65 deletions
+184 -20
View File
@@ -8,6 +8,7 @@ use TPsoft\APIlite\APIlite;
use TPsoft\BugreportBackend\Models\Reports; use TPsoft\BugreportBackend\Models\Reports;
use TPsoft\BugreportBackend\Models\Attachments; use TPsoft\BugreportBackend\Models\Attachments;
use TPsoft\BugreportBackend\Models\Options; use TPsoft\BugreportBackend\Models\Options;
use TPsoft\BugreportBackend\Models\Projects;
class API extends APIlite class API extends APIlite
@@ -18,21 +19,22 @@ class API extends APIlite
* @param string $title * @param string $title
* @param string $description * @param string $description
* @param int $status 0 = Uncategorized, 1 = Waiting, 2 = InProgress, 3 = Blocked, 4 = Archived * @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 * @param int $priority 0 = Low, 1 = Medium, 2 = High, 3 = Urgent
* *
* @return int * @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); $status = intval($status);
$priority = intval($priority); $priority = intval($priority);
$project_id = $this->resolveProjectId($project_id);
$reports = new Reports(); $reports = new Reports();
$report_id = $reports->report(null, [ $report_id = $reports->report(null, [
'report_title' => $title, 'report_title' => $title,
'report_description' => $description, 'report_description' => $description,
'report_status' => $status, 'report_status' => $status,
'report_group' => $group, 'project_id' => $project_id,
'report_priority' => $priority, 'report_priority' => $priority,
]); ]);
return $report_id; return $report_id;
@@ -48,6 +50,12 @@ class API extends APIlite
*/ */
public function update(int $report_id, array $report_data): bool 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(); $reports = new Reports();
$suc = $reports->report($report_id, $report_data); $suc = $reports->report($report_id, $report_data);
return $suc !== false; return $suc !== false;
@@ -77,7 +85,9 @@ class API extends APIlite
public function get(int $report_id): array public function get(int $report_id): array
{ {
$reports = new Reports(); $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 array $status 0 = Uncategorized, 1 = Waiting, 2 = InProgress, 3 = Blocked, 4 = Archived
* @param int $page Pagination from 0 * @param int $page Pagination from 0
* @param int $project_id Optional project filter
* *
* @return array * @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); $page = intval($page);
$reports = new Reports(); $reports = new Reports();
if ($status === null) $status = array(0, 1, 2, 3); if ($status === null) $status = array(0, 1, 2, 3);
$ret = $reports->search('reports') if (!is_null($project_id)) $this->requireProject($project_id, false);
->where(['report_status' => $status]) return $reports->getListWithProjects($status, $page, $project_id);
->order(array('report_priority' => 'DESC', 'ordnum' => 'ASC'))
->limit($page * 10, 10)
->toArray();
return $ret;
} }
/** /**
@@ -106,13 +113,14 @@ class API extends APIlite
* *
* @param array $status 0 = Uncategorized, 1 = Waiting, 2 = InProgress, 3 = Blocked, 4 = Archived * @param array $status 0 = Uncategorized, 1 = Waiting, 2 = InProgress, 3 = Blocked, 4 = Archived
* @param int $page Pagination from 0 * @param int $page Pagination from 0
* @param int $project_id Optional project filter
* *
* @return array * @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); $page = intval($page);
$all = $this->getAll($status, $page); $all = $this->getAll($status, $page, $project_id);
$groups = []; $groups = [];
foreach ($all as $report) { foreach ($all as $report) {
$groups[$report['report_status']][] = $report; $groups[$report['report_status']][] = $report;
@@ -124,19 +132,16 @@ class API extends APIlite
* Get archived reports * Get archived reports
* *
* @param int $page Pagination from 0 * @param int $page Pagination from 0
* @param int $project_id Optional project filter
* *
* @return array * @return array
*/ */
public function getArchived(int $page = 0): array public function getArchived(int $page = 0, ?int $project_id = null): array
{ {
$page = intval($page); $page = intval($page);
if (!is_null($project_id)) $this->requireProject($project_id, false);
$reports = new Reports(); $reports = new Reports();
$ret = $reports->search('reports') return $reports->getListWithProjects(array(4), $page, $project_id, true);
->where(['report_status' => 4])
->order(array('created_dt' => 'DESC'))
->limit($page * 10, 10)
->toArray();
return $ret;
} }
/** /**
@@ -171,6 +176,165 @@ class API extends APIlite
return $suc !== false; 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 * Add report attachment
* *
+1
View File
@@ -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); $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') { } else if (Configuration::DB_TYPE == 'sqlite') {
$dbh = new DBmodel(sprintf('sqlite:%s', Configuration::DB_FILEPATH)); $dbh = new DBmodel(sprintf('sqlite:%s', Configuration::DB_FILEPATH));
$dbh->query('PRAGMA foreign_keys = ON');
} else { } else {
throw new Exception('Unknown database type'); throw new Exception('Unknown database type');
} }
+123
View File
@@ -41,6 +41,129 @@ class Maintenance extends \TPsoft\DBmodel\Maintenance
$this->dbver(3); $this->dbver(3);
$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 protected function settings(string $key, ?string $value = null): string|false
+108
View File
@@ -0,0 +1,108 @@
<?php
/*
TPsoft.org 2000-2026
file for controlers/*.php
Milestones:
2026-08-22 20:39 Created
*/
namespace TPsoft\BugreportBackend\Models;
class Projects extends \TPsoft\DBmodel\DBmodel {
public $tables = array(
'projects' => 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'
);
}
}
?>
+40 -1
View File
@@ -19,7 +19,7 @@ class Reports extends \TPsoft\DBmodel\DBmodel {
'report_title' => 'VARCHAR(255)', 'report_title' => 'VARCHAR(255)',
'report_description' => 'TEXT', 'report_description' => 'TEXT',
'report_status' => 'INTEGER', 'report_status' => 'INTEGER',
'report_group' => 'VARCHAR(255)', 'project_id' => 'INTEGER',
'report_priority' => 'INTEGER', 'report_priority' => 'INTEGER',
'created_dt' => 'DATETIME', 'created_dt' => 'DATETIME',
'ordnum' => 'INTEGER' 'ordnum' => 'INTEGER'
@@ -85,6 +85,45 @@ class Reports extends \TPsoft\DBmodel\DBmodel {
->toCombo($col_key, $col_value, $add_empty); ->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
));
}
} }
?> ?>
+27 -2
View File
@@ -15,7 +15,20 @@
v-model="short_bug" v-model="short_bug"
@keyup.enter="onShortBugEnter" @keyup.enter="onShortBugEnter"
/> />
<button @click="shortBugAdd"> <select
v-model="selectedProjectId"
aria-label="Projekt rýchleho tasku"
@change="setSelectedProject(selectedProjectId)"
>
<option
v-for="project in activeProjects"
:key="project.project_id"
:value="project.project_id"
>
{{ project.project_name }}
</option>
</select>
<button :disabled="selectedProjectId == null" @click="shortBugAdd">
<font-awesome-icon :icon="['fas', 'circle-check']" /> Pridať <font-awesome-icon :icon="['fas', 'circle-check']" /> Pridať
</button> </button>
</div> </div>
@@ -44,6 +57,10 @@
><font-awesome-icon :icon="['fas', 'box-archive']" /> ><font-awesome-icon :icon="['fas', 'box-archive']" />
Archív</router-link Archív</router-link
> >
<router-link to="/projects"
><font-awesome-icon :icon="['fas', 'diagram-project']" />
Projekty</router-link
>
<router-link to="/api" <router-link to="/api"
><font-awesome-icon :icon="['fas', 'plug']" /> API</router-link ><font-awesome-icon :icon="['fas', 'plug']" /> API</router-link
> >
@@ -66,6 +83,12 @@ import {
getDragDropPreference, getDragDropPreference,
setDragDropEnabled, setDragDropEnabled,
} from "./dragDropSettings"; } from "./dragDropSettings";
import {
activeProjects,
loadProjects,
selectedProjectId,
setSelectedProject,
} from "./projects";
const short_bug = ref(""); const short_bug = ref("");
const dragMediaQuery = getDragDropMediaQuery(); const dragMediaQuery = getDragDropMediaQuery();
@@ -88,14 +111,16 @@ function onShortBugEnter(event) {
} }
function shortBugAdd() { function shortBugAdd() {
if (selectedProjectId.value == null) return;
let content = short_bug.value; let content = short_bug.value;
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"); events.emit("reports-changed");
}); });
} }
onMounted(() => { onMounted(() => {
loadProjects().catch((error) => console.log(error));
if (dragMediaQuery != null) { if (dragMediaQuery != null) {
dragMediaQuery.addEventListener("change", updateAutomaticDragDropState); dragMediaQuery.addEventListener("change", updateAutomaticDragDropState);
} }
+151 -5
View File
@@ -75,7 +75,8 @@ h1 {
} }
button, button,
.button { .button,
select {
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--color-bg0); border: 1px solid var(--color-bg0);
padding: 0.6em 1.2em; padding: 0.6em 1.2em;
@@ -88,16 +89,27 @@ button,
transition: all 0.3s; transition: all 0.3s;
} }
button:hover, button:hover,
.button:hover { .button:hover,
select:hover {
border-color: var(--color-bg1); border-color: var(--color-bg1);
background-color: var(--color-bg0); background-color: var(--color-bg0);
} }
button:focus, button:focus,
.button:focus, .button:focus,
select:focus,
button:focus-visible, button:focus-visible,
.button:focus-visible { .button:focus-visible,
select:focus-visible {
outline: 4px auto -webkit-focus-ring-color; 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 { .card {
padding: 2em; padding: 2em;
@@ -141,13 +153,21 @@ button:focus-visible,
justify-content: center justify-content: center
} }
#header .short-bug input { #header .short-bug input {
width: 80%; width: auto;
flex-grow: 1;
padding: 5px; padding: 5px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 3px; border-radius: 3px;
background-color: var(--color-bg2); background-color: var(--color-bg2);
color: var(--color-text0); 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 { #header .menu {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -190,7 +210,7 @@ button:focus-visible,
background-color: var(--color-bg1); background-color: var(--color-bg1);
} }
#header .short-bug button { #header .short-bug button {
margin: 0px; margin: 0 0 0 5px;
/* height: 30px; */ /* height: 30px; */
} }
@@ -237,11 +257,27 @@ button:focus-visible,
#header .short-bug { #header .short-bug {
flex-direction: column; 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 03 - DASHBOARD
*/ */
.dashboard-toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
padding: 10px 20px;
background-color: var(--color-bg2);
}
#dashboard { #dashboard {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -307,6 +343,15 @@ button:focus-visible,
text-align: left; text-align: left;
padding: 5px; 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 { #dashboard .report .report-id {
text-align: center; text-align: center;
padding: 5px; padding: 5px;
@@ -571,12 +616,109 @@ button:focus-visible,
/* margin-right: 20px; */ /* margin-right: 20px; */
align-items: center; 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 { #report .report-header div span {
background-color: var(--color-bg0); background-color: var(--color-bg0);
color: var(--color-text0); color: var(--color-text0);
padding: 2px 10px; padding: 2px 10px;
align-items: center; align-items: center;
} }
#report .report-header div span.project-color {
padding: 0;
}
#report .report-header div strong { #report .report-header div strong {
background-color: var(--color-bg1); background-color: var(--color-bg1);
color: var(--color-text0); color: var(--color-text0);
@@ -644,6 +786,10 @@ button:focus-visible,
background-color: var(--color-bg2); background-color: var(--color-bg2);
color: var(--color-text0); color: var(--color-text0);
} }
.form-group select {
width: 99%;
box-sizing: border-box;
}
.form-actions { .form-actions {
margin-top: 10px; margin-top: 10px;
text-align: right; text-align: right;
+29 -9
View File
@@ -2,7 +2,7 @@
* Generated by APIlite * Generated by APIlite
* https://gitea.tpsoft.org/TPsoft.org/APIlite * https://gitea.tpsoft.org/TPsoft.org/APIlite
* *
* 2026-07-07 22:15:38 */ * 2026-08-22 20:42:16 */
class backend { class backend {
endpoint = import.meta.env.VITE_BACKENDAPI_URL; endpoint = import.meta.env.VITE_BACKENDAPI_URL;
@@ -154,8 +154,8 @@ class backend {
return this.callPromise('__HELP__', {}); return this.callPromise('__HELP__', {});
} }
add(title, description, status, group, priority) { add(title, description, status, project_id, priority) {
return this.callPromise('add', {title: title, description: description, status: status, group: group, priority: priority}); return this.callPromise('add', {title: title, description: description, status: status, project_id: project_id, priority: priority});
} }
update(report_id, report_data) { update(report_id, report_data) {
@@ -170,16 +170,16 @@ class backend {
return this.callPromise('get', {report_id: report_id}); return this.callPromise('get', {report_id: report_id});
} }
getAll(status, page) { getAll(status, page, project_id) {
return this.callPromise('getAll', {status: status, page: page}); return this.callPromise('getAll', {status: status, page: page, project_id: project_id});
} }
getAllGrouped(status, page) { getAllGrouped(status, page, project_id) {
return this.callPromise('getAllGrouped', {status: status, page: page}); return this.callPromise('getAllGrouped', {status: status, page: page, project_id: project_id});
} }
getArchived(page) { getArchived(page, project_id) {
return this.callPromise('getArchived', {page: page}); return this.callPromise('getArchived', {page: page, project_id: project_id});
} }
updateOrdNum(ordnums) { updateOrdNum(ordnums) {
@@ -190,6 +190,26 @@ class backend {
return this.callPromise('updateStatus', {report_id: report_id, status: status}); 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) { attachmentAdd(report_id, attachment_type, attachment_content) {
return this.callPromise('attachmentAdd', {report_id: report_id, attachment_type: attachment_type, attachment_content: attachment_content}); return this.callPromise('attachmentAdd', {report_id: report_id, attachment_type: attachment_type, attachment_content: attachment_content});
} }
+7 -3
View File
@@ -5,7 +5,8 @@ defineProps({
description: String, description: String,
date: String, date: String,
priority: Number, priority: Number,
group: String, projectName: String,
projectColor: String,
}); });
</script> </script>
<template> <template>
@@ -21,7 +22,10 @@ defineProps({
</p> </p>
</div> </div>
<div class="report-footer"> <div class="report-footer">
<div class="report-group"><font-awesome-icon :icon="['fas', 'diagram-project']" /> {{ group }}</div> <div class="report-group">
<span class="project-color" :style="{ backgroundColor: projectColor }"></span>
<font-awesome-icon :icon="['fas', 'diagram-project']" /> {{ projectName }}
</div>
<div class="report-id"><font-awesome-icon :icon="['fas', 'hashtag']" /> {{ report_id }}</div> <div class="report-id"><font-awesome-icon :icon="['fas', 'hashtag']" /> {{ report_id }}</div>
<div class="report-date"><font-awesome-icon :icon="['fas', 'calendar-days']" /> {{ date }}</div> <div class="report-date"><font-awesome-icon :icon="['fas', 'calendar-days']" /> {{ date }}</div>
</div> </div>
@@ -42,4 +46,4 @@ export default {
}, },
}, },
}; };
</script> </script>
+58
View File
@@ -0,0 +1,58 @@
const DRAG_DROP_STORAGE_KEY = "bugreport_drag_drop_enabled";
const DRAG_DROP_MEDIA_QUERY = "(max-width: 768px), (any-pointer: coarse)";
let sessionDragDropPreference = null;
function getStorage() {
try {
if (typeof window !== "undefined" && window.localStorage) {
return window.localStorage;
}
} catch (error) {
return null;
}
return null;
}
export function getDragDropMediaQuery() {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
return null;
}
return window.matchMedia(DRAG_DROP_MEDIA_QUERY);
}
export function getDragDropPreference() {
if (sessionDragDropPreference != null) return sessionDragDropPreference;
let storage = getStorage();
if (storage != null) {
try {
let storedPreference = storage.getItem(DRAG_DROP_STORAGE_KEY);
if (storedPreference === "true") return true;
if (storedPreference === "false") return false;
} catch (error) {
return null;
}
}
return null;
}
export function getDragDropEnabled(mediaQuery = null) {
let preference = getDragDropPreference();
if (preference != null) return preference;
if (mediaQuery == null) mediaQuery = getDragDropMediaQuery();
return mediaQuery == null || !mediaQuery.matches;
}
export function setDragDropEnabled(enabled) {
sessionDragDropPreference = enabled;
let storage = getStorage();
if (storage == null) return;
try {
storage.setItem(DRAG_DROP_STORAGE_KEY, String(enabled));
} catch (error) {
// The in-memory preference still applies until the page is reloaded.
}
}
+77
View File
@@ -0,0 +1,77 @@
import { computed, ref } from "vue";
import backend from "./backend";
const SELECTED_PROJECT_KEY = "bugreport_selected_project_id";
export const projects = ref([]);
export const projectsLoading = ref(false);
export const activeProjects = computed(() =>
projects.value.filter((project) => Number(project.project_active) === 1)
);
export const selectedProjectId = ref(readSelectedProjectId());
let loadPromise = null;
function getStorage() {
try {
if (typeof window !== "undefined" && window.localStorage) {
return window.localStorage;
}
} catch (error) {
return null;
}
return null;
}
function readSelectedProjectId() {
let storage = getStorage();
if (storage == null) return null;
try {
let projectId = Number(storage.getItem(SELECTED_PROJECT_KEY));
return Number.isInteger(projectId) && projectId > 0 ? projectId : null;
} catch (error) {
return null;
}
}
function ensureSelectedProject() {
let selected = activeProjects.value.find(
(project) => Number(project.project_id) === Number(selectedProjectId.value)
);
if (selected != null) return;
let fallback = activeProjects.value.find(
(project) => project.project_code === "unassigned"
) ?? activeProjects.value[0];
setSelectedProject(fallback == null ? null : fallback.project_id);
}
export function setSelectedProject(projectId) {
selectedProjectId.value = projectId == null ? null : Number(projectId);
let storage = getStorage();
if (storage == null) return;
try {
if (selectedProjectId.value == null) {
storage.removeItem(SELECTED_PROJECT_KEY);
return;
}
storage.setItem(SELECTED_PROJECT_KEY, String(selectedProjectId.value));
} catch (error) {
// The selected project still applies until the page is reloaded.
}
}
export function loadProjects() {
if (loadPromise != null) return loadPromise;
projectsLoading.value = true;
loadPromise = backend.projectGetAll(true)
.then((response) => {
projects.value = response.data;
ensureSelectedProject();
return projects.value;
})
.finally(() => {
projectsLoading.value = false;
loadPromise = null;
});
return loadPromise;
}
+2
View File
@@ -6,6 +6,7 @@ import BugAdd from "./views/BugAdd.vue";
import Archive from "./views/Archive.vue"; import Archive from "./views/Archive.vue";
import API from "./views/API.vue"; import API from "./views/API.vue";
import Report from "./views/Report.vue"; import Report from "./views/Report.vue";
import Projects from "./views/Projects.vue";
const routes = [ const routes = [
{ path: "/", component: Dashboard }, { path: "/", component: Dashboard },
@@ -14,6 +15,7 @@ const routes = [
{ path: "/archive", component: Archive }, { path: "/archive", component: Archive },
{ path: "/api", component: API }, { path: "/api", component: API },
{ path: "/report/:id", component: Report }, { path: "/report/:id", component: Report },
{ path: "/projects", component: Projects },
]; ];
export const router = createRouter({ export const router = createRouter({
+4 -1
View File
@@ -17,7 +17,10 @@
</div> </div>
<div class="title">{{ report.report_title }}</div> <div class="title">{{ report.report_title }}</div>
<div class="date">{{ report.created_dt }}</div> <div class="date">{{ report.created_dt }}</div>
<div class="group">{{ report.report_group }}</div> <div class="group">
<span class="project-color" :style="{ backgroundColor: report.project_color }"></span>
{{ report.project_name }}
</div>
</div> </div>
</div> </div>
+38 -11
View File
@@ -47,18 +47,22 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="group">Skupina:</label> <label for="project">Projekt:</label>
<select <select
id="group" id="project"
v-model="bugReport.group" v-model="bugReport.project_id"
class="form-control" class="form-control"
required required
@change="rememberProject"
> >
<option value="" disabled>Vyberte skupinu</option> <option value="" disabled>Vyberte projekt</option>
<option value="cp">Control Panel</option> <option
<option value="task">Task.Platon.sk</option> v-for="project in projects"
<option value="websiteip">WebsiteIP</option> :key="project.project_id"
<option value="antispam">Antispam</option> :value="project.project_id"
>
{{ project.project_name }}
</option>
</select> </select>
</div> </div>
@@ -81,24 +85,46 @@
<script> <script>
import backend from "../backend"; import backend from "../backend";
import FullScreenLoader from "../components/FullScreenLoader.vue"; import FullScreenLoader from "../components/FullScreenLoader.vue";
import {
activeProjects,
loadProjects,
selectedProjectId,
setSelectedProject,
} from "../projects";
export default { export default {
name: "BugAdd", name: "BugAdd",
components: { FullScreenLoader }, components: { FullScreenLoader },
mounted() {
this.loading = true;
loadProjects()
.then(() => {
this.projects = activeProjects.value;
this.bugReport.project_id = selectedProjectId.value;
})
.catch((error) => console.log(error))
.finally(() => {
this.loading = false;
});
},
data() { data() {
return { return {
bugReport: { bugReport: {
title: "", title: "",
description: "", description: "",
priority: "1", priority: "1",
group: "cp", project_id: null,
files: [], files: [],
}, },
selectedFiles: [], selectedFiles: [],
projects: [],
loading: false, loading: false,
}; };
}, },
methods: { methods: {
rememberProject() {
setSelectedProject(this.bugReport.project_id);
},
submitForm() { submitForm() {
this.loading = true; this.loading = true;
// Vytvorenie FormData objektu pre odoslanie súborov // Vytvorenie FormData objektu pre odoslanie súborov
@@ -118,7 +144,7 @@ export default {
this.bugReport.title, this.bugReport.title,
this.bugReport.description, this.bugReport.description,
"0", "0",
this.bugReport.group, this.bugReport.project_id,
this.bugReport.priority this.bugReport.priority
) )
.then((result) => { .then((result) => {
@@ -137,7 +163,8 @@ export default {
this.bugReport = { this.bugReport = {
title: "", title: "",
description: "", description: "",
priority: "", priority: "1",
project_id: selectedProjectId.value,
files: [], files: [],
}; };
}, },
+35 -9
View File
@@ -1,6 +1,20 @@
<template> <template>
<FullScreenLoader v-if="loading" /> <FullScreenLoader v-if="loading" />
<div class="dashboard-toolbar">
<label for="dashboard-project">Projekt:</label>
<select id="dashboard-project" v-model="projectFilter" @change="loadData()">
<option value="">Všetky projekty</option>
<option
v-for="project in projects"
:key="project.project_id"
:value="project.project_id"
>
{{ project.project_name }}{{ Number(project.project_active) === 1 ? "" : " (archivovaný)" }}
</option>
</select>
</div>
<div id="dashboard" :class="{ 'drag-disabled': isDragDisabled }"> <div id="dashboard" :class="{ 'drag-disabled': isDragDisabled }">
<div id="inbox"> <div id="inbox">
<h2>Nezaradené</h2> <h2>Nezaradené</h2>
@@ -26,7 +40,8 @@
:description="element.report_description" :description="element.report_description"
:date="element.created_dt" :date="element.created_dt"
:priority="element.report_priority" :priority="element.report_priority"
:group="element.report_group" :project-name="element.project_name"
:project-color="element.project_color"
/> />
</div> </div>
</template> </template>
@@ -56,7 +71,8 @@
:description="element.report_description" :description="element.report_description"
:date="element.created_dt" :date="element.created_dt"
:priority="element.report_priority" :priority="element.report_priority"
:group="element.report_group" :project-name="element.project_name"
:project-color="element.project_color"
/> />
</div> </div>
</template> </template>
@@ -86,7 +102,8 @@
:description="element.report_description" :description="element.report_description"
:date="element.created_dt" :date="element.created_dt"
:priority="element.report_priority" :priority="element.report_priority"
:group="element.report_group" :project-name="element.project_name"
:project-color="element.project_color"
/> />
</div> </div>
</template> </template>
@@ -116,7 +133,8 @@
:description="element.report_description" :description="element.report_description"
:date="element.created_dt" :date="element.created_dt"
:priority="element.report_priority" :priority="element.report_priority"
:group="element.report_group" :project-name="element.project_name"
:project-color="element.project_color"
/> />
</div> </div>
</template> </template>
@@ -150,6 +168,7 @@ import {
getDragDropEnabled, getDragDropEnabled,
getDragDropMediaQuery, getDragDropMediaQuery,
} from "../dragDropSettings"; } from "../dragDropSettings";
import { loadProjects, projects } from "../projects";
export default { export default {
components: { components: {
@@ -166,6 +185,8 @@ export default {
itemsWaiting: [], itemsWaiting: [],
itemsInProgress: [], itemsInProgress: [],
itemsBlocked: [], itemsBlocked: [],
projectFilter: "",
projects: [],
}; };
}, },
methods: { methods: {
@@ -181,12 +202,14 @@ export default {
}, },
loadData(use_loader = true) { loadData(use_loader = true) {
if (use_loader) this.loading = true; if (use_loader) this.loading = true;
backend.getAllGrouped(Array(0, 1, 2, 3)).then((response) => { let project_id = this.projectFilter === "" ? undefined : this.projectFilter;
backend.getAllGrouped(Array(0, 1, 2, 3), 0, project_id).then((response) => {
let all_grouped = response.data; let all_grouped = response.data;
this.itemsUncategorized = all_grouped[0]; this.itemsUncategorized = all_grouped[0] ?? [];
this.itemsWaiting = all_grouped[1]; this.itemsWaiting = all_grouped[1] ?? [];
this.itemsInProgress = all_grouped[2]; this.itemsInProgress = all_grouped[2] ?? [];
this.itemsBlocked = all_grouped[3]; this.itemsBlocked = all_grouped[3] ?? [];
}).catch((error) => console.log(error)).finally(() => {
if (use_loader) this.loading = false; if (use_loader) this.loading = false;
}); });
}, },
@@ -258,6 +281,9 @@ export default {
this.dragMediaQuery.addEventListener("change", this.updateDragState); this.dragMediaQuery.addEventListener("change", this.updateDragState);
} }
this.loadData(); this.loadData();
loadProjects().then(() => {
this.projects = projects.value;
}).catch((error) => console.log(error));
events.on("reports-changed", this.onReportsChanged); events.on("reports-changed", this.onReportsChanged);
events.on("drag-drop-changed", this.updateDragPreference); events.on("drag-drop-changed", this.updateDragPreference);
}, },
+183
View File
@@ -0,0 +1,183 @@
<template>
<FullScreenLoader v-if="loading" />
<div id="projects">
<h1>Projekty</h1>
<form class="form project-new" @submit.prevent="projectAdd">
<div class="form-group">
<label for="project-name">Názov:</label>
<input
id="project-name"
v-model="newProject.name"
type="text"
required
maxlength="255"
@input="suggestProjectCode"
/>
</div>
<div class="form-group">
<label for="project-code">Kód:</label>
<input
id="project-code"
v-model="newProject.code"
type="text"
required
maxlength="64"
pattern="[a-z0-9][a-z0-9_-]{0,63}"
@input="codeEdited = true"
/>
</div>
<div class="form-group project-color-field">
<label for="project-color">Farba:</label>
<input id="project-color" v-model="newProject.color" type="color" />
</div>
<div class="form-actions">
<button type="submit">
<font-awesome-icon :icon="['fas', 'circle-plus']" /> Pridať projekt
</button>
</div>
</form>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<div class="project-list">
<div
v-for="project in projects"
:key="project.project_id"
class="project-row"
:class="{ archived: Number(project.project_active) !== 1 }"
>
<span class="project-color" :style="{ backgroundColor: project.project_color }"></span>
<div class="project-data">
<template v-if="editingProjectId === project.project_id">
<input v-model="editProject.name" type="text" maxlength="255" required />
<input v-model="editProject.color" type="color" />
</template>
<template v-else>
<strong>{{ project.project_name }}</strong>
<code>{{ project.project_code }}</code>
</template>
</div>
<div class="project-meta">
{{ project.report_count }} taskov
<span v-if="Number(project.project_active) !== 1">Archivovaný</span>
</div>
<div class="project-actions" v-if="Number(project.project_system) !== 1">
<template v-if="editingProjectId === project.project_id">
<button @click="projectUpdate(project)">Uložiť</button>
<button @click="editingProjectId = null">Zrušiť</button>
</template>
<template v-else>
<button @click="startEdit(project)">
<font-awesome-icon :icon="['fas', 'pen']" /> Upraviť
</button>
<button
v-if="Number(project.project_active) === 1"
@click="projectArchive(project, true)"
>
<font-awesome-icon :icon="['fas', 'box-archive']" /> Archivovať
</button>
<button v-else @click="projectArchive(project, false)">
<font-awesome-icon :icon="['fas', 'rotate-left']" /> Obnoviť
</button>
</template>
</div>
<div v-else class="project-actions project-system">Systémový projekt</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from "vue";
import backend from "../backend";
import FullScreenLoader from "../components/FullScreenLoader.vue";
import { loadProjects, projects } from "../projects";
const loading = ref(false);
const errorMessage = ref("");
const codeEdited = ref(false);
const editingProjectId = ref(null);
const newProject = reactive({ name: "", code: "", color: "#449EF8" });
const editProject = reactive({ name: "", color: "#449EF8" });
refreshProjects();
function slugify(value) {
return value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9_-]+/g, "-")
.replace(/^-+|-+$/g, "")
.substring(0, 64);
}
function suggestProjectCode() {
if (!codeEdited.value) newProject.code = slugify(newProject.name);
}
function refreshProjects() {
loading.value = true;
return loadProjects()
.catch(showError)
.finally(() => {
loading.value = false;
});
}
function projectAdd() {
loading.value = true;
errorMessage.value = "";
backend.projectAdd(newProject.name, newProject.code, newProject.color)
.then(() => {
newProject.name = "";
newProject.code = "";
newProject.color = "#449EF8";
codeEdited.value = false;
return refreshProjects();
})
.catch(showError)
.finally(() => {
loading.value = false;
});
}
function startEdit(project) {
editingProjectId.value = project.project_id;
editProject.name = project.project_name;
editProject.color = project.project_color;
}
function projectUpdate(project) {
loading.value = true;
errorMessage.value = "";
backend.projectUpdate(project.project_id, editProject.name, editProject.color)
.then(() => {
editingProjectId.value = null;
return refreshProjects();
})
.catch(showError)
.finally(() => {
loading.value = false;
});
}
function projectArchive(project, archived) {
let action = archived ? "archivovať" : "obnoviť";
if (!confirm(`Naozaj chcete ${action} projekt ${project.project_name}? Projekt obsahuje ${project.report_count} taskov.`)) return;
loading.value = true;
errorMessage.value = "";
backend.projectArchive(project.project_id, archived)
.then(() => refreshProjects())
.catch(showError)
.finally(() => {
loading.value = false;
});
}
function showError(error) {
errorMessage.value = String(error);
}
</script>
+51 -4
View File
@@ -20,8 +20,31 @@
<strong>{{ report.report_priority }}</strong> <strong>{{ report.report_priority }}</strong>
</div> </div>
<div> <div>
<span>Skupina</span> <span>Projekt</span>
<strong>{{ report.report_group }}</strong> <span
v-if="editable"
class="project-color"
:style="{ backgroundColor: report.project_color }"
></span>
<select
v-if="editable"
v-model="report.project_id"
aria-label="Projekt tasku"
@change="onProjectChange"
>
<option
v-for="project in availableProjects"
:key="project.project_id"
:value="project.project_id"
:disabled="Number(project.project_active) !== 1"
>
{{ project.project_name }}{{ Number(project.project_active) === 1 ? "" : " (archivovaný)" }}
</option>
</select>
<strong v-else class="project-label">
<span class="project-color" :style="{ backgroundColor: report.project_color }"></span>
{{ report.project_name }}
</strong>
</div> </div>
<div> <div>
<button @click="reportDelete"> <button @click="reportDelete">
@@ -145,11 +168,12 @@
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { router } from "../router"; import { router } from "../router";
import backend from "../backend"; import backend from "../backend";
import FullScreenLoader from "../components/FullScreenLoader.vue"; import FullScreenLoader from "../components/FullScreenLoader.vue";
import JSConfetti from 'js-confetti' import JSConfetti from 'js-confetti'
import { loadProjects, projects } from "../projects";
let tadas = ['/sounds/tada.mp3', '/sounds/tada2.mp3', '/sounds/crazy-phrog-short.mp3']; let tadas = ['/sounds/tada.mp3', '/sounds/tada2.mp3', '/sounds/crazy-phrog-short.mp3'];
const jungle = new Audio(tadas[Math.floor(Math.random() * tadas.length)]); const jungle = new Audio(tadas[Math.floor(Math.random() * tadas.length)]);
@@ -161,7 +185,11 @@ const report = ref({
report_title: "Nacitavam report", report_title: "Nacitavam report",
report_description: "...", report_description: "...",
report_status: 4, report_status: 4,
report_group: "--", project_id: null,
project_name: "--",
project_code: "--",
project_color: "#797979",
project_active: 1,
report_priority: 1, report_priority: 1,
created_dt: "--", created_dt: "--",
ordnum: 0, ordnum: 0,
@@ -179,9 +207,14 @@ const attachmentNewContent = ref(null);
const attachmentNewFiles = ref(null); const attachmentNewFiles = ref(null);
const selectedFiles = ref([]); const selectedFiles = ref([]);
const selectedFilesContent = ref([]); const selectedFilesContent = ref([]);
const availableProjects = computed(() => projects.value.filter((project) =>
Number(project.project_active) === 1
|| Number(project.project_id) === Number(report.value.project_id)
));
onMounted(() => { onMounted(() => {
// console.log(report_id); // console.log(report_id);
loadProjects().catch((error) => console.log(error));
loadReportData(); loadReportData();
}); });
@@ -207,6 +240,20 @@ function onDescriptionChange(event) {
}); });
} }
function onProjectChange() {
loading.value = true;
backend.update(report_id, {
project_id: report.value.project_id,
}).then(() => {
loadReportData();
}).catch((error) => {
console.log(error);
loadReportData();
}).finally(() => {
loading.value = false;
});
}
function reportDelete() { function reportDelete() {
if (!confirm("Naozaj chcete report zmazať?")) return; if (!confirm("Naozaj chcete report zmazať?")) return;
loading.value = true; loading.value = true;