Compare commits

..

11 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
23 changed files with 499 additions and 859 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

@ -6,7 +6,7 @@ BugReport je webová aplikácia na sledovanie a správu chýb (bug tracking syst
<br /><br /><br /> <br /><br /><br />
## Funkcie ## 🚀 Funkcie
- **Kanban rozhranie** - vizuálne sledovanie stavu bug reportov v štyroch kategóriách (Nezaradené, Čakajúce, Rozpracované, Blokované) - **Kanban rozhranie** - vizuálne sledovanie stavu bug reportov v štyroch kategóriách (Nezaradené, Čakajúce, Rozpracované, Blokované)
- **Drag-and-drop** - jednoduché presúvanie reportov medzi kategóriami - **Drag-and-drop** - jednoduché presúvanie reportov medzi kategóriami
@ -15,23 +15,23 @@ BugReport je webová aplikácia na sledovanie a správu chýb (bug tracking syst
- **Detailný pohľad** - zobrazenie a úprava detailov reportu - **Detailný pohľad** - zobrazenie a úprava detailov reportu
- **REST API** - prístup k dátam cez API endpoint - **REST API** - prístup k dátam cez API endpoint
## Diagram stavov pre BUG ## 🔷 Diagram stavov pre BUG
<img src="frontend/src/assets/images/FlowDiagram.drawio.svg" /> <img src="frontend/src/assets/images/FlowDiagram.drawio.svg" />
## Screenshot ## 🖼️ Screenshot
<img src="doc/Screenshot_2025-05-17_111345.png" /> <img src="doc/Screenshot_2025-05-17_111345.png" />
## Technológie ## 🖥️ Technológie
### Backend ### ⚙️ Backend
- PHP - PHP
- SQLite databáza - SQLite databáza
- [TPsoft/DBmodel](https://gitea.tpsoft.org/TPsoft.org/DBmodel) - PHP databázové rozšírenie - [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 - [TPsoft/APIlite](https://gitea.tpsoft.org/TPsoft.org/APIlite) - Jednoduchý nástroj pre zostavenie API
### Frontend ### 📺 Frontend
- [Vue.js 3](https://vuejs.org/) - JavaScript framework - [Vue.js 3](https://vuejs.org/) - JavaScript framework
- [Vue Router](https://router.vuejs.org/) - smerovanie v aplikácii - [Vue Router](https://router.vuejs.org/) - smerovanie v aplikácii
@ -40,17 +40,24 @@ BugReport je webová aplikácia na sledovanie a správu chýb (bug tracking syst
- [Mitt](https://github.com/developit/mitt) - knižnica pre správu udalostí - [Mitt](https://github.com/developit/mitt) - knižnica pre správu udalostí
- [Vite](https://vitejs.dev/) - build nástroj - [Vite](https://vitejs.dev/) - build nástroj
## Štruktúra projektu ## 🌳 Štruktúra projektu
``` ```
BugReport/ BugReport/
├── backend/ # Backend aplikácia (PHP) ├── backend/ # Backend aplikácia (PHP)
│ ├── src/ # Zdrojový kód
│ ├── config/ # Konfigurácia aplikácie │ ├── config/ # Konfigurácia aplikácie
── public/ # Vstupný bod aplikácie ── public/ # Vstupný bod aplikácie, tu nasmerovat vo webserveri DOCUMENT ROOT
│ ├── scripts/ # Skripty pre pred-spracovanie, build a ine
│ ├── src/ # Zdrojový kód
│ │ ├── Models/ # Classy pre jednotlive DB tabulky pouziva TPsoft\DBmodel
│ │ ├── API.php # Logika pre API BugReport pouziva TPsoft\APIlite
│ │ ├── Init.php # Inicializacia pripojenia k DB, kontroly a iné
│ │ └── Maintenance.php # Údržba pre projekt, zatiaľ hlavne pre upgrade databazovej štruktúry, používa \TPsoft\DBmodel\Maintenance
│ └── test/ # Testovacie skripty
├── data/ # Dáta aplikácie ├── data/ # Dáta aplikácie
│ ├── attachments/ # Súborové uložisko príloh │ ├── attachments/ # Súborové uložisko príloh
│ └── database.db # SQLite databáza │ └── database.db # SQLite databáza
├── doc/ # Dokumentácia a iné súbory počas vývoja
└── frontend/ # Frontend aplikácia (Vue.js) └── frontend/ # Frontend aplikácia (Vue.js)
├── public/ # Statické súbory ├── public/ # Statické súbory
├── scripts/ # Skripty pre build ├── scripts/ # Skripty pre build
@ -65,21 +72,22 @@ BugReport/
└── router.js # Vue Router konfigurácia └── router.js # Vue Router konfigurácia
``` ```
## Inštalácia a spustenie ## 🛠️ Inštalácia a spustenie
### Požiadavky ### 📋 Požiadavky
- PHP 7.4 alebo novší - PHP 8.2 alebo novší
- Webový server (Apache, Nginx) - Webový server (Apache, Nginx)
- Node.js a npm pre vývoj frontendu - Node.js a npm pre vývoj frontendu
### Backend ### ⚙️ Backend
1. Skopírujte súbory do webového adresára 1. Skopírujte súbory do webového adresára
2. Uistite sa, že adresár`data` má práva na zápis 2. Uistite sa, že adresár `data` má práva na zápis
3. Nastavte DOCUMENT ROOT na adresár `backend/public`
3. Prístup k aplikácii cez webový prehliadač 3. Prístup k aplikácii cez webový prehliadač
### Frontend (pre vývoj) ### 📺 Frontend (pre vývoj)
1. Prejdite do adresára`frontend` 1. Prejdite do adresára`frontend`
2. Nainštalujte závislosti: 2. Nainštalujte závislosti:
@ -95,11 +103,11 @@ BugReport/
npm run build npm run build
``` ```
## API dokumentácia ## 📖 API dokumentácia
API je dostupné cez `api.php` endpoint. Všetky požiadavky vracajú JSON odpoveď. API je dostupné cez `API.php` endpoint fyzicky umiestnený v DOCUMENT ROOT `backend/public/API.php`. Všetky požiadavky vracajú JSON odpoveď.kompletnú a aktuálnu dokumentáciu je možné získať aj HTML formáte `API.php?format=html`.
### Dostupné endpointy ### 🚏 Dostupné endpointy
| Akcia | Popis | Parametre | | Akcia | Popis | Parametre |
| -------------------- | -------------------------------------- | --------------------------------------------------------------- | | -------------------- | -------------------------------------- | --------------------------------------------------------------- |
@ -116,7 +124,7 @@ API je dostupné cez `api.php` endpoint. Všetky požiadavky vracajú JSON odpov
| `attachmentUpdate` | Aktualizuje prílohu | `attachment_id`, `attachment_content` | | `attachmentUpdate` | Aktualizuje prílohu | `attachment_id`, `attachment_content` |
| `attachmentGetAll` | Získa všetky prílohy reportu | `report_id` | | `attachmentGetAll` | Získa všetky prílohy reportu | `report_id` |
### Príklad API volania ### 🔍 Príklad API volania
```javascript ```javascript
// Získanie všetkých reportov // Získanie všetkých reportov
@ -133,7 +141,7 @@ fetch('api.php?action=add', {
.then(data => console.log(data)); .then(data => console.log(data));
``` ```
## Stavy reportov ## 🏗️ Stavy reportov
| ID | Stav | | ID | Stav |
| -- | ------------- | | -- | ------------- |
@ -143,7 +151,7 @@ fetch('api.php?action=add', {
| 3 | Blokované | | 3 | Blokované |
| 4 | Vyriešený | | 4 | Vyriešený |
## Priority reportov ## 📣 Priority reportov
| ID | Priorita | | ID | Priorita |
| -- | --------- | | -- | --------- |
@ -152,13 +160,16 @@ fetch('api.php?action=add', {
| 2 | Vysoká | | 2 | Vysoká |
| 3 | Kritická | | 3 | Kritická |
## Skupiny reportov ## 🕸️ Skupiny reportov
- `cp` - Control Panel - `cp` - Control Panel
- `task` - Task.Platon.sk - `task` - Task.Platon.sk
- `websiteip` - WebsiteIP - `websiteip` - WebsiteIP
- `antispam` - Antispam - `antispam` - Antispam
## Licencia ## ✨ Plánované vylepšenia - Planned Features
- Skupiny reportov v samostatnej tabuľke s CRUD manažmentom
## ⚖️ Licencia
Tento projekt je licencovaný pod [MIT licenciou](https://opensource.org/licenses/MIT). Tento projekt je licencovaný pod [MIT licenciou](https://opensource.org/licenses/MIT).

201
api.php
View File

@ -1,201 +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(json_decode($_REQUEST['status'], true), $_REQUEST['page'] == 'null' ? null : $_REQUEST['page']);
break;
case 'getArchived':
$result = reportGetArchived($_REQUEST['page'] == 'null' ? null : $_REQUEST['page']);
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;
case 'attachmentAdd':
$suc = attachmentAdd($_REQUEST['report_id'], $_REQUEST['attachment_type'], $_REQUEST['attachment_content']);
if ($suc === false) $error = 'Attachment add failed';
$result = array('processed' => $suc);
break;
case 'attachmentUpdate':
$suc = attachmentUpdate($_REQUEST['attachment_id'], $_REQUEST['attachment_content']);
if ($suc === false) $error = 'Attachment update failed';
$result = array('processed' => $suc);
break;
case 'attachmentGetAll':
$result = attachmentGetAll($_REQUEST['report_id']);
break;
case 'attachmentDelete':
$suc = attachmentDelete($_REQUEST['attachment_id']);
if ($suc === false) $error = 'Attachment delete 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',
'page' => '(ptional) Page number, default: null = vsetky',
]
],
'getArchived' => [
'name' => 'getArchived',
'description' => 'Get archived reports',
'params' => [
'page' => '(ptional) Page number, default: null = vsetky',
]
],
'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',
]
],
'attachmentAdd' => [
'name' => 'attachmentAdd',
'description' => 'Add attachment to report',
'params' => [
'report_id' => 'Report id',
'content_type' => 'Attachment content type',
'content' => 'Attachment content',
]
],
'attachmentUpdate' => [
'name' => 'attachmentUpdate',
'description' => 'Update attachment',
'params' => [
'attachment_id' => 'Attachment id',
'content' => 'Attachment content; if empty, attachment will be deleted',
]
],
'attachmentGetAll' => [
'name' => 'attachmentGetAll',
'description' => 'Get all attachments for report',
'params' => [
'report_id' => 'Report id',
]
],
'attachmentGet' => [
'name' => 'attachmentGet',
'description' => 'Get attachment',
'params' => [
'attachment_id' => 'Attachment id',
]
]
]
];
}

16
backend/composer.lock generated
View File

@ -8,11 +8,11 @@
"packages": [ "packages": [
{ {
"name": "tpsoft/apilite", "name": "tpsoft/apilite",
"version": "v1.0.2", "version": "v1.0.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://gitea.tpsoft.org/TPsoft.org/APIlite.git", "url": "https://gitea.tpsoft.org/TPsoft.org/APIlite.git",
"reference": "2d3f8bfdd46d0d304bac44057ff8444bfb2a4a17" "reference": "c0fd7b3fe5270ee44a84a92e9255ada2438812b7"
}, },
"require": { "require": {
"php": ">=8.2" "php": ">=8.2"
@ -49,15 +49,15 @@
"type": "other" "type": "other"
} }
], ],
"time": "2025-06-12T05:54:37+00:00" "time": "2025-10-13T21:36:34+00:00"
}, },
{ {
"name": "tpsoft/dbmodel", "name": "tpsoft/dbmodel",
"version": "v1.0.4", "version": "v1.0.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://gitea.tpsoft.org/TPsoft.org/DBmodel.git", "url": "https://gitea.tpsoft.org/TPsoft.org/DBmodel.git",
"reference": "ae59ffaa97094854bcd1d863c6648a5b4dada671" "reference": "80e889946bc4e38e987f46a13f95ee177ea934dc"
}, },
"require": { "require": {
"ext-pdo": "*", "ext-pdo": "*",
@ -85,7 +85,9 @@
"keywords": [ "keywords": [
"db", "db",
"model", "model",
"pdo" "mysql",
"pdo",
"sqlite"
], ],
"funding": [ "funding": [
{ {
@ -93,7 +95,7 @@
"type": "other" "type": "other"
} }
], ],
"time": "2025-06-15T17:07:42+00:00" "time": "2025-10-13T21:26:19+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],

View File

@ -4,7 +4,7 @@ require __DIR__ . '/../vendor/autoload.php';
ob_start(); ob_start();
$backend_api = new TPsoft\BugreportBackend\API('typescript', 'import.meta.env.VITE_BACKENDAPI_URL'); $backend_api = new TPsoft\BugreportBackend\API('typescript', 'import.meta.env.VITE_BACKENDAPI_URL', 'backend');
$output = ob_get_contents(); $output = ob_get_contents();
ob_end_clean(); ob_end_clean();

View File

@ -187,7 +187,7 @@ class API extends APIlite
if (!is_array($data)) return false; if (!is_array($data)) return false;
$base64 = preg_replace('/^data:.*?;base64,/', '', $data['base64']); $base64 = preg_replace('/^data:.*?;base64,/', '', $data['base64']);
$base64_data = base64_decode($base64); $base64_data = base64_decode($base64);
$filename = 'report_' . $report_id . '_' . time() . '_' . sanitizeFilename($data['filename']); $filename = 'report_' . $report_id . '_' . time() . '_' . $this->sanitizeFilename($data['filename']);
file_put_contents(UPLOAD_DIR_ATTACHMENTS . $filename, $base64_data); file_put_contents(UPLOAD_DIR_ATTACHMENTS . $filename, $base64_data);
$attachment_content = $filename; $attachment_content = $filename;
} }
@ -201,6 +201,26 @@ class API extends APIlite
return $suc !== false; 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 * Update report attachment
* *
@ -236,7 +256,7 @@ class API extends APIlite
->toArray(); ->toArray();
if (is_array($all)) foreach ($all as $key => $row) { if (is_array($all)) foreach ($all as $key => $row) {
if ($all[$key]['attachment_type'] == 'file') { if ($all[$key]['attachment_type'] == 'file') {
$all[$key]['attachment_content'] = UPLOAD_URL_ATTACHMENTS . $all[$key]['attachment_content']; $all[$key]['attachment_content'] = '?action=attachmentDownload&filename=' . $all[$key]['attachment_content'];
} }
} }
return $all; return $all;
@ -254,4 +274,28 @@ class API extends APIlite
$suc = $attachments->attachment($attachment_id, null); $suc = $attachments->attachment($attachment_id, null);
return $suc !== false; 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;
}
}
} }

View File

@ -4,6 +4,7 @@ require __DIR__ . '/../vendor/autoload.php';
use \Exception; use \Exception;
use \TPsoft\DBmodel\DBmodel; use \TPsoft\DBmodel\DBmodel;
use \TPsoft\BugreportBackend\Maintenance;
global $dbh; global $dbh;
@ -14,20 +15,6 @@ if (Configuration::DB_TYPE == 'mysql') {
} else { } else {
throw new Exception('Unknown database type'); throw new Exception('Unknown database type');
} }
/*
$dbh->tables = [ $maintenance = new Maintenance($dbh);
'reports' => [ $maintenance->database();
'name' => 'reports',
'primary_key_name' => 'report_id',
'allow_attributes' => [
'report_id' => 'varchar()',
'report_title' => 'varchar()',
'report_description' => 'varchar()',
'report_status' => 'varchar()',
'report_group' => 'varchar()',
'report_priority' => 'varchar()',
'created_dt' => 'varchar()',
],
],
];
*/

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

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,31 +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';
$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/');
global $db;
$db = new Medoo\Medoo([
'type' => 'sqlite',
'database' => __DIR__ . '/data/database.db'
]);
dbCheck();
?>

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

@ -1,7 +1,7 @@
{ {
"name": "bugreport", "name": "bugreport",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -2,7 +2,7 @@
<div id="header"> <div id="header">
<div class="logo"> <div class="logo">
<router-link to="/"> <router-link to="/">
<img src="/public/bugreport.svg" height="48" width="48" /> <img src="/bugreport.svg" height="48" width="48" />
</router-link> </router-link>
<router-link to="/"> <router-link to="/">
<h1>Bug Report</h1> <h1>Bug Report</h1>

View File

@ -265,6 +265,7 @@ button:focus-visible,
#dashboard .report .report-description { #dashboard .report .report-description {
padding: 10px; padding: 10px;
text-align: justify; text-align: justify;
white-space: pre-line;
border-right: 5px var(--color-bg0) solid; border-right: 5px var(--color-bg0) solid;
background-color: var(--color-bg2); background-color: var(--color-bg2);
} }
@ -462,6 +463,36 @@ button:focus-visible,
padding: 10px; padding: 10px;
text-align: justify; text-align: justify;
} }
#api ul li {
margin-bottom: 5px;
}
#api .param-type {
background-color: var(--color-bg0);
margin: 0px;
padding: 2px 5px;
text-align: center;
border-radius: 5px;
margin-left: 10px;
}
#api .param-optional {
background-color: var(--color-bg1);
margin: 0px;
padding: 2px 5px;
text-align: center;
border-radius: 5px;
margin-left: 10px;
}
#api .param-default {
background-color: var(--color-bg2);
margin: 0px;
padding: 2px 5px;
text-align: center;
border-radius: 5px;
margin-left: 10px;
}
#api .param-doc {
color: var(--color-text3);
}
/* ---------------------------------------------------- /* ----------------------------------------------------
07 - ABOUT 07 - ABOUT

View File

@ -2,10 +2,10 @@
* Generated by APIlite * Generated by APIlite
* https://gitea.tpsoft.org/TPsoft.org/APIlite * https://gitea.tpsoft.org/TPsoft.org/APIlite
* *
* 2025-10-01 00:23:37 */ * 2025-10-16 01:38:51 */
class backend { class backend {
endpont = import.meta.env.VITE_BACKENDAPI_URL; endpoint = import.meta.env.VITE_BACKENDAPI_URL;
/* ---------------------------------------------------- /* ----------------------------------------------------
* General API call * General API call
@ -28,7 +28,7 @@ class backend {
if (typeof val == 'object') val = JSON.stringify(val); if (typeof val == 'object') val = JSON.stringify(val);
form_data.append(key, val); form_data.append(key, val);
}); });
xhttp.open('POST', this.endpont + '?action=' + method); xhttp.open('POST', this.endpoint + '?action=' + method);
xhttp.send(form_data); xhttp.send(form_data);
} }
@ -107,6 +107,10 @@ class backend {
return this.callPromise('attachmentDelete', {attachment_id: attachment_id}); return this.callPromise('attachmentDelete', {attachment_id: attachment_id});
} }
attachmentDownload(filename) {
return this.callPromise('attachmentDownload', {filename: filename});
}
}; };

View File

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

View File

@ -89,9 +89,9 @@
v-else-if="attachment.attachment_type == 'file'" v-else-if="attachment.attachment_type == 'file'"
class="attachment-file" class="attachment-file"
> >
<a :href="attachment.attachment_content" target="_blank">Stiahnut {{ attachment.attachment_content.split('/').pop().split('?')[0].split('#')[0] }}</a> <a :href="backend.endpoint + attachment.attachment_content" target="_blank">Stiahnut {{ attachment.attachment_content.split('/').pop().split('?')[0].split('#')[0] }}</a>
<br> <br>
<img :src="attachment.attachment_content" v-if="isImageUrl(attachment.attachment_content)" /> <img :src="backend.endpoint + attachment.attachment_content" v-if="isImageUrl(attachment.attachment_content)" />
</div> </div>
<div v-else class="attachment-content"> <div v-else class="attachment-content">
Neznamy typ prilohy: <strong>{{ attachment.attachment_type }}</strong> Neznamy typ prilohy: <strong>{{ attachment.attachment_type }}</strong>
@ -102,7 +102,7 @@
<div class="form-group"> <div class="form-group">
<label for="description">Nový komentár:</label> <label for="description">Nový komentár:</label>
<textarea <textarea
ref="attachmentNewContent" v-model="attachmentNewContent"
rows="5" rows="5"
class="form-control" class="form-control"
placeholder="Nove zistenia alebo riesenia" placeholder="Nove zistenia alebo riesenia"
@ -144,7 +144,9 @@
</div> </div>
</template> </template>
<script> <script setup>
import { onMounted, ref } from "vue";
import { router } from "../router";
import backend from "../backend"; import backend from "../backend";
import FullScreenLoader from "../components/FullScreenLoader.vue"; import FullScreenLoader from "../components/FullScreenLoader.vue";
import JSConfetti from 'js-confetti' import JSConfetti from 'js-confetti'
@ -152,14 +154,9 @@ import JSConfetti from 'js-confetti'
let tadas = ['/sounds/tada.mp3', '/sounds/tada2.mp3', '/sounds/crazy-phrog-short.mp3']; let tadas = ['/sounds/tada.mp3', '/sounds/tada2.mp3', '/sounds/crazy-phrog-short.mp3'];
const jungle = new Audio(tadas[Math.floor(Math.random() * tadas.length)]); const jungle = new Audio(tadas[Math.floor(Math.random() * tadas.length)]);
export default { const loading = ref(false);
name: "Report", const report_id = router.currentRoute.value.params.id;
components: { FullScreenLoader }, const report = ref({
data() {
return {
loading: false,
report_id: this.$route.params.id,
report: {
report_id: 0, report_id: 0,
report_title: "Nacitavam report", report_title: "Nacitavam report",
report_description: "...", report_description: "...",
@ -168,137 +165,147 @@ export default {
report_priority: 1, report_priority: 1,
created_dt: "--", created_dt: "--",
ordnum: 0, ordnum: 0,
}, });
editable: true, const editable = ref(true);
attachments: [ const attachments = ref([
{ {
attachment_id: 0, attachment_id: 0,
attachment_type: "comment", attachment_type: "comment",
attachment_content: "Nacitavam report", attachment_content: "Nacitavam report",
created_dt: "--", created_dt: "--",
}, },
], ]);
attachmentNewFiles: null, const attachmentNewContent = ref(null);
selectedFiles: [], const attachmentNewFiles = ref(null);
selectedFilesContent: [], const selectedFiles = ref([]);
}; const selectedFilesContent = ref([]);
},
mounted() { onMounted(() => {
// console.log(this.report_id); // console.log(report_id);
this.loadReportData(); loadReportData();
}, });
methods: {
loadReportData() { function loadReportData() {
backend.get(this.report_id).then((response) => { backend.get(report_id).then((response) => {
this.report = response.data; report.value = response.data;
// console.log(this.report); // console.log(report);
this.editable = response.data.report_status < 4; editable.value = response.data.report_status < 4;
}); });
backend.attachmentGetAll(this.report_id).then((response) => { backend.attachmentGetAll(report_id).then((response) => {
this.attachments = response.data; attachments.value = response.data;
// console.log(this.attachments); // console.log(attachments);
}); });
}, }
onTitleChange(event) {
backend.update(this.report_id, { report_title: event.target.innerText }); function onTitleChange(event) {
}, backend.update(report_id, { report_title: event.target.innerText });
onDescriptionChange(event) { }
backend.update(this.report_id, {
function onDescriptionChange(event) {
backend.update(report_id, {
report_description: event.target.innerText, report_description: event.target.innerText,
}); });
}, }
reportDelete() {
function reportDelete() {
if (!confirm("Naozaj chcete report zmazať?")) return; if (!confirm("Naozaj chcete report zmazať?")) return;
this.loading = true; loading.value = true;
backend.delete(this.report_id).then(() => { backend.delete(report_id).then(() => {
this.$router.push("/"); router.push("/");
}); });
}, }
reportDone() {
backend.update(this.report_id, { function reportDone() {
backend.update(report_id, {
report_status: 4, report_status: 4,
}).then(() => { }).then(() => {
jungle.play(); jungle.play();
const confetti = new JSConfetti(); const confetti = new JSConfetti();
confetti.addConfetti(); confetti.addConfetti();
setTimeout(() => { setTimeout(() => {
this.$router.push("/"); router.push("/");
}, 3000); }, 3000);
}); });
}, }
attachmentAdd() {
let comment = this.$refs.attachmentNewContent.value; function attachmentAdd() {
let comment = attachmentNewContent.value;
if (comment.trim().length > 0) { if (comment.trim().length > 0) {
this.loading = true; loading.value = true;
backend backend
.attachmentAdd( .attachmentAdd(
this.report_id, report_id,
"comment", "comment",
this.$refs.attachmentNewContent.value attachmentNewContent.value
) )
.then(() => { .then(() => {
this.$refs.attachmentNewContent.value = ""; attachmentNewContent.value = "";
this.loadReportData(); loadReportData();
this.loading = false; loading.value = false;
}); });
} }
if (this.selectedFiles.length > 0) { if (selectedFiles.value.length > 0) {
let for_upload = this.selectedFiles.length; let for_upload = selectedFiles.value.length;
this.loading = true; loading.value = true;
for (let i = 0; i < this.selectedFiles.length; i++) { for (let i = 0; i < selectedFiles.value.length; i++) {
backend backend
.attachmentAdd(this.report_id, "file", { .attachmentAdd(report_id, "file", {
'filename': this.selectedFiles[i].name, 'filename': selectedFiles.value[i].name,
'base64': this.selectedFilesContent[i] 'base64': selectedFilesContent.value[i]
}) })
.then(() => { .then(() => {
for_upload--; for_upload--;
if (for_upload == 0) { if (for_upload == 0) {
this.selectedFiles = []; selectedFiles.value = [];
this.selectedFilesContent = []; selectedFilesContent.value = [];
this.$refs.attachmentNewFiles.value = null; attachmentNewFiles.value = null;
this.loadReportData(); loadReportData();
this.loading = false; loading.value = false;
} }
}); });
} }
} }
}, }
attachmentDelete(attachment) {
function attachmentDelete(attachment) {
if (!confirm("Naozaj chcete zmazať prilohu?")) return; if (!confirm("Naozaj chcete zmazať prilohu?")) return;
this.loading = true; loading.value = true;
backend.attachmentDelete(attachment.attachment_id).then(() => { backend.attachmentDelete(attachment.attachment_id).then(() => {
this.loadReportData(); loadReportData();
this.loading = false; loading.value = false;
}); });
}, }
updateAttachmentContent(event, attachment) {
this.loading = true; function updateAttachmentContent(event, attachment) {
loading.value = true;
backend backend
.attachmentUpdate(attachment.attachment_id, event.target.innerText) .attachmentUpdate(attachment.attachment_id, event.target.innerText)
.then(() => { .then(() => {
this.loadReportData(); loadReportData();
this.loading = false; loading.value = false;
}); });
}, }
handleFileUpload(event) {
function handleFileUpload(event) {
const files = event.target.files; const files = event.target.files;
if (files) { if (files) {
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
this.selectedFiles.push(files[i]); selectedFiles.value.push(files[i]);
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
this.selectedFilesContent[i] = reader.result; selectedFilesContent.value[i] = reader.result;
}; };
reader.readAsDataURL(files[i]); reader.readAsDataURL(files[i]);
} }
} }
}, }
removeFile(index) {
this.selectedFiles.splice(index, 1); function removeFile(index) {
this.selectedFilesContent.splice(index, 1); selectedFiles.value.splice(index, 1);
}, selectedFilesContent.value.splice(index, 1);
formatFileSize(size) { }
function formatFileSize(size) {
if (size < 1024) { if (size < 1024) {
return size + " B"; return size + " B";
} else if (size < 1024 * 1024) { } else if (size < 1024 * 1024) {
@ -306,10 +313,10 @@ export default {
} else { } else {
return (size / (1024 * 1024)).toFixed(2) + " MB"; return (size / (1024 * 1024)).toFixed(2) + " MB";
} }
}, }
isImageUrl(url) {
function isImageUrl(url) {
return /\.(jpg|jpeg|png|gif|svg|webp)$/.test(url); return /\.(jpg|jpeg|png|gif|svg|webp)$/.test(url);
} }
},
};
</script> </script>

View File

@ -8,7 +8,7 @@ const require = createRequire(import.meta.url);
const pkg = require("./package.json"); const pkg = require("./package.json");
const subpath = "/bugreport/"; const subpath = "/bugreport/";
export const baseUrl = subpath + "webapp/dist/"; export const baseUrl = "/"; //subpath + "frontend/dist/";
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig(({ command, mode }) => { export default defineConfig(({ command, mode }) => {
@ -25,6 +25,10 @@ export default defineConfig(({ command, mode }) => {
__IS_BUILD__: JSON.stringify(isBuild), __IS_BUILD__: JSON.stringify(isBuild),
__IS_DEV__: JSON.stringify(isDev), __IS_DEV__: JSON.stringify(isDev),
}, },
server: {
host: true,
port: 5173
},
build: { build: {
outDir: "dist", outDir: "dist",
chunkSizeWarningLimit: 1000, // zvýšenie limitu na 1000 kB chunkSizeWarningLimit: 1000, // zvýšenie limitu na 1000 kB

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,344 +0,0 @@
<?php
/**
* String functions
*/
function allowedChars($str, $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-')
{
return preg_match('/^[' . $chars . ']+$/', $str);
}
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;
}
/**
* 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, $page = null)
{
global $db;
if ($status === null) $status = array(0, 1, 2, 3);
$params = [
'ORDER' => ['report_priority' => 'DESC', 'ordnum' => 'ASC'],
'report_status' => $status
];
if ($page !== null) $params['LIMIT'] = [$page * 10, 10];
return $db->select('reports', '*', $params);
}
function reportGetAllGrouped($status = null, $page = null)
{
$all = reportGetAll($status, $page);
$groups = [];
foreach ($all as $report) {
$groups[$report['report_status']][] = $report;
}
return $groups;
}
function reportGetArchived($page = null)
{
global $db;
$params = [
'ORDER' => ['created_dt' => 'DESC'],
'report_status' => '4'
];
if ($page !== null) $params['LIMIT'] = [$page * 10, 10];
return $db->select('reports', '*', $params);
}
/**
* Attachments
*/
function attachmentGet($attachment_id)
{
global $db;
return $db->get('attachments', '*', [
'attachment_id' => $attachment_id
]);
}
function attachmentAdd($report_id, $attachment_type, $attachment_content)
{
global $db;
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() . '_' . sanitizeFilename($data['filename']);
file_put_contents(UPLOAD_DIR_ATTACHMENTS . $filename, $base64_data);
$attachment_content = $filename;
}
$stm = $db->insert('attachments', [
'report_id' => $report_id,
'attachment_type' => $attachment_type,
'attachment_content' => $attachment_content,
'created_dt' => date('Y-m-d H:i:s')
]);
return ($stm->rowCount() > 0);
}
function attachmentUpdate($attachment_id, $attachment_content)
{
global $db;
if (strlen(trim($attachment_content)) <= 0) return attachmentDelete($attachment_id);
$stm = $db->update('attachments', [
'attachment_content' => $attachment_content,
'updated_dt' => date('Y-m-d H:i:s')
], [
'attachment_id' => $attachment_id
]);
return ($stm->rowCount() > 0);
}
function attachmentDelete($attachment_id)
{
global $db;
$attachment = attachmentGet($attachment_id);
if ($attachment['attachment_type'] == 'file'
&& file_exists(UPLOAD_DIR_ATTACHMENTS . $attachment['attachment_content']))
{
unlink(UPLOAD_DIR_ATTACHMENTS . $attachment['attachment_content']);
}
$stm = $db->delete('attachments', [
'attachment_id' => $attachment_id
]);
return ($stm->rowCount() > 0);
}
function attachmentGetAll($report_id)
{
global $db;
$all = $db->select('attachments', '*', [
'ORDER' => ['created_dt' => 'ASC'],
'report_id' => $report_id
]);
if (is_array($all)) foreach ($all as $key => $row) {
if ($all[$key]['attachment_type'] == 'file') {
$all[$key]['attachment_content'] = UPLOAD_URL_ATTACHMENTS . $all[$key]['attachment_content'];
}
}
return $all;
}