diff --git a/backend/composer.json b/backend/composer.json new file mode 100644 index 0000000..4a7316b --- /dev/null +++ b/backend/composer.json @@ -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" + } +} diff --git a/backend/composer.lock b/backend/composer.lock new file mode 100644 index 0000000..304dfce --- /dev/null +++ b/backend/composer.lock @@ -0,0 +1,108 @@ +{ + "_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.2", + "source": { + "type": "git", + "url": "https://gitea.tpsoft.org/TPsoft.org/APIlite.git", + "reference": "2d3f8bfdd46d0d304bac44057ff8444bfb2a4a17" + }, + "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-06-12T05:54:37+00:00" + }, + { + "name": "tpsoft/dbmodel", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://gitea.tpsoft.org/TPsoft.org/DBmodel.git", + "reference": "ae59ffaa97094854bcd1d863c6648a5b4dada671" + }, + "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", + "pdo" + ], + "funding": [ + { + "url": "https://www.anycoin.cz/donate/igormino", + "type": "other" + } + ], + "time": "2025-06-15T17:07:42+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" +} diff --git a/backend/config/Configuration.php b/backend/config/Configuration.php new file mode 100644 index 0000000..6c96e15 --- /dev/null +++ b/backend/config/Configuration.php @@ -0,0 +1,41 @@ +rootDir(realpath(__DIR__.'/../src/')); +$creator->interact(); diff --git a/backend/src/API.php b/backend/src/API.php new file mode 100644 index 0000000..00e33aa --- /dev/null +++ b/backend/src/API.php @@ -0,0 +1,257 @@ +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() . '_' . 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; + } + + /** + * 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'] = UPLOAD_URL_ATTACHMENTS . $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; + } +} diff --git a/backend/src/Init.php b/backend/src/Init.php new file mode 100644 index 0000000..bc4c5a9 --- /dev/null +++ b/backend/src/Init.php @@ -0,0 +1,33 @@ +tables = [ + 'reports' => [ + '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()', + ], + ], +]; +*/ \ No newline at end of file diff --git a/backend/src/Models/Attachments.php b/backend/src/Models/Attachments.php new file mode 100644 index 0000000..833ab57 --- /dev/null +++ b/backend/src/Models/Attachments.php @@ -0,0 +1,88 @@ + 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); + } + +} + +?> diff --git a/backend/src/Models/Options.php b/backend/src/Models/Options.php new file mode 100644 index 0000000..2387b31 --- /dev/null +++ b/backend/src/Models/Options.php @@ -0,0 +1,81 @@ + 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); + } + +} + +?> diff --git a/backend/src/Models/Reports.php b/backend/src/Models/Reports.php new file mode 100644 index 0000000..ad3dc57 --- /dev/null +++ b/backend/src/Models/Reports.php @@ -0,0 +1,90 @@ + 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); + } + +} + +?> diff --git a/backend/tests/testDB.php b/backend/tests/testDB.php new file mode 100644 index 0000000..78a0177 --- /dev/null +++ b/backend/tests/testDB.php @@ -0,0 +1,84 @@ +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"; +} diff --git a/webapp/.gitignore b/frontend/.gitignore similarity index 100% rename from webapp/.gitignore rename to frontend/.gitignore diff --git a/webapp/.vscode/extensions.json b/frontend/.vscode/extensions.json similarity index 100% rename from webapp/.vscode/extensions.json rename to frontend/.vscode/extensions.json diff --git a/webapp/README.md b/frontend/README.md similarity index 100% rename from webapp/README.md rename to frontend/README.md diff --git a/webapp/index.html b/frontend/index.html similarity index 100% rename from webapp/index.html rename to frontend/index.html diff --git a/webapp/package-lock.json b/frontend/package-lock.json similarity index 100% rename from webapp/package-lock.json rename to frontend/package-lock.json diff --git a/webapp/package.json b/frontend/package.json similarity index 100% rename from webapp/package.json rename to frontend/package.json diff --git a/webapp/public/bugreport.svg b/frontend/public/bugreport.svg similarity index 100% rename from webapp/public/bugreport.svg rename to frontend/public/bugreport.svg diff --git a/webapp/public/sounds/crazy-phrog-short.mp3 b/frontend/public/sounds/crazy-phrog-short.mp3 similarity index 100% rename from webapp/public/sounds/crazy-phrog-short.mp3 rename to frontend/public/sounds/crazy-phrog-short.mp3 diff --git a/webapp/public/sounds/crazy-phrog.mp3 b/frontend/public/sounds/crazy-phrog.mp3 similarity index 100% rename from webapp/public/sounds/crazy-phrog.mp3 rename to frontend/public/sounds/crazy-phrog.mp3 diff --git a/webapp/public/sounds/tada.mp3 b/frontend/public/sounds/tada.mp3 similarity index 100% rename from webapp/public/sounds/tada.mp3 rename to frontend/public/sounds/tada.mp3 diff --git a/webapp/public/sounds/tada2.mp3 b/frontend/public/sounds/tada2.mp3 similarity index 100% rename from webapp/public/sounds/tada2.mp3 rename to frontend/public/sounds/tada2.mp3 diff --git a/webapp/scripts/generateHtaccess.js b/frontend/scripts/generateHtaccess.js similarity index 100% rename from webapp/scripts/generateHtaccess.js rename to frontend/scripts/generateHtaccess.js diff --git a/webapp/src/App.vue b/frontend/src/App.vue similarity index 97% rename from webapp/src/App.vue rename to frontend/src/App.vue index 3368ae1..66a276a 100644 --- a/webapp/src/App.vue +++ b/frontend/src/App.vue @@ -46,7 +46,7 @@