Compare commits
27 Commits
f0e7c236d5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| afc5229f5b | |||
| e50c53e50a | |||
| 3e9056f4a6 | |||
| ed3d202488 | |||
| e67d4843bd | |||
| fbfe46542b | |||
| 948fce896f | |||
| 027815ee3f | |||
| 19600ac7bf | |||
| e63be43639 | |||
| 995e0e40a5 | |||
| b51ae03fcf | |||
| 1cf79a20b3 | |||
| 8184ffb46d | |||
| e6f3ef4ab2 | |||
| 506e847b5d | |||
| c7dcdf228e | |||
| d6be781024 | |||
| 4a050b0ff7 | |||
| 02af51b834 | |||
| 16fa3d1ed1 | |||
| dfa51da14b | |||
| 5250a6d279 | |||
| a02bdb4cbe | |||
| 603c8ea37b | |||
| 82e14b8fa4 | |||
| e870a62b89 |
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
|||||||
[submodule "lib/Medoo"]
|
|
||||||
path = lib/Medoo
|
|
||||||
url = https://github.com/catfan/Medoo.git
|
|
||||||
|
|||||||
93
README.md
@ -1,12 +1,12 @@
|
|||||||
# BugReport
|
# BugReport
|
||||||
|
|
||||||
<img src="webapp/public/bugreport.svg" alt="BugReport logo" height="100" align="left" />
|
<img src="frontend/public/bugreport.svg" alt="BugReport logo" height="100" align="left" />
|
||||||
|
|
||||||
BugReport je webová aplikácia na sledovanie a správu chýb (bug tracking system) s jednoduchým a intuitívnym používateľským rozhraním. Aplikácia umožňuje pridávať, upravovať, kategorizovať a sledovať stav bug reportov pomocou Kanban-štýl rozhrania s drag-and-drop funkcionalitou.
|
BugReport je webová aplikácia na sledovanie a správu chýb (bug tracking system) s jednoduchým a intuitívnym používateľským rozhraním. Aplikácia umožňuje pridávať, upravovať, kategorizovať a sledovať stav bug reportov pomocou Kanban-štýl rozhrania s drag-and-drop funkcionalitou.
|
||||||
|
|
||||||
<br /><br /><br />
|
<br /><br /><br />
|
||||||
|
|
||||||
## Funkcie
|
## 🚀 Funkcie
|
||||||
|
|
||||||
- **Kanban rozhranie** - vizuálne sledovanie stavu bug reportov v štyroch kategóriách (Nezaradené, Čakajúce, Rozpracované, Blokované)
|
- **Kanban rozhranie** - vizuálne sledovanie stavu bug reportov v štyroch kategóriách (Nezaradené, Čakajúce, Rozpracované, Blokované)
|
||||||
- **Drag-and-drop** - jednoduché presúvanie reportov medzi kategóriami
|
- **Drag-and-drop** - jednoduché presúvanie reportov medzi kategóriami
|
||||||
@ -15,35 +15,50 @@ BugReport je webová aplikácia na sledovanie a správu chýb (bug tracking syst
|
|||||||
- **Detailný pohľad** - zobrazenie a úprava detailov reportu
|
- **Detailný pohľad** - zobrazenie a úprava detailov reportu
|
||||||
- **REST API** - prístup k dátam cez API endpoint
|
- **REST API** - prístup k dátam cez API endpoint
|
||||||
|
|
||||||
## Technológie
|
## 🔷 Diagram stavov pre BUG
|
||||||
|
|
||||||
### Backend
|
<img src="frontend/src/assets/images/FlowDiagram.drawio.svg" />
|
||||||
|
|
||||||
|
## 🖼️ Screenshot
|
||||||
|
<img src="doc/Screenshot_2025-05-17_111345.png" />
|
||||||
|
|
||||||
|
## 🖥️ Technológie
|
||||||
|
|
||||||
|
### ⚙️ Backend
|
||||||
|
|
||||||
- PHP
|
- PHP
|
||||||
- SQLite databáza
|
- SQLite databáza
|
||||||
- [Medoo](https://medoo.in/) - PHP databázový framework
|
- [TPsoft/DBmodel](https://gitea.tpsoft.org/TPsoft.org/DBmodel) - PHP databázové rozšírenie
|
||||||
|
- [TPsoft/APIlite](https://gitea.tpsoft.org/TPsoft.org/APIlite) - Jednoduchý nástroj pre zostavenie API
|
||||||
|
|
||||||
### Frontend
|
### 📺 Frontend
|
||||||
|
|
||||||
- [Vue.js 3](https://vuejs.org/) - JavaScript framework
|
- [Vue.js 3](https://vuejs.org/) - JavaScript framework
|
||||||
- [Vue Router](https://router.vuejs.org/) - smerovanie v aplikácii
|
- [Vue Router](https://router.vuejs.org/) - smerovanie v aplikácii
|
||||||
- [Vuedraggable](https://github.com/SortableJS/vue.draggable.next) - drag-and-drop funkcionalita
|
- [Vuedraggable](https://github.com/SortableJS/vue.draggable.next) - drag-and-drop funkcionalita
|
||||||
- [Font Awesome](https://fontawesome.com/) - ikony
|
- [Font Awesome](https://fontawesome.com/) - ikony
|
||||||
|
- [Mitt](https://github.com/developit/mitt) - knižnica pre správu udalostí
|
||||||
- [Vite](https://vitejs.dev/) - build nástroj
|
- [Vite](https://vitejs.dev/) - build nástroj
|
||||||
|
|
||||||
## Štruktúra projektu
|
## 🌳 Štruktúra projektu
|
||||||
|
|
||||||
```
|
```
|
||||||
BugReport/
|
BugReport/
|
||||||
├── api.php # API endpoint
|
├── backend/ # Backend aplikácia (PHP)
|
||||||
├── config.php # Konfigurácia aplikácie
|
│ ├── config/ # Konfigurácia aplikácie
|
||||||
├── index.php # Vstupný bod aplikácie
|
│ ├── public/ # Vstupný bod aplikácie, tu nasmerovat vo webserveri DOCUMENT ROOT
|
||||||
|
│ ├── scripts/ # Skripty pre pred-spracovanie, build a ine
|
||||||
|
│ ├── src/ # Zdrojový kód
|
||||||
|
│ │ ├── Models/ # Classy pre jednotlive DB tabulky pouziva TPsoft\DBmodel
|
||||||
|
│ │ ├── API.php # Logika pre API BugReport pouziva TPsoft\APIlite
|
||||||
|
│ │ ├── Init.php # Inicializacia pripojenia k DB, kontroly a iné
|
||||||
|
│ │ └── Maintenance.php # Údržba pre projekt, zatiaľ hlavne pre upgrade databazovej štruktúry, používa \TPsoft\DBmodel\Maintenance
|
||||||
|
│ └── test/ # Testovacie skripty
|
||||||
├── data/ # Dáta aplikácie
|
├── data/ # Dáta aplikácie
|
||||||
|
│ ├── attachments/ # Súborové uložisko príloh
|
||||||
│ └── database.db # SQLite databáza
|
│ └── database.db # SQLite databáza
|
||||||
├── lib/ # PHP knižnice
|
├── doc/ # Dokumentácia a iné súbory počas vývoja
|
||||||
│ ├── functions.inc.php # Pomocné funkcie
|
└── frontend/ # Frontend aplikácia (Vue.js)
|
||||||
│ └── Medoo/ # Medoo databázový framework
|
|
||||||
└── webapp/ # Frontend aplikácia (Vue.js)
|
|
||||||
├── public/ # Statické súbory
|
├── public/ # Statické súbory
|
||||||
├── scripts/ # Skripty pre build
|
├── scripts/ # Skripty pre build
|
||||||
└── src/ # Zdrojový kód
|
└── src/ # Zdrojový kód
|
||||||
@ -52,27 +67,29 @@ BugReport/
|
|||||||
├── views/ # Vue stránky
|
├── views/ # Vue stránky
|
||||||
├── App.vue # Hlavná Vue komponenta
|
├── App.vue # Hlavná Vue komponenta
|
||||||
├── backend.js # Komunikácia s API
|
├── backend.js # Komunikácia s API
|
||||||
|
├── events.js # Správa udalostí
|
||||||
├── main.js # Vstupný bod Vue aplikácie
|
├── main.js # Vstupný bod Vue aplikácie
|
||||||
└── router.js # Vue Router konfigurácia
|
└── router.js # Vue Router konfigurácia
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inštalácia a spustenie
|
## 🛠️ Inštalácia a spustenie
|
||||||
|
|
||||||
### Požiadavky
|
### 📋 Požiadavky
|
||||||
|
|
||||||
- PHP 7.4 alebo novší
|
- PHP 8.2 alebo novší
|
||||||
- Webový server (Apache, Nginx)
|
- Webový server (Apache, Nginx)
|
||||||
- Node.js a npm pre vývoj frontendu
|
- Node.js a npm pre vývoj frontendu
|
||||||
|
|
||||||
### Backend
|
### ⚙️ Backend
|
||||||
|
|
||||||
1. Skopírujte súbory do webového adresára
|
1. Skopírujte súbory do webového adresára
|
||||||
2. Uistite sa, že adresár`data` má práva na zápis
|
2. Uistite sa, že adresár `data` má práva na zápis
|
||||||
|
3. Nastavte DOCUMENT ROOT na adresár `backend/public`
|
||||||
3. Prístup k aplikácii cez webový prehliadač
|
3. Prístup k aplikácii cez webový prehliadač
|
||||||
|
|
||||||
### Frontend (pre vývoj)
|
### 📺 Frontend (pre vývoj)
|
||||||
|
|
||||||
1. Prejdite do adresára`webapp`
|
1. Prejdite do adresára`frontend`
|
||||||
2. Nainštalujte závislosti:
|
2. Nainštalujte závislosti:
|
||||||
```
|
```
|
||||||
npm install
|
npm install
|
||||||
@ -86,29 +103,32 @@ BugReport/
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
## API dokumentácia
|
## 📖 API dokumentácia
|
||||||
|
|
||||||
API je dostupné cez `api.php` endpoint. Všetky požiadavky vracajú JSON odpoveď.
|
API je dostupné cez `API.php` endpoint fyzicky umiestnený v DOCUMENT ROOT `backend/public/API.php`. Všetky požiadavky vracajú JSON odpoveď.kompletnú a aktuálnu dokumentáciu je možné získať aj HTML formáte `API.php?format=html`.
|
||||||
|
|
||||||
### Dostupné endpointy
|
### 🚏 Dostupné endpointy
|
||||||
|
|
||||||
| Akcia | Popis | Parametre |
|
| Akcia | Popis | Parametre |
|
||||||
| ----------------- | -------------------------------------- | --------------------------------------------------------------- |
|
| -------------------- | -------------------------------------- | --------------------------------------------------------------- |
|
||||||
| `help` | Zobrazí nápovedu | - |
|
| `help` | Zobrazí nápovedu | - |
|
||||||
| `add` | Pridá nový report | `title`, `description`, `status`, `group`, `priority` |
|
| `add` | Pridá nový report | `title`, `description`, `status`, `group`, `priority` |
|
||||||
| `update` | Aktualizuje report | `report_id`, `report_data` (JSON) |
|
| `update` | Aktualizuje report | `report_id`, `report_data` (JSON) |
|
||||||
| `delete` | Vymaže report | `report_id` |
|
| `delete` | Vymaže report | `report_id` |
|
||||||
| `get` | Získa jeden report | `report_id` |
|
| `get` | Získa jeden report | `report_id` |
|
||||||
| `getall` | Získa všetky reporty | `status` (voliteľné) |
|
| `getAll` | Získa všetky reporty | `status` (voliteľné) |
|
||||||
| `getallgrouped` | Získa reporty zoskupené podľa stavu | `status` (voliteľné) |
|
| `getAllGrouped` | Získa reporty zoskupené podľa stavu | `status` (voliteľné) |
|
||||||
| `updateordnum` | Aktualizuje poradie reportov | `ordnums` (JSON) |
|
| `updateOrdNum` | Aktualizuje poradie reportov | `ordnums` (JSON) |
|
||||||
| `updatestatus` | Aktualizuje stav reportu | `report_id`, `status` |
|
| `updateStatus` | Aktualizuje stav reportu | `report_id`, `status` |
|
||||||
|
| `attachmentAdd` | Pridá prílohu k reportu | `report_id`, `attachment_type`, `attachment_content` |
|
||||||
|
| `attachmentUpdate` | Aktualizuje prílohu | `attachment_id`, `attachment_content` |
|
||||||
|
| `attachmentGetAll` | Získa všetky prílohy reportu | `report_id` |
|
||||||
|
|
||||||
### Príklad API volania
|
### 🔍 Príklad API volania
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Získanie všetkých reportov
|
// Získanie všetkých reportov
|
||||||
fetch('api.php?action=getall')
|
fetch('api.php?action=getAll')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => console.log(data));
|
.then(data => console.log(data));
|
||||||
|
|
||||||
@ -121,7 +141,7 @@ fetch('api.php?action=add', {
|
|||||||
.then(data => console.log(data));
|
.then(data => console.log(data));
|
||||||
```
|
```
|
||||||
|
|
||||||
## Stavy reportov
|
## 🏗️ Stavy reportov
|
||||||
|
|
||||||
| ID | Stav |
|
| ID | Stav |
|
||||||
| -- | ------------- |
|
| -- | ------------- |
|
||||||
@ -131,7 +151,7 @@ fetch('api.php?action=add', {
|
|||||||
| 3 | Blokované |
|
| 3 | Blokované |
|
||||||
| 4 | Vyriešený |
|
| 4 | Vyriešený |
|
||||||
|
|
||||||
## Priority reportov
|
## 📣 Priority reportov
|
||||||
|
|
||||||
| ID | Priorita |
|
| ID | Priorita |
|
||||||
| -- | --------- |
|
| -- | --------- |
|
||||||
@ -140,13 +160,16 @@ fetch('api.php?action=add', {
|
|||||||
| 2 | Vysoká |
|
| 2 | Vysoká |
|
||||||
| 3 | Kritická |
|
| 3 | Kritická |
|
||||||
|
|
||||||
## Skupiny reportov
|
## 🕸️ Skupiny reportov
|
||||||
|
|
||||||
- `cp` - Control Panel
|
- `cp` - Control Panel
|
||||||
- `task` - Task.Platon.sk
|
- `task` - Task.Platon.sk
|
||||||
- `websiteip` - WebsiteIP
|
- `websiteip` - WebsiteIP
|
||||||
- `antispam` - Antispam
|
- `antispam` - Antispam
|
||||||
|
|
||||||
## Licencia
|
## ✨ Plánované vylepšenia - Planned Features
|
||||||
|
- Skupiny reportov v samostatnej tabuľke s CRUD manažmentom
|
||||||
|
|
||||||
|
## ⚖️ Licencia
|
||||||
|
|
||||||
Tento projekt je licencovaný pod [MIT licenciou](https://opensource.org/licenses/MIT).
|
Tento projekt je licencovaný pod [MIT licenciou](https://opensource.org/licenses/MIT).
|
||||||
|
|||||||
141
api.php
@ -1,141 +0,0 @@
|
|||||||
<?php
|
|
||||||
include_once 'config.php';
|
|
||||||
|
|
||||||
$action = $_REQUEST['action'];
|
|
||||||
$result = null;
|
|
||||||
$error = null;
|
|
||||||
|
|
||||||
switch ($action) {
|
|
||||||
default:
|
|
||||||
case 'help':
|
|
||||||
$result = help();
|
|
||||||
break;
|
|
||||||
case 'add':
|
|
||||||
$report_id = reportAdd($_REQUEST['title'], $_REQUEST['description'], $_REQUEST['status'], $_REQUEST['group'], $_REQUEST['priority']);
|
|
||||||
if ($report_id === false) $error = 'Report add failed';
|
|
||||||
$result = array('report_id' => $report_id);
|
|
||||||
break;
|
|
||||||
case 'update':
|
|
||||||
$suc = reportUpdate($_REQUEST['report_id'], json_decode($_REQUEST['report_data'], true));
|
|
||||||
if ($suc === false) $error = 'Update failed';
|
|
||||||
$result = array('processed' => $suc);
|
|
||||||
break;
|
|
||||||
case 'delete':
|
|
||||||
$suc = reportDelete($_REQUEST['report_id']);
|
|
||||||
if ($suc === false) $error = 'Update failed';
|
|
||||||
$result = array('processed' => $suc);
|
|
||||||
break;
|
|
||||||
case 'get':
|
|
||||||
$result = reportGet($_REQUEST['report_id']);
|
|
||||||
break;
|
|
||||||
case 'getall':
|
|
||||||
$result = reportGetAll($_REQUEST['status']);
|
|
||||||
break;
|
|
||||||
case 'getallgrouped':
|
|
||||||
$result = reportGetAllGrouped($_REQUEST['status']);
|
|
||||||
break;
|
|
||||||
case 'updateordnum':
|
|
||||||
$suc = reportUpdateOrdnum($_REQUEST['ordnums']);
|
|
||||||
if ($suc === false) $error = 'Update Ordnum failed';
|
|
||||||
$result = array('processed' => $suc);
|
|
||||||
break;
|
|
||||||
case 'updatestatus':
|
|
||||||
$suc = reportUpdateStatus($_REQUEST['report_id'], $_REQUEST['status']);
|
|
||||||
if ($suc === false) $error = 'Update Status failed';
|
|
||||||
$result = array('processed' => $suc);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*';
|
|
||||||
header('Access-Control-Allow-Origin: ' . $origin);
|
|
||||||
header('Access-Control-Allow-Credentials: true');
|
|
||||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
|
||||||
header('Access-Control-Allow-Headers: Origin, Content-Type, Accept');
|
|
||||||
echo json_encode(
|
|
||||||
is_null($error)
|
|
||||||
? array('status' => 'OK', 'data' => $result)
|
|
||||||
: array('status' => 'ERROR', 'msg' => $error),
|
|
||||||
);
|
|
||||||
exit;
|
|
||||||
|
|
||||||
function help()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'actions' => [
|
|
||||||
'help' => [
|
|
||||||
'name' => 'help',
|
|
||||||
'description' => 'Show this help',
|
|
||||||
'params' => []
|
|
||||||
],
|
|
||||||
'add' => [
|
|
||||||
'name' => 'add',
|
|
||||||
'description' => 'Add report',
|
|
||||||
'params' => [
|
|
||||||
'title' => 'Report title',
|
|
||||||
'description' => 'Report description',
|
|
||||||
'status' => 'Report status',
|
|
||||||
'group' => 'Report group',
|
|
||||||
'priority' => 'Report priority',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'update' => [
|
|
||||||
'name' => 'update',
|
|
||||||
'description' => 'Update report',
|
|
||||||
'params' => [
|
|
||||||
'report_id' => 'Report id',
|
|
||||||
'report_data' => [
|
|
||||||
'title' => 'Report title',
|
|
||||||
'description' => 'Report description',
|
|
||||||
'status' => 'Report status',
|
|
||||||
'group' => 'Report group',
|
|
||||||
'priority' => 'Report priority',
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'delete' => [
|
|
||||||
'name' => 'delete',
|
|
||||||
'description' => 'Delete report',
|
|
||||||
'params' => [
|
|
||||||
'report_id' => 'Report id',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'get' => [
|
|
||||||
'name' => 'get',
|
|
||||||
'description' => 'Get report',
|
|
||||||
'params' => [
|
|
||||||
'report_id' => 'Report id',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'getall' => [
|
|
||||||
'name' => 'getall',
|
|
||||||
'description' => 'Get all reports',
|
|
||||||
'params' => [
|
|
||||||
'status' => '(ptional) Report status, default: 0,1,2,3',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'getallgrouped' => [
|
|
||||||
'name' => 'getallgrouped',
|
|
||||||
'description' => 'Get all reports grouped by group',
|
|
||||||
'params' => [
|
|
||||||
'status' => '(ptional) Report status, default: 0,1,2,3',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'updateordnum' => [
|
|
||||||
'name' => 'updateordnum',
|
|
||||||
'description' => 'Update report ordnum',
|
|
||||||
'params' => [
|
|
||||||
'ordnums' => 'Report ordnums in json format {report_id: ordnum, ...}',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'updatestatus' => [
|
|
||||||
'name' => 'updatestatus',
|
|
||||||
'description' => 'Update report status',
|
|
||||||
'params' => [
|
|
||||||
'report_id' => 'Report id',
|
|
||||||
'status' => 'Report status',
|
|
||||||
]
|
|
||||||
],
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
29
backend/composer.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "tpsoft/bugreport-backend",
|
||||||
|
"description": "Backend for project BugReport",
|
||||||
|
"type": "project",
|
||||||
|
"require": {
|
||||||
|
"tpsoft/apilite": "^1.0",
|
||||||
|
"tpsoft/dbmodel": "^1.0"
|
||||||
|
},
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"TPsoft\\BugreportBackend\\": "src/"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/Configuration.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "igor",
|
||||||
|
"email": "mino@tpsoft.org"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"scripts": {
|
||||||
|
"model": "php scripts/createModel.php",
|
||||||
|
"build": "php scripts/buildTypeScript.php"
|
||||||
|
}
|
||||||
|
}
|
||||||
110
backend/composer.lock
generated
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"_readme": [
|
||||||
|
"This file locks the dependencies of your project to a known state",
|
||||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
|
"This file is @generated automatically"
|
||||||
|
],
|
||||||
|
"content-hash": "61c996f7e342564a37ff61ac90fcdc7b",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "tpsoft/apilite",
|
||||||
|
"version": "v1.0.3",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.tpsoft.org/TPsoft.org/APIlite.git",
|
||||||
|
"reference": "c0fd7b3fe5270ee44a84a92e9255ada2438812b7"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"TPsoft\\APIlite\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"GPL-3.0-or-later"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Igor Mino",
|
||||||
|
"email": "mino@tpsoft.org",
|
||||||
|
"homepage": "https://www.tpsoft.org",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A set of tools to simplify the work of creating backend APIs for your frontend projects.",
|
||||||
|
"keywords": [
|
||||||
|
"api",
|
||||||
|
"json",
|
||||||
|
"php",
|
||||||
|
"rest",
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.anycoin.cz/donate/igormino",
|
||||||
|
"type": "other"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-10-13T21:36:34+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tpsoft/dbmodel",
|
||||||
|
"version": "v1.0.5",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.tpsoft.org/TPsoft.org/DBmodel.git",
|
||||||
|
"reference": "80e889946bc4e38e987f46a13f95ee177ea934dc"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"php": ">=8.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"TPsoft\\DBmodel\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"GPL-3.0-or-later"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Igor Mino",
|
||||||
|
"email": "mino@tpsoft.org",
|
||||||
|
"homepage": "https://www.tpsoft.org",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "This library extends the builtin PDO object by several useful features. ",
|
||||||
|
"keywords": [
|
||||||
|
"db",
|
||||||
|
"model",
|
||||||
|
"mysql",
|
||||||
|
"pdo",
|
||||||
|
"sqlite"
|
||||||
|
],
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.anycoin.cz/donate/igormino",
|
||||||
|
"type": "other"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-10-13T21:26:19+00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"packages-dev": [],
|
||||||
|
"aliases": [],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"stability-flags": {},
|
||||||
|
"prefer-stable": false,
|
||||||
|
"prefer-lowest": false,
|
||||||
|
"platform": {},
|
||||||
|
"platform-dev": {},
|
||||||
|
"plugin-api-version": "2.6.0"
|
||||||
|
}
|
||||||
41
backend/config/Configuration.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (file_exists('c:/php/includes/igor.php')) {
|
||||||
|
require_once 'c:/php/includes/igor.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$others_config = scandir(__DIR__);
|
||||||
|
$loaded = false;
|
||||||
|
foreach ($others_config as $file) {
|
||||||
|
if ($file == basename(__FILE__)) continue;
|
||||||
|
if (substr($file, -4) == '.php') {
|
||||||
|
require_once __DIR__ . '/' . $file;
|
||||||
|
$loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$loaded) {
|
||||||
|
class Configuration
|
||||||
|
{
|
||||||
|
public const DB_TYPE = 'sqlite';
|
||||||
|
// MySQL
|
||||||
|
public const DB_HOST = 'localhost';
|
||||||
|
public const DB_USER = 'tpsoft_bugreport';
|
||||||
|
public const DB_PASS = '****************';
|
||||||
|
public const DB_NAME = 'tpsoft_bugreport';
|
||||||
|
// SQLite
|
||||||
|
public const DB_FILEPATH = __DIR__ . '/../../data/database.db';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https://" : "http://";
|
||||||
|
$host = $_SERVER['HTTP_HOST'];
|
||||||
|
$uri = $_SERVER['REQUEST_URI']; // obsahuje aj query string
|
||||||
|
|
||||||
|
define('URL_PREFIX', $protocol.$host.str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']));
|
||||||
|
|
||||||
|
define('UPLOAD_DIR_ATTACHMENTS', __DIR__.'/../../data/attachments/');
|
||||||
|
if (!file_exists(UPLOAD_DIR_ATTACHMENTS)) {
|
||||||
|
mkdir(UPLOAD_DIR_ATTACHMENTS, 0777, true);
|
||||||
|
}
|
||||||
|
define('UPLOAD_URL_ATTACHMENTS', URL_PREFIX.'data/attachments/');
|
||||||
4
backend/public/API.php
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__.'/../src/API.php';
|
||||||
|
use TPsoft\BugreportBackend\API;
|
||||||
|
new API();
|
||||||
19
backend/scripts/buildTypeScript.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
$backend_api = new TPsoft\BugreportBackend\API('typescript', 'import.meta.env.VITE_BACKENDAPI_URL', 'backend');
|
||||||
|
|
||||||
|
$output = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
|
|
||||||
|
$ts_path = realpath(__DIR__ . '/../../frontend/src').'/backend.js';
|
||||||
|
$suc = file_put_contents($ts_path, $output);
|
||||||
|
if ($suc === false) {
|
||||||
|
echo "✗ TypeScript store into file failed\n";
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "✓ TypeScript backend script created\n";
|
||||||
9
backend/scripts/createModel.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../src/Init.php';
|
||||||
|
use \TPsoft\DBmodel\Creator;
|
||||||
|
|
||||||
|
global $dbh;
|
||||||
|
$creator = new Creator($dbh);
|
||||||
|
$creator->rootDir(realpath(__DIR__.'/../src/'));
|
||||||
|
$creator->interact();
|
||||||
301
backend/src/API.php
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TPsoft\BugreportBackend;
|
||||||
|
|
||||||
|
require_once __DIR__ . '/Init.php';
|
||||||
|
|
||||||
|
use TPsoft\APIlite\APIlite;
|
||||||
|
use TPsoft\BugreportBackend\Models\Reports;
|
||||||
|
use TPsoft\BugreportBackend\Models\Attachments;
|
||||||
|
use TPsoft\BugreportBackend\Models\Options;
|
||||||
|
|
||||||
|
|
||||||
|
class API extends APIlite
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Add new report
|
||||||
|
*
|
||||||
|
* @param string $title
|
||||||
|
* @param string $description
|
||||||
|
* @param int $status 0 = Uncategorized, 1 = Waiting, 2 = InProgress, 3 = Blocked, 4 = Archived
|
||||||
|
* @param string $group
|
||||||
|
* @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
|
||||||
|
{
|
||||||
|
$status = intval($status);
|
||||||
|
$priority = intval($priority);
|
||||||
|
$reports = new Reports();
|
||||||
|
$report_id = $reports->report(null, [
|
||||||
|
'report_title' => $title,
|
||||||
|
'report_description' => $description,
|
||||||
|
'report_status' => $status,
|
||||||
|
'report_group' => $group,
|
||||||
|
'report_priority' => $priority,
|
||||||
|
]);
|
||||||
|
return $report_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update report
|
||||||
|
*
|
||||||
|
* @param int $report_id
|
||||||
|
* @param array $report_data
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function update(int $report_id, array $report_data): bool
|
||||||
|
{
|
||||||
|
$reports = new Reports();
|
||||||
|
$suc = $reports->report($report_id, $report_data);
|
||||||
|
return $suc !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete report
|
||||||
|
*
|
||||||
|
* @param int $report_id
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function delete(int $report_id): bool
|
||||||
|
{
|
||||||
|
$reports = new Reports();
|
||||||
|
$suc = $reports->report($report_id, null);
|
||||||
|
return $suc !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get report
|
||||||
|
*
|
||||||
|
* @param int $report_id
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get(int $report_id): array
|
||||||
|
{
|
||||||
|
$reports = new Reports();
|
||||||
|
return $reports->report($report_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all reports
|
||||||
|
*
|
||||||
|
* @param array $status 0 = Uncategorized, 1 = Waiting, 2 = InProgress, 3 = Blocked, 4 = Archived
|
||||||
|
* @param int $page Pagination from 0
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getAll(?array $status = null, int $page = 0): 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all reports grouped by status
|
||||||
|
*
|
||||||
|
* @param array $status 0 = Uncategorized, 1 = Waiting, 2 = InProgress, 3 = Blocked, 4 = Archived
|
||||||
|
* @param int $page Pagination from 0
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getAllGrouped(?array $status = null, int $page = 0): array
|
||||||
|
{
|
||||||
|
$page = intval($page);
|
||||||
|
$all = $this->getAll($status, $page);
|
||||||
|
$groups = [];
|
||||||
|
foreach ($all as $report) {
|
||||||
|
$groups[$report['report_status']][] = $report;
|
||||||
|
}
|
||||||
|
return $groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get archived reports
|
||||||
|
*
|
||||||
|
* @param int $page Pagination from 0
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getArchived(int $page = 0): array
|
||||||
|
{
|
||||||
|
$page = intval($page);
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->search('reports')
|
||||||
|
->where(['report_status' => 4])
|
||||||
|
->order(array('created_dt' => 'DESC'))
|
||||||
|
->limit($page * 10, 10)
|
||||||
|
->toArray();
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update report order number
|
||||||
|
*
|
||||||
|
* @param array $ordnums report_id => ordnum
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function updateOrdNum(array $ordnums): bool
|
||||||
|
{
|
||||||
|
$reports = new Reports();
|
||||||
|
$suc = true;
|
||||||
|
foreach ($ordnums as $report_id => $ordnum) {
|
||||||
|
$suc &= $reports->report($report_id, ['ordnum' => $ordnum]);
|
||||||
|
}
|
||||||
|
return $suc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update report status
|
||||||
|
*
|
||||||
|
* @param int $report_id
|
||||||
|
* @param int $status 0 = Uncategorized, 1 = Waiting, 2 = InProgress, 3 = Blocked, 4 = Archived
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function updateStatus(int $report_id, int $status): bool
|
||||||
|
{
|
||||||
|
$reports = new Reports();
|
||||||
|
$suc = $reports->report($report_id, ['report_status' => $status]);
|
||||||
|
return $suc !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add report attachment
|
||||||
|
*
|
||||||
|
* @param int $report_id
|
||||||
|
* @param string $attachment_type "comment" or "file"
|
||||||
|
* @param string $attachment_content
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function attachmentAdd(int $report_id, string $attachment_type, string $attachment_content): bool
|
||||||
|
{
|
||||||
|
if ($attachment_type == 'file') {
|
||||||
|
$data = json_decode($attachment_content, true);
|
||||||
|
if (!is_array($data)) return false;
|
||||||
|
$base64 = preg_replace('/^data:.*?;base64,/', '', $data['base64']);
|
||||||
|
$base64_data = base64_decode($base64);
|
||||||
|
$filename = 'report_' . $report_id . '_' . time() . '_' . $this->sanitizeFilename($data['filename']);
|
||||||
|
file_put_contents(UPLOAD_DIR_ATTACHMENTS . $filename, $base64_data);
|
||||||
|
$attachment_content = $filename;
|
||||||
|
}
|
||||||
|
$attachments = new Attachments();
|
||||||
|
$suc = $attachments->attachment(null, [
|
||||||
|
'report_id' => $report_id,
|
||||||
|
'attachment_type' => $attachment_type,
|
||||||
|
'attachment_content' => $attachment_content,
|
||||||
|
'created_dt' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
return $suc !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeFilename($filename, $allowedExtensions = [])
|
||||||
|
{
|
||||||
|
// Rozdelenie názvu a prípony
|
||||||
|
$pathInfo = pathinfo($filename);
|
||||||
|
$name = $pathInfo['filename'] ?? 'file';
|
||||||
|
$extension = strtolower($pathInfo['extension'] ?? '');
|
||||||
|
// Odstránenie nebezpečných znakov z názvu
|
||||||
|
$name = preg_replace('/[^a-zA-Z0-9_-]/', '_', $name);
|
||||||
|
$name = substr($name, 0, 100); // voliteľné obmedzenie dĺžky
|
||||||
|
// Validácia prípony, ak je zoznam povolený
|
||||||
|
if (
|
||||||
|
$allowedExtensions
|
||||||
|
&& count($allowedExtensions) > 0
|
||||||
|
&& !in_array($extension, $allowedExtensions)
|
||||||
|
) {
|
||||||
|
$extension = 'bin'; // fallback ak prípona nie je povolená
|
||||||
|
}
|
||||||
|
return $name . '.' . $extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update report attachment
|
||||||
|
*
|
||||||
|
* @param int $attachment_id
|
||||||
|
* @param string $attachment_content
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function attachmentUpdate(int $attachment_id, string $attachment_content): bool
|
||||||
|
{
|
||||||
|
if (strlen(trim($attachment_content)) <= 0) return $this->attachmentDelete($attachment_id);
|
||||||
|
$attachments = new Attachments();
|
||||||
|
$suc = $attachments->attachment($attachment_id, [
|
||||||
|
'attachment_content' => $attachment_content,
|
||||||
|
'updated_dt' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
return $suc !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all report attachments
|
||||||
|
*
|
||||||
|
* @param int $report_id
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function attachmentGetAll(int $report_id): array
|
||||||
|
{
|
||||||
|
$attachments = new Attachments();
|
||||||
|
$all = $attachments->search('attachments')
|
||||||
|
->where(['report_id' => $report_id])
|
||||||
|
->order(array('created_dt' => 'ASC'))
|
||||||
|
->toArray();
|
||||||
|
if (is_array($all)) foreach ($all as $key => $row) {
|
||||||
|
if ($all[$key]['attachment_type'] == 'file') {
|
||||||
|
$all[$key]['attachment_content'] = '?action=attachmentDownload&filename=' . $all[$key]['attachment_content'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete report attachment
|
||||||
|
*
|
||||||
|
* @param int $attachment_id
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function attachmentDelete(int $attachment_id): bool {
|
||||||
|
$attachments = new Attachments();
|
||||||
|
$suc = $attachments->attachment($attachment_id, null);
|
||||||
|
return $suc !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download report attachment
|
||||||
|
*
|
||||||
|
* @param string $filename
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function attachmentDownload(string $filename): void {
|
||||||
|
$filename = $this->sanitizeFilename($filename);
|
||||||
|
$filename = UPLOAD_DIR_ATTACHMENTS . $filename;
|
||||||
|
if (file_exists($filename)) {
|
||||||
|
header('Content-Description: File Transfer');
|
||||||
|
header('Content-Type: application/octet-stream');
|
||||||
|
header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
|
||||||
|
header('Expires: 0');
|
||||||
|
header('Cache-Control: must-revalidate');
|
||||||
|
header('Pragma: public');
|
||||||
|
header('Content-Length: ' . filesize($filename));
|
||||||
|
readfile($filename);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
backend/src/Init.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use \Exception;
|
||||||
|
use \TPsoft\DBmodel\DBmodel;
|
||||||
|
use \TPsoft\BugreportBackend\Maintenance;
|
||||||
|
|
||||||
|
global $dbh;
|
||||||
|
|
||||||
|
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));
|
||||||
|
} else {
|
||||||
|
throw new Exception('Unknown database type');
|
||||||
|
}
|
||||||
|
|
||||||
|
$maintenance = new Maintenance($dbh);
|
||||||
|
$maintenance->database();
|
||||||
81
backend/src/Maintenance.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TPsoft\BugreportBackend;
|
||||||
|
|
||||||
|
class Maintenance extends \TPsoft\DBmodel\Maintenance
|
||||||
|
{
|
||||||
|
|
||||||
|
public function database()
|
||||||
|
{
|
||||||
|
if (!$this->existsTable('options')) {
|
||||||
|
$this->checkDBTable('options', '
|
||||||
|
`key` VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||||
|
`value` VARCHAR(255) NOT NULL
|
||||||
|
');
|
||||||
|
$this->dbver(1);
|
||||||
|
}
|
||||||
|
$dbver = $this->dbver();
|
||||||
|
if ($dbver == 1) {
|
||||||
|
$this->checkDBTable('reports', '
|
||||||
|
`report_id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
`report_title` varchar(255) DEFAULT NULL,
|
||||||
|
`report_description` text DEFAULT NULL,
|
||||||
|
`report_status` int(11) DEFAULT NULL,
|
||||||
|
`report_group` varchar(255) NOT NULL,
|
||||||
|
`report_priority` int(11) DEFAULT NULL,
|
||||||
|
`ordnum` int(11) DEFAULT NULL,
|
||||||
|
`created_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
');
|
||||||
|
$this->dbver(2);
|
||||||
|
$dbver = 2;
|
||||||
|
}
|
||||||
|
if ($dbver == 2) {
|
||||||
|
$this->checkDBTable('attachments', '
|
||||||
|
`attachment_id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
`report_id` int(11) NOT NULL,
|
||||||
|
`attachment_type` varchar(255) DEFAULT NULL,
|
||||||
|
`attachment_content` text DEFAULT NULL,
|
||||||
|
`created_dt` datetime DEFAULT NULL,
|
||||||
|
`updated_dt` datetime DEFAULT NULL
|
||||||
|
');
|
||||||
|
$this->dbver(3);
|
||||||
|
$dbver = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function settings(string $key, ?string $value = null): string|false
|
||||||
|
{
|
||||||
|
if (is_null($value)) {
|
||||||
|
return $this->dbh->getOne(sprintf('SELECT `value` FROM `options` WHERE `key` = %s', $this->dbh->quote($key)));
|
||||||
|
} else {
|
||||||
|
$db_type = $this->dbh->getDBtype();
|
||||||
|
switch ($db_type) {
|
||||||
|
case 'mysql':
|
||||||
|
return $this->dbh->query(sprintf(
|
||||||
|
'INSERT INTO `options` (`key`, `value`) VALUES (%s, %s) ON DUPLICATE KEY UPDATE `value` = %s',
|
||||||
|
$this->dbh->quote($key),
|
||||||
|
$this->dbh->quote($value),
|
||||||
|
$this->dbh->quote($value)
|
||||||
|
)) !== false;
|
||||||
|
break;
|
||||||
|
case 'sqlite':
|
||||||
|
return $this->dbh->query(sprintf(
|
||||||
|
'INSERT INTO `options` (`key`, `value`) VALUES (%s, %s) ON CONFLICT(`key`) DO UPDATE SET `value` = %s',
|
||||||
|
$this->dbh->quote($key),
|
||||||
|
$this->dbh->quote($value),
|
||||||
|
$this->dbh->quote($value)
|
||||||
|
)) !== false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
new \Exception('Unknown DB type: ' . $db_type);
|
||||||
|
return false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function dbver(?string $ver = null)
|
||||||
|
{
|
||||||
|
return $this->settings('version', $ver);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
backend/src/Models/Attachments.php
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
TPsoft.org 2000-2025
|
||||||
|
file for controlers/*.php
|
||||||
|
|
||||||
|
Milestones:
|
||||||
|
2025-09-30 22:49 Created
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TPsoft\BugreportBackend\Models;
|
||||||
|
|
||||||
|
class Attachments extends \TPsoft\DBmodel\DBmodel {
|
||||||
|
|
||||||
|
public $tables = array(
|
||||||
|
'attachments' => array(
|
||||||
|
'name' => 'attachments',
|
||||||
|
'primary_key_name' => 'attachment_id',
|
||||||
|
'allow_attributes' => array(
|
||||||
|
'report_id' => 'INTEGER',
|
||||||
|
'attachment_type' => 'VARCHAR(255)',
|
||||||
|
'attachment_content' => 'TEXT',
|
||||||
|
'created_dt' => 'DATETIME',
|
||||||
|
'updated_dt' => 'DATETIME'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
public function exist($primary_key = null) {
|
||||||
|
return $this->existRecord('attachments', $primary_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attachment($primary_key = null, $data = array()) {
|
||||||
|
if (is_null($primary_key)
|
||||||
|
&& !isset($data['created_dt']))
|
||||||
|
{
|
||||||
|
$data['created_dt'] = date('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
return $this->record('attachments', $primary_key, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attachmentBy($colname, $colvalue) {
|
||||||
|
return $this->recordBy('attachments', $colname, $colvalue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attachmentSave($data = array()) {
|
||||||
|
return $this->attachment($this->exist($data) ? $data : null, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attachmentEmpty() {
|
||||||
|
return $this->recordEmpty('attachments');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attachmentAttributes() {
|
||||||
|
return $this->typesAttributes('attachments');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attachmentCount() {
|
||||||
|
return $this->count('attachments');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getList($search = array(), $reverse = false, $concat_or = false) {
|
||||||
|
return $this->search('attachments')
|
||||||
|
->where($search, $concat_or)
|
||||||
|
->order(array('attachment_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('attachment_id', $search, $reverse, $concat_or);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attachmentCombo($col_key, $col_value, $add_empty = false) {
|
||||||
|
return $this->search('attachments')
|
||||||
|
->toCombo($col_key, $col_value, $add_empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
81
backend/src/Models/Options.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
TPsoft.org 2000-2025
|
||||||
|
file for controlers/*.php
|
||||||
|
|
||||||
|
Milestones:
|
||||||
|
2025-09-30 22:49 Created
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TPsoft\BugreportBackend\Models;
|
||||||
|
|
||||||
|
class Options extends \TPsoft\DBmodel\DBmodel {
|
||||||
|
|
||||||
|
public $tables = array(
|
||||||
|
'options' => array(
|
||||||
|
'name' => 'options',
|
||||||
|
'primary_key_name' => '',
|
||||||
|
'allow_attributes' => array(
|
||||||
|
'key' => 'VARCHAR(64)',
|
||||||
|
'value' => 'TEXT',
|
||||||
|
'created_at' => 'DATETIME'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
public function exist($primary_key = null) {
|
||||||
|
return $this->existRecord('options', $primary_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function option($primary_key = null, $data = array()) {
|
||||||
|
return $this->record('options', $primary_key, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function optionBy($colname, $colvalue) {
|
||||||
|
return $this->recordBy('options', $colname, $colvalue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function optionSave($data = array()) {
|
||||||
|
return $this->option($this->exist($data) ? $data : null, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function optionEmpty() {
|
||||||
|
return $this->recordEmpty('options');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function optionAttributes() {
|
||||||
|
return $this->typesAttributes('options');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function optionCount() {
|
||||||
|
return $this->count('options');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getList($search = array(), $reverse = false, $concat_or = false) {
|
||||||
|
return $this->search('options')
|
||||||
|
->where($search, $concat_or)
|
||||||
|
->order(array('' => $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('', $search, $reverse, $concat_or);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function optionCombo($col_key, $col_value, $add_empty = false) {
|
||||||
|
return $this->search('options')
|
||||||
|
->toCombo($col_key, $col_value, $add_empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
90
backend/src/Models/Reports.php
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
TPsoft.org 2000-2025
|
||||||
|
file for controlers/*.php
|
||||||
|
|
||||||
|
Milestones:
|
||||||
|
2025-09-30 22:21 Created
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace TPsoft\BugreportBackend\Models;
|
||||||
|
|
||||||
|
class Reports extends \TPsoft\DBmodel\DBmodel {
|
||||||
|
|
||||||
|
public $tables = array(
|
||||||
|
'reports' => array(
|
||||||
|
'name' => 'reports',
|
||||||
|
'primary_key_name' => 'report_id',
|
||||||
|
'allow_attributes' => array(
|
||||||
|
'report_title' => 'VARCHAR(255)',
|
||||||
|
'report_description' => 'TEXT',
|
||||||
|
'report_status' => 'INTEGER',
|
||||||
|
'report_group' => 'VARCHAR(255)',
|
||||||
|
'report_priority' => 'INTEGER',
|
||||||
|
'created_dt' => 'DATETIME',
|
||||||
|
'ordnum' => 'INTEGER'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
public function exist($primary_key = null) {
|
||||||
|
return $this->existRecord('reports', $primary_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function report($primary_key = null, $data = array()) {
|
||||||
|
if (is_null($primary_key)
|
||||||
|
&& !isset($data['created_dt']))
|
||||||
|
{
|
||||||
|
$data['created_dt'] = date('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
return $this->record('reports', $primary_key, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reportBy($colname, $colvalue) {
|
||||||
|
return $this->recordBy('reports', $colname, $colvalue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reportSave($data = array()) {
|
||||||
|
return $this->report($this->exist($data) ? $data : null, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reportEmpty() {
|
||||||
|
return $this->recordEmpty('reports');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reportAttributes() {
|
||||||
|
return $this->typesAttributes('reports');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reportCount() {
|
||||||
|
return $this->count('reports');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getList($search = array(), $reverse = false, $concat_or = false) {
|
||||||
|
return $this->search('reports')
|
||||||
|
->where($search, $concat_or)
|
||||||
|
->order(array('report_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('report_id', $search, $reverse, $concat_or);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reportCombo($col_key, $col_value, $add_empty = false) {
|
||||||
|
return $this->search('reports')
|
||||||
|
->toCombo($col_key, $col_value, $add_empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
84
backend/tests/testDB.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../src/Init.php';
|
||||||
|
use TPsoft\BugreportBackend\Models\Reports;
|
||||||
|
|
||||||
|
global $dbh;
|
||||||
|
|
||||||
|
$test_id = $argv[1];
|
||||||
|
|
||||||
|
if ($test_id == 1) { // cista query
|
||||||
|
$query = sprintf('SELECT * FROM reports');
|
||||||
|
$list = $dbh->getAll($query);
|
||||||
|
print_r($list);
|
||||||
|
} else if ($test_id == 2) { // zistenie typu DB
|
||||||
|
$type = $dbh->getDBtype();
|
||||||
|
echo "DB type: '$type'\n";
|
||||||
|
} else if ($test_id == 3) { // ziskanie stlpcov tabulky
|
||||||
|
$table_columns = $dbh->getTableColumns('reports');
|
||||||
|
print_r($table_columns);
|
||||||
|
} else if ($test_id == 4) { // test modelu a ziskanie zoznamu
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->getList();
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 5) { // test SELECT
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->report(52);
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 6) { // test UPDATE
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->report(52, array('report_description' => 'zmenene o '.date('Y-m-d H:i:s')));
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 7) { // test INSERT
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->report(null, array('report_title' => 'napis pre bug', 'report_description' => 'vytvorene o '.date('Y-m-d H:i:s')));
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 8) { // test DELETE
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->report(55, null);
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 9) { // test exists()
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->exist(52);
|
||||||
|
var_dump($ret);
|
||||||
|
$ret = $reports->exist(525);
|
||||||
|
var_dump($ret);
|
||||||
|
} else if ($test_id == 10) { // test reportBy()
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->reportBy('report_title', 'test9');
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 11) { // test reportSave()
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->reportSave(array('report_id' => 54, 'report_description' => 'zmenene o '.date('Y-m-d H:i:s')));
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 12) { // test reportEmpty()
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->reportEmpty();
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 13) { // test reportAttributes()
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->reportAttributes();
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 14) { // test reportCount()
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->reportCount();
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 15) { // test getList()
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->getList(array('report_status' => 3), true);
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 16) { // test getListOrganize()
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->getListOrganize('report_id', array('report_status' => 3));
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 17) { // test getListByID()
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->getListByID(array('report_status' => 3));
|
||||||
|
print_r($ret);
|
||||||
|
} else if ($test_id == 18) { // test reportCombo()
|
||||||
|
$reports = new Reports();
|
||||||
|
$ret = $reports->reportCombo('report_id', 'report_title');
|
||||||
|
print_r($ret);
|
||||||
|
} else {
|
||||||
|
echo "Unknown test id (first argument): '$test_id'\n";
|
||||||
|
}
|
||||||
46
build.bat
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
echo ➡️ Delete old DIST folder
|
||||||
|
rmdir /s /q "dist"
|
||||||
|
echo ✅ Old DIST deleted
|
||||||
|
|
||||||
|
echo ➡️ Build Backend
|
||||||
|
cd backend
|
||||||
|
call composer run build
|
||||||
|
cd ..
|
||||||
|
echo ✅ Backend built
|
||||||
|
|
||||||
|
echo ➡️ Build frontend
|
||||||
|
cd frontend
|
||||||
|
call npm run build
|
||||||
|
cd ..
|
||||||
|
echo ✅ Frontend built
|
||||||
|
|
||||||
|
echo ➡️ Make new DIST folder structure
|
||||||
|
mkdir "dist"
|
||||||
|
mkdir "dist\data"
|
||||||
|
mkdir "dist\data\attachments"
|
||||||
|
mkdir "dist\app"
|
||||||
|
echo ✅ New DIST structur created
|
||||||
|
|
||||||
|
echo ➡️ Copy APP files
|
||||||
|
robocopy backend dist\app /S /XD tests scripts /XF composer.*
|
||||||
|
robocopy frontend\dist dist\app\public /S
|
||||||
|
echo ✅ APP files copied
|
||||||
|
|
||||||
|
echo ➡️ Packaging build
|
||||||
|
for /f "tokens=2 delims=:" %%a in ('findstr /c:"\"version\"" frontend\package.json') do (
|
||||||
|
set ver=%%a
|
||||||
|
set ver=!ver: =!
|
||||||
|
set ver=!ver:"=!
|
||||||
|
set ver=!ver:,=!
|
||||||
|
)
|
||||||
|
echo 👉 Version: %ver%
|
||||||
|
cd dist
|
||||||
|
tar -a -c -f ..\build\BugReport.%ver%.zip *
|
||||||
|
cd ..
|
||||||
|
echo ✅ Build packaged
|
||||||
|
|
||||||
|
echo 🚀 Done.
|
||||||
18
config.php
@ -1,18 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
if (file_exists('c:/php/includes/igor.php')) {
|
|
||||||
require_once 'c:/php/includes/igor.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once __DIR__.'/lib/functions.inc.php';
|
|
||||||
require_once __DIR__.'/lib/Medoo/src/Medoo.php';
|
|
||||||
|
|
||||||
global $db;
|
|
||||||
$db = new Medoo\Medoo([
|
|
||||||
'type' => 'sqlite',
|
|
||||||
'database' => __DIR__ . '/data/database.db'
|
|
||||||
]);
|
|
||||||
dbCheck();
|
|
||||||
|
|
||||||
|
|
||||||
?>
|
|
||||||
120
doc/FlowDiagram.drawio
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<mxfile host="Electron" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/27.0.5 Chrome/134.0.6998.205 Electron/35.3.0 Safari/537.36" version="27.0.5">
|
||||||
|
<diagram name="Page-1" id="74e2e168-ea6b-b213-b513-2b3c1d86103e">
|
||||||
|
<mxGraphModel dx="1783" dy="1051" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" background="none" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="77e6c97f196da883-1" value="BugReport v 0.1" style="swimlane;html=1;childLayout=stackLayout;startSize=20;rounded=0;shadow=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=8;align=center;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="150" y="40" width="800" height="560" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="77e6c97f196da883-2" value="Nezaradené" style="swimlane;html=1;startSize=20;" parent="77e6c97f196da883-1" vertex="1">
|
||||||
|
<mxGeometry y="20" width="160" height="540" as="geometry">
|
||||||
|
<mxRectangle y="20" width="40" height="730" as="alternateBounds" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="77e6c97f196da883-8" value="V tomto stave ostáva kým na ne pozriem a zhodnotim, či niečo potrebujem doplniť" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=8;align=center;" parent="77e6c97f196da883-2" vertex="1">
|
||||||
|
<mxGeometry x="20" y="65" width="100" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="77e6c97f196da883-2" source="77e6c97f196da883-8" target="bH6rxZphyOiop9y04Wjb-9">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="70" y="170" as="targetPoint" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-9" value="" style="rhombus;whiteSpace=wrap;html=1;rounded=0;shadow=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=8;align=center;" vertex="1" parent="77e6c97f196da883-2">
|
||||||
|
<mxGeometry x="30" y="180" width="80" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="77e6c97f196da883-3" value="Čakajúce" style="swimlane;html=1;startSize=20;" parent="77e6c97f196da883-1" vertex="1">
|
||||||
|
<mxGeometry x="160" y="20" width="160" height="540" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-6" value="Uloha má všetko pre začatie, čaká na najbližši voľný čas" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=8;align=center;" vertex="1" parent="77e6c97f196da883-3">
|
||||||
|
<mxGeometry x="30" y="250" width="100" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="77e6c97f196da883-4" value="Rozpracované" style="swimlane;html=1;startSize=20;" parent="77e6c97f196da883-1" vertex="1">
|
||||||
|
<mxGeometry x="320" y="20" width="160" height="540" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="77e6c97f196da883-4" source="bH6rxZphyOiop9y04Wjb-17" target="bH6rxZphyOiop9y04Wjb-20">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-17" value="Aktuálne sa na úlohe pracuje, alebo je rozpracovaná a v najbližšom pracovnom čase sa pokračuje" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=8;align=center;" vertex="1" parent="77e6c97f196da883-4">
|
||||||
|
<mxGeometry x="30" y="250" width="100" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-20" value="" style="rhombus;whiteSpace=wrap;html=1;rounded=0;shadow=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=8;align=center;" vertex="1" parent="77e6c97f196da883-4">
|
||||||
|
<mxGeometry x="40" y="360" width="80" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="77e6c97f196da883-5" value="Blokované" style="swimlane;html=1;startSize=20;" parent="77e6c97f196da883-1" vertex="1">
|
||||||
|
<mxGeometry x="480" y="20" width="160" height="540" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-10" value="Nemám dostatok informácií k začatiu, alebo za niečim čakám" style="rounded=1;whiteSpace=wrap;html=1;shadow=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=8;align=center;" vertex="1" parent="77e6c97f196da883-5">
|
||||||
|
<mxGeometry x="30" y="180" width="100" height="60" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="77e6c97f196da883-6" value="Archivované" style="swimlane;html=1;startSize=20;" parent="77e6c97f196da883-1" vertex="1">
|
||||||
|
<mxGeometry x="640" y="20" width="160" height="540" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-25" value="Hotovo" style="verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.flowchart.or;" vertex="1" parent="77e6c97f196da883-6">
|
||||||
|
<mxGeometry x="45" y="430" width="70" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="77e6c97f196da883-1" source="bH6rxZphyOiop9y04Wjb-9" target="bH6rxZphyOiop9y04Wjb-6">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<mxPoint x="110" y="280" as="sourcePoint" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-8" value="Mám všetko" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="bH6rxZphyOiop9y04Wjb-7">
|
||||||
|
<mxGeometry x="-0.3755" relative="1" as="geometry">
|
||||||
|
<mxPoint x="40" y="-10" as="offset" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="77e6c97f196da883-1" source="bH6rxZphyOiop9y04Wjb-9" target="bH6rxZphyOiop9y04Wjb-10">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-12" value="Chýba niečo" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="bH6rxZphyOiop9y04Wjb-11">
|
||||||
|
<mxGeometry x="-0.9025" relative="1" as="geometry">
|
||||||
|
<mxPoint x="11" y="-10" as="offset" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="77e6c97f196da883-1" source="bH6rxZphyOiop9y04Wjb-10" target="77e6c97f196da883-8">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-14" value="Po doplnení opäť vyžaduje zhodnotenie" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="bH6rxZphyOiop9y04Wjb-13">
|
||||||
|
<mxGeometry x="-0.8157" relative="1" as="geometry">
|
||||||
|
<mxPoint x="-50" y="-52" as="offset" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="77e6c97f196da883-1" source="bH6rxZphyOiop9y04Wjb-6" target="bH6rxZphyOiop9y04Wjb-17">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-19" value="Začínam&nbsp;<div>pracovať</div>" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="bH6rxZphyOiop9y04Wjb-18">
|
||||||
|
<mxGeometry x="-0.6651" y="1" relative="1" as="geometry">
|
||||||
|
<mxPoint x="20" y="-19" as="offset" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="77e6c97f196da883-1" source="bH6rxZphyOiop9y04Wjb-20" target="bH6rxZphyOiop9y04Wjb-10">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-23" value="Počas práce niečo chýba" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="bH6rxZphyOiop9y04Wjb-22">
|
||||||
|
<mxGeometry x="-0.8409" relative="1" as="geometry">
|
||||||
|
<mxPoint x="29" y="-20" as="offset" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-26" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="77e6c97f196da883-1" source="bH6rxZphyOiop9y04Wjb-20" target="bH6rxZphyOiop9y04Wjb-25">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-27" value="Všetko fixnuté" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="bH6rxZphyOiop9y04Wjb-26">
|
||||||
|
<mxGeometry x="-0.0121" y="1" relative="1" as="geometry">
|
||||||
|
<mxPoint x="-68" y="-14" as="offset" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="bH6rxZphyOiop9y04Wjb-1" target="77e6c97f196da883-8">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-3" value="Vytvorený nový BUG" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="bH6rxZphyOiop9y04Wjb-2">
|
||||||
|
<mxGeometry x="-0.475" y="-1" relative="1" as="geometry">
|
||||||
|
<mxPoint x="28" y="-11" as="offset" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="bH6rxZphyOiop9y04Wjb-1" value="" style="shape=waypoint;sketch=0;fillStyle=solid;size=6;pointerEvents=1;points=[];fillColor=none;resizable=0;rotatable=0;perimeter=centerPerimeter;snapToPoint=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry y="145" width="20" height="20" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
BIN
doc/Screenshot_2025-05-17_111345.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
21
doc/notes.txt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
projekt/
|
||||||
|
│
|
||||||
|
├── backend/ # PHP časť (API, logika, DB)
|
||||||
|
│ ├── public/ # root pre webserver (index.php, assets)
|
||||||
|
│ │ └── index.php
|
||||||
|
│ ├── src/ # zdrojový PHP kód (kontroléry, modely, služby)
|
||||||
|
│ ├── vendor/ # Composer balíčky (gitignore!)
|
||||||
|
│ ├── composer.json
|
||||||
|
│ └── composer.lock
|
||||||
|
│
|
||||||
|
├── frontend/ # Vue časť
|
||||||
|
│ ├── src/ # Vue komponenty, views, store
|
||||||
|
│ ├── public/ # statické súbory (favicon, index.html template)
|
||||||
|
│ ├── dist/ # build výstup (gitignore!) → deploy do backend/public
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── package-lock.json
|
||||||
|
│ └── vite.config.js / vue.config.js
|
||||||
|
│
|
||||||
|
├── docker/ (voliteľné) # ak používaš Docker (Nginx, PHP-FPM, Node build)
|
||||||
|
│
|
||||||
|
└── README.md
|
||||||
1
frontend/.env.development
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_BACKENDAPI_URL="https://192.168.0.101/BugReport/backend/public/API.php"
|
||||||
1
frontend/.env.production
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_BACKENDAPI_URL="/API.php"
|
||||||
0
webapp/.gitignore → frontend/.gitignore
vendored
319
webapp/package-lock.json → frontend/package-lock.json
generated
@ -12,12 +12,15 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||||
|
"js-confetti": "^0.12.0",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"rollup-plugin-visualizer": "^5.14.0",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1039,6 +1042,32 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
@ -1048,12 +1077,64 @@
|
|||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wrap-ansi": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/define-lazy-prop": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
@ -1107,6 +1188,16 @@
|
|||||||
"@esbuild/win32-x64": "0.25.2"
|
"@esbuild/win32-x64": "0.25.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escalade": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
@ -1128,6 +1219,61 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-docker": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"is-docker": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-wsl": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-docker": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-confetti": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-confetti/-/js-confetti-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-1R0Akxn3Zn82pMqW65N1V2NwKkZJ75bvBN/VAb36Ya0YHwbaSiAJZVRr/19HBxH/O8x2x01UFAbYI18VqlDN6g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.17",
|
"version": "0.30.17",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
@ -1137,6 +1283,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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",
|
||||||
@ -1155,12 +1307,43 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/open": {
|
||||||
|
"version": "8.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
|
||||||
|
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"define-lazy-prop": "^2.0.0",
|
||||||
|
"is-docker": "^2.1.1",
|
||||||
|
"is-wsl": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.3",
|
"version": "8.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||||
@ -1189,6 +1372,16 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.40.0",
|
"version": "4.40.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
|
||||||
@ -1229,6 +1422,47 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer": {
|
||||||
|
"version": "5.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz",
|
||||||
|
"integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"open": "^8.4.0",
|
||||||
|
"picomatch": "^4.0.2",
|
||||||
|
"source-map": "^0.7.4",
|
||||||
|
"yargs": "^17.5.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rollup-plugin-visualizer": "dist/bin/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rolldown": "1.x",
|
||||||
|
"rollup": "2.x || 3.x || 4.x"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rolldown": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup-plugin-visualizer/node_modules/source-map": {
|
||||||
|
"version": "0.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
|
||||||
|
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sortablejs": {
|
"node_modules/sortablejs": {
|
||||||
"version": "1.14.0",
|
"version": "1.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
|
||||||
@ -1269,6 +1503,34 @@
|
|||||||
"source-map": "^0.6.0"
|
"source-map": "^0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.39.0",
|
"version": "5.39.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
|
||||||
@ -1418,6 +1680,63 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "^3.0.1"
|
"vue": "^3.0.1"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "17.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^8.0.1",
|
||||||
|
"escalade": "^3.1.1",
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"y18n": "^5.0.5",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bugreport",
|
"name": "bugreport",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@ -13,12 +13,15 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||||
|
"js-confetti": "^0.12.0",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"rollup-plugin-visualizer": "^5.14.0",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
frontend/public/sounds/crazy-phrog-short.mp3
Normal file
BIN
frontend/public/sounds/crazy-phrog.mp3
Normal file
BIN
frontend/public/sounds/tada.mp3
Normal file
BIN
frontend/public/sounds/tada2.mp3
Normal file
67
frontend/src/App.vue
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div id="header">
|
||||||
|
<div class="logo">
|
||||||
|
<router-link to="/">
|
||||||
|
<img src="/bugreport.svg" height="48" width="48" />
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/">
|
||||||
|
<h1>Bug Report</h1>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="short-bug">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Rýchly task + <ENTER>"
|
||||||
|
v-model="short_bug"
|
||||||
|
@keyup.enter="onShortBugEnter"
|
||||||
|
/>
|
||||||
|
<button @click="shortBugAdd">
|
||||||
|
<font-awesome-icon :icon="['fas', 'circle-check']" /> Pridať
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="menu">
|
||||||
|
<router-link to="/add"
|
||||||
|
><font-awesome-icon :icon="['fas', 'square-plus']" /> Pridať
|
||||||
|
bug</router-link
|
||||||
|
>
|
||||||
|
<router-link to="/"
|
||||||
|
><font-awesome-icon :icon="['fas', 'list-check']" /> Zoznam
|
||||||
|
reportov</router-link
|
||||||
|
>
|
||||||
|
<router-link to="/archive"
|
||||||
|
><font-awesome-icon :icon="['fas', 'box-archive']" />
|
||||||
|
Archív</router-link
|
||||||
|
>
|
||||||
|
<router-link to="/api"
|
||||||
|
><font-awesome-icon :icon="['fas', 'plug']" /> API</router-link
|
||||||
|
>
|
||||||
|
<router-link to="/about"
|
||||||
|
><font-awesome-icon :icon="['fas', 'address-card']" /> O
|
||||||
|
aplikácii</router-link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<router-view></router-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import backend from "./backend";
|
||||||
|
import events from "./events";
|
||||||
|
|
||||||
|
const short_bug = ref("");
|
||||||
|
|
||||||
|
function onShortBugEnter(event) {
|
||||||
|
if (event.keyCode == 13) {
|
||||||
|
shortBugAdd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortBugAdd() {
|
||||||
|
let content = short_bug.value;
|
||||||
|
short_bug.value = "";
|
||||||
|
backend.add(content, "", "0", "0", "1").then(() => {
|
||||||
|
events.emit("reports-changed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -131,21 +131,89 @@ button:focus-visible,
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
#header .short-bug {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 2;
|
||||||
|
/* border: 1px red solid; */
|
||||||
|
padding: 0px 20px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center
|
||||||
|
}
|
||||||
|
#header .short-bug input {
|
||||||
|
width: 80%;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: var(--color-bg2);
|
||||||
|
color: var(--color-text0);
|
||||||
|
}
|
||||||
|
#header .menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
#header .menu a,
|
#header .menu a,
|
||||||
#header .menu a:visited {
|
#header .menu a:visited,
|
||||||
|
#header button {
|
||||||
border: 1px solid var(--color-text0);
|
border: 1px solid var(--color-text0);
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
#header .menu a:hover {
|
#header .menu a:hover,
|
||||||
|
#header button:hover {
|
||||||
color: var(--color-text1);
|
color: var(--color-text1);
|
||||||
background-color: var(--color-bg1);
|
background-color: var(--color-bg1);
|
||||||
}
|
}
|
||||||
|
#header .short-bug button {
|
||||||
|
margin: 0px;
|
||||||
|
|
||||||
|
/* height: 30px; */
|
||||||
|
}
|
||||||
#header h1 {
|
#header h1 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 1400px) {
|
||||||
|
#header {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
#header .short-bug {
|
||||||
|
width: 50%;
|
||||||
|
padding: 0px;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
#header .menu {
|
||||||
|
width: 100%;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
#header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#header .short-bug {
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center
|
||||||
|
}
|
||||||
|
#header .menu {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
#header .menu a {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#header .short-bug {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
03 - DASHBOARD
|
03 - DASHBOARD
|
||||||
@ -197,6 +265,7 @@ button:focus-visible,
|
|||||||
#dashboard .report .report-description {
|
#dashboard .report .report-description {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
|
white-space: pre-line;
|
||||||
border-right: 5px var(--color-bg0) solid;
|
border-right: 5px var(--color-bg0) solid;
|
||||||
background-color: var(--color-bg2);
|
background-color: var(--color-bg2);
|
||||||
}
|
}
|
||||||
@ -211,6 +280,10 @@ button:focus-visible,
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
#dashboard .report .report-id {
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
#dashboard .report .report-date {
|
#dashboard .report .report-date {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@ -249,6 +322,20 @@ button:focus-visible,
|
|||||||
#dashboard .report-priority-3 .report-description {
|
#dashboard .report-priority-3 .report-description {
|
||||||
border-right-color: var(--color-bgRed);
|
border-right-color: var(--color-bgRed);
|
||||||
}
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
#dashboard {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
#dashboard > div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#dashboard > div:not(:first-child) {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
#dashboard > div:not(:first-child) {
|
||||||
|
border-top: 5px var(--color-bg0) dotted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
04 - BUG ADD
|
04 - BUG ADD
|
||||||
@ -263,10 +350,86 @@ button:focus-visible,
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#bug-add .cols {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
05 - ARCHIVE
|
05 - ARCHIVE
|
||||||
*/
|
*/
|
||||||
|
#archive {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
min-height: 110vh;
|
||||||
|
}
|
||||||
|
#archive .reports {
|
||||||
|
/* border: 1px red solid; */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
#archive .report-row {
|
||||||
|
/* border: 1px blue solid; */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background-color: var(--color-bg2);
|
||||||
|
justify-content: space-between;
|
||||||
|
align-content: stretch;
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#archive .report-row:hover {
|
||||||
|
filter: brightness(1.4);
|
||||||
|
}
|
||||||
|
#archive .report-row .report-id,
|
||||||
|
#archive .report-row .title,
|
||||||
|
#archive .report-row .date,
|
||||||
|
#archive .report-row .group {
|
||||||
|
/* border: 1px yellow solid; */
|
||||||
|
width: auto;
|
||||||
|
padding: 10px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
#archive .report-row .report-id {
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: var(--color-bg0);
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
#archive .loadmore {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
#archive .report-row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
#archive .report-row .title,
|
||||||
|
#archive .report-row .date,
|
||||||
|
#archive .report-row .group {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
#archive .report-row .title {
|
||||||
|
width: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
#archive .report-row .date {
|
||||||
|
margin-left: 70px;
|
||||||
|
}
|
||||||
|
#archive .report-row .date,
|
||||||
|
#archive .report-row .group {
|
||||||
|
width: calc((100% - 140px) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#archive .report-row .date,
|
||||||
|
#archive .report-row .group {
|
||||||
|
margin-left: 70px;
|
||||||
|
width: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
@ -300,6 +463,36 @@ button:focus-visible,
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
}
|
}
|
||||||
|
#api ul li {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
#api .param-type {
|
||||||
|
background-color: var(--color-bg0);
|
||||||
|
margin: 0px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
#api .param-optional {
|
||||||
|
background-color: var(--color-bg1);
|
||||||
|
margin: 0px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
#api .param-default {
|
||||||
|
background-color: var(--color-bg2);
|
||||||
|
margin: 0px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
#api .param-doc {
|
||||||
|
color: var(--color-text3);
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
07 - ABOUT
|
07 - ABOUT
|
||||||
@ -325,6 +518,11 @@ button:focus-visible,
|
|||||||
#about .cols div:hover {
|
#about .cols div:hover {
|
||||||
filter: brightness(1.2);
|
filter: brightness(1.2);
|
||||||
}
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#about .cols {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
08 - REPORT
|
08 - REPORT
|
||||||
@ -337,11 +535,13 @@ button:focus-visible,
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
/* justify-content: space-between; */
|
/* justify-content: space-between; */
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
#report .report-header div {
|
#report .report-header div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-right: 20px;
|
/* margin-right: 20px; */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
#report .report-header div span {
|
#report .report-header div span {
|
||||||
@ -362,7 +562,40 @@ button:focus-visible,
|
|||||||
text-align: justify;
|
text-align: justify;
|
||||||
white-space: pre-line
|
white-space: pre-line
|
||||||
}
|
}
|
||||||
|
#report .attachments {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
#report .attachments .attachment {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--color-bg2);
|
||||||
|
text-align: justify;
|
||||||
|
white-space: pre-line;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
#report .attachments .attachment .attachment-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: var(--color-bg0);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#report .attachments .attachment .attachment-header .created,
|
||||||
|
#report .attachments .attachment .attachment-header .author {
|
||||||
|
padding: 2px 10px;
|
||||||
|
}
|
||||||
|
#report .attachments .attachment .attachment-content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
#report .attachment-new {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#report .report-header {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
/* ----------------------------------------------------
|
||||||
80 - FORM
|
80 - FORM
|
||||||
4
frontend/src/assets/images/FlowDiagram.drawio.svg
Normal file
|
After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 949 B After Width: | Height: | Size: 949 B |
|
Before Width: | Height: | Size: 496 B After Width: | Height: | Size: 496 B |
117
frontend/src/backend.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* Generated by APIlite
|
||||||
|
* https://gitea.tpsoft.org/TPsoft.org/APIlite
|
||||||
|
*
|
||||||
|
* 2025-10-16 01:38:51 */
|
||||||
|
|
||||||
|
class backend {
|
||||||
|
endpoint = import.meta.env.VITE_BACKENDAPI_URL;
|
||||||
|
|
||||||
|
/* ----------------------------------------------------
|
||||||
|
* General API call
|
||||||
|
*/
|
||||||
|
call(method, data, callback) {
|
||||||
|
var xhttp = new XMLHttpRequest();
|
||||||
|
xhttp.withCredentials = true;
|
||||||
|
xhttp.onreadystatechange = function() {
|
||||||
|
if (this.readyState == 4) {
|
||||||
|
if (this.status == 200) {
|
||||||
|
if (callback != null) callback(JSON.parse(this.responseText));
|
||||||
|
} else {
|
||||||
|
if (callback != null) callback({'status': 'ERROR', 'message': 'HTTP STATUS ' + this.status});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var form_data = new FormData();
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
let val = data[key];
|
||||||
|
if (typeof val == 'object') val = JSON.stringify(val);
|
||||||
|
form_data.append(key, val);
|
||||||
|
});
|
||||||
|
xhttp.open('POST', this.endpoint + '?action=' + method);
|
||||||
|
xhttp.send(form_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
callPromise(method, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.call(method, data, function(response) {
|
||||||
|
if (method == '__HELP__') {
|
||||||
|
resolve(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.status == 'OK') {
|
||||||
|
resolve(response.data);
|
||||||
|
} else {
|
||||||
|
reject(response.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------
|
||||||
|
* API actions
|
||||||
|
*/
|
||||||
|
help() {
|
||||||
|
return this.callPromise('__HELP__', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
add(title, description, status, group, priority) {
|
||||||
|
return this.callPromise('add', {title: title, description: description, status: status, group: group, priority: priority});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(report_id, report_data) {
|
||||||
|
return this.callPromise('update', {report_id: report_id, report_data: report_data});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(report_id) {
|
||||||
|
return this.callPromise('delete', {report_id: report_id});
|
||||||
|
}
|
||||||
|
|
||||||
|
get(report_id) {
|
||||||
|
return this.callPromise('get', {report_id: report_id});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(status, page) {
|
||||||
|
return this.callPromise('getAll', {status: status, page: page});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllGrouped(status, page) {
|
||||||
|
return this.callPromise('getAllGrouped', {status: status, page: page});
|
||||||
|
}
|
||||||
|
|
||||||
|
getArchived(page) {
|
||||||
|
return this.callPromise('getArchived', {page: page});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOrdNum(ordnums) {
|
||||||
|
return this.callPromise('updateOrdNum', {ordnums: ordnums});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(report_id, status) {
|
||||||
|
return this.callPromise('updateStatus', {report_id: report_id, status: status});
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentAdd(report_id, attachment_type, attachment_content) {
|
||||||
|
return this.callPromise('attachmentAdd', {report_id: report_id, attachment_type: attachment_type, attachment_content: attachment_content});
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentUpdate(attachment_id, attachment_content) {
|
||||||
|
return this.callPromise('attachmentUpdate', {attachment_id: attachment_id, attachment_content: attachment_content});
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentGetAll(report_id) {
|
||||||
|
return this.callPromise('attachmentGetAll', {report_id: report_id});
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentDelete(attachment_id) {
|
||||||
|
return this.callPromise('attachmentDelete', {attachment_id: attachment_id});
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentDownload(filename) {
|
||||||
|
return this.callPromise('attachmentDownload', {filename: filename});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default new backend();
|
||||||
@ -11,11 +11,16 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
/* zatmavene */
|
||||||
|
/* background-color: rgba(0, 0, 0, 0.8); */
|
||||||
|
/* rozmazane */
|
||||||
|
background-color: rgba(255, 255, 255, 0.1); /* jemne priehľadné alebo aj 0 */
|
||||||
|
backdrop-filter: blur(8px); /* rozmazanie pozadia */
|
||||||
|
-webkit-backdrop-filter: blur(8px); /* podpora pre Safari */
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
@ -22,6 +22,7 @@ defineProps({
|
|||||||
</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"><font-awesome-icon :icon="['fas', 'diagram-project']" /> {{ group }}</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>
|
||||||
|
|
||||||
5
frontend/src/events.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import mitt from "mitt";
|
||||||
|
|
||||||
|
const events = mitt();
|
||||||
|
|
||||||
|
export default events;
|
||||||
@ -24,51 +24,42 @@
|
|||||||
<h4>Parametre</h4>
|
<h4>Parametre</h4>
|
||||||
<p v-if="Object.keys(action.params).length == 0">
|
<p v-if="Object.keys(action.params).length == 0">
|
||||||
<font-awesome-icon :icon="['fas', 'circle-info']" />
|
<font-awesome-icon :icon="['fas', 'circle-info']" />
|
||||||
|
Ziadne parametre
|
||||||
Ziadne parametre
|
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(param_desc, param_name) in action.params" :key="param_name">
|
<li v-for="param in action.params" :key="param_name">
|
||||||
<strong>{{ param_name }}</strong>
|
<strong>{{ param.name }}</strong>
|
||||||
–
|
<span class="param-type">{{ param.type }}</span>
|
||||||
{{ param_desc }}
|
<span v-if="param.optional" class="param-optional">optional</span>
|
||||||
|
<span v-if="param.default != null" class="param-default">{{
|
||||||
|
param.default
|
||||||
|
}}</span>
|
||||||
|
<br />
|
||||||
|
<span class="param-doc">{{ param.doc }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<p>
|
||||||
|
<strong>Return</strong>
|
||||||
|
<span class="param-type">{{ action.return }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import { backend } from "../backend";
|
import { onMounted, ref } from "vue";
|
||||||
|
import backend from "../backend";
|
||||||
|
|
||||||
export default {
|
const api_endpoint = ref(backend.endpont);
|
||||||
name: "API",
|
const help = ref({ actions: {} });
|
||||||
components: {},
|
|
||||||
data() {
|
onMounted(() => {
|
||||||
return {
|
loadHelp();
|
||||||
api_endpoint: backend.endpont,
|
});
|
||||||
help: {
|
|
||||||
actions: {
|
function loadHelp() {
|
||||||
help: {
|
|
||||||
name: "help",
|
|
||||||
description: "This help",
|
|
||||||
params: {
|
|
||||||
foo: "bar",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.loadHelp();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
loadHelp() {
|
|
||||||
backend.help().then((response) => {
|
backend.help().then((response) => {
|
||||||
this.help = response;
|
help.value = response;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<div id="about">
|
<div id="about">
|
||||||
<div>
|
<div>
|
||||||
<h1>O aplikácii</h1>
|
<h1>O aplikácii</h1>
|
||||||
<img src="/public/bugreport.svg" height="100" alt="" />
|
<img src="/bugreport.svg" height="100" alt="" />
|
||||||
<h2>Bug Report</h2>
|
<h2>Bug Report</h2>
|
||||||
<p>
|
<p>
|
||||||
Verzia aplikácie: {{ version }} | Zostavené: {{ build }}
|
Verzia aplikácie: {{ version }} | Zostavené: {{ build }}
|
||||||
@ -12,21 +12,21 @@
|
|||||||
<div>
|
<div>
|
||||||
Backend thanks for <br />
|
Backend thanks for <br />
|
||||||
<a href="https://www.php.net" target="_blank">
|
<a href="https://www.php.net" target="_blank">
|
||||||
<img src="/public/php-power-micro.png" height="20" alt="" /> <br />
|
<img :src="php_power_micro" height="20" alt="" /> <br />
|
||||||
PHP 8.2
|
PHP 8.2
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Database thanks for <br>
|
Database thanks for <br>
|
||||||
<a href="https://www.sqlite.org" target="_blank">
|
<a href="https://www.sqlite.org" target="_blank">
|
||||||
<img src="/public/SQLite370.svg" height="20" alt="" /> <br>
|
<img :src="sqlite" height="20" alt="" /> <br>
|
||||||
SQLite
|
SQLite
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Frontend thanks for <br>
|
Frontend thanks for <br>
|
||||||
<a href="https://vuejs.org" target="_blank">
|
<a href="https://vuejs.org" target="_blank">
|
||||||
<img src="/public/vue.svg" height="20" alt="" /> <br>
|
<img :src="vue" height="20" alt="" /> <br>
|
||||||
Vue 3
|
Vue 3
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -38,17 +38,20 @@
|
|||||||
>https://gitea.tpsoft.org/TPsoft.org/BugReport</a
|
>https://gitea.tpsoft.org/TPsoft.org/BugReport</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
<h2>Diagram stavov pre BUG</h2>
|
||||||
|
<img :src="flowdiagram" alt="" width="90%" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { ref } from 'vue';
|
||||||
data() {
|
import php_power_micro from '../assets/images/php-power-micro.png';
|
||||||
return {
|
import sqlite from '../assets/images/SQLite370.svg';
|
||||||
version: __APP_VERSION__,
|
import vue from '../assets/images/vue.svg';
|
||||||
build: __BUILD_DATE__,
|
import flowdiagram from '../assets/images/FlowDiagram.drawio.svg';
|
||||||
};
|
|
||||||
},
|
const version = ref(__APP_VERSION__);
|
||||||
};
|
const build = ref(__BUILD_DATE__);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
78
frontend/src/views/Archive.vue
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<FullScreenLoader v-if="loading" />
|
||||||
|
|
||||||
|
<div id="archive">
|
||||||
|
<h1>Archív vyriesenych BUG reportov</h1>
|
||||||
|
|
||||||
|
<div class="reports">
|
||||||
|
<div
|
||||||
|
class="report-row"
|
||||||
|
v-for="report in reports"
|
||||||
|
:key="report.report_id"
|
||||||
|
@click="$router.push('/report/' + report.report_id)"
|
||||||
|
>
|
||||||
|
<div class="report-id">
|
||||||
|
<font-awesome-icon :icon="['fas', 'hashtag']" />
|
||||||
|
{{ report.report_id }}
|
||||||
|
</div>
|
||||||
|
<div class="title">{{ report.report_title }}</div>
|
||||||
|
<div class="date">{{ report.created_dt }}</div>
|
||||||
|
<div class="group">{{ report.report_group }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="loadmore">
|
||||||
|
<button v-if="!loadedAll" @click="loadMoreReports"><font-awesome-icon :icon="['fas', 'arrow-down']" /> Nacitat dalsie</button>
|
||||||
|
<div v-else><font-awesome-icon :icon="['fas', 'check']" /> Vsetky archivovane reporty su zobrazene</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, onBeforeUnmount, ref } from "vue";
|
||||||
|
import backend from "../backend";
|
||||||
|
import FullScreenLoader from "../components/FullScreenLoader.vue";
|
||||||
|
|
||||||
|
const reports = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const page = ref(0);
|
||||||
|
const loadedAll = ref(false);
|
||||||
|
|
||||||
|
function loadMoreReports() {
|
||||||
|
loading.value = true;
|
||||||
|
backend.getArchived(page.value).then((response) => {
|
||||||
|
// console.log(data);
|
||||||
|
if (response.data.length == 0) {
|
||||||
|
loadedAll.value = true;
|
||||||
|
} else {
|
||||||
|
reports.value.push(...response.data);
|
||||||
|
}
|
||||||
|
page.value++;
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let _in_scroll_handler = false;
|
||||||
|
function handleScroll() {
|
||||||
|
if (_in_scroll_handler || loading.value || loadedAll.value) return;
|
||||||
|
_in_scroll_handler = true;
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
const documentHeight = document.documentElement.scrollHeight;
|
||||||
|
if (scrollY + windowHeight >= documentHeight - 100) {
|
||||||
|
loadMoreReports();
|
||||||
|
}
|
||||||
|
_in_scroll_handler = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadMoreReports();
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('scroll', handleScroll)
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
@ -30,33 +30,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cols">
|
<div class="cols">
|
||||||
<div class="form-group">
|
|
||||||
<label for="files">Prílohy:</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="files"
|
|
||||||
@change="handleFileUpload"
|
|
||||||
multiple
|
|
||||||
class="form-control file-input"
|
|
||||||
/>
|
|
||||||
<div class="selected-files" v-if="selectedFiles.length > 0">
|
|
||||||
<p>Vybrané súbory:</p>
|
|
||||||
|
|
||||||
<p v-for="(file, index) in selectedFiles" :key="index">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="remove-file"
|
|
||||||
@click="removeFile(index)"
|
|
||||||
>
|
|
||||||
<font-awesome-icon :icon="['fas', 'circle-xmark']" /> Odober
|
|
||||||
súbor
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{{ file.name }} ({{ formatFileSize(file.size) }})
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="priority">Priorita:</label>
|
<label for="priority">Priorita:</label>
|
||||||
<select
|
<select
|
||||||
@ -88,7 +61,6 @@
|
|||||||
<option value="antispam">Antispam</option>
|
<option value="antispam">Antispam</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<router-link to="/" class="button"
|
<router-link to="/" class="button"
|
||||||
@ -99,12 +71,15 @@
|
|||||||
<font-awesome-icon :icon="['fas', 'circle-check']" /> Odoslať
|
<font-awesome-icon :icon="['fas', 'circle-check']" /> Odoslať
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<p>Poznamka: Subory je mozne pridat ako prilohy az po vytvoreni noveho reportu..</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { backend } from "../backend";
|
import backend from "../backend";
|
||||||
import FullScreenLoader from "../components/FullScreenLoader.vue";
|
import FullScreenLoader from "../components/FullScreenLoader.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -124,26 +99,6 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleFileUpload(event) {
|
|
||||||
const files = event.target.files;
|
|
||||||
if (files) {
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
this.selectedFiles.push(files[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeFile(index) {
|
|
||||||
this.selectedFiles.splice(index, 1);
|
|
||||||
},
|
|
||||||
formatFileSize(size) {
|
|
||||||
if (size < 1024) {
|
|
||||||
return size + " B";
|
|
||||||
} else if (size < 1024 * 1024) {
|
|
||||||
return (size / 1024).toFixed(2) + " KB";
|
|
||||||
} else {
|
|
||||||
return (size / (1024 * 1024)).toFixed(2) + " MB";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submitForm() {
|
submitForm() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
// Vytvorenie FormData objektu pre odoslanie súborov
|
// Vytvorenie FormData objektu pre odoslanie súborov
|
||||||
@ -152,17 +107,11 @@ export default {
|
|||||||
formData.append("description", this.bugReport.description);
|
formData.append("description", this.bugReport.description);
|
||||||
formData.append("priority", this.bugReport.priority);
|
formData.append("priority", this.bugReport.priority);
|
||||||
|
|
||||||
// Pridanie súborov do FormData
|
|
||||||
this.selectedFiles.forEach((file, index) => {
|
|
||||||
formData.append(`file${index}`, file);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tu by nasledovalo odoslanie dát na server
|
// Tu by nasledovalo odoslanie dát na server
|
||||||
console.log("Odosielam bug report:", {
|
console.log("Odosielam bug report:", {
|
||||||
title: this.bugReport.title,
|
title: this.bugReport.title,
|
||||||
description: this.bugReport.description,
|
description: this.bugReport.description,
|
||||||
priority: this.bugReport.priority,
|
priority: this.bugReport.priority,
|
||||||
files: this.selectedFiles.map((f) => f.name),
|
|
||||||
});
|
});
|
||||||
backend
|
backend
|
||||||
.add(
|
.add(
|
||||||
@ -175,7 +124,7 @@ export default {
|
|||||||
.then((result) => {
|
.then((result) => {
|
||||||
console.log(result);
|
console.log(result);
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
this.$router.push("/");
|
this.$router.push("/report/" + result.report_id);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@ -191,10 +140,6 @@ export default {
|
|||||||
priority: "",
|
priority: "",
|
||||||
files: [],
|
files: [],
|
||||||
};
|
};
|
||||||
this.selectedFiles = [];
|
|
||||||
// Resetovanie file input
|
|
||||||
const fileInput = document.getElementById("files");
|
|
||||||
if (fileInput) fileInput.value = "";
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -139,8 +139,9 @@ function isDragable(element) {
|
|||||||
<script>
|
<script>
|
||||||
import ReportBox from "../components/ReportBox.vue";
|
import ReportBox from "../components/ReportBox.vue";
|
||||||
import draggable from "vuedraggable";
|
import draggable from "vuedraggable";
|
||||||
import { backend } from "../backend";
|
import backend from "../backend";
|
||||||
import FullScreenLoader from "../components/FullScreenLoader.vue";
|
import FullScreenLoader from "../components/FullScreenLoader.vue";
|
||||||
|
import events from "../events";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -160,7 +161,8 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
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((all_grouped) => {
|
backend.getAllGrouped(Array(0, 1, 2, 3)).then((response) => {
|
||||||
|
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];
|
||||||
@ -231,6 +233,13 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadData();
|
this.loadData();
|
||||||
|
events.on("reports-changed", () => {
|
||||||
|
console.log("Dashoboard reports-changed");
|
||||||
|
this.loadData(false);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
unmounted() {
|
||||||
|
events.off("reports-changed");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
322
frontend/src/views/Report.vue
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
<template>
|
||||||
|
<FullScreenLoader v-if="loading" />
|
||||||
|
|
||||||
|
<div id="report">
|
||||||
|
<div class="report-header">
|
||||||
|
<div>
|
||||||
|
<span>Report</span>
|
||||||
|
<strong>{{ report_id }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Vytvorene</span>
|
||||||
|
<strong>{{ report.created_dt }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Stav</span>
|
||||||
|
<strong>{{ report.report_status }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Priorita</span>
|
||||||
|
<strong>{{ report.report_priority }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Skupina</span>
|
||||||
|
<strong>{{ report.report_group }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button @click="reportDelete">
|
||||||
|
<font-awesome-icon :icon="['fas', 'trash-can']" /> Zmazať
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="editable">
|
||||||
|
<button @click="reportDone">
|
||||||
|
<font-awesome-icon :icon="['fas', 'circle-check']" /> Hotovo, presunut do archivu
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 :contenteditable="editable" @blur="onTitleChange" ref="reportTitle">
|
||||||
|
{{ report.report_title }}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="description"
|
||||||
|
:contenteditable="editable"
|
||||||
|
@blur="onDescriptionChange"
|
||||||
|
ref="reportDescription"
|
||||||
|
>
|
||||||
|
{{ report.report_description }}
|
||||||
|
</p>
|
||||||
|
<div class="attachments">
|
||||||
|
<div
|
||||||
|
class="attachment"
|
||||||
|
v-for="attachment in attachments"
|
||||||
|
:key="attachment.attachment_id"
|
||||||
|
>
|
||||||
|
<div class="attachment-header">
|
||||||
|
<span class="created" title="Vytvorene"
|
||||||
|
><font-awesome-icon :icon="['fas', 'calendar-days']" />
|
||||||
|
{{ attachment.created_dt }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="updated"
|
||||||
|
v-if="attachment.updated_dt"
|
||||||
|
title="Posledná zmena"
|
||||||
|
><font-awesome-icon :icon="['fas', 'pen']" />
|
||||||
|
{{ attachment.updated_dt }}</span
|
||||||
|
>
|
||||||
|
<span class="author"
|
||||||
|
><font-awesome-icon :icon="['fas', 'user']" />
|
||||||
|
{{ attachment.attachment_author }}</span
|
||||||
|
>
|
||||||
|
<span class="actions">
|
||||||
|
<button @click="attachmentDelete(attachment)">
|
||||||
|
<font-awesome-icon :icon="['fas', 'circle-xmark']" /> Odstranit
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="attachment.attachment_type == 'comment'"
|
||||||
|
class="attachment-content"
|
||||||
|
:contenteditable="attachment.editable ?? false"
|
||||||
|
@dblclick="attachment.editable = true && editable"
|
||||||
|
@blur="
|
||||||
|
attachment.editable = false;
|
||||||
|
updateAttachmentContent($event, attachment);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ attachment.attachment_content }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="attachment.attachment_type == 'file'"
|
||||||
|
class="attachment-file"
|
||||||
|
>
|
||||||
|
<a :href="backend.endpoint + attachment.attachment_content" target="_blank">Stiahnut {{ attachment.attachment_content.split('/').pop().split('?')[0].split('#')[0] }}</a>
|
||||||
|
<br>
|
||||||
|
<img :src="backend.endpoint + attachment.attachment_content" v-if="isImageUrl(attachment.attachment_content)" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="attachment-content">
|
||||||
|
Neznamy typ prilohy: <strong>{{ attachment.attachment_type }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="attachment-new" v-if="editable">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Nový komentár:</label>
|
||||||
|
<textarea
|
||||||
|
v-model="attachmentNewContent"
|
||||||
|
rows="5"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Nove zistenia alebo riesenia"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="files">Prílohy:</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="files"
|
||||||
|
ref="attachmentNewFiles"
|
||||||
|
@change="handleFileUpload"
|
||||||
|
multiple
|
||||||
|
class="form-control file-input"
|
||||||
|
/>
|
||||||
|
<div class="selected-files" v-if="selectedFiles.length > 0">
|
||||||
|
<p>Vybrané súbory:</p>
|
||||||
|
|
||||||
|
<p v-for="(file, index) in selectedFiles" :key="index">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="remove-file"
|
||||||
|
@click="removeFile(index)"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="['fas', 'circle-xmark']" /> Odober súbor
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{ file.name }} ({{ formatFileSize(file.size) }})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button @click="attachmentAdd">
|
||||||
|
<font-awesome-icon :icon="['fas', 'circle-plus']" /> Pridať
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { router } from "../router";
|
||||||
|
import backend from "../backend";
|
||||||
|
import FullScreenLoader from "../components/FullScreenLoader.vue";
|
||||||
|
import JSConfetti from 'js-confetti'
|
||||||
|
|
||||||
|
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 loading = ref(false);
|
||||||
|
const report_id = router.currentRoute.value.params.id;
|
||||||
|
const report = ref({
|
||||||
|
report_id: 0,
|
||||||
|
report_title: "Nacitavam report",
|
||||||
|
report_description: "...",
|
||||||
|
report_status: 4,
|
||||||
|
report_group: "--",
|
||||||
|
report_priority: 1,
|
||||||
|
created_dt: "--",
|
||||||
|
ordnum: 0,
|
||||||
|
});
|
||||||
|
const editable = ref(true);
|
||||||
|
const attachments = ref([
|
||||||
|
{
|
||||||
|
attachment_id: 0,
|
||||||
|
attachment_type: "comment",
|
||||||
|
attachment_content: "Nacitavam report",
|
||||||
|
created_dt: "--",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const attachmentNewContent = ref(null);
|
||||||
|
const attachmentNewFiles = ref(null);
|
||||||
|
const selectedFiles = ref([]);
|
||||||
|
const selectedFilesContent = ref([]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// console.log(report_id);
|
||||||
|
loadReportData();
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadReportData() {
|
||||||
|
backend.get(report_id).then((response) => {
|
||||||
|
report.value = response.data;
|
||||||
|
// console.log(report);
|
||||||
|
editable.value = response.data.report_status < 4;
|
||||||
|
});
|
||||||
|
backend.attachmentGetAll(report_id).then((response) => {
|
||||||
|
attachments.value = response.data;
|
||||||
|
// console.log(attachments);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTitleChange(event) {
|
||||||
|
backend.update(report_id, { report_title: event.target.innerText });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDescriptionChange(event) {
|
||||||
|
backend.update(report_id, {
|
||||||
|
report_description: event.target.innerText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportDelete() {
|
||||||
|
if (!confirm("Naozaj chcete report zmazať?")) return;
|
||||||
|
loading.value = true;
|
||||||
|
backend.delete(report_id).then(() => {
|
||||||
|
router.push("/");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportDone() {
|
||||||
|
backend.update(report_id, {
|
||||||
|
report_status: 4,
|
||||||
|
}).then(() => {
|
||||||
|
jungle.play();
|
||||||
|
const confetti = new JSConfetti();
|
||||||
|
confetti.addConfetti();
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push("/");
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentAdd() {
|
||||||
|
let comment = attachmentNewContent.value;
|
||||||
|
if (comment.trim().length > 0) {
|
||||||
|
loading.value = true;
|
||||||
|
backend
|
||||||
|
.attachmentAdd(
|
||||||
|
report_id,
|
||||||
|
"comment",
|
||||||
|
attachmentNewContent.value
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
attachmentNewContent.value = "";
|
||||||
|
loadReportData();
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (selectedFiles.value.length > 0) {
|
||||||
|
let for_upload = selectedFiles.value.length;
|
||||||
|
loading.value = true;
|
||||||
|
for (let i = 0; i < selectedFiles.value.length; i++) {
|
||||||
|
backend
|
||||||
|
.attachmentAdd(report_id, "file", {
|
||||||
|
'filename': selectedFiles.value[i].name,
|
||||||
|
'base64': selectedFilesContent.value[i]
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
for_upload--;
|
||||||
|
if (for_upload == 0) {
|
||||||
|
selectedFiles.value = [];
|
||||||
|
selectedFilesContent.value = [];
|
||||||
|
attachmentNewFiles.value = null;
|
||||||
|
loadReportData();
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentDelete(attachment) {
|
||||||
|
if (!confirm("Naozaj chcete zmazať prilohu?")) return;
|
||||||
|
loading.value = true;
|
||||||
|
backend.attachmentDelete(attachment.attachment_id).then(() => {
|
||||||
|
loadReportData();
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAttachmentContent(event, attachment) {
|
||||||
|
loading.value = true;
|
||||||
|
backend
|
||||||
|
.attachmentUpdate(attachment.attachment_id, event.target.innerText)
|
||||||
|
.then(() => {
|
||||||
|
loadReportData();
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileUpload(event) {
|
||||||
|
const files = event.target.files;
|
||||||
|
if (files) {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
selectedFiles.value.push(files[i]);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
selectedFilesContent.value[i] = reader.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(files[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFile(index) {
|
||||||
|
selectedFiles.value.splice(index, 1);
|
||||||
|
selectedFilesContent.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(size) {
|
||||||
|
if (size < 1024) {
|
||||||
|
return size + " B";
|
||||||
|
} else if (size < 1024 * 1024) {
|
||||||
|
return (size / 1024).toFixed(2) + " KB";
|
||||||
|
} else {
|
||||||
|
return (size / (1024 * 1024)).toFixed(2) + " MB";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImageUrl(url) {
|
||||||
|
return /\.(jpg|jpeg|png|gif|svg|webp)$/.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import { visualizer } from "rollup-plugin-visualizer";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
// import pkg from "./package.json";
|
// import pkg from "./package.json";
|
||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
@ -7,7 +8,7 @@ const require = createRequire(import.meta.url);
|
|||||||
const pkg = require("./package.json");
|
const pkg = require("./package.json");
|
||||||
|
|
||||||
const subpath = "/bugreport/";
|
const subpath = "/bugreport/";
|
||||||
export const baseUrl = subpath + "webapp/dist/";
|
export const baseUrl = "/"; //subpath + "frontend/dist/";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig(({ command, mode }) => {
|
export default defineConfig(({ command, mode }) => {
|
||||||
@ -24,5 +25,31 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
__IS_BUILD__: JSON.stringify(isBuild),
|
__IS_BUILD__: JSON.stringify(isBuild),
|
||||||
__IS_DEV__: JSON.stringify(isDev),
|
__IS_DEV__: JSON.stringify(isDev),
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
host: true,
|
||||||
|
port: 5173
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
chunkSizeWarningLimit: 1000, // zvýšenie limitu na 1000 kB
|
||||||
|
rollupOptions: {
|
||||||
|
plugins: [
|
||||||
|
visualizer({
|
||||||
|
open: false, // otvorí report v prehliadači po builde
|
||||||
|
filename: "stats.html",
|
||||||
|
gzipSize: true,
|
||||||
|
brotliSize: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
fontawesome: [
|
||||||
|
"@fortawesome/free-solid-svg-icons",
|
||||||
|
"@fortawesome/fontawesome-svg-core",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
11
index.php
@ -1,11 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>BugReport</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,220 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check database
|
|
||||||
*/
|
|
||||||
function dbCheck() {
|
|
||||||
global $db;
|
|
||||||
$db_version = option('version');
|
|
||||||
if ($db_version === null) {
|
|
||||||
$db->create('options', [
|
|
||||||
'key' => [
|
|
||||||
'VARCHAR(64)',
|
|
||||||
'NOT NULL',
|
|
||||||
'UNIQUE'
|
|
||||||
],
|
|
||||||
'value' => [
|
|
||||||
'TEXT',
|
|
||||||
'NOT NULL'
|
|
||||||
],
|
|
||||||
'created_at' => [
|
|
||||||
'DATETIME',
|
|
||||||
'DEFAULT CURRENT_TIMESTAMP'
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
option('version', '0');
|
|
||||||
$db_version = '0';
|
|
||||||
}
|
|
||||||
if ($db_version === '0') {
|
|
||||||
$db->create('reports', [
|
|
||||||
'report_id' => [
|
|
||||||
'INTEGER',
|
|
||||||
'PRIMARY KEY',
|
|
||||||
'AUTOINCREMENT'
|
|
||||||
],
|
|
||||||
'report_title' => [
|
|
||||||
'VARCHAR(255)',
|
|
||||||
'DEFAULT NULL'
|
|
||||||
],
|
|
||||||
'report_description' => [
|
|
||||||
'TEXT',
|
|
||||||
'DEFAULT NULL'
|
|
||||||
],
|
|
||||||
'report_status' => [
|
|
||||||
'INTEGER',
|
|
||||||
'DEFAULT 0'
|
|
||||||
],
|
|
||||||
'report_group' => [
|
|
||||||
'VARCHAR(255)',
|
|
||||||
'DEFAULT NULL'
|
|
||||||
],
|
|
||||||
'report_priority' => [
|
|
||||||
'INTEGER',
|
|
||||||
'DEFAULT 0'
|
|
||||||
],
|
|
||||||
'created_dt' => [
|
|
||||||
'DATETIME',
|
|
||||||
'DEFAULT NULL'
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
option('version', '1');
|
|
||||||
$db_version = '1';
|
|
||||||
}
|
|
||||||
if ($db_version === '1') {
|
|
||||||
$db->create('attachments', [
|
|
||||||
'attachment_id' => [
|
|
||||||
'INTEGER',
|
|
||||||
'PRIMARY KEY',
|
|
||||||
'AUTOINCREMENT'
|
|
||||||
],
|
|
||||||
'report_id' => [
|
|
||||||
'INTEGER',
|
|
||||||
'NOT NULL'
|
|
||||||
],
|
|
||||||
'attachment_type' => [
|
|
||||||
'VARCHAR(255)',
|
|
||||||
'DEFAULT NULL'
|
|
||||||
],
|
|
||||||
'attachment_content' => [
|
|
||||||
'TEXT',
|
|
||||||
'DEFAULT NULL'
|
|
||||||
],
|
|
||||||
'created_dt' => [
|
|
||||||
'DATETIME',
|
|
||||||
'DEFAULT NULL'
|
|
||||||
],
|
|
||||||
'updated_dt' => [
|
|
||||||
'DATETIME',
|
|
||||||
'DEFAULT NULL'
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
option('version', '2');
|
|
||||||
$db_version = '2';
|
|
||||||
}
|
|
||||||
if ($db_version === '2') {
|
|
||||||
$db->query("ALTER TABLE reports ADD COLUMN ordnum INTEGER DEFAULT 0");
|
|
||||||
option('version', '3');
|
|
||||||
$db_version = '3';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function option($key, $value = null)
|
|
||||||
{
|
|
||||||
global $db;
|
|
||||||
if (tableExits('options') === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if ($value === null) {
|
|
||||||
return $db->get('options', 'value', [
|
|
||||||
'key' => $key
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
$exits = $db->get('options', 'value', [
|
|
||||||
'key' => $key
|
|
||||||
]);
|
|
||||||
if ($exits !== null) {
|
|
||||||
return $db->update('options', [
|
|
||||||
'value' => $value
|
|
||||||
], [
|
|
||||||
'key' => $key
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return $db->insert('options', [
|
|
||||||
'key' => $key,
|
|
||||||
'value' => $value
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function tableExits($table) {
|
|
||||||
global $db;
|
|
||||||
return $db->get('sqlite_master', 'name', [
|
|
||||||
'type' => 'table',
|
|
||||||
'name' => $table
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reports
|
|
||||||
*/
|
|
||||||
function reportAdd($title, $description, $status = 0, $group = null, $priority = 0) {
|
|
||||||
global $db;
|
|
||||||
$status = intval($status);
|
|
||||||
$priority = intval($priority);
|
|
||||||
$db->insert('reports', [
|
|
||||||
'report_title' => $title,
|
|
||||||
'report_description' => $description,
|
|
||||||
'report_status' => $status,
|
|
||||||
'report_group' => $group,
|
|
||||||
'report_priority' => $priority,
|
|
||||||
'created_dt' => date('Y-m-d H:i:s')
|
|
||||||
]);
|
|
||||||
return $db->id();
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportUpdate($report_id, $report_data) {
|
|
||||||
global $db;
|
|
||||||
$stm = $db->update('reports', $report_data, [
|
|
||||||
'report_id' => $report_id
|
|
||||||
]);
|
|
||||||
return ($stm->rowCount() > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportUpdateStatus($report_id, $status) {
|
|
||||||
global $db;
|
|
||||||
$stm = $db->update('reports', [
|
|
||||||
'report_status' => $status
|
|
||||||
], [
|
|
||||||
'report_id' => $report_id
|
|
||||||
]);
|
|
||||||
return ($stm->rowCount() > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportUpdateOrdnum($ordnums) {
|
|
||||||
global $db;
|
|
||||||
$ordnums = json_decode($ordnums, true);
|
|
||||||
$suc = true;
|
|
||||||
foreach ($ordnums as $report_id => $ordnum) {
|
|
||||||
$stm = $db->update('reports', [
|
|
||||||
'ordnum' => $ordnum
|
|
||||||
], [
|
|
||||||
'report_id' => $report_id
|
|
||||||
]);
|
|
||||||
$suc &= ($stm->rowCount() > 0);
|
|
||||||
}
|
|
||||||
return $suc;
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportDelete($report_id) {
|
|
||||||
global $db;
|
|
||||||
$stm = $db->delete('reports', [
|
|
||||||
'report_id' => $report_id
|
|
||||||
]);
|
|
||||||
return ($stm->rowCount() > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportGet($report_id) {
|
|
||||||
global $db;
|
|
||||||
return $db->get('reports', '*', [
|
|
||||||
'report_id' => $report_id
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportGetAll($status = null) {
|
|
||||||
global $db;
|
|
||||||
if ($status === null) $status = array(0, 1, 2, 3);
|
|
||||||
return $db->select('reports', '*', [
|
|
||||||
'ORDER' => ['report_priority' => 'DESC', 'ordnum' => 'ASC'],
|
|
||||||
'report_status' => $status
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportGetAllGrouped($status = null) {
|
|
||||||
$all = reportGetAll($status);
|
|
||||||
$groups = [];
|
|
||||||
foreach ($all as $report) {
|
|
||||||
$groups[$report['report_status']][] = $report;
|
|
||||||
}
|
|
||||||
return $groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="header">
|
|
||||||
<div class="logo">
|
|
||||||
<router-link to="/">
|
|
||||||
<img src="/public/bugreport.svg" height="48" width="48" />
|
|
||||||
</router-link>
|
|
||||||
<router-link to="/">
|
|
||||||
<h1>Bug Report</h1>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="menu">
|
|
||||||
<router-link to="/add"><font-awesome-icon :icon="['fas', 'square-plus']" /> Pridať bug</router-link>
|
|
||||||
<router-link to="/"><font-awesome-icon :icon="['fas', 'list-check']" /> Zoznam reportov</router-link>
|
|
||||||
<router-link to="/archive"><font-awesome-icon :icon="['fas', 'box-archive']" /> Archív</router-link>
|
|
||||||
<router-link to="/api"><font-awesome-icon :icon="['fas', 'plug']" /> API</router-link>
|
|
||||||
<router-link to="/about"><font-awesome-icon :icon="['fas', 'address-card']" /> O aplikácii</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<router-view></router-view>
|
|
||||||
</template>
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
export const backend = {
|
|
||||||
endpont: __IS_BUILD__
|
|
||||||
? window.location.origin + __SUBPATH__ + "api.php"
|
|
||||||
: "http://localhost/bugreport/api.php",
|
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
|
||||||
* Vsebecne API volanie
|
|
||||||
*/
|
|
||||||
call(method, data, callback) {
|
|
||||||
var xhttp = new XMLHttpRequest();
|
|
||||||
xhttp.withCredentials = true;
|
|
||||||
xhttp.onreadystatechange = function() {
|
|
||||||
if (this.readyState == 4) {
|
|
||||||
if (this.status == 200) {
|
|
||||||
if (callback != null) callback(JSON.parse(this.responseText));
|
|
||||||
} else {
|
|
||||||
if (callback != null) callback({'status': 'ERROR', 'message': 'HTTP STATUS ' + this.status});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var form_data = new FormData();
|
|
||||||
Object.keys(data).forEach(key => {
|
|
||||||
let val = data[key];
|
|
||||||
if (typeof val == 'object') val = JSON.stringify(val);
|
|
||||||
form_data.append(key, val);
|
|
||||||
});
|
|
||||||
xhttp.open('POST', this.endpont + '?action=' + method);
|
|
||||||
//xhttp.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
|
|
||||||
xhttp.send(form_data);
|
|
||||||
},
|
|
||||||
|
|
||||||
callPromise(method, data) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.call(method, data, function(response) {
|
|
||||||
if (response.status == 'OK') {
|
|
||||||
resolve(response.data);
|
|
||||||
} else {
|
|
||||||
reject(response.msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ----------------------------------------------------
|
|
||||||
* API akcie
|
|
||||||
*/
|
|
||||||
help() {
|
|
||||||
return this.callPromise('help', {});
|
|
||||||
},
|
|
||||||
|
|
||||||
add(title, description, status, group, priority) {
|
|
||||||
return this.callPromise('add', {
|
|
||||||
title: title,
|
|
||||||
description: description,
|
|
||||||
status: status,
|
|
||||||
group: group,
|
|
||||||
priority: priority});
|
|
||||||
},
|
|
||||||
|
|
||||||
update(id, report_data) {
|
|
||||||
return this.callPromise('update', {report_id: id, report_data: report_data});
|
|
||||||
},
|
|
||||||
|
|
||||||
delete(id) {
|
|
||||||
return this.callPromise('delete', {report_id: id});
|
|
||||||
},
|
|
||||||
|
|
||||||
get(id) {
|
|
||||||
return this.callPromise('get', {report_id: id});
|
|
||||||
},
|
|
||||||
|
|
||||||
getAll() {
|
|
||||||
return this.callPromise('getall', {});
|
|
||||||
},
|
|
||||||
|
|
||||||
getAllGrouped(status) {
|
|
||||||
return this.callPromise('getallgrouped', {});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateOrdnum(ordnums) {
|
|
||||||
return this.callPromise('updateordnum', {ordnums: ordnums});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateStatus(id, status) {
|
|
||||||
return this.callPromise('updatestatus', {report_id: id, status: status});
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<h1>Archív</h1>
|
|
||||||
<router-link to="/">Späť domov</router-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
<template>
|
|
||||||
<FullScreenLoader v-if="loading" />
|
|
||||||
|
|
||||||
<div id="report">
|
|
||||||
<div class="report-header">
|
|
||||||
<div>
|
|
||||||
<span>Report</span>
|
|
||||||
<strong>{{ report_id }}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Vytvorene</span>
|
|
||||||
<strong>{{ report.created_dt }}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Stav</span>
|
|
||||||
<strong>{{ report.report_status }}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Priorita</span>
|
|
||||||
<strong>{{ report.report_priority }}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Skupina</span>
|
|
||||||
<strong>{{ report.report_group }}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button @click="reportDelete"><font-awesome-icon :icon="['fas', 'trash-can']" /> Zmazať</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h1 contenteditable="true" @blur="onTitleChange" ref="reportTitle">
|
|
||||||
{{ report.report_title }}
|
|
||||||
</h1>
|
|
||||||
<p class="description" contenteditable="true" @blur="onDescriptionChange" ref="reportDescription">{{ report.report_description }}</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { backend } from "../backend";
|
|
||||||
import FullScreenLoader from "../components/FullScreenLoader.vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "Report",
|
|
||||||
components: { FullScreenLoader },
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
loading: false,
|
|
||||||
report_id: this.$route.params.id,
|
|
||||||
report: {
|
|
||||||
report_id: 0,
|
|
||||||
report_title: "Nacitavam report",
|
|
||||||
report_description: "...",
|
|
||||||
report_status: 0,
|
|
||||||
report_group: "--",
|
|
||||||
report_priority: 1,
|
|
||||||
created_dt: "--",
|
|
||||||
ordnum: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
console.log(this.report_id);
|
|
||||||
this.loadReportData();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
loadReportData() {
|
|
||||||
backend.get(this.report_id).then((report) => {
|
|
||||||
this.report = report;
|
|
||||||
console.log(this.report);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onTitleChange(event) {
|
|
||||||
backend.update(this.report_id, { report_title: event.target.innerText });
|
|
||||||
},
|
|
||||||
onDescriptionChange(event) {
|
|
||||||
backend.update(this.report_id, { report_description: event.target.innerText });
|
|
||||||
},
|
|
||||||
reportDelete() {
|
|
||||||
if (!confirm("Naozaj chcete report zmazať?")) return;
|
|
||||||
this.loading = true;
|
|
||||||
backend.delete(this.report_id).then(() => {
|
|
||||||
this.$router.push("/");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {},
|
|
||||||
watch: {},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||