Compare commits

...

27 Commits

Author SHA1 Message Date
afc5229f5b fix referencii 2025-10-16 02:11:15 +02:00
e50c53e50a release 0.0.2 2025-10-16 01:41:52 +02:00
3e9056f4a6 fixed sanitizeFilename for backend\API,
added new API call attachmentDownload,
fixed attachment URL to call attachmentDownload,
refactoring Report.vue for <script setup>
2025-10-16 01:40:44 +02:00
ed3d202488 pridany build skript,
uprava produkcnej konfiguracie
2025-10-13 23:12:15 +02:00
e67d4843bd aktualizacia README.md a doplnene UTF Emoji ikonky 2025-10-11 22:40:45 +02:00
fbfe46542b odstranene stare nepoterbne PHP subory
- nepouzivane api, po novom je v backend zostavenie api pomocou TPsoft/APIlite
- subor funkcii
- vobec nepouyzivany index
- uz nepouyzivane config, novy je v subadresary /backend/config
2025-10-11 22:02:41 +02:00
948fce896f remoed submodule lib/Medoo 2025-10-11 21:53:12 +02:00
027815ee3f zobrazenie na viac riadkov aj v nahlade reportov 2025-10-11 21:41:17 +02:00
19600ac7bf Merge branch 'main' of https://gitea.tpsoft.org/TPsoft.org/BugReport 2025-10-11 16:50:11 +02:00
e63be43639 pridany Maintenance pre modul TPsoft\DBmodel,
pridane poznamky,
pridana konfiguracia pre DEV a PROD,
2025-10-11 16:49:46 +02:00
995e0e40a5 uprava zobrazenie API pre parametre,
fix cesty pre logo SVG,
nastavenie DEV servera VITE pre vsetky interface
2025-10-11 15:37:51 +02:00
b51ae03fcf Aktualizované Backend 2025-10-01 01:08:52 +02:00
1cf79a20b3 pridana implementacia backend s composerom a kniznicami TPsoft/APIlite a TPsoft/DBmodel 2025-10-01 00:57:41 +02:00
8184ffb46d pridane infinity scroll pre Archiv 2025-05-18 00:27:46 +02:00
e6f3ef4ab2 implementovane zobrazenie archivu,
TODO: dorobit load dalsich stran,
zmena FullScreenLoader namiesto zatmavenia na rozmazanie,
pridana podmienka na editable pre zobrazenie BUG, pouzitelne ked sa otvara archivovany bug
2025-05-17 21:21:42 +02:00
506e847b5d pridane ID reportu do zobrazenia na dashboard,
odstrany upload suboru pre vytvorenie noveho reportu,
pridane tlacitko na Hotovo pre bug,
pridane komfety a zvukove jingle pre Hotovo bug
2025-05-17 15:55:48 +02:00
c7dcdf228e zmena implementacie suborov, ukladanie na filesystem a nie do DB 2025-05-17 15:06:04 +02:00
d6be781024 Merge branch 'main' of https://gitea.tpsoft.org/TPsoft.org/BugReport 2025-05-17 13:46:49 +02:00
4a050b0ff7 pridane tlacitko na zmazanie prilohy,
implementovany upload a zobrazenie suboru obrazku
2025-05-17 13:46:26 +02:00
02af51b834 fix kod 2025-05-17 11:19:43 +02:00
16fa3d1ed1 doplneny diagram a screenshot do README 2025-05-17 11:17:47 +02:00
dfa51da14b presun obrazkov z /public do /assets/images,
prelinkovanie obrazkov v About.vue
2025-05-15 11:42:49 +02:00
5250a6d279 pridany flow diagram pre BUG,
aktualizovany v README Technologie, struktura a API endpointy
2025-05-15 11:09:09 +02:00
a02bdb4cbe pridane rychle pridanie tasku,
pridana kniznica Mitt pre globalne events
2025-05-15 10:16:53 +02:00
603c8ea37b pridane zobrazenie datumu poslednej zmeny 2025-05-14 00:46:33 +02:00
82e14b8fa4 implementovane pridavanie priloh typu "comment",
dynamicka uprava komentarov dvojklikom
2025-05-14 00:36:56 +02:00
e870a62b89 pridani visualiser pre chunks,
oddeleny FontAwesome do samostatnych chunks,
optimalizovane pre mobil
2025-05-13 22:57:48 +02:00
63 changed files with 2478 additions and 776 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "lib/Medoo"]
path = lib/Medoo
url = https://github.com/catfan/Medoo.git

View File

@ -1,12 +1,12 @@
# 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.
<br /><br /><br />
## Funkcie
## 🚀 Funkcie
- **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
@ -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
- **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
- 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 Router](https://router.vuejs.org/) - smerovanie v aplikácii
- [Vuedraggable](https://github.com/SortableJS/vue.draggable.next) - drag-and-drop funkcionalita
- [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
## Štruktúra projektu
## 🌳 Štruktúra projektu
```
BugReport/
├── api.php # API endpoint
├── config.php # Konfigurácia aplikácie
├── index.php # Vstupný bod aplikácie
├── backend/ # Backend aplikácia (PHP)
├── config/ # Konfigurácia 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
│ ├── attachments/ # Súborové uložisko príloh
│ └── database.db # SQLite databáza
├── lib/ # PHP knižnice
│ ├── functions.inc.php # Pomocné funkcie
│ └── Medoo/ # Medoo databázový framework
└── webapp/ # Frontend aplikácia (Vue.js)
├── doc/ # Dokumentácia a iné súbory počas vývoja
└── frontend/ # Frontend aplikácia (Vue.js)
├── public/ # Statické súbory
├── scripts/ # Skripty pre build
└── src/ # Zdrojový kód
@ -52,27 +67,29 @@ BugReport/
├── views/ # Vue stránky
├── App.vue # Hlavná Vue komponenta
├── backend.js # Komunikácia s API
├── events.js # Správa udalostí
├── main.js # Vstupný bod Vue aplikácie
└── 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)
- Node.js a npm pre vývoj frontendu
### Backend
### ⚙️ Backend
1. Skopírujte súbory do webového adresára
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č
### 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:
```
npm install
@ -86,29 +103,32 @@ BugReport/
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 |
| ----------------- | -------------------------------------- | --------------------------------------------------------------- |
| -------------------- | -------------------------------------- | --------------------------------------------------------------- |
| `help` | Zobrazí nápovedu | - |
| `add` | Pridá nový report | `title`, `description`, `status`, `group`, `priority` |
| `update` | Aktualizuje report | `report_id`, `report_data` (JSON) |
| `delete` | Vymaže report | `report_id` |
| `get` | Získa jeden report | `report_id` |
| `getall` | Získa všetky reporty | `status` (voliteľné) |
| `getallgrouped` | Získa reporty zoskupené podľa stavu | `status` (voliteľné) |
| `updateordnum` | Aktualizuje poradie reportov | `ordnums` (JSON) |
| `updatestatus` | Aktualizuje stav reportu | `report_id`, `status` |
| `getAll` | Získa všetky reporty | `status` (voliteľné) |
| `getAllGrouped` | Získa reporty zoskupené podľa stavu | `status` (voliteľné) |
| `updateOrdNum` | Aktualizuje poradie reportov | `ordnums` (JSON) |
| `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
// Získanie všetkých reportov
fetch('api.php?action=getall')
fetch('api.php?action=getAll')
.then(response => response.json())
.then(data => console.log(data));
@ -121,7 +141,7 @@ fetch('api.php?action=add', {
.then(data => console.log(data));
```
## Stavy reportov
## 🏗️ Stavy reportov
| ID | Stav |
| -- | ------------- |
@ -131,7 +151,7 @@ fetch('api.php?action=add', {
| 3 | Blokované |
| 4 | Vyriešený |
## Priority reportov
## 📣 Priority reportov
| ID | Priorita |
| -- | --------- |
@ -140,13 +160,16 @@ fetch('api.php?action=add', {
| 2 | Vysoká |
| 3 | Kritická |
## Skupiny reportov
## 🕸️ Skupiny reportov
- `cp` - Control Panel
- `task` - Task.Platon.sk
- `websiteip` - WebsiteIP
- `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).

141
api.php
View File

@ -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
View 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
View 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"
}

View 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
View File

@ -0,0 +1,4 @@
<?php
require_once __DIR__.'/../src/API.php';
use TPsoft\BugreportBackend\API;
new API();

View 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";

View 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
View 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
View 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();

View 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);
}
}

View 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);
}
}
?>

View 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);
}
}
?>

View 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
View 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
View 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.

View File

@ -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
View 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&amp;nbsp;&lt;div&gt;pracovať&lt;/div&gt;" 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

21
doc/notes.txt Normal file
View 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

View File

@ -0,0 +1 @@
VITE_BACKENDAPI_URL="https://192.168.0.101/BugReport/backend/public/API.php"

1
frontend/.env.production Normal file
View File

@ -0,0 +1 @@
VITE_BACKENDAPI_URL="/API.php"

View File

@ -12,12 +12,15 @@
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/vue-fontawesome": "^3.0.8",
"js-confetti": "^0.12.0",
"mitt": "^3.0.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"rollup-plugin-visualizer": "^5.14.0",
"vite": "^6.2.0"
}
},
@ -1039,6 +1042,32 @@
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -1048,12 +1077,64 @@
"optional": 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": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"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": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@ -1107,6 +1188,16 @@
"@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": {
"version": "2.0.2",
"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_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": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@ -1137,6 +1283,12 @@
"@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": {
"version": "3.3.11",
"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_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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"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": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
@ -1189,6 +1372,16 @@
"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": {
"version": "4.40.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz",
@ -1229,6 +1422,47 @@
"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": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
@ -1269,6 +1503,34 @@
"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": {
"version": "5.39.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
@ -1418,6 +1680,63 @@
"peerDependencies": {
"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"
}
}
}
}

View File

@ -1,7 +1,7 @@
{
"name": "bugreport",
"private": true,
"version": "0.0.1",
"version": "0.0.2",
"type": "module",
"scripts": {
"dev": "vite",
@ -13,12 +13,15 @@
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/vue-fontawesome": "^3.0.8",
"js-confetti": "^0.12.0",
"mitt": "^3.0.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"rollup-plugin-visualizer": "^5.14.0",
"vite": "^6.2.0"
}
}

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

67
frontend/src/App.vue Normal file
View 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>

View File

@ -131,21 +131,89 @@ button:focus-visible,
flex-direction: row;
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:visited {
#header .menu a:visited,
#header button {
border: 1px solid var(--color-text0);
padding: 5px 10px;
border-radius: 5px;
margin-left: 20px;
transition: all 0.3s;
}
#header .menu a:hover {
#header .menu a:hover,
#header button:hover {
color: var(--color-text1);
background-color: var(--color-bg1);
}
#header .short-bug button {
margin: 0px;
/* height: 30px; */
}
#header h1 {
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
@ -197,6 +265,7 @@ button:focus-visible,
#dashboard .report .report-description {
padding: 10px;
text-align: justify;
white-space: pre-line;
border-right: 5px var(--color-bg0) solid;
background-color: var(--color-bg2);
}
@ -211,6 +280,10 @@ button:focus-visible,
text-align: left;
padding: 5px;
}
#dashboard .report .report-id {
text-align: center;
padding: 5px;
}
#dashboard .report .report-date {
text-align: right;
padding: 5px;
@ -249,6 +322,20 @@ button:focus-visible,
#dashboard .report-priority-3 .report-description {
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
@ -263,10 +350,86 @@ button:focus-visible,
flex-direction: row;
justify-content: space-between;
}
@media (max-width: 600px) {
#bug-add .cols {
flex-direction: column;
}
}
/* ----------------------------------------------------
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;
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
@ -325,6 +518,11 @@ button:focus-visible,
#about .cols div:hover {
filter: brightness(1.2);
}
@media (max-width: 600px) {
#about .cols {
flex-direction: column;
}
}
/* ----------------------------------------------------
08 - REPORT
@ -337,11 +535,13 @@ button:focus-visible,
display: flex;
flex-direction: row;
/* justify-content: space-between; */
flex-wrap: wrap;
gap: 10px;
}
#report .report-header div {
display: flex;
flex-direction: row;
margin-right: 20px;
/* margin-right: 20px; */
align-items: center;
}
#report .report-header div span {
@ -362,7 +562,40 @@ button:focus-visible,
text-align: justify;
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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 208 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 949 B

After

Width:  |  Height:  |  Size: 949 B

View File

Before

Width:  |  Height:  |  Size: 496 B

After

Width:  |  Height:  |  Size: 496 B

117
frontend/src/backend.js Normal file
View 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();

View File

@ -11,11 +11,16 @@
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
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 {

View File

@ -22,6 +22,7 @@ defineProps({
</div>
<div class="report-footer">
<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>

5
frontend/src/events.js Normal file
View File

@ -0,0 +1,5 @@
import mitt from "mitt";
const events = mitt();
export default events;

View File

@ -24,51 +24,42 @@
<h4>Parametre</h4>
<p v-if="Object.keys(action.params).length == 0">
<font-awesome-icon :icon="['fas', 'circle-info']" />
&nbsp;
Ziadne parametre
&nbsp; Ziadne parametre
</p>
<ul>
<li v-for="(param_desc, param_name) in action.params" :key="param_name">
<strong>{{ param_name }}</strong>
&ndash;
{{ param_desc }}
<li v-for="param in action.params" :key="param_name">
<strong>{{ param.name }}</strong>
<span class="param-type">{{ param.type }}</span>
<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>
</ul>
<p>
<strong>Return</strong>
<span class="param-type">{{ action.return }}</span>
</p>
</div>
</div>
</template>
<script>
import { backend } from "../backend";
<script setup>
import { onMounted, ref } from "vue";
import backend from "../backend";
export default {
name: "API",
components: {},
data() {
return {
api_endpoint: backend.endpont,
help: {
actions: {
help: {
name: "help",
description: "This help",
params: {
foo: "bar",
},
},
},
},
};
},
mounted() {
this.loadHelp();
},
methods: {
loadHelp() {
backend.help().then((response) => {
this.help = response;
const api_endpoint = ref(backend.endpont);
const help = ref({ actions: {} });
onMounted(() => {
loadHelp();
});
},
},
};
function loadHelp() {
backend.help().then((response) => {
help.value = response;
});
}
</script>

View File

@ -2,7 +2,7 @@
<div id="about">
<div>
<h1>O aplikácii</h1>
<img src="/public/bugreport.svg" height="100" alt="" />
<img src="/bugreport.svg" height="100" alt="" />
<h2>Bug Report</h2>
<p>
Verzia aplikácie: {{ version }} &nbsp;|&nbsp; Zostavené: {{ build }}
@ -12,21 +12,21 @@
<div>
Backend thanks for <br />
<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
</a>
</div>
<div>
Database thanks for <br>
<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
</a>
</div>
<div>
Frontend thanks for <br>
<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
</a>
</div>
@ -38,17 +38,20 @@
>https://gitea.tpsoft.org/TPsoft.org/BugReport</a
>
</p>
<h2>Diagram stavov pre BUG</h2>
<img :src="flowdiagram" alt="" width="90%" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
version: __APP_VERSION__,
build: __BUILD_DATE__,
};
},
};
<script setup>
import { ref } from 'vue';
import php_power_micro from '../assets/images/php-power-micro.png';
import sqlite from '../assets/images/SQLite370.svg';
import vue from '../assets/images/vue.svg';
import flowdiagram from '../assets/images/FlowDiagram.drawio.svg';
const version = ref(__APP_VERSION__);
const build = ref(__BUILD_DATE__);
</script>

View 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>

View File

@ -30,33 +30,6 @@
</div>
<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>
&nbsp;&nbsp;
{{ file.name }} ({{ formatFileSize(file.size) }})
</p>
</div>
</div>
<div class="form-group">
<label for="priority">Priorita:</label>
<select
@ -88,7 +61,6 @@
<option value="antispam">Antispam</option>
</select>
</div>
</div>
<div class="form-actions">
<router-link to="/" class="button"
@ -99,12 +71,15 @@
<font-awesome-icon :icon="['fas', 'circle-check']" /> Odoslať
</button>
</div>
</div>
</form>
<p>Poznamka: Subory je mozne pridat ako prilohy az po vytvoreni noveho reportu..</p>
</div>
</template>
<script>
import { backend } from "../backend";
import backend from "../backend";
import FullScreenLoader from "../components/FullScreenLoader.vue";
export default {
@ -124,26 +99,6 @@ export default {
};
},
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() {
this.loading = true;
// Vytvorenie FormData objektu pre odoslanie súborov
@ -152,17 +107,11 @@ export default {
formData.append("description", this.bugReport.description);
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
console.log("Odosielam bug report:", {
title: this.bugReport.title,
description: this.bugReport.description,
priority: this.bugReport.priority,
files: this.selectedFiles.map((f) => f.name),
});
backend
.add(
@ -175,7 +124,7 @@ export default {
.then((result) => {
console.log(result);
this.resetForm();
this.$router.push("/");
this.$router.push("/report/" + result.report_id);
})
.catch((error) => {
console.log(error);
@ -191,10 +140,6 @@ export default {
priority: "",
files: [],
};
this.selectedFiles = [];
// Resetovanie file input
const fileInput = document.getElementById("files");
if (fileInput) fileInput.value = "";
},
},
};

View File

@ -139,8 +139,9 @@ function isDragable(element) {
<script>
import ReportBox from "../components/ReportBox.vue";
import draggable from "vuedraggable";
import { backend } from "../backend";
import backend from "../backend";
import FullScreenLoader from "../components/FullScreenLoader.vue";
import events from "../events";
export default {
components: {
@ -160,7 +161,8 @@ export default {
methods: {
loadData(use_loader = 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.itemsWaiting = all_grouped[1];
this.itemsInProgress = all_grouped[2];
@ -231,6 +233,13 @@ export default {
},
mounted() {
this.loadData();
events.on("reports-changed", () => {
console.log("Dashoboard reports-changed");
this.loadData(false);
});
},
unmounted() {
events.off("reports-changed");
}
};
</script>

View 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>
&nbsp;&nbsp;
{{ 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>

View File

@ -1,4 +1,5 @@
import { defineConfig } from "vite";
import { visualizer } from "rollup-plugin-visualizer";
import vue from "@vitejs/plugin-vue";
// import pkg from "./package.json";
import { createRequire } from "node:module";
@ -7,7 +8,7 @@ const require = createRequire(import.meta.url);
const pkg = require("./package.json");
const subpath = "/bugreport/";
export const baseUrl = subpath + "webapp/dist/";
export const baseUrl = "/"; //subpath + "frontend/dist/";
// https://vite.dev/config/
export default defineConfig(({ command, mode }) => {
@ -24,5 +25,31 @@ export default defineConfig(({ command, mode }) => {
__IS_BUILD__: JSON.stringify(isBuild),
__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",
],
},
},
},
},
};
});

View File

@ -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>

Submodule lib/Medoo deleted from 2613656761

View File

@ -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;
}
?>

View File

@ -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>

View File

@ -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});
},
};

View File

@ -1,6 +0,0 @@
<template>
<div>
<h1>Archív</h1>
<router-link to="/">Späť domov</router-link>
</div>
</template>

View File

@ -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>