pridana implementacia backend s composerom a kniznicami TPsoft/APIlite a TPsoft/DBmodel
This commit is contained in:
67
frontend/src/App.vue
Normal file
67
frontend/src/App.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div id="header">
|
||||
<div class="logo">
|
||||
<router-link to="/">
|
||||
<img src="/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>
|
||||
611
frontend/src/assets/css/style.css
Normal file
611
frontend/src/assets/css/style.css
Normal 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;
|
||||
}
|
||||
} */
|
||||
4
frontend/src/assets/images/FlowDiagram.drawio.svg
Normal file
4
frontend/src/assets/images/FlowDiagram.drawio.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 208 KiB |
34
frontend/src/assets/images/SQLite370.svg
Normal file
34
frontend/src/assets/images/SQLite370.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.4 KiB |
BIN
frontend/src/assets/images/php-power-micro.png
Normal file
BIN
frontend/src/assets/images/php-power-micro.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 949 B |
1
frontend/src/assets/images/vue.svg
Normal file
1
frontend/src/assets/images/vue.svg
Normal 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
113
frontend/src/backend.js
Normal 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();
|
||||
40
frontend/src/components/FullScreenLoader.vue
Normal file
40
frontend/src/components/FullScreenLoader.vue
Normal 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>
|
||||
43
frontend/src/components/HelloWorld.vue
Normal file
43
frontend/src/components/HelloWorld.vue
Normal 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>
|
||||
23
frontend/src/components/MojTest1.vue
Normal file
23
frontend/src/components/MojTest1.vue
Normal 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>
|
||||
45
frontend/src/components/ReportBox.vue
Normal file
45
frontend/src/components/ReportBox.vue
Normal 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
5
frontend/src/events.js
Normal file
@ -0,0 +1,5 @@
|
||||
import mitt from "mitt";
|
||||
|
||||
const events = mitt();
|
||||
|
||||
export default events;
|
||||
17
frontend/src/main.js
Normal file
17
frontend/src/main.js
Normal 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
22
frontend/src/router.js
Normal 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,
|
||||
});
|
||||
74
frontend/src/views/API.vue
Normal file
74
frontend/src/views/API.vue
Normal 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']" />
|
||||
|
||||
Ziadne parametre
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="(param_desc, param_name) in action.params" :key="param_name">
|
||||
<strong>{{ param_name }}</strong>
|
||||
–
|
||||
{{ 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>
|
||||
57
frontend/src/views/About.vue
Normal file
57
frontend/src/views/About.vue
Normal 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 }} | 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>
|
||||
78
frontend/src/views/Archive.vue
Normal file
78
frontend/src/views/Archive.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<FullScreenLoader v-if="loading" />
|
||||
|
||||
<div id="archive">
|
||||
<h1>Archív vyriesenych BUG reportov</h1>
|
||||
|
||||
<div class="reports">
|
||||
<div
|
||||
class="report-row"
|
||||
v-for="report in reports"
|
||||
:key="report.report_id"
|
||||
@click="$router.push('/report/' + report.report_id)"
|
||||
>
|
||||
<div class="report-id">
|
||||
<font-awesome-icon :icon="['fas', 'hashtag']" />
|
||||
{{ report.report_id }}
|
||||
</div>
|
||||
<div class="title">{{ report.report_title }}</div>
|
||||
<div class="date">{{ report.created_dt }}</div>
|
||||
<div class="group">{{ report.report_group }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="loadmore">
|
||||
<button v-if="!loadedAll" @click="loadMoreReports"><font-awesome-icon :icon="['fas', 'arrow-down']" /> Nacitat dalsie</button>
|
||||
<div v-else><font-awesome-icon :icon="['fas', 'check']" /> Vsetky archivovane reporty su zobrazene</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, ref } from "vue";
|
||||
import backend from "../backend";
|
||||
import FullScreenLoader from "../components/FullScreenLoader.vue";
|
||||
|
||||
const reports = ref([]);
|
||||
const loading = ref(false);
|
||||
const page = ref(0);
|
||||
const loadedAll = ref(false);
|
||||
|
||||
function loadMoreReports() {
|
||||
loading.value = true;
|
||||
backend.getArchived(page.value).then((response) => {
|
||||
// console.log(data);
|
||||
if (response.data.length == 0) {
|
||||
loadedAll.value = true;
|
||||
} else {
|
||||
reports.value.push(...response.data);
|
||||
}
|
||||
page.value++;
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
let _in_scroll_handler = false;
|
||||
function handleScroll() {
|
||||
if (_in_scroll_handler || loading.value || loadedAll.value) return;
|
||||
_in_scroll_handler = true;
|
||||
const scrollY = window.scrollY;
|
||||
const windowHeight = window.innerHeight;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
if (scrollY + windowHeight >= documentHeight - 100) {
|
||||
loadMoreReports();
|
||||
}
|
||||
_in_scroll_handler = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMoreReports();
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
});
|
||||
|
||||
</script>
|
||||
146
frontend/src/views/BugAdd.vue
Normal file
146
frontend/src/views/BugAdd.vue
Normal 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>
|
||||
245
frontend/src/views/Dashboard.vue
Normal file
245
frontend/src/views/Dashboard.vue
Normal 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>
|
||||
315
frontend/src/views/Report.vue
Normal file
315
frontend/src/views/Report.vue
Normal 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>
|
||||
|
||||
{{ 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>
|
||||
Reference in New Issue
Block a user