pridana implementacia backend s composerom a kniznicami TPsoft/APIlite a TPsoft/DBmodel

This commit is contained in:
2025-10-01 00:57:41 +02:00
parent 8184ffb46d
commit 1cf79a20b3
45 changed files with 907 additions and 58 deletions

24
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
frontend/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
frontend/README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

13
frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/bugreport.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BugReport</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1742
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
frontend/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "bugreport",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && node scripts/generateHtaccess.js",
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@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

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools, Modified by Igor Mino 2025-04-12 23:58:02 -->
<svg width="800px" height="800px" viewBox="0 0 24 24" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>
.cls-1{fill:none;stroke:#020202;stroke-miterlimit:10;stroke-width:1.91px;}
.cls-2{fill:none;stroke:#5555FF;stroke-miterlimit:10;stroke-width:1.91px;}
.cls-3{fill:#cccccc;stroke:#cccccc;}
</style>
</defs>
<polygon class="cls-3" points="3 2 16 2 20 6 20 22 3 22" />
<line class="cls-2" x1="17.73" y1="14.86" x2="14.86" y2="14.86"/>
<line class="cls-2" x1="9.14" y1="14.86" x2="6.27" y2="14.86"/>
<polygon class="cls-1" points="20.59 6.27 20.59 22.5 3.41 22.5 3.41 1.5 15.82 1.5 20.59 6.27"/>
<polygon class="cls-1" points="20.59 6.27 20.59 7.23 14.86 7.23 14.86 1.5 15.82 1.5 20.59 6.27"/>
<path class="cls-2" d="M7.23,10.09v1A1.91,1.91,0,0,0,9.14,13"/>
<path class="cls-2" d="M16.77,10.09v1A1.91,1.91,0,0,1,14.86,13"/>
<path class="cls-2" d="M7.23,19.64v-1a1.92,1.92,0,0,1,1.91-1.91"/>
<path class="cls-2" d="M16.77,19.64v-1a1.92,1.92,0,0,0-1.91-1.91"/>
<rect class="cls-2" x="9.14" y="11.05" width="5.73" height="7.64" rx="2.86"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,27 @@
// generateHtaccess.js
console.log('Vygenerujem .htaccess...');
import fs from 'fs';
import path from 'path';
import { baseUrl } from '../vite.config.js';
// Obsah .htaccess súboru
const htaccessContent = `
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase ${baseUrl}
RewriteRule ^index\\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . ${baseUrl}index.html [L]
</IfModule>
`;
// Určte cestu pre vygenerovanie .htaccess do dist/
const distPath = path.join(process.cwd(), 'dist', '.htaccess');
// Zapíšte obsah do súboru .htaccess
fs.writeFileSync(distPath, htaccessContent, 'utf8');
console.log('.htaccess bol úspešne vygenerovaný do dist/');

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="/public/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

@ -0,0 +1,611 @@
/*
TPsoft.org 2000-2025
Cascade Style Document
posledna editacia:
2025-04-13 00:09 Igor
01 - GENERAL
02 - HEADER
03 - DASHBOARD
04 - BUG ADD
05 - ARCHIVE
06 - API
07 - ABOUT
08 - REPORT
80 - FORM
99 - LIGHT MODE
*/
/* ----------------------------------------------------
01 - GENERAL
*/
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--color-bg: #242424;
--color-bg0: #0d5eaf;
--color-bg1: #449ef8;
--color-bg2: #449ef820;
--color-bgGray: #797979;
--color-bgOrange: #af6000;
--color-bgRed: #be0101;
--color-text0: #fff;
--color-text1: rgb(11, 11, 104);
--color-text2: #449ef8;
--color-text3: #7bb8f5;
/* color-scheme: light dark; */
color: var(--color-text0);
background-color: var(--color-bg);
}
a {
font-weight: 500;
color: var(--color-text2);
text-decoration: inherit;
transition: all 0.3s;
}
a:hover {
color: var(--color-text3);
text-shadow: 1px 1px 5px var(--color-bg1);
}
body {
margin: 0;
display: flex;
flex-direction: column;
/* place-items: center; */
min-width: 320px;
min-height: 100vh;
font-size: 14px;
}
h1 {
font-size: 24px;
line-height: 1.1;
}
button,
.button {
border-radius: 8px;
border: 1px solid var(--color-bg0);
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: var(--color-bg2);
color: var(--color-text0);
cursor: pointer;
transition: all 0.3s;
}
button:hover,
.button:hover {
border-color: var(--color-bg1);
background-color: var(--color-bg0);
}
button:focus,
.button:focus,
button:focus-visible,
.button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
/* max-width: 1280px; */
/* margin: 0 auto; */
/* padding: 10px; */
/* text-align: center; */
}
/* ----------------------------------------------------
02 - HEADER
*/
#header {
background-color: var(--color-bg0);
color: var(--color-text0);
padding: 20px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: end;
}
#header a,
#header a:visited {
color: var(--color-text0);
}
#header .logo {
display: flex;
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 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 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
*/
#dashboard {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: stretch;
/* border: 1px red solid; */
min-height: calc(100vh - 150px);
}
#dashboard > div {
width: 25%;
}
#dashboard > div:not(:first-child) {
border-left: 5px var(--color-bg0) dotted;
}
#dashboard > div h2 {
background-color: var(--color-bg1);
margin: 0px;
padding: 5px;
text-align: center;
}
#dashboard .report {
margin: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
border-left: 5px var(--color-bg1) solid;
border-radius: 5px;
transition: all 0.3s;
}
#dashboard .report .report-header {
background-color: var(--color-bg0);
display: flex;
flex-direction: row;
justify-content: space-between;
border-top-right-radius: 5px;
cursor: grab;
}
#dashboard .report .report-header .report-title {
}
#dashboard .report .report-header h3 {
margin: 0px;
padding: 5px 10px;
}
#dashboard .report .report-description {
padding: 10px;
text-align: justify;
border-right: 5px var(--color-bg0) solid;
background-color: var(--color-bg2);
}
#dashboard .report .report-footer {
background-color: var(--color-bg1);
border-bottom-right-radius: 5px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
#dashboard .report .report-group {
text-align: left;
padding: 5px;
}
#dashboard .report .report-id {
text-align: center;
padding: 5px;
}
#dashboard .report .report-date {
text-align: right;
padding: 5px;
}
#dashboard .report:hover {
filter: brightness(1.2);
}
#dashboard .dragging .report {
/* border: 2px red solid; */
box-shadow: 0px 0px 10px var(--color-bg1);
}
#dashboard:has(.dragging) .draggable-item:not(.dragging) .report {
opacity: 0.4;
}
#dashboard .report-priority-0 .report-header {
background-color: var(--color-bgGray);
}
#dashboard .report-priority-0 .report-description {
border-right-color: var(--color-bgGray);
}
#dashboard .report-priority-1 .report-header {
background-color: var(--color-bg0);
}
#dashboard .report-priority-1 .report-description {
border-right-color: var(--color-bg0);
}
#dashboard .report-priority-2 .report-header {
background-color: var(--color-bgOrange);
}
#dashboard .report-priority-2 .report-description {
border-right-color: var(--color-bgOrange);
}
#dashboard .report-priority-3 .report-header {
background-color: var(--color-bgRed);
}
#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
*/
#bug-add {
margin: 0 auto;
padding: 20px;
}
#bug-add .cols {
/* border: 1px red solid; */
display: flex;
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);
}
}
/* ----------------------------------------------------
06 - API
*/
#api {
margin: 0 auto;
padding: 20px;
}
#api .action {
margin-top: 10px;
padding-bottom: 10px;
background-color: var(--color-bg2);
transition: all 0.3s;
min-width: 150px;
}
#api .action:hover {
filter: brightness(1.2);
}
#api .action h3 {
background-color: var(--color-bg0);
margin: 0px;
padding: 5px;
text-align: center;
}
#api .action h4 {
margin: 0px;
padding-left: 10px;
}
#api .action p {
padding: 10px;
text-align: justify;
}
/* ----------------------------------------------------
07 - ABOUT
*/
#about {
margin: 0 auto;
padding: 10px;
text-align: center;
}
#about .cols {
/* border: 1px red solid; */
display: flex;
flex-direction: row;
justify-content: center;
}
#about .cols div {
margin: 10px;
padding: 10px;
background-color: var(--color-bg0);
transition: all 0.3s;
min-width: 150px;
}
#about .cols div:hover {
filter: brightness(1.2);
}
@media (max-width: 600px) {
#about .cols {
flex-direction: column;
}
}
/* ----------------------------------------------------
08 - REPORT
*/
#report {
margin: 0 auto;
padding: 10px;
}
#report .report-header {
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; */
align-items: center;
}
#report .report-header div span {
background-color: var(--color-bg0);
color: var(--color-text0);
padding: 2px 10px;
align-items: center;
}
#report .report-header div strong {
background-color: var(--color-bg1);
color: var(--color-text0);
padding: 2px 10px;
align-items: center;
}
#report .description {
background-color: var(--color-bg2);
padding: 10px;
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
*/
.form-group {
margin-bottom: 10px;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
.form-group input,
.form-group textarea {
width: 99%;
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
background-color: var(--color-bg2);
color: var(--color-text0);
}
.form-actions {
margin-top: 10px;
text-align: right;
}
.form-actions button {
margin-left: 10px;
}
/* ----------------------------------------------------
99 - LIGHT MODE
*/
/* @media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
} */

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 208 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

113
frontend/src/backend.js Normal file
View File

@ -0,0 +1,113 @@
/**
* Generated by APIlite
* https://gitea.tpsoft.org/TPsoft.org/APIlite
*
* 2025-10-01 00:23:37 */
class backend {
endpont = 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.endpont + '?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});
}
};
export default new backend();

View File

@ -0,0 +1,40 @@
<template>
<div class="fullscreen-loader">
<div class="spinner"></div>
</div>
</template>
<style scoped>
.fullscreen-loader {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
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 {
width: 60px;
height: 60px;
border: 6px solid #fff;
border-top: 6px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,43 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@ -0,0 +1,23 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,45 @@
<script setup>
defineProps({
report_id: Number,
title: String,
description: String,
date: String,
priority: Number,
group: String,
});
</script>
<template>
<div :class="[ 'report', 'report-priority-' + priority]" @click="goToReport(report_id)">
<div class="report-header">
<div class="report-title">
<h3><font-awesome-icon :icon="['fas', 'bug']" /> {{ title }}</h3>
</div>
</div>
<div class="report-description">
<p>
{{ description }}
</p>
</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>
</div>
</template>
<script>
export default {
name: "ReportBox",
data() {
return {
};
},
methods: {
goToReport(report_id) {
this.$router.push("/report/" + report_id);
},
},
};
</script>

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

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

17
frontend/src/main.js Normal file
View File

@ -0,0 +1,17 @@
import { createApp } from 'vue'
import './assets/css/style.css'
import App from './App.vue'
import { router } from './router'
// Font Awesome
import { library } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { fas } from '@fortawesome/free-solid-svg-icons' // alebo pridaj konkrétne ikony
library.add(fas) // alebo napr. library.add(faSearch, faUser)
const app = createApp(App)
app.component('font-awesome-icon', FontAwesomeIcon)
app.use(router)
app.mount('#app')

22
frontend/src/router.js Normal file
View File

@ -0,0 +1,22 @@
import { createRouter, createWebHistory } from "vue-router";
import Dashboard from "./views/Dashboard.vue";
import About from "./views/About.vue";
import BugAdd from "./views/BugAdd.vue";
import Archive from "./views/Archive.vue";
import API from "./views/API.vue";
import Report from "./views/Report.vue";
const routes = [
{ path: "/", component: Dashboard },
{ path: "/about", component: About },
{ path: "/add", component: BugAdd },
{ path: "/archive", component: Archive },
{ path: "/api", component: API },
{ path: "/report/:id", component: Report },
];
export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});

View File

@ -0,0 +1,74 @@
<template>
<div id="api">
<h1>API</h1>
<p>
API prípojny bod je dostupný na adrese
<a href="{{ api_endpoint }}">{{ api_endpoint }}</a
>.
</p>
<h2>Zoznam akcii</h2>
<ul>
<li v-for="action in help.actions" :key="action.name">
<a :href="'#' + action.name">{{ action.name }}</a>
</li>
</ul>
<h2>Prehľad parametrov akcií</h2>
<div v-for="action in help.actions" :key="action.name" class="action">
<h3 :id="action.name">{{ action.name }}</h3>
<p>
{{ action.description }} <br />
<a :href="api_endpoint + '?action=' + action.name" target="_blank">{{
api_endpoint + "?action=" + action.name
}}</a>
</p>
<h4>Parametre</h4>
<p v-if="Object.keys(action.params).length == 0">
<font-awesome-icon :icon="['fas', 'circle-info']" />
&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>
</ul>
</div>
</div>
</template>
<script>
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;
});
},
},
};
</script>

View File

@ -0,0 +1,57 @@
<template>
<div id="about">
<div>
<h1>O aplikácii</h1>
<img src="/bugreport.svg" height="100" alt="" />
<h2>Bug Report</h2>
<p>
Verzia aplikácie: {{ version }} &nbsp;|&nbsp; Zostavené: {{ build }}
</p>
<p>Autor: <a href="mailto:mino@tpsoft.org">Ing. Igor Miňo</a></p>
<div class="cols">
<div>
Backend thanks for <br />
<a href="https://www.php.net" target="_blank">
<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="sqlite" height="20" alt="" /> <br>
SQLite
</a>
</div>
<div>
Frontend thanks for <br>
<a href="https://vuejs.org" target="_blank">
<img :src="vue" height="20" alt="" /> <br>
Vue 3
</a>
</div>
</div>
<p>
Zdrojové kódy:
<a href="https://gitea.tpsoft.org/TPsoft.org/BugReport" target="_blank"
>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 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

@ -0,0 +1,146 @@
<template>
<FullScreenLoader v-if="loading" />
<div id="bug-add">
<h1>Pridať nový bug</h1>
<form @submit.prevent="submitForm" class="form">
<div class="form-group">
<label for="title">Názov:</label>
<input
type="text"
id="title"
v-model="bugReport.title"
required
class="form-control"
placeholder="Zadajte názov bug reportu"
/>
</div>
<div class="form-group">
<label for="description">Popis:</label>
<textarea
id="description"
v-model="bugReport.description"
rows="5"
class="form-control"
placeholder="Detailný popis problému"
required
></textarea>
</div>
<div class="cols">
<div class="form-group">
<label for="priority">Priorita:</label>
<select
id="priority"
v-model="bugReport.priority"
class="form-control"
required
>
<option value="" disabled>Vyberte prioritu</option>
<option value="0">Nízka</option>
<option value="1">Stredná</option>
<option value="2">Vysoká</option>
<option value="3">Kritická</option>
</select>
</div>
<div class="form-group">
<label for="group">Skupina:</label>
<select
id="group"
v-model="bugReport.group"
class="form-control"
required
>
<option value="" disabled>Vyberte skupinu</option>
<option value="cp">Control Panel</option>
<option value="task">Task.Platon.sk</option>
<option value="websiteip">WebsiteIP</option>
<option value="antispam">Antispam</option>
</select>
</div>
<div class="form-actions">
<router-link to="/" class="button"
><font-awesome-icon :icon="['fas', 'circle-arrow-left']" />
Zrušiť</router-link
>
<button type="submit">
<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 FullScreenLoader from "../components/FullScreenLoader.vue";
export default {
name: "BugAdd",
components: { FullScreenLoader },
data() {
return {
bugReport: {
title: "",
description: "",
priority: "1",
group: "cp",
files: [],
},
selectedFiles: [],
loading: false,
};
},
methods: {
submitForm() {
this.loading = true;
// Vytvorenie FormData objektu pre odoslanie súborov
const formData = new FormData();
formData.append("title", this.bugReport.title);
formData.append("description", this.bugReport.description);
formData.append("priority", this.bugReport.priority);
// 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,
});
backend
.add(
this.bugReport.title,
this.bugReport.description,
"0",
this.bugReport.group,
this.bugReport.priority
)
.then((result) => {
console.log(result);
this.resetForm();
this.$router.push("/report/" + result.report_id);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
this.loading = false;
});
},
resetForm() {
this.bugReport = {
title: "",
description: "",
priority: "",
files: [],
};
},
},
};
</script>

View File

@ -0,0 +1,245 @@
<template>
<FullScreenLoader v-if="loading" />
<div id="dashboard">
<div id="inbox">
<h2>Nezaradené</h2>
<draggable
v-model="itemsUncategorized"
item-key="report_id"
:group="{ name: 'itemsUncategorized', pull: true, put: true }"
@change="onDragChange"
@start="onDragStart"
@end="onDragEnd"
>
<template #item="{ element }">
<div
:class="{
'draggable-item': true,
dragging: isDragable(element),
}"
>
<ReportBox
:report_id="element.report_id"
:title="element.report_title"
:description="element.report_description"
:date="element.created_dt"
:priority="element.report_priority"
:group="element.report_group"
/>
</div>
</template>
</draggable>
</div>
<div id="waiting">
<h2>Čakajúce</h2>
<draggable
v-model="itemsWaiting"
item-key="report_id"
:group="{ name: 'itemsWaiting', pull: true, put: true }"
@change="onDragChange"
@start="onDragStart"
@end="onDragEnd"
>
<template #item="{ element }">
<div
:class="{
'draggable-item': true,
dragging: isDragable(element),
}"
>
<ReportBox
:report_id="element.report_id"
:title="element.report_title"
:description="element.report_description"
:date="element.created_dt"
:priority="element.report_priority"
:group="element.report_group"
/>
</div>
</template>
</draggable>
</div>
<div id="inprogress">
<h2>Rozpracované</h2>
<draggable
v-model="itemsInProgress"
item-key="report_id"
:group="{ name: 'itemsInProgress', pull: true, put: true }"
@change="onDragChange"
@start="onDragStart"
@end="onDragEnd"
>
<template #item="{ element }">
<div
:class="{
'draggable-item': true,
dragging: isDragable(element),
}"
>
<ReportBox
:report_id="element.report_id"
:title="element.report_title"
:description="element.report_description"
:date="element.created_dt"
:priority="element.report_priority"
:group="element.report_group"
/>
</div>
</template>
</draggable>
</div>
<div id="blocked">
<h2>Blokované</h2>
<draggable
v-model="itemsBlocked"
item-key="report_id"
:group="{ name: 'itemsBlocked', pull: true, put: true }"
@change="onDragChange"
@start="onDragStart"
@end="onDragEnd"
>
<template #item="{ element }">
<div
:class="{
'draggable-item': true,
dragging: isDragable(element),
}"
>
<ReportBox
:report_id="element.report_id"
:title="element.report_title"
:description="element.report_description"
:date="element.created_dt"
:priority="element.report_priority"
:group="element.report_group"
/>
</div>
</template>
</draggable>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const itemDragable = ref(0);
function onDragStart(evt) {
itemDragable.value = evt.item.__vnode.key;
}
function onDragEnd(evt) {
itemDragable.value = 0;
}
function isDragable(element) {
return itemDragable.value === element.report_id;
}
</script>
<script>
import ReportBox from "../components/ReportBox.vue";
import draggable from "vuedraggable";
import backend from "../backend";
import FullScreenLoader from "../components/FullScreenLoader.vue";
import events from "../events";
export default {
components: {
ReportBox,
draggable,
FullScreenLoader,
},
data() {
return {
loading: false,
itemsUncategorized: [],
itemsWaiting: [],
itemsInProgress: [],
itemsBlocked: [],
};
},
methods: {
loadData(use_loader = true) {
if (use_loader) this.loading = true;
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];
this.itemsBlocked = all_grouped[3];
if (use_loader) this.loading = false;
});
},
getDataByStatus(report_status) {
let for_reorder = null;
switch (report_status) {
case 0:
for_reorder = this.itemsUncategorized;
break;
case 1:
for_reorder = this.itemsWaiting;
break;
case 2:
for_reorder = this.itemsInProgress;
break;
case 3:
for_reorder = this.itemsBlocked;
break;
}
return for_reorder;
},
searchStatusByReportId(report_id) {
for (let i = 0; i < this.itemsUncategorized.length; i++) {
if (this.itemsUncategorized[i].report_id === report_id) return 0;
}
for (let i = 0; i < this.itemsWaiting.length; i++) {
if (this.itemsWaiting[i].report_id === report_id) return 1;
}
for (let i = 0; i < this.itemsInProgress.length; i++) {
if (this.itemsInProgress[i].report_id === report_id) return 2;
}
for (let i = 0; i < this.itemsBlocked.length; i++) {
if (this.itemsBlocked[i].report_id === report_id) return 3;
}
},
onDragChange(event) {
// console.log("onDragChange", event);
if (event.added) {
// console.log("Pridané:", event.added.element);
let report_id = event.added.element.report_id;
let new_reprort_status = this.searchStatusByReportId(report_id);
let for_reorder = this.getDataByStatus(new_reprort_status);
backend.updateStatus(report_id, new_reprort_status).then(() => {
this.updateOrdnum(for_reorder);
});
}
if (event.moved) {
// console.log("Presunuté:", event.moved.element);
let report_status = event.moved.element.report_status;
let for_reorder = this.getDataByStatus(report_status);
this.updateOrdnum(for_reorder);
}
if (event.removed) {
// console.log("Odstranené:", event.removed.element);
}
},
updateOrdnum(for_reorder) {
let new_ordnums = {};
for (let i = 0; i < for_reorder.length; i++) {
new_ordnums[for_reorder[i].report_id] = i;
}
backend.updateOrdnum(new_ordnums);
},
},
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,315 @@
<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="attachment.attachment_content" target="_blank">Stiahnut {{ attachment.attachment_content.split('/').pop().split('?')[0].split('#')[0] }}</a>
<br>
<img :src="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
ref="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>
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)]);
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: 4,
report_group: "--",
report_priority: 1,
created_dt: "--",
ordnum: 0,
},
editable: true,
attachments: [
{
attachment_id: 0,
attachment_type: "comment",
attachment_content: "Nacitavam report",
created_dt: "--",
},
],
attachmentNewFiles: null,
selectedFiles: [],
selectedFilesContent: [],
};
},
mounted() {
// console.log(this.report_id);
this.loadReportData();
},
methods: {
loadReportData() {
backend.get(this.report_id).then((response) => {
this.report = response.data;
// console.log(this.report);
this.editable = response.data.report_status < 4;
});
backend.attachmentGetAll(this.report_id).then((response) => {
this.attachments = response.data;
// console.log(this.attachments);
});
},
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("/");
});
},
reportDone() {
backend.update(this.report_id, {
report_status: 4,
}).then(() => {
jungle.play();
const confetti = new JSConfetti();
confetti.addConfetti();
setTimeout(() => {
this.$router.push("/");
}, 3000);
});
},
attachmentAdd() {
let comment = this.$refs.attachmentNewContent.value;
if (comment.trim().length > 0) {
this.loading = true;
backend
.attachmentAdd(
this.report_id,
"comment",
this.$refs.attachmentNewContent.value
)
.then(() => {
this.$refs.attachmentNewContent.value = "";
this.loadReportData();
this.loading = false;
});
}
if (this.selectedFiles.length > 0) {
let for_upload = this.selectedFiles.length;
this.loading = true;
for (let i = 0; i < this.selectedFiles.length; i++) {
backend
.attachmentAdd(this.report_id, "file", {
'filename': this.selectedFiles[i].name,
'base64': this.selectedFilesContent[i]
})
.then(() => {
for_upload--;
if (for_upload == 0) {
this.selectedFiles = [];
this.selectedFilesContent = [];
this.$refs.attachmentNewFiles.value = null;
this.loadReportData();
this.loading = false;
}
});
}
}
},
attachmentDelete(attachment) {
if (!confirm("Naozaj chcete zmazať prilohu?")) return;
this.loading = true;
backend.attachmentDelete(attachment.attachment_id).then(() => {
this.loadReportData();
this.loading = false;
});
},
updateAttachmentContent(event, attachment) {
this.loading = true;
backend
.attachmentUpdate(attachment.attachment_id, event.target.innerText)
.then(() => {
this.loadReportData();
this.loading = false;
});
},
handleFileUpload(event) {
const files = event.target.files;
if (files) {
for (let i = 0; i < files.length; i++) {
this.selectedFiles.push(files[i]);
const reader = new FileReader();
reader.onload = () => {
this.selectedFilesContent[i] = reader.result;
};
reader.readAsDataURL(files[i]);
}
}
},
removeFile(index) {
this.selectedFiles.splice(index, 1);
this.selectedFilesContent.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";
}
},
isImageUrl(url) {
return /\.(jpg|jpeg|png|gif|svg|webp)$/.test(url);
}
},
};
</script>

51
frontend/vite.config.js Normal file
View File

@ -0,0 +1,51 @@
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";
const require = createRequire(import.meta.url);
const pkg = require("./package.json");
const subpath = "/bugreport/";
export const baseUrl = subpath + "webapp/dist/";
// https://vite.dev/config/
export default defineConfig(({ command, mode }) => {
const isBuild = command === "build";
const isDev = command === "serve";
return {
base: isBuild ? baseUrl : "/",
plugins: [vue()],
define: {
__APP_VERSION__: JSON.stringify(pkg.version),
__BUILD_DATE__: JSON.stringify(new Date().toISOString()),
__SUBPATH__: JSON.stringify(isBuild ? subpath : "/"),
__IS_BUILD__: JSON.stringify(isBuild),
__IS_DEV__: JSON.stringify(isDev),
},
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",
],
},
},
},
},
};
});