Merge branch 'master' into feature-68298/Tempus_Grundstruktur

# Conflicts:
#	public/js/components/Cis/Renderer/Lehreinheit/calendarEvent.js
#	system/dbupdate_3.4.php
This commit is contained in:
ma0048
2026-03-30 09:18:31 +02:00
275 changed files with 22979 additions and 5325 deletions
+9
View File
@@ -0,0 +1,9 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^[0-9]{10}/(.*)$ $1 [L]
RewriteCond %{REQUEST_URI} ^(.*?)/public/index.ci.php/
RewriteRule ^index.ci.php/(.*)$ %1/index.ci.php/$1 [R=303]
</IfModule>
+6 -1
View File
@@ -407,7 +407,6 @@ html {
background-color: var(--fhc-background);
border-color: var(--fhc-border);
padding: var(--fhc-cis-main-py) var(--fhc-cis-main-px);
min-width: 0; /* fix flex-grow with tabulator exceeding width */
}
#cis-main .fa-arrow-up-right-from-square {
@@ -856,3 +855,9 @@ html {
background-color: var(--fhc-secondary);
}
.bordered-modal {
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 0.5rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}
+102 -6
View File
@@ -12,22 +12,22 @@ html {
font-size: .875em;
}
html.fs_xx-small {
font-size: .5em;
font-size: .625em;
}
html.fs_x-small {
font-size: .625em;
font-size: .6875em;
}
html.fs_small {
font-size: .75em;
}
html.fs_normal {
font-size: .875em;
font-size: .8125em;
}
html.fs_big {
font-size: 1em;
font-size: .875em;
}
html.fs_huge {
font-size: 1.125em;
font-size: 1em;
}
#appMenu {
@@ -74,6 +74,12 @@ html.fs_huge {
color: var(--gray-500);
}
/* Aufnahme Termine: background color green*/
.stv-details-admission-table .row-green{
background-color: lightgreen !important;
//color: var(--green-200);
}
/* Dropdown Toolbar Interessent, submenu */
.dropend .dropdown-toggle.d-flex::after {
height: 0;
@@ -116,6 +122,15 @@ html.fs_huge {
position: inherit;
z-index: 1;
}
.sidebar-collapsed #sidebarMenu {
display: none !important;
}
.sidebar-collapsed .container-fluid > .row > main {
flex: 0 0 100%;
max-width: 100%;
}
}
@@ -168,7 +183,7 @@ html.fs_huge {
}
.has-filter .fa-filter {
color: var(--bs-success);
color: var(--bs-danger);
}
.override_filtercmpt_actions_style div.d-flex.align-items-baseline {
align-items: end !important;
@@ -185,3 +200,84 @@ html.fs_huge {
.tiny-90 div.tox.tox-tinymce {
height: 90% !important;
}
/* slim begin */
.stv .form-label {
margin-bottom: .15rem;
font-weight: bold;
}
.stv .form-control,
.stv .form-select,
.stv .input-group-text {
padding-top: .15rem;
padding-bottom: .15rem;
}
:root {
--bs-body-line-height: 1.2;
}
.stv .tabulator-row .tabulator-cell,
.stv .tabulator-header-filter input {
padding-top: 1px !important;
padding-bottom: 1px !important;
}
.stv .tabulator-row {
min-height: 18px;
}
.stv .btn {
--bs-btn-padding-y: 0.25rem;
--bs-btn-line-height: 1.2;
}
.stv .p-button.p-button-icon-only {
padding-top: 0;
padding-bottom: 0;
}
.stv .p-tabview .p-tabview-nav li .p-tabview-nav-link {
padding: 0.25rem .5rem;
}
/*
.stv .p-tabview .p-tabview-panels {
background-color: #e5eff5;
}
.stv .p-tabview .p-tabview-nav li.p-highlight .p-tabview-nav-link {
background: #e5eff5;
border-color: #dee2e6 #dee2e6 #e5eff5 #dee2e6;
}
*/
.stv-details-details-foto img {
max-height: 120px;
}
.stv .tabulator-row .tabulator-frozen,
.stv .tabulator-row .tabulator-cell {
border-bottom: none;
}
/*
.stv .p-treetable .p-treetable-thead > tr > th,
.stv .p-treetable .p-treetable-tbody > tr {
background-color: #e5eff5;
}
.stv .p-treetable .p-treetable-tbody > tr.p-highlight {
background: #007bff;
color: #fff;
}
.stv .p-treetable.p-treetable-hoverable-rows .p-treetable-tbody > tr:not(.p-highlight):hover {
background: #fff;
color: #212529;
}
.tabulator-row.tabulator-row-even .tabulator-cell {
background-color: #e5eff5;
}
*/
/* slim ende */
+20
View File
@@ -0,0 +1,20 @@
@import './components/verticalsplit.css';
html {
font-size: .875em;
}
.vv{
display: flex;
flex-direction: column;
height: 100vh;
}
.vv> header {
flex: 0 0 auto;
}
.vv> div {
flex: 1 1 auto;
}
.vv {
margin-left: 0 !important;
}
+64
View File
@@ -0,0 +1,64 @@
.foto-container:hover .fotoedit {
opacity: 1 !important;
}
.bg-unruly {
background-color: #ad1010 !important;
color: white;
}
/*.fotosperre {
z-index: 1;
font-size: 1rem;
width: 1.25rem;
height: 1.25rem;
}*/
.foto-container .fotoedit {
opacity: 0;
transition: opacity 0.3s;
backdrop-filter: blur(2px);
}
.fotoedit {
background-color: rgba(0, 0, 0, 0.4);
color: white;
border: none;
z-index: 4;
font-size: 1rem;
width: 1.8rem;
height: 1.8rem;
opacity:0;
transition: opacity 0.2s;
top:20%;
}
/* fotoeditMa {
background-color: rgba(0, 0, 0, 0.4);
color: white;
border: none;
z-index: 4;
font-size: 1rem;
opacity:0;
transition: opacity 0.2s;
top:20%;
}*/
.buttonleft {
margin-left: .25rem;
}
.buttonright {
margin-right: .25rem;
}
/*.fotoEditMa {
z-index: 104;
font-size: 1rem;
width: 2.5rem;
height: 2.5rem;
opacity:0;
transition: opacity 0.2s;
top:13%;"
z-index: 104; font-size: 1rem; width: 2.5rem; height: 2.5rem; opacity:0; transition: opacity 0.2s; top:13%;"
}*/
+308
View File
@@ -0,0 +1,308 @@
/* Base Header */
.verpasst-header {
background-color: var(--fhc-red-70);
font-weight: 600;
border-radius: 6px;
padding: 0px 0px 0px 34px;
transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
/* Hover State */
.verpasst-header:hover {
background-color: var(--fhc-red-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Active / Expanded State */
.p-accordion-tab-active > .verpasst-header {
background-color: var(--fhc-red-50);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* Hover State Active*/
.p-accordion-tab-active > .verpasst-header:hover {
background-color: var(--fhc-red-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Base Header */
.abzugeben-header {
background-color: var(--fhc-yellow-70);
font-weight: 600;
border-radius: 6px;
padding: 0px 0px 0px 34px;
transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
/* Hover State */
.abzugeben-header:hover {
background-color: var(--fhc-yellow-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Active / Expanded State */
.p-accordion-tab-active > .abzugeben-header {
background-color: var(--fhc-yellow-50);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* Hover State Active*/
.p-accordion-tab-active > .abzugeben-header:hover {
background-color: var(--fhc-yellow-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Base Header */
.beurteilungerforderlich-header {
background-color: var(--fhc-orange-70);
font-weight: 600;
border-radius: 6px;
padding: 0px 0px 0px 34px;
transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
/* Hover State */
.beurteilungerforderlich-header:hover {
background-color: var(--fhc-orange-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Active / Expanded State */
.p-accordion-tab-active > .beurteilungerforderlich-header {
background-color: var(--fhc-orange-50);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* Hover State Active*/
.p-accordion-tab-active > .beurteilungerforderlich-header:hover {
background-color: var(--fhc-orange-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Base Header */
.verspaetet-header {
background-color: var(--fhc-pink-40);
font-weight: 600;
border-radius: 6px;
padding: 0px 0px 0px 34px;
transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
/* Hover State */
.verspaetet-header:hover {
background-color: var(--fhc-pink-20);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Active / Expanded State */
.p-accordion-tab-active > .verspaetet-header {
background-color: var(--fhc-pink-30);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* Hover State Active*/
.p-accordion-tab-active > .verspaetet-header:hover {
background-color: var(--fhc-pink-20);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Base Header */
.abgegeben-header {
background-color: var(--fhc-green-70);
font-weight: 600;
border-radius: 6px;
padding: 0px 0px 0px 34px;
transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
/* Hover State */
.abgegeben-header:hover {
background-color: var(--fhc-green-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Active / Expanded State */
.p-accordion-tab-active > .abgegeben-header {
background-color: var(--fhc-green-50);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* Hover State Active*/
.p-accordion-tab-active > .abgegeben-header:hover {
background-color: var(--fhc-green-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Base Header */
.bestanden-header {
background-color: var(--fhc-green-70);
font-weight: 600;
border-radius: 6px;
padding: 0px 0px 0px 34px;
transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
/* Hover State */
.bestanden-header:hover {
background-color: var(--fhc-green-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Active / Expanded State */
.p-accordion-tab-active > .bestanden-header {
background-color: var(--fhc-green-50);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* Hover State Active*/
.p-accordion-tab-active > .bestanden-header:hover {
background-color: var(--fhc-green-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Base Header */
.nichtbestanden-header {
background-color: var(--fhc-red-70);
font-weight: 600;
border-radius: 6px;
padding: 0px 0px 0px 34px;
transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
/* Hover State */
.nichtbestanden-header:hover {
background-color: var(--fhc-red-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Active / Expanded State */
.p-accordion-tab-active > .nichtbestanden-header {
background-color: var(--fhc-red-50);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* Hover State Active*/
.p-accordion-tab-active > .nichtbestanden-header:hover {
background-color: var(--fhc-red-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Base Header */
.erledigt-header {
background-color: var(--fhc-red-70);
font-weight: 600;
border-radius: 6px;
padding: 0px 0px 0px 34px;
transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
/* Hover State */
.erledigt-header:hover {
background-color: var(--fhc-red-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Active / Expanded State */
.p-accordion-tab-active > .erledigt-header {
background-color: var(--fhc-red-50);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* Hover State Active*/
.p-accordion-tab-active > .erledigt-header:hover {
background-color: var(--fhc-red-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Base Header */
.standard-header {
background-color: var(--fhc-white-70);
font-weight: 600;
border-radius: 6px;
padding: 0px 0px 0px 34px;
transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
/* Hover State */
.standard-header:hover {
background-color: var(--fhc-white-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Active / Expanded State */
.p-accordion-tab-active > .standard-header {
background-color: var(--fhc-white-50);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
/* Hover State Active*/
.p-accordion-tab-active > .standard-header:hover {
background-color: var(--fhc-white-60);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
#abgabetoolroot .modal-header{
background-color: var(--fhc-blue-primary);
color: var(--fhc-white-50);
}
#abgabetoolroot .modal-header .btn-close{
filter: invert(1);
}
#abgabetoolroot .modal-footer {
background-color: var(--fhc-white-20);
}
.bordered-modal {
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 0.5rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}
.bordered-modal .modal-body {
overflow-y: visible;
}
.p-accordion .p-accordion-header .p-accordion-header-link {
padding: 12px!important;
}
/* 1. Stick the Header */
#abgabetable .tabulator-header .tabulator-col.sticky-col {
position: sticky;
left: 0;
z-index: 10; /* Must be higher than other headers */
background-color: #fff; /* Opaque background is required */
border-right: 2px solid #ddd; /* Optional: Separator line */
}
/* 2. Stick the Data Cells */
#abgabetable .tabulator-tableholder .tabulator-row .tabulator-cell.sticky-col {
position: sticky;
left: 0;
z-index: 10; /* Ensure it floats above other cells */
background-color: #fff; /* Match your row background color */
border-right: 2px solid #ddd; /* Optional: Separator line */
}
/* 3. Fix for Hover Effects (Optional) */
/* If you use hover rows, you need to ensure the sticky cell matches the hover color */
#abgabetable .tabulator-row:hover .tabulator-cell.sticky-col {
background-color: #ccc; /* Match your existing hover color */
}
+3
View File
@@ -193,3 +193,6 @@
word-break: break-word;
}
.news-list-item p {
word-break: break-word;
}
+6
View File
@@ -3578,6 +3578,12 @@
transition: box-shadow 0.15s;
margin-right: 0.5rem;
}
.p-treetable .p-treetable-tbody > tr > td .p-treetable-toggler > svg,
.p-treetable .p-treetable-tbody > tr > td .p-treetable-toggler > svg * {
pointer-events: none;
}
.p-treetable .p-treetable-tbody > tr > td .p-treetable-toggler:enabled:hover {
color: #495057;
border-color: transparent;
+8
View File
@@ -51,6 +51,14 @@
background-color: #6d4c41;
}
.tag_dark_grey {
background-color: #595959;
}
.tag_light_grey {
background-color: #9a9a9a;
}
.tag_blau {
background-color: #508498;
}
+92 -3
View File
@@ -38,15 +38,104 @@
--fhc-blue-primary: #006095;
--fhc-blue-primary-highlight: #0086CB;
--fhc-red-10: #842029;
--fhc-red-20: #800000;
/* --- Green --- */
--fhc-green-5: rgb(240, 250, 240);
--fhc-green-10: rgb(220, 245, 220);
--fhc-green-20: rgb(190, 235, 190);
--fhc-green-30: rgb(150, 220, 150);
--fhc-green-40: rgb(110, 200, 110);
--fhc-green-50: rgb(70, 170, 70);
--fhc-green-60: rgb(50, 145, 50);
--fhc-green-70: rgb(35, 120, 35);
--fhc-green-80: rgb(25, 95, 25);
--fhc-green-90: rgb(15, 70, 15);
--fhc-green-10: #008000;
/* --- Red --- */
--fhc-red-5: rgb(255, 245, 246);
--fhc-red-10: rgb(255, 225, 228);
--fhc-red-20: rgb(250, 190, 195);
--fhc-red-30: rgb(240, 150, 160);
--fhc-red-40: rgb(225, 110, 120);
--fhc-red-50: rgb(200, 70, 85);
--fhc-red-60: rgb(170, 50, 65);
--fhc-red-70: rgb(140, 35, 50);
--fhc-red-80: rgb(110, 20, 35);
--fhc-red-90: rgb(85, 10, 25);
/* --- Yellow --- */
--fhc-yellow-5: rgb(255, 255, 240);
--fhc-yellow-10: rgb(255, 250, 210);
--fhc-yellow-20: rgb(255, 240, 160);
--fhc-yellow-30: rgb(255, 225, 100);
--fhc-yellow-40: rgb(250, 210, 50);
--fhc-yellow-50: rgb(240, 190, 0);
--fhc-yellow-60: rgb(220, 165, 0);
--fhc-yellow-70: rgb(190, 135, 0);
--fhc-yellow-80: rgb(160, 105, 0);
--fhc-yellow-90: rgb(120, 75, 0);
/* --- Pink --- */
--fhc-pink-5: rgb(255, 245, 250);
--fhc-pink-10: rgb(255, 225, 235);
--fhc-pink-20: rgb(250, 195, 215);
--fhc-pink-30: rgb(245, 160, 190);
--fhc-pink-40: rgb(235, 120, 160);
--fhc-pink-50: rgb(220, 80, 130);
--fhc-pink-60: rgb(190, 60, 110);
--fhc-pink-70: rgb(160, 40, 90);
--fhc-pink-80: rgb(130, 25, 70);
--fhc-pink-90: rgb(100, 15, 50);
/* --- Orange --- */
--fhc-orange-5: rgb(255, 250, 240);
--fhc-orange-10: rgb(255, 235, 200);
--fhc-orange-20: rgb(255, 210, 140);
--fhc-orange-30: rgb(255, 185, 80);
--fhc-orange-40: rgb(255, 155, 40);
--fhc-orange-50: rgb(255, 128, 0);
--fhc-orange-60: rgb(230, 110, 0);
--fhc-orange-70: rgb(200, 90, 0);
--fhc-orange-80: rgb(170, 70, 0);
--fhc-orange-90: rgb(130, 50, 0);
--fhc-beige-10: rgba(245, 233, 215, 0.5);
--fhc-beige-20: rgba(172, 153, 125, 0.5);
/* --- Purple --- */
--fhc-purple-5: rgb(250, 245, 255);
--fhc-purple-10: rgb(240, 230, 255);
--fhc-purple-20: rgb(220, 200, 250);
--fhc-purple-30: rgb(190, 160, 245);
--fhc-purple-40: rgb(160, 120, 235);
--fhc-purple-50: rgb(130, 80, 220);
--fhc-purple-60: rgb(110, 60, 190);
--fhc-purple-70: rgb(90, 40, 160);
--fhc-purple-80: rgb(70, 25, 130);
--fhc-purple-90: rgb(50, 15, 100);
/* --- Teal --- */
--fhc-teal-5: rgb(240, 252, 252);
--fhc-teal-10: rgb(220, 245, 245);
--fhc-teal-20: rgb(180, 235, 235);
--fhc-teal-30: rgb(130, 220, 220);
--fhc-teal-40: rgb(80, 200, 200);
--fhc-teal-50: rgb(30, 170, 170);
--fhc-teal-60: rgb(20, 140, 140);
--fhc-teal-70: rgb(15, 115, 115);
--fhc-teal-80: rgb(10, 90, 90);
--fhc-teal-90: rgb(5, 65, 65);
/* --- Indigo --- */
--fhc-indigo-5: rgb(245, 247, 255);
--fhc-indigo-10: rgb(230, 235, 255);
--fhc-indigo-20: rgb(200, 210, 250);
--fhc-indigo-30: rgb(160, 175, 245);
--fhc-indigo-40: rgb(120, 140, 235);
--fhc-indigo-50: rgb(80, 100, 220);
--fhc-indigo-60: rgb(60, 80, 190);
--fhc-indigo-70: rgb(45, 60, 160);
--fhc-indigo-80: rgb(30, 40, 130);
--fhc-indigo-90: rgb(20, 25, 100);
}
+50
View File
@@ -0,0 +1,50 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const FhcApps = (function() {
// Logic to extend app here (like adding components, plugins, ...)
function makeExtendable(app) {
// apply extensions here
return app;
}
return {
makeExtendable
};
})();
FhcApps.router = (() => {
const extraRoutes = [];
function addRoute(...route) {
extraRoutes.push(route);
}
function makeExtendable(router) {
while (extraRoutes.length) {
router.addRoute(...extraRoutes.shift());
}
router.replace(router.currentRoute.value.fullPath);
}
return {
addRoute,
makeExtendable
};
})();
+144
View File
@@ -0,0 +1,144 @@
export default {
getConfig() {
return {
method: 'get',
url: '/api/frontend/v1/Abgabe/getConfig'
};
},
getConfigStudent() {
return {
method: 'get',
url: '/api/frontend/v1/Abgabe/getConfigStudent'
};
},
getStudentProjektarbeiten(uid) {
return {
method: 'get',
url: '/api/frontend/v1/Abgabe/getStudentProjektarbeiten',
params: { uid }
};
},
getStudentProjektabgaben(detail) {
return {
method: 'get',
url: '/api/frontend/v1/Abgabe/getStudentProjektabgaben',
params: { projektarbeit_id: detail.projektarbeit_id, student_uid: detail.student_uid }
};
},
postStudentProjektarbeitEndupload(formData) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/postStudentProjektarbeitEndupload',
params: formData,
config: {Headers: { "Content-Type": "multipart/form-data" }}
};
},
postStudentProjektarbeitZwischenabgabe(formData) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/postStudentProjektarbeitZwischenabgabe',
params: formData,
config: {Headers: { "Content-Type": "multipart/form-data" }}
};
},
getMitarbeiterProjektarbeiten(all) {
return {
method: 'get',
url: '/api/frontend/v1/Abgabe/getMitarbeiterProjektarbeiten',
params: { showall: all }
};
},
postProjektarbeitAbgabe(termin) {
let dateString = termin.datum
if(termin.datum instanceof Date) {
const year = termin.datum.getFullYear();
const month = String(termin.datum.getMonth() + 1).padStart(2, '0');
const day = String(termin.datum.getDate()).padStart(2, '0');
dateString = `${year}-${month}-${day}`
}
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/postProjektarbeitAbgabe',
params: {
paabgabe_id: termin.paabgabe_id,
paabgabetyp_kurzbz: termin.bezeichnung.paabgabetyp_kurzbz,
datum: dateString,
note: termin.note_pk,
upload_allowed: !!termin.upload_allowed,
beurteilungsnotiz: termin.beurteilungsnotiz ?? '',
fixtermin: termin.fixtermin,
insertvon: termin.insertvon,
kurzbz: termin.kurzbz,
projektarbeit_id: termin.projektarbeit_id,
betreuer_person_id: termin.betreuer_person_id
}
};
},
deleteProjektarbeitAbgabe(paabgabe_id) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/deleteProjektarbeitAbgabe',
params: { paabgabe_id }
};
},
postSerientermin(datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, upload_allowed, projektarbeit_ids, fixtermin) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/postSerientermin',
params: { datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, upload_allowed, projektarbeit_ids, fixtermin }
};
},
fetchDeadlines(person_id) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/fetchDeadlines',
params: { person_id }
};
},
getPaAbgabetypen() {
return {
method: 'get',
url: '/api/frontend/v1/Abgabe/getPaAbgabetypen'
};
},
//TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API
getNoten(){
return {
method: 'get',
url: '/api/frontend/v1/Abgabe/getNoten'
};
},
getProjektarbeitenForStudiengang(studiengang_kz, benotet = 0) {
return {
method: 'get',
url: '/api/frontend/v1/Abgabe/getProjektarbeitenForStudiengang',
params: { studiengang_kz, benotet }
};
},
// TODO: this could also very well be generic info api
getStudiengaenge() {
return {
method: 'get',
url: '/api/frontend/v1/Abgabe/getStudiengaenge'
};
},
postStudentProjektarbeitZusatzdaten(formData) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/postStudentProjektarbeitZusatzdaten',
params: formData,
config: {Headers: { "Content-Type": "multipart/form-data" }}
};
},
getSignaturStatusForProjektarbeitAbgaben(paabgabe_ids, student_uid) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/getSignaturStatusForProjektarbeitAbgaben',
params: {paabgabe_ids, student_uid},
};
}
};
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright (C) 2026 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
list() {
return {
method: 'get',
url: 'api/frontend/v1/dashboard/board/list'
};
},
add(params) {
return {
method: 'post',
url: 'api/frontend/v1/dashboard/board/create',
params
};
},
update(params) {
return {
method: 'post',
url: 'api/frontend/v1/dashboard/board/update',
params
};
},
delete(dashboard_id) {
return {
method: 'post',
url: 'api/frontend/v1/dashboard/board/delete',
params: { dashboard_id }
};
}
}
+47
View File
@@ -0,0 +1,47 @@
/**
* Copyright (C) 2026 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
list(dashboard_kurzbz) {
return {
method: 'get',
url: 'api/frontend/v1/dashboard/preset/list/'
+ encodeURIComponent(dashboard_kurzbz)
};
},
getBatch(params) {
return {
method: 'post',
url: 'api/frontend/v1/dashboard/preset/getBatch',
params
};
},
addWidget(params) {
return {
method: 'post',
url: 'api/frontend/v1/dashboard/preset/addWidget',
params
};
},
removeWidget(params) {
return {
method: 'post',
url: 'api/frontend/v1/dashboard/preset/removeWidget',
params
};
}
};
+45
View File
@@ -0,0 +1,45 @@
/**
* Copyright (C) 2026 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
get(dashboard) {
return {
method: 'get',
url: '/api/frontend/v1/dashboard/user/get/' + dashboard
};
},
addWidget(dashboard, widget) {
return {
method: 'post',
url: '/api/frontend/v1/dashboard/user/addWidget',
params: {
dashboard,
widget
}
};
},
removeWidget(dashboard, widget) {
return {
method: 'post',
url: '/api/frontend/v1/dashboard/user/removeWidget',
params: {
dashboard,
widget
}
};
}
};
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright (C) 2026 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
get(widget) {
return {
method: 'get',
url: '/api/frontend/v1/dashboard/widget/get/' + widget
};
},
list(dashboard) {
return {
method: 'get',
url: '/api/frontend/v1/dashboard/widget/list/' + dashboard
};
},
listAllowed(dashboard) {
return {
method: 'get',
url: '/api/frontend/v1/dashboard/widget/listAllowed/' + dashboard
};
},
setAllowed(dashboard_id, widget_id, allowed) {
return {
method: 'post',
url: '/api/frontend/v1/dashboard/widget/setAllowed',
params: {
dashboard_id, widget_id, allowed
}
};
}
};
+3 -3
View File
@@ -19,19 +19,19 @@ export default {
getHeader(person_id){
return {
method: 'get',
url: 'api/frontend/v1/vertraege/vertraege/getHeader/' + person_id,
url: 'api/frontend/v1/detailheader/detailheader/getHeader/' + person_id,
};
},
getPersonAbteilung(mitarbeiter_uid){
return {
method: 'get',
url: 'api/frontend/v1/vertraege/vertraege/getPersonAbteilung/' + mitarbeiter_uid,
url: 'api/frontend/v1/detailheader/detailheader/getPersonAbteilung/' + mitarbeiter_uid,
};
},
getLeitungOrg(oekurzbz){
return {
method: 'get',
url: 'api/frontend/v1/vertraege/vertraege/getLeitungOrg/' + oekurzbz,
url: 'api/frontend/v1/detailheader/detailheader/getLeitungOrg/' + oekurzbz,
};
},
}
+32
View File
@@ -0,0 +1,32 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
uploadFoto(person_id, params) {
return {
method: 'post',
url: 'api/frontend/v1/fotoHandling/Foto/uploadFoto/' + person_id,
params
};
},
deleteFoto(person_id){
return {
method: 'post',
url: 'api/frontend/v1/fotoHandling/Foto/deleteFoto/' + person_id
};
}
}
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Person from './person.js';
export default {
...Person,
getNotizen(id, type) {
return {
method: 'get',
url: 'api/frontend/v1/notiz/notizAnrechnung/getNotizen/' + encodeURIComponent(id) + '/' + encodeURIComponent(type)
};
},
};
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Person from './person.js';
export default {
...Person,
getNotizen(id, type) {
return {
method: 'get',
url: 'api/frontend/v1/notiz/notizBestellung/getNotizen/' + encodeURIComponent(id) + '/' + encodeURIComponent(type)
};
},
};
@@ -26,4 +26,29 @@ export default {
url: 'api/frontend/v1/notiz/notizLehreinheit/getNotizen/' + encodeURIComponent(id) + '/' + encodeURIComponent(type)
};
},
addNewNotiz(lehreinheit_id, params) {
return {
method: 'post',
url: 'api/frontend/v1/notiz/notizLehreinheit/addNewNotiz/' + lehreinheit_id,
params
};
},
updateNotiz(notiz_id, params) {
return {
method: 'post',
url: 'api/frontend/v1/notiz/notizLehreinheit/updateNotiz/' + notiz_id,
params
};
},
deleteNotiz(notiz_id, type_id, id) {
return {
method: 'post',
url: 'api/frontend/v1/notiz/notizLehreinheit/deleteNotiz/',
params: {
notiz_id,
type_id,
id
}
};
},
};
@@ -0,0 +1,29 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Person from './person.js';
export default {
...Person,
getNotizen(id, type) {
return {
method: 'get',
url: 'api/frontend/v1/notiz/notizMitarbeiter/getNotizen/' + encodeURIComponent(id) + '/' + encodeURIComponent(type)
};
},
};
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Person from './person.js';
export default {
...Person,
getNotizen(id, type) {
return {
method: 'get',
url: 'api/frontend/v1/notiz/notizPrestudent/getNotizen/' + encodeURIComponent(id) + '/' + encodeURIComponent(type)
};
},
};
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Person from './person.js';
export default {
...Person,
getNotizen(id, type) {
return {
method: 'get',
url: 'api/frontend/v1/notiz/notizProjekt/getNotizen/' + encodeURIComponent(id) + '/' + encodeURIComponent(type)
};
},
};
@@ -0,0 +1,29 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Person from './person.js';
export default {
...Person,
getNotizen(id, type) {
return {
method: 'get',
url: 'api/frontend/v1/notiz/notizProjektphase/getNotizen/' + encodeURIComponent(id) + '/' + encodeURIComponent(type)
};
},
};
@@ -0,0 +1,29 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Person from './person.js';
export default {
...Person,
getNotizen(id, type) {
return {
method: 'get',
url: 'api/frontend/v1/notiz/notizProjekttask/getNotizen/' + encodeURIComponent(id) + '/' + encodeURIComponent(type)
};
},
};
+24 -2
View File
@@ -1,5 +1,27 @@
export default {
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getAllStudiensemesterAndAktOrNext() {
return {
method: 'get',
url: '/api/frontend/v1/organisation/Studiensemester/getAllStudiensemesterAndAktOrNext',
};
},
getAll(order = null, start = null)
{
return {
@@ -8,4 +30,4 @@ export default {
params: { order, start }
};
}
}
};
+54
View File
@@ -0,0 +1,54 @@
export default {
getTag(data)
{
return {
method: 'get',
url: 'api/frontend/v1/stv/Tags/getTag',
params: data
};
},
getTags(data)
{
return {
method: 'get',
url: 'api/frontend/v1/stv/Tags/getTags'
};
},
addTag(data)
{
return {
method: 'post',
url: 'api/frontend/v1/stv/Tags/addTag',
params: data
};
},
updateTag(data)
{
return {
method: 'post',
url: 'api/frontend/v1/stv/Tags/updateTag',
params: data
};
},
doneTag(data)
{
return {
method: 'post',
url: 'api/frontend/v1/stv/Tags/doneTag',
params: data
};
},
deleteTag(data)
{
return {
method: 'post',
url: 'api/frontend/v1/stv/Tags/deleteTag',
params: data
};
},
};
@@ -0,0 +1,136 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getAllVertraege(person_id) {
return {
method: 'get',
url: 'api/frontend/v1/vertraege/Vertraege/getAllVertraege/' + person_id
};
},
getAllContractsNotAssigned(person_id) {
return {
method: 'get',
url: 'api/frontend/v1/vertraege/Vertraege/getAllContractsNotAssigned/' + person_id
};
},
getAllContractsAssigned(person_id, vertrag_id) {
return {
method: 'get',
url: 'api/frontend/v1/vertraege/Vertraege/getAllContractsAssigned/' + person_id + '/' + vertrag_id + ''
};
},
getAllContractTypes() {
return {
method: 'get',
url: 'api/frontend/v1/vertraege/Vertraege/getAllContractTypes/'
};
},
getStatiOfContract(person_id, vertrag_id){
return {
method: 'get',
url: 'api/frontend/v1/vertraege/Vertraege/getStatiOfContract/' + person_id + '/' + vertrag_id
};
},
configPrintDocument() {
return {
method: 'get',
url: 'api/frontend/v1/vertraege/Config/printDocument/'
};
},
getAllContractStati() {
return {
method: 'get',
url: 'api/frontend/v1/vertraege/vertraege/getAllContractStati/'
};
},
deleteContract(vertrag_id) {
return {
method: 'post',
url: 'api/frontend/v1/vertraege/vertraege/deleteContract/' + vertrag_id
};
},
addNewContract(params) {
return {
method: 'post',
url: 'api/frontend/v1/vertraege/vertraege/addNewContract/',
params
};
},
loadContract(vertrag_id){
return {
method: 'get',
url: 'api/frontend/v1/vertraege/vertraege/loadContract/' + vertrag_id
};
},
updateContract(params) {
return {
method: 'post',
url: 'api/frontend/v1/vertraege/vertraege/updateContract/',
params
};
},
loadContractStatus(params){
return {
method: 'get',
url: 'api/frontend/v1/vertraege/vertraege/loadContractStatus/',
params
};
},
insertContractStatus(params) {
return {
method: 'post',
url: 'api/frontend/v1/vertraege/vertraege/insertContractStatus/',
params
};
},
updateContractStatus(params) {
return {
method: 'post',
url: 'api/frontend/v1/vertraege/vertraege/updateContractStatus/',
params
};
},
deleteContractStatus(params) {
return {
method: 'post',
url: 'api/frontend/v1/vertraege/vertraege/deleteContractStatus/',
params
};
},
deleteLehrauftrag(params) {
return {
method: 'post',
url: 'api/frontend/v1/vertraege/vertraege/deleteLehrauftrag/',
params
};
},
deleteBetreuung(params) {
return {
method: 'post',
url: 'api/frontend/v1/vertraege/vertraege/deleteBetreuung/',
params
};
},
//loaded by mitarbeiter_header_js
getMitarbeiter(){
return {
method: 'get',
url: 'api/frontend/v1/vertraege/vertraege/getMitarbeiter/',
};
},
};
+3 -1
View File
@@ -39,6 +39,7 @@ import studiengang from "./studiengang.js";
import menu from "./menu.js";
import dashboard from "./dashboard.js";
import authinfo from "./authinfo.js";
import vertraege from "./vertraege.js";
import studium from "./studium.js";
import language from "./language.js";
@@ -68,6 +69,7 @@ export default {
studiengang,
menu,
authinfo,
vertraege,
studium,
language
language
};
-75
View File
@@ -18,80 +18,5 @@ export default {
`/api/frontend/v1/Lehre/Pruefungen/${lehrveranstaltung_id}`
, {}
);
},
getStudentProjektarbeiten(uid) {
return this.$fhcApi.get(
`/api/frontend/v1/Lehre/getStudentProjektarbeiten/${uid}`
, {}
);
},
getStudentProjektabgaben(detail) {
return this.$fhcApi.get(
`/api/frontend/v1/Lehre/getStudentProjektabgaben`
, {
projektarbeit_id: detail.projektarbeit_id,
student_uid: detail.student_uid
}
);
},
postStudentProjektarbeitEndupload(formData) {
const url = '/api/frontend/v1/Lehre/postStudentProjektarbeitEndupload';
const headers = {Headers: { "Content-Type": "multipart/form-data" }}
return this.$fhcApi.post(url, formData, headers)
},
postStudentProjektarbeitZwischenabgabe(formData) {
const url = '/api/frontend/v1/Lehre/postStudentProjektarbeitZwischenabgabe';
const headers = {Headers: { "Content-Type": "multipart/form-data" }}
return this.$fhcApi.post(url, formData, headers)
},
getStudentProjektarbeitAbgabeFile(paabgabe_id, student_uid) {
const url = `/Cis/Abgabetool/getStudentProjektarbeitAbgabeFile?paabgabe_id=${paabgabe_id}&student_uid=${student_uid}`;
window.location = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url
},
getMitarbeiterProjektarbeiten(uid, all) {
return this.$fhcApi.get(
`/api/frontend/v1/Lehre/getMitarbeiterProjektarbeiten?showall=${all}`
, {}
);
},
postProjektarbeitAbgabe(termin) {
const payload = {
paabgabe_id: termin.paabgabe_id,
paabgabetyp_kurzbz: termin.bezeichnung.paabgabetyp_kurzbz,
datum: termin.datum,
fixtermin: termin.fixtermin,
insertvon: termin.insertvon,
kurzbz: termin.kurzbz,
projektarbeit_id: termin.projektarbeit_id
}
const url = '/api/frontend/v1/Lehre/postProjektarbeitAbgabe';
return this.$fhcApi.post(url, payload, null)
},
deleteProjektarbeitAbgabe(paabgabe_id) {
const payload = {
paabgabe_id
}
const url = '/api/frontend/v1/Lehre/deleteProjektarbeitAbgabe';
return this.$fhcApi.post(url, payload, null)
},
postSerientermin(datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, projektarbeit_ids) {
const payload = {
datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, projektarbeit_ids
}
const url = '/api/frontend/v1/Lehre/postSerientermin';
return this.$fhcApi.post(url, payload, null)
},
fetchDeadlines(person_id) {
const payload = {
person_id
}
const url = '/api/frontend/v1/Lehre/fetchDeadlines';
return this.$fhcApi.post(url, payload, null)
}
}
+19 -2
View File
@@ -1,5 +1,22 @@
import person from "./notiz/person.js";
import prestudent from "./notiz/prestudent.js";
import mitarbeiter from "./notiz/mitarbeiter.js";
import projekt from "./notiz/projekt.js";
import anrechnung from "./notiz/anrechnung.js";
import bestellung from "./notiz/bestellung.js";
import lehreinheit from "./notiz/lehreinheit.js";
import projektphase from "./notiz/projektphase.js";
import projekttask from "./notiz/projekttask.js";
export default {
person
}
person,
prestudent,
mitarbeiter,
anrechnung,
bestellung,
lehreinheit,
projekt,
projektphase,
projekttask,
}
+38
View File
@@ -0,0 +1,38 @@
export default {
getNotizen(url, config, params){
return this.$fhcApi.get('api/frontend/v1/notiz/notizAnrechnung/getNotizen/' + params.id + '/' + params.type);
},
getUid(){
return this.$fhcApi.get('api/frontend/v1/notiz/notizAnrechnung/getUid/');
},
addNewNotiz(id, formData) {
return this.$fhcApi.post('api/frontend/v1/notiz/notizAnrechnung/addNewNotiz/' + id,
formData
);
},
loadNotiz(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizAnrechnung/loadNotiz/', {
notiz_id
});
},
loadDokumente(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizAnrechnung/loadDokumente/', {
notiz_id
});
},
deleteNotiz(notiz_id, type_id, id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizAnrechnung/deleteNotiz/', {
notiz_id,
type_id,
id
});
},
updateNotiz(notiz_id, formData){
return this.$fhcApi.post('api/frontend/v1/notiz/notizAnrechnung/updateNotiz/' + notiz_id,
formData
);
},
getMitarbeiter(event){
return this.$fhcApi.get('api/frontend/v1/notiz/notizAnrechnung/getMitarbeiter/' + event);
}
}
+38
View File
@@ -0,0 +1,38 @@
export default {
getNotizen(url, config, params){
return this.$fhcApi.get('api/frontend/v1/notiz/notizBestellung/getNotizen/' + params.id + '/' + params.type);
},
getUid(){
return this.$fhcApi.get('api/frontend/v1/notiz/notizBestellung/getUid/');
},
addNewNotiz(id, formData) {
return this.$fhcApi.post('api/frontend/v1/notiz/notizBestellung/addNewNotiz/' + id,
formData
);
},
loadNotiz(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizBestellung/loadNotiz/', {
notiz_id
});
},
loadDokumente(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizBestellung/loadDokumente/', {
notiz_id
});
},
deleteNotiz(notiz_id, type_id, id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizBestellung/deleteNotiz/', {
notiz_id,
type_id,
id
});
},
updateNotiz(notiz_id, formData){
return this.$fhcApi.post('api/frontend/v1/notiz/notizBestellung/updateNotiz/' + notiz_id,
formData
);
},
getMitarbeiter(event){
return this.$fhcApi.get('api/frontend/v1/notiz/notizBestellung/getMitarbeiter/' + event);
}
}
+38
View File
@@ -0,0 +1,38 @@
export default {
getNotizen(url, config, params){
return this.$fhcApi.get('api/frontend/v1/notiz/notizLehreinheit/getNotizen/' + params.id + '/' + params.type);
},
getUid(){
return this.$fhcApi.get('api/frontend/v1/notiz/notizLehreinheit/getUid/');
},
addNewNotiz(id, formData) {
return this.$fhcApi.post('api/frontend/v1/notiz/notizLehreinheit/addNewNotiz/' + id,
formData
);
},
loadNotiz(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizLehreinheit/loadNotiz/', {
notiz_id
});
},
loadDokumente(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizLehreinheit/loadDokumente/', {
notiz_id
});
},
deleteNotiz(notiz_id, type_id, id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizLehreinheit/deleteNotiz/', {
notiz_id,
type_id,
id
});
},
updateNotiz(notiz_id, formData){
return this.$fhcApi.post('api/frontend/v1/notiz/notizLehreinheit/updateNotiz/' + notiz_id,
formData
);
},
getMitarbeiter(event){
return this.$fhcApi.get('api/frontend/v1/notiz/notizLehreinheit/getMitarbeiter/' + event);
}
}
+38
View File
@@ -0,0 +1,38 @@
export default {
getNotizen(url, config, params){
return this.$fhcApi.get('api/frontend/v1/notiz/notizMitarbeiter/getNotizen/' + params.id + '/' + params.type);
},
getUid(){
return this.$fhcApi.get('api/frontend/v1/notiz/notizMitarbeiter/getUid/');
},
addNewNotiz(id, formData) {
return this.$fhcApi.post('api/frontend/v1/notiz/notizMitarbeiter/addNewNotiz/' + id,
formData
);
},
loadNotiz(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizMitarbeiter/loadNotiz/', {
notiz_id
});
},
loadDokumente(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizMitarbeiter/loadDokumente/', {
notiz_id
});
},
deleteNotiz(notiz_id, type_id, id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizMitarbeiter/deleteNotiz/', {
notiz_id,
type_id,
id
});
},
updateNotiz(notiz_id, formData){
return this.$fhcApi.post('api/frontend/v1/notiz/notizMitarbeiter/updateNotiz/' + notiz_id,
formData
);
},
getMitarbeiter(event){
return this.$fhcApi.get('api/frontend/v1/notiz/notizMitarbeiter/getMitarbeiter/' + event);
}
}
+1 -1
View File
@@ -38,4 +38,4 @@ export default {
isBerechtigt(id, type_id){
return this.$fhcApi.get('api/frontend/v1/notiz/notizPerson/isBerechtigt/');
}
}
}
+38
View File
@@ -0,0 +1,38 @@
export default {
getNotizen(url, config, params){
return this.$fhcApi.get('api/frontend/v1/notiz/notizPrestudent/getNotizen/' + params.id + '/' + params.type);
},
getUid(){
return this.$fhcApi.get('api/frontend/v1/notiz/notizPrestudent/getUid/');
},
addNewNotiz(id, formData) {
return this.$fhcApi.post('api/frontend/v1/notiz/notizPrestudent/addNewNotiz/' + id,
formData
);
},
loadNotiz(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizPrestudent/loadNotiz/', {
notiz_id
});
},
loadDokumente(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizPrestudent/loadDokumente/', {
notiz_id
});
},
deleteNotiz(notiz_id, type_id, id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizPrestudent/deleteNotiz/', {
notiz_id,
type_id,
id
});
},
updateNotiz(notiz_id, formData){
return this.$fhcApi.post('api/frontend/v1/notiz/notizPrestudent/updateNotiz/' + notiz_id,
formData
);
},
getMitarbeiter(event){
return this.$fhcApi.get('api/frontend/v1/notiz/notizPrestudent/getMitarbeiter/' + event);
}
}
+38
View File
@@ -0,0 +1,38 @@
export default {
getNotizen(url, config, params){
return this.$fhcApi.get('api/frontend/v1/notiz/NotizProjekt/getNotizen/' + params.id + '/' + params.type);
},
getUid(){
return this.$fhcApi.get('api/frontend/v1/notiz/NotizProjekt/getUid/');
},
addNewNotiz(id, formData) {
return this.$fhcApi.post('api/frontend/v1/notiz/NotizProjekt/addNewNotiz/' + id,
formData
);
},
loadNotiz(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/NotizProjekt/loadNotiz/', {
notiz_id
});
},
loadDokumente(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/NotizProjekt/loadDokumente/', {
notiz_id
});
},
deleteNotiz(notiz_id, type_id, id){
return this.$fhcApi.post('api/frontend/v1/notiz/NotizProjekt/deleteNotiz/', {
notiz_id,
type_id,
id
});
},
updateNotiz(notiz_id, formData){
return this.$fhcApi.post('api/frontend/v1/notiz/NotizProjekt/updateNotiz/' + notiz_id,
formData
);
},
getMitarbeiter(event){
return this.$fhcApi.get('api/frontend/v1/notiz/NotizProjekt/getMitarbeiter/' + event);
}
}
+38
View File
@@ -0,0 +1,38 @@
export default {
getNotizen(url, config, params){
return this.$fhcApi.get('api/frontend/v1/notiz/notizProjektphase/getNotizen/' + params.id + '/' + params.type);
},
getUid(){
return this.$fhcApi.get('api/frontend/v1/notiz/notizProjektphase/getUid/');
},
addNewNotiz(id, formData) {
return this.$fhcApi.post('api/frontend/v1/notiz/notizProjektphase/addNewNotiz/' + id,
formData
);
},
loadNotiz(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizProjektphase/loadNotiz/', {
notiz_id
});
},
loadDokumente(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizProjektphase/loadDokumente/', {
notiz_id
});
},
deleteNotiz(notiz_id, type_id, id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizProjektphase/deleteNotiz/', {
notiz_id,
type_id,
id
});
},
updateNotiz(notiz_id, formData){
return this.$fhcApi.post('api/frontend/v1/notiz/notizProjektphase/updateNotiz/' + notiz_id,
formData
);
},
getMitarbeiter(event){
return this.$fhcApi.get('api/frontend/v1/notiz/notizProjektphase/getMitarbeiter/' + event);
}
}
+38
View File
@@ -0,0 +1,38 @@
export default {
getNotizen(url, config, params){
return this.$fhcApi.get('api/frontend/v1/notiz/notizProjekttask/getNotizen/' + params.id + '/' + params.type);
},
getUid(){
return this.$fhcApi.get('api/frontend/v1/notiz/notizProjekttask/getUid/');
},
addNewNotiz(id, formData) {
return this.$fhcApi.post('api/frontend/v1/notiz/notizProjekttask/addNewNotiz/' + id,
formData
);
},
loadNotiz(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizProjekttask/loadNotiz/', {
notiz_id
});
},
loadDokumente(notiz_id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizProjekttask/loadDokumente/', {
notiz_id
});
},
deleteNotiz(notiz_id, type_id, id){
return this.$fhcApi.post('api/frontend/v1/notiz/notizProjekttask/deleteNotiz/', {
notiz_id,
type_id,
id
});
},
updateNotiz(notiz_id, formData){
return this.$fhcApi.post('api/frontend/v1/notiz/notizProjekttask/updateNotiz/' + notiz_id,
formData
);
},
getMitarbeiter(event){
return this.$fhcApi.get('api/frontend/v1/notiz/notizProjekttask/getMitarbeiter/' + event);
}
}
+8
View File
@@ -0,0 +1,8 @@
import person from "./vertraege/person.js";
export default {
person,
configPrintDocument() {
return this.$fhcApi.get('api/frontend/v1/vertraege/config/printDocument');
}
}
+69
View File
@@ -0,0 +1,69 @@
export default {
getAllVertraege(url, config, params){
return this.$fhcApi.get('api/frontend/v1/vertraege/vertraege/getAllVertraege/' + params.person_id);
},
getAllContractsNotAssigned(url, config, params){
return this.$fhcApi.get('api/frontend/v1/vertraege/vertraege/getAllContractsNotAssigned/' + params.person_id);
},
getAllContractsAssigned(url, config, params){
return this.$fhcApi.get('api/frontend/v1/vertraege/vertraege/getAllContractsAssigned/' + params.person_id + '/' + params.vertrag_id);
},
getAllContractsNotAssigned2(person_id){
return this.$fhcApi.get('api/frontend/v1/vertraege/vertraege/getAllContractsNotAssigned/' + person_id);
},
getStatiOfContract(url, config, params){
return this.$fhcApi.get('api/frontend/v1/vertraege/vertraege/getStatiOfContract/' + params.vertrag_id);
},
getAllContractTypes(){
return this.$fhcApi.get('api/frontend/v1/vertraege/vertraege/getAllContractTypes/');
},
getAllContractStati(){
return this.$fhcApi.get('api/frontend/v1/vertraege/vertraege/getAllContractStati/');
},
addNewContract(form, data) {
return this.$fhcApi.post(form,'api/frontend/v1/vertraege/vertraege/addNewContract/', data);
},
loadContract(vertrag_id){
return this.$fhcApi.post('api/frontend/v1/vertraege/vertraege/loadContract/' + vertrag_id);
},
updateContract(form, data) {
return this.$fhcApi.post(form,'api/frontend/v1/vertraege/vertraege/updateContract/', data);
},
deleteContract(vertrag_id){
return this.$fhcApi.post('api/frontend/v1/vertraege/vertraege/deleteContract/' + vertrag_id);
},
loadContractStatus(params){
return this.$fhcApi.post('api/frontend/v1/vertraege/vertraege/loadContractStatus/' + params.vertrag_id, params);
},
insertContractStatus(form, params) {
return this.$fhcApi.post(form,'api/frontend/v1/vertraege/vertraege/insertContractStatus/' + params.vertrag_id, params);
},
updateContractStatus(form, params) {
return this.$fhcApi.post(form,'api/frontend/v1/vertraege/vertraege/updateContractStatus/' + params.vertrag_id, params);
},
deleteContractStatus(params) {
return this.$fhcApi.post('api/frontend/v1/vertraege/vertraege/deleteContractStatus/' + params.vertrag_id, params);
},
deleteLehrauftrag(params) {
return this.$fhcApi.post('api/frontend/v1/vertraege/vertraege/deleteLehrauftrag/' + params.vertrag_id, params);
},
deleteBetreuung(params) {
return this.$fhcApi.post('api/frontend/v1/vertraege/vertraege/deleteBetreuung/' + params.vertrag_id, params);
},
getMitarbeiter(params){
return this.$fhcApi.post('api/frontend/v1/vertraege/vertraege/getMitarbeiter/');
},
getHeader(person_id){
return this.$fhcApi.post('api/frontend/v1/vertraege/vertraege/getHeader/' + person_id);
},
getPersonAbteilung(person_id){
return this.$fhcApi.post('api/frontend/v1/vertraege/vertraege/getPersonAbteilung/' + person_id);
},
getLeitungOrg(oekurzbz){
return this.$fhcApi.post('api/frontend/v1/vertraege/vertraege/getLeitungOrg/' + oekurzbz);
},
getMitarbeiterUid(person_id){
return this.$fhcApi.get('api/frontend/v1/vertraege/vertraege/getMitarbeiterUid/' + person_id);
},
}
+74
View File
@@ -0,0 +1,74 @@
import PluginsPhrasen from '../../plugins/Phrasen.js';
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js";
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
import {capitalize} from "../../helpers/StringHelpers.js";
const app = Vue.createApp({
name: 'AbgabetoolApp',
components: {
AbgabetoolStudent,
AbgabetoolMitarbeiter,
AbgabetoolAssistenz,
DeadlineOverview
},
data: function() {
return {
comp: null,
uid: null,
student_uid: null,
stg_kz: null
};
},
methods: {
},
computed: {
viewData() {
return { uid: this.uid}
},
student_uid_computed() {
return this.student_uid ?? this.uid
},
stg_kz_computed() {
return this.stg_kz ?? null
}
},
created() {
},
mounted() {
const root = document.getElementById('abgabetoolroot')
const route = root.getAttribute("route");
this.comp = route
const uid = root.getAttribute("uid");
this.uid = uid
const stg_kz = root.getAttribute("stg_kz_prop");
this.stg_kz = stg_kz
const student_uid = root.getAttribute("student_uid_prop");
this.student_uid = student_uid
},
template: `
<template v-if="comp && uid">
<AbgabetoolStudent v-if="comp == 'AbgabetoolStudent'" :viewData="viewData" :student_uid_prop="student_uid_computed"></AbgabetoolStudent>
<AbgabetoolMitarbeiter v-if="comp == 'AbgabetoolMitarbeiter'" :viewData="viewData"></AbgabetoolMitarbeiter>
<AbgabetoolAssistenz v-if="comp == 'AbgabetoolAssistenz'" :viewData="viewData" :stg_kz_prop="stg_kz_computed"></AbgabetoolAssistenz>
<DeadlineOverview v-if="comp == 'DeadlinesOverview'" :viewData="viewData"></DeadlineOverview>
</template>
`
});
app.config.globalProperties.$capitalize = capitalize;
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
tooltip: 8000
}
})
app.directive('tooltip', primevue.tooltip);
app.use(PluginsPhrasen);
app.mount('#abgabetoolroot');
@@ -237,4 +237,6 @@ const bismeldestichtagApp = Vue.createApp({
}
});
FhcApps.makeExtendable(bismeldestichtagApp);
bismeldestichtagApp.use(PluginsPhrasen).mount('#main');
+3
View File
@@ -142,6 +142,9 @@ const app = Vue.createApp({
}
}
});
FhcApps.makeExtendable(app);
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
+2
View File
@@ -87,6 +87,8 @@ const app = Vue.createApp({
}
});
FhcApps.makeExtendable(app);
setScrollbarWidth();
app.use(PluginsPhrasen);
@@ -34,4 +34,7 @@ const app = Vue.createApp({
});
},
});
FhcApps.makeExtendable(app);
app.use(PluginsPhrasen).mount("#content");
+60 -9
View File
@@ -1,16 +1,67 @@
import {CoreNavigationCmpt} from '../../components/navigation/Navigation.js';
import { CoreNavigationCmpt } from '../../components/navigation/Navigation.js';
import DashboardAdmin from '../../components/Dashboard/Admin.js';
import PluginsPhrasen from '../../plugins/Phrasen.js';
import ApiRenderers from '../../api/factory/renderers.js';
const app = Vue.createApp({
name: 'AdminApp',
data: () => ({
appSideMenuEntries: {}
}),
components: {
CoreNavigationCmpt,
DashboardAdmin
}
name: 'DashboardAdminApp',
data: () => ({
appSideMenuEntries: {},
renderers: null
}),
components: {
CoreNavigationCmpt,
DashboardAdmin
},
provide() {
return {
// TODO(chris): move those two into the components that need it
renderers: Vue.computed(() => this.renderers),
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone
};
},
created() {
this.$api
.call(ApiRenderers.loadRenderers())
.then(res => {
for (let rendertype of Object.keys(res.data)) {
let modalTitle = null;
let modalContent = null;
let calendarEvent = null;
if (res.data[rendertype].modalTitle)
modalTitle = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].modalTitle)));
if (res.data[rendertype].modalContent)
modalContent = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].modalContent)));
if (res.data[rendertype].calendarEvent)
calendarEvent = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].calendarEvent)));
if (res.data[rendertype].calendarEventStyles) {
var head = document.head;
if (!head.querySelector(`link[href="${res.data[rendertype].calendarEventStyles}"]`)) {
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = res.data[rendertype].calendarEventStyles;
head.appendChild(link);
}
}
if (this.renderers === null) {
this.renderers = {};
}
if (!this.renderers[rendertype]) {
this.renderers[rendertype] = {}
}
this.renderers[rendertype].modalTitle = modalTitle;
this.renderers[rendertype].modalContent = modalContent;
this.renderers[rendertype].calendarEvent = calendarEvent;
}
})
.catch(this.$fhcAlert.handleSystemErrors);
}
});
app.use(PluginsPhrasen);
app.directive('tooltip', primevue.tooltip);
app.mount('#main');
+17 -1
View File
@@ -14,6 +14,7 @@ import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js";
import RoomInformation, {DEFAULT_MODE_RAUMINFO} from "../../components/Cis/Mylv/RoomInformation.js";
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js";
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
import Studium from "../../components/Cis/Studium/Studium.js";
@@ -56,6 +57,12 @@ const router = VueRouter.createRouter({
component: AbgabetoolMitarbeiter,
props: true
},
{
path: `/Cis/Abgabetool/Assistenz/:stg_kz_prop?`,
name: 'AbgabetoolAssistenz',
component: AbgabetoolAssistenz,
props: true
},
{
path: `/Cis/Abgabetool/Deadlines/:person_uid_prop?`,
name: 'DeadlineOverview',
@@ -228,13 +235,16 @@ const app = Vue.createApp({
components: {},
computed: {
isMobile() {
return /Mobi|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const smallScreen = window.matchMedia("(max-width: 767px)").matches;
const touchCapable = ("ontouchstart" in window) || navigator.maxTouchPoints > 0;
return smallScreen;// && touchCapable;
}
},
provide() {
return { // provide injectable & watchable language property
language: Vue.computed(() => this.$p.user_language),
renderers: Vue.computed(() => this.renderers),
isMobile: this.isMobile
}
},
methods: {
@@ -273,6 +283,7 @@ const app = Vue.createApp({
}
},
async created(){
await this.$api
.call(ApiRenderers.loadRenderers())
.then(res => res.data)
@@ -313,6 +324,7 @@ const app = Vue.createApp({
},
mounted() {
document.addEventListener('click', this.handleClick);
},
beforeUnmount() {
document.removeEventListener('click', this.handleClick);
@@ -322,6 +334,10 @@ const app = Vue.createApp({
// kind of a bandaid for bad css on some pages to avoid horizontal scroll
setScrollbarWidth();
app.config.globalProperties.$capitalize = capitalize;
FhcApps.router.makeExtendable(router);
FhcApps.makeExtendable(app);
app.use(router);
app.use(primevue.config.default, {
zIndex: {
+17
View File
@@ -0,0 +1,17 @@
import {CoreNavigationCmpt} from '../../components/navigation/Navigation.js';
import CoreDashboard from '../../components/Dashboard/Dashboard.js';
import PluginsPhrasen from '../../plugins/Phrasen.js';
const app = Vue.createApp({
name: 'DashboardPreviewApp',
data: () => ({
appSideMenuEntries: {}
}),
components: {
CoreNavigationCmpt,
CoreDashboard
}
});
app.use(PluginsPhrasen);
app.directive('tooltip', primevue.tooltip);
app.mount('#main');
+4
View File
@@ -89,8 +89,12 @@ const router = VueRouter.createRouter({
]
});
FhcApps.router.makeExtendable(router);
const app = Vue.createApp();
FhcApps.makeExtendable(app);
app
.use(router)
.use(primevue.config.default, {
+2
View File
@@ -42,5 +42,7 @@ const logsViewerApp = Vue.createApp({
}
});
FhcApps.makeExtendable(logsViewerApp);
logsViewerApp.use(PluginsPhrasen).mount('#main');
+4
View File
@@ -206,10 +206,14 @@ router.afterEach((to, from, failure) => {
document.title = title;
});
FhcApps.router.makeExtendable(router);
const app = Vue.createApp({
name: 'StudentenverwaltungApp'
});
FhcApps.makeExtendable(app);
app
.use(router)
.use(primevue.config.default, {
+25
View File
@@ -0,0 +1,25 @@
import Vertragsverwaltung from "../components/Vertraege/Vertragsverwaltung.js";
import Phrasen from "../plugins/Phrasen.js";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes: [
{ path: `/${ciPath}/vertragsverwaltung`, component: Vertragsverwaltung },
]
});
const app = Vue.createApp({
name: 'VertragsverwaltungApp'
});
app
.use(router)
.use(primevue.config.default, {
zIndex: {
overlay: 1100
}
})
.use(Phrasen)
.mount('#main');
+3
View File
@@ -20,6 +20,9 @@ const app = Vue.createApp({
};
}
});
FhcApps.makeExtendable(app);
app
.use(PluginsPhrasen)
.mount('#wrapper');
+3
View File
@@ -7,6 +7,9 @@ const app = Vue.createApp({
StudierendenantragLeitung
}
});
FhcApps.makeExtendable(app);
app
.use(PluginsPhrasen)
.use(primevue.config.default,{zIndex: {overlay: 9999}})
@@ -12,6 +12,9 @@ const app = Vue.createApp({
}
}
});
FhcApps.makeExtendable(app);
app
.use(PluginsPhrasen)
.mount('#wrapper');
+3
View File
@@ -7,6 +7,9 @@ const app = Vue.createApp({
LvPopup
}
});
FhcApps.makeExtendable(app);
app
.use(PluginsPhrasen)
.mount('#wrapper');
@@ -28,6 +28,8 @@ const lvTemplatesApp = Vue.createApp({
}
});
FhcApps.makeExtendable(lvTemplatesApp);
lvTemplatesApp
.use(primevue.config.default,{zIndex: {overlay: 9999}})
.use(PluginsPhrasen)
@@ -43,12 +43,28 @@ export default {
},
data() {
return {
tabulatorOptions: {
listBetriebsmitteltyp: [],
formData: {
ausgegebenam : new Date(),
betriebsmitteltyp: 'Zutrittskarte'
},
statusNew: true,
filteredInventar: [],
layout: 'fitColumns',
layoutColumnsOnNewData: false,
height: '550',
}
},
computed: {
tabulatorOptions() {
const options = {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(
this.endpoint.getAllBetriebsmittel(this.typeId, this.id, (this.filterByProvidedTypes ? this.betriebsmittelTypes : null))
),
ajaxResponse: (url, params, response) => response.data,
persistenceID: 'core-betriebsmittel-20260217',
selectableRows: true,
columns: [
{title: "Nummer", field: "nummer", width: 150},
{title: "PersonId", field: "person_id", visible: false},
@@ -115,7 +131,7 @@ export default {
'/content/pdfExport.php?xml=betriebsmittelperson.rdf.php&xsl=Uebernahme&id=' + cellData.betriebsmittelperson_id + '&output=pdf';
window.open(linkToPdf, '_blank');
});
});
container.append(button);
button = document.createElement('button');
@@ -143,64 +159,47 @@ export default {
return container;
},
frozen: true
}],
layout: 'fitColumns',
layoutColumnsOnNewData: false,
height: '550',
persistenceID: 'core-betriebsmittel'
},
tabulatorEvents: [
}
],
};
return options;
},
tabulatorEvents() {
const events = [
{
event: 'tableBuilt',
handler: async() => {
await this.$p.loadCategory(['wawi', 'global', 'infocenter', 'betriebsmittel', 'person']);
let cm = this.$refs.table.tabulator.columnManager;
const setHeader = (field, text) => {
const col = this.$refs.table.tabulator.getColumn(field);
if (!col) return;
cm.getColumnByField('nummer').component.updateDefinition({
title: this.$p.t('wawi', 'nummer')
});
cm.getColumnByField('betriebsmitteltyp').component.updateDefinition({
title: this.$p.t('global', 'typ')
});
cm.getColumnByField('anmerkung').component.updateDefinition({
title: this.$p.t('global', 'anmerkung')
});
cm.getColumnByField('retouram').component.updateDefinition({
title: this.$p.t('wawi', 'retourdatum')
});
cm.getColumnByField('beschreibung').component.updateDefinition({
title: this.$p.t('global', 'beschreibung')
});
cm.getColumnByField('kaution').component.updateDefinition({
title: this.$p.t('infocenter', 'kaution')
});
cm.getColumnByField('ausgegebenam').component.updateDefinition({
title: this.$p.t('wawi', 'ausgabedatum')
});
cm.getColumnByField('betriebsmittel_id').component.updateDefinition({
title: this.$p.t('ui', 'betriebsmittel_id')
});
cm.getColumnByField('betriebsmittelperson_id').component.updateDefinition({
title: this.$p.t('ui', 'betriebsmittelperson_id')
});
cm.getColumnByField('person_id').component.updateDefinition({
title: this.$p.t('person', 'person_id')
});
cm.getColumnByField('uid').component.updateDefinition({
title: this.$p.t('person', 'uid')
});
const el = col.getElement();
if (!el || !el.querySelector) return;
const titleEl = el.querySelector('.tabulator-col-title');
if (titleEl) {
titleEl.textContent = text;
}
};
setHeader('nummer', this.$p.t('wawi', 'nummer'));
setHeader('betriebsmitteltyp', this.$p.t('global', 'typ'));
setHeader('anmerkung', this.$p.t('global', 'anmerkung'));
setHeader('retouram', this.$p.t('wawi', 'retourdatum'));
setHeader('beschreibung', this.$p.t('global', 'beschreibung'));
setHeader('kaution', this.$p.t('infocenter', 'kaution'));
setHeader('ausgegebenam', this.$p.t('wawi', 'ausgabedatum'));
setHeader('betriebsmittel_id', this.$p.t('ui', 'betriebsmittel_id'));
setHeader('betriebsmittelperson_id', this.$p.t('ui', 'betriebsmittelperson_id'));
setHeader('person_id', this.$p.t('person', 'person_id'));
setHeader('uid', this.$p.t('person', 'uid'));
}
}
],
listBetriebsmitteltyp: [],
formData: {
ausgegebenam : new Date(),
betriebsmitteltyp: 'Zutrittskarte'
},
statusNew: true,
filteredInventar: []
];
return events;
}
},
watch: {
@@ -466,5 +465,4 @@ export default {
</template>
</bs-modal>
</div>`
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ export default {
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="result=true;this.hide()">OK</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{this.$p.t('ui', 'cancel')}}</button>
</template>
</bs-modal>`
}
+13 -5
View File
@@ -46,7 +46,8 @@ export default {
"hiddenBsModal",
"hidePreventedBsModal",
"showBsModal",
"shownBsModal"
"shownBsModal",
"toggleFullscreen"
],
methods: {
dispose() {
@@ -66,6 +67,7 @@ export default {
},
toggleFullscreen() {
this.fullscreen = !this.fullscreen
this.$emit('toggleFullscreen')
}
},
mounted() {
@@ -135,10 +137,16 @@ export default {
<div class="modal-content">
<div v-if="$slots.title" class="modal-header" :class="headerClass">
<h5 class="modal-title"><slot name="title"/></h5>
<div class="d-flex align-items-center ms-auto">
<button type="button" class="btn ms-auto" style="filter: invert(1)" v-if="allowFullscreenExpand" @click="toggleFullscreen">
<i v-if="!fullscreen" class="fa-solid fa-expand"></i>
<i v-else class="fa-solid fa-compress"></i>
<div class="d-flex align-items-center ms-auto gap-2">
<button
type="button"
class="btn mb-1"
v-if="allowFullscreenExpand"
@click="toggleFullscreen"
:aria-label="fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'"
>
<i v-if="!fullscreen" class="fa-solid fa-expand"></i>
<i v-else class="fa-solid fa-compress"></i>
</button>
<button v-if="!noCloseBtn" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
+151
View File
@@ -0,0 +1,151 @@
export default {
name: 'BootstrapOffcanvas',
data: () => ({
offcanvas: null
}),
props: {
backdrop: {
type: [Boolean, String],
default: true,
validator(value) {
return ['static', true, false].includes(value);
}
},
keyboard: {
type: Boolean,
default: true
},
scroll: {
type: Boolean,
default: false
},
placement: {
type: String,
default: 'start', // start | end | top | bottom
validator(value) {
return ['start', 'end', 'top', 'bottom'].includes(value);
}
},
noCloseBtn: Boolean,
headerClass: {
type: [String, Array, Object],
default: ''
},
bodyClass: {
type: [String, Array, Object],
default: 'p-4'
},
footerClass: {
type: [String, Array, Object],
default: ''
},
dialogClass: [String, Array, Object]
},
emits: [
"hideBsOffcanvas",
"hiddenBsOffcanvas",
"hidePreventedBsOffcanvas",
"showBsOffcanvas",
"shownBsOffcanvas"
],
methods: {
dispose() {
return this.offcanvas?.dispose();
},
hide() {
return this.offcanvas?.hide();
},
show(relatedTarget) {
return this.offcanvas?.show(relatedTarget);
},
toggle() {
return this.offcanvas?.toggle();
},
popup(body, options, title, footer) {
const BsOffcanvas = this,
slots = {};
if (body !== undefined)
slots.default = () => body;
if (title !== undefined)
slots.title = () => title;
if (footer !== undefined)
slots.footer = () => footer;
let includedPrimevue = false;
if (typeof primevue !== 'undefined')
includedPrimevue = true;
return new Promise((resolve, reject) => {
const instance = Vue.createApp({
name: 'OffcanvasTmpApp',
setup() {
return () =>
Vue.h(BsOffcanvas, {
class: 'offcanvas-wrapper',
ref: 'offcanvas',
...options
}, slots);
},
mounted() {
this.$refs.offcanvas.show();
},
beforeUnmount() {
if (this.$refs.offcanvas)
this.$refs.offcanvas.result !== false ? resolve(this.$refs.offcanvas.result) : reject();
},
unmounted() {
wrapper.parentElement.removeChild(wrapper);
}
});
const wrapper = document.createElement('div');
if (includedPrimevue) {
instance.use(primevue.config.default, { zIndex: { overlay: 9999 } });
}
import('../../plugins/Phrasen.js').then((Phrasen) => {
instance.use(Phrasen.default);
instance.mount(wrapper);
document.body.appendChild(wrapper);
});
});
}
},
mounted() {
if (this.$refs.offcanvas) {
this.offcanvas = new bootstrap.Offcanvas(this.$refs.offcanvas, {
backdrop: this.backdrop,
keyboard: this.keyboard,
scroll: this.scroll
});
}
},
template: `
<div ref="offcanvas"
class="bootstrap-offcanvas offcanvas"
:class="['offcanvas-' + placement, dialogClass]"
tabindex="-1"
@[\`hide.bs.offcanvas\`]="$emit('hideBsOffcanvas')"
@[\`hidden.bs.offcanvas\`]="$emit('hiddenBsOffcanvas')"
@[\`hidePrevented.bs.offcanvas\`]="$emit('hidePreventedBsOffcanvas')"
@[\`show.bs.offcanvas\`]="$emit('showBsOffcanvas')"
@[\`shown.bs.offcanvas\`]="$emit('shownBsOffcanvas')"
>
<div class="offcanvas-header" :class="headerClass" v-if="$slots.title">
<h5 class="offcanvas-title">
<slot name="title"></slot>
</h5>
<button v-if="!noCloseBtn" type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body" :class="bodyClass">
<slot></slot>
</div>
<div v-if="$slots.footer" class="offcanvas-footer" :class="footerClass">
<slot name="footer"></slot>
</div>
</div>
`
}
-6
View File
@@ -8,12 +8,6 @@ export const FhcChart = {
},
template: `
<div style="width:100%;height:100%;overflow:auto">
<div role="group" aria-label="Chart Modus">
<Button :class="(chartOptions.chart.type === 'pie' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='pie'"><i class="fa-solid fa-chart-pie"></i></Button>
<Button :class="(chartOptions.chart.type === 'bar' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='bar'"><i class="fa-solid fa-chart-bar"></i></Button>
<Button :class="(chartOptions.chart.type === 'column' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='column'"><i class="fa-solid fa-chart-simple"></i></Button>
<Button :class="(chartOptions.chart.type === 'line' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='line'"><i class="fa-solid fa-chart-line"></i></Button>
</div>
<figure>
<highcharts class="chart" :options="chartOptions"></highcharts>
</figure>
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,9 @@
import Upload from '../../../components/Form/Upload/Dms.js';
import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js'
import FhcOverlay from "../../Overlay/FhcOverlay.js";
const today = new Date()
export const AbgabeStudentDetail = {
name: "AbgabeStudentDetail",
components: {
@@ -12,8 +13,14 @@ export const AbgabeStudentDetail = {
Checkbox: primevue.checkbox,
Dropdown: primevue.dropdown,
Textarea: primevue.textarea,
VueDatePicker
Accordion: primevue.accordion,
AccordionTab: primevue.accordiontab,
Message: primevue.message,
Inplace: primevue.inplace,
VueDatePicker,
FhcOverlay
},
inject: ['notenOptions', 'isMobile', 'isViewMode', 'moodle_link'],
props: {
projektarbeit: {
type: Object,
@@ -26,6 +33,7 @@ export const AbgabeStudentDetail = {
},
data() {
return {
loading: false,
eidAkzeptiert: false,
enduploadTermin: null,
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
@@ -41,16 +49,40 @@ export const AbgabeStudentDetail = {
}
},
methods: {
validate: function(termin) {
getNoteBezeichnung(termin){
const noteOpt = this.notenOptions.find(opt => opt.note == termin.note)
if(noteOpt?.bezeichnung) {
return noteOpt?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌'
} else if(noteOpt?.benotbar === true && !termin.note) {
return this.$capitalize(this.$p.t('abgabetool/c4notYetGraded'));
} else {
return ''
}
},
async validate(termin, endupload = false) {
if(!termin.file.length) {
this.$fhcAlert.alertWarning(this.$p.t('global/warningChooseFile'));
this.$fhcAlert.alertWarning(this.$capitalize(this.$p.t('global/warningChooseFile')));
return false
}
if(endupload) {
if(await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/confirmEnduploadSpeichern'),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'p-button-primary',
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'p-button-secondary'
}) === false) {
return false
}
}
return true;
},
triggerEndupload() {
if (!this.validate(this.enduploadTermin))
async triggerEndupload() {
if (!await this.validate(this.enduploadTermin, true))
{
return false;
}
@@ -63,7 +95,6 @@ export const AbgabeStudentDetail = {
formData.append('student_uid', this.projektarbeit.student_uid)
formData.append('bperson_id', this.projektarbeit.bperson_id)
// TODO: validate/check for null etc.
formData.append('sprache', this.form['sprache'].sprache)
formData.append('abstract', this.form['abstract'])
formData.append('abstract_en', this.form['abstract_en'])
@@ -74,15 +105,21 @@ export const AbgabeStudentDetail = {
for (let i = 0; i < this.enduploadTermin.file.length; i++) {
formData.append('file', this.enduploadTermin.file[i]);
}
this.$fhcApi.factory.lehre.postStudentProjektarbeitEndupload(formData)
this.loading = true
this.$api.call(ApiAbgabe.postStudentProjektarbeitEndupload(formData))
.then(res => {
this.handleUploadRes(res)
})
this.handleUploadRes(res, this.enduploadTermin)
}).finally(()=> {
this.loading = false
})
this.$refs.modalContainerEnduploadZusatzdaten.hide()
},
downloadAbgabe(termin) {
this.$fhcApi.factory.lehre.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid)
const url = `/api/frontend/v1/Abgabe/getStudentProjektarbeitAbgabeFile?paabgabe_id=${termin.paabgabe_id}&student_uid=${this.projektarbeit.student_uid}&projektarbeit_id=${this.projektarbeit.projektarbeit_id}`;
window.open(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url)
// this.$api.call(ApiAbgabe.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid))
},
formatDate(dateParam) {
const date = new Date(dateParam)
@@ -93,16 +130,17 @@ export const AbgabeStudentDetail = {
const day = padZero(date.getDate());
const year = date.getFullYear();
return `${day}.${month}.${year}`;
return `${day}.${month}.${year}`
},
upload(termin) {
async upload(termin) {
if (!this.validate(termin))
// only do this on endupload
if (! await this.validate(termin))
{
return false;
}
if(termin.bezeichnung === 'Endupload') {
if(termin.bezeichnung?.paabgabetyp_kurzbz === 'end') {
// open endupload form modal for further inputs
this.enduploadTermin = termin
this.$refs.modalContainerEnduploadZusatzdaten.show()
@@ -117,166 +155,402 @@ export const AbgabeStudentDetail = {
for (let i = 0; i < termin.file.length; i++) {
formData.append('file', termin.file[i]);
}
this.$fhcApi.factory.lehre.postStudentProjektarbeitZwischenabgabe(formData)
this.loading = true
this.$api.call(ApiAbgabe.postStudentProjektarbeitZwischenabgabe(formData))
.then(res => {
this.handleUploadRes(res)
})
this.handleUploadRes(res, termin)
}).finally(()=> {
this.loading = false
})
}
},
handleUploadRes(res) {
handleUploadRes(res, termin) {
if(res.meta.status == "success") {
this.$fhcAlert.alertSuccess('File erfolgreich hochgeladen')
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4fileUploadSuccessv3')))
// update 'abgabedatum' for successful upload -> shows the pdf icon and date once set
termin.abgabedatum = new Date().toISOString().split('T')[0];
if(res?.data?.signatur !== undefined) {
termin.signatur = res.data.signatur
}
} else {
this.$fhcAlert.alertError('File upload error')
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4fileUploadErrorv3')))
}
if(res.meta.signaturInfo) {
this.$fhcAlert.alertInfo(res.meta.signaturInfo)
}
},
dateDiffInDays(datum, today){
const oneDayMs = 1000 * 60 * 60 * 24
return Math.round((new Date(datum) - new Date(today)) / oneDayMs)
},
getDateStyle(termin, mode) {
const datum = new Date(termin.datum)
const abgabedatum = new Date(termin.abgabedatum)
// todo: rework styling but keep the color pattern logic
// https://wiki.fhcomplete.info/doku.php?id=cis:abgabetool_fuer_studierende
let color = 'white'
let fontColor = 'black'
let icon = '';
if (termin.abgabedatum === null) {
if(datum < today) {
color = 'red'
fontColor = 'white'
icon = 'fa-triangle-exclamation'
} else if (datum > today && this.dateDiffInDays(datum, today) <= 12) {
color = 'yellow'
icon = 'fa-circle-exclamation'
}
} else if(abgabedatum > datum) {
color = 'pink' // aka "hellrot"
fontColor = 'white'
icon = 'fa-circle-question'
} else {
color = 'green'
icon = 'fa-square-check'
}
//return `font-color: ${fontColor} ; background-color: ${color}; border-radius: 50%;`
if( typeof mode !== 'undefined' || mode === 'icon') {
return icon;
} else {
return 'abgabe-zieldatum-border-' + color;
}
},
openBeurteilungLink(link) {
window.open(link, '_blank')
},
getOptionLabel(option) {
return option.sprache
}
},
getTerminNoteBezeichnung(termin) {
const noteOpt = this.notenOptions.find(opt => opt.note == termin.note)
return noteOpt ? noteOpt.bezeichnung : ''
},
},
watch: {
projektarbeit(newVal) {
// default select german if projektarbeit sprache was null
this.form.sprache = newVal.sprache ? this.allActiveLanguages.find(lang => lang.sprache == newVal.sprache) : this.allActiveLanguages.find(lang => lang.sprache == 'German')
this.form.abstract = newVal.abstract
this.form.abstract_en = newVal.abstract_en
this.form.schlagwoerter = newVal.schlagwoerter
this.form.schlagwoerter_en = newVal.schlagwoerter_en
this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter
this.form.seitenanzahl = newVal.seitenanzahl
this.form.abstract = newVal.abstract ?? ''
this.form.abstract_en = newVal.abstract_en ?? ''
this.form.schlagwoerter = newVal.schlagwoerter ?? ''
this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? ''
this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? ''
this.form.seitenanzahl = newVal.seitenanzahl ?? 1
}
},
computed: {
getMoodleLink() {
return this.moodle_link + this.projektarbeit.studiengang_kz
},
getMessagePtStyle() {
// adjust outer spacing and internal padding to appear similar to doenload button in size
return {
root: {
style: {
margin: '0px'
}
},
wrapper: {
style: {
padding: '6px'
}
}
}
},
getEid() {
return this.$p.t('abgabetool/c4eidesstattlicheErklaerung')
return this.$capitalize(this.$p.t('abgabetool/c4eidesstattlicheErklaerung'))
},
allowedToSaveZusatzdaten() {
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter_en.length > 0 && this.form.abstract.length > 0 && this.form.abstract_en.length > 0 && this.form.seitenanzahl > 0
},
getAllowedToSendEndupload() {
return this.eidAkzeptiert && this.allowedToSaveZusatzdaten
},
qualityGateTerminAvailable() {
let qgatefound = false
this.projektarbeit?.abgabetermine.forEach(abgabe => {
if(abgabe.paabgabetyp_kurzbz == 'qualgate1'
|| abgabe.paabgabetyp_kurzbz == 'qualgate2') {
qgatefound = true
}
})
return qgatefound
},
getTooltipVerspaetet() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')),
class: "custom-tooltip"
}
},
getTooltipVerpasst() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')),
class: "custom-tooltip"
}
},
getTooltipAbzugeben() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')),
class: "custom-tooltip"
}
},
getTooltipStandard() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')),
class: "custom-tooltip"
}
},
getTooltipAbgegeben() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')),
class: "custom-tooltip"
}
},
getTooltipFixtermin() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')),
class: "custom-tooltip"
}
},
getTooltipAbgabeDetected() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')),
class: "custom-tooltip"
}
},
getTooltipNotAllowedToUpload() {
if(this.isViewMode) {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')),
class: "custom-tooltip"
}
} else {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegular')),
class: "custom-tooltip"
}
}
},
getTooltipBeurteilungerforderlich() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')),
class: "custom-tooltip"
}
},
getTooltipBestanden() {
return {
value: this.$p.t('abgabetool/c4tooltipBestanden'),
class: "custom-tooltip"
}
},
getTooltipNichtBestanden() {
return {
value: this.$p.t('abgabetool/c4tooltipNichtBestanden'),
class: "custom-tooltip"
}
},
getEnduploadErlaubt() {
return !this.eidAkzeptiert
}
},
created() {
},
mounted() {
},
template: `
<FhcOverlay :active="loading"></FhcOverlay>
<div v-if="projektarbeit">
<h5>{{$p.t('abgabetool/c4abgabeStudentenbereich')}}</h5>
<h5>{{$capitalize( $p.t('abgabetool/c4abgabeStudentenbereich') )}}</h5>
<div class="row">
<p> {{projektarbeit?.betreuer}}</p>
<p> {{projektarbeit?.titel}}</p>
</div>
<div id="uploadWrapper">
<div class="row" style="margin-bottom: 12px;">
<div class="col-1 fw-bold text-center">{{$p.t('abgabetool/c4fixtermin')}}</div>
<div class="col-2 fw-bold">{{$p.t('abgabetool/c4zieldatum')}}</div>
<div class="col-2 fw-bold">{{$p.t('abgabetool/c4abgabetyp')}}</div>
<div class="col-3 fw-bold">{{$p.t('abgabetool/c4abgabekurzbz')}}</div>
<div class="col-1 fw-bold text-center">{{$p.t('abgabetool/c4abgabedatum')}}</div>
<div class="col-3 fw-bold">
{{$p.t('abgabetool/c4fileupload')}}
</div>
<div class="col-8">
<p> {{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}</p>
<p> {{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}</p>
<p> {{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}</p>
</div>
<div class="row" v-for="termin in projektarbeit.abgabetermine">
<div class="col-1 d-flex justify-content-center align-items-start">
<i v-if="termin.fixtermin" class="fa-solid fa-2x fa-circle-check fhc-bullet-red"></i>
<i v-else="" class="fa-solid fa-2x fa-circle-xmark fhc-bullet-green"></i>
<!--
<p class="fhc-bullet" :class="{ 'fhc-bullet-red': termin.fixtermin, 'fhc-bullet-green': !termin.fixtermin }"></p>
-->
</div>
<div class="col-2 d-flex justify-content-start align-items-start">
<div class="position-relative" :class="getDateStyle(termin)">
<VueDatePicker
v-model="termin.datum"
:clearable="false"
:disabled="true"
:enable-time-picker="false"
:format="formatDate"
:text-input="true"
auto-apply>
</VueDatePicker>
<i class="position-absolute abgabe-zieldatum-overlay fa-solid fa-2x" :class="getDateStyle(termin, 'icon')"></i>
</div>
</div>
<div class="col-2 d-flex justify-content-start align-items-start">{{ termin.bezeichnung }}</div>
<div class="col-3 d-flex justify-content-start align-items-start">
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="3" cols="45" :disabled="true"></Textarea>
</div>
<div class="col-1 d-flex flex-column justify-content-start align-items-center">
{{ termin.abgabedatum?.split("-").reverse().join(".") }}
<a v-if="termin?.abgabedatum" @click="downloadAbgabe(termin)" style="margin-left:4px; cursor: pointer;">
<i class="fa-solid fa-3x fa-file-pdf"></i>
</a>
</div>
<div class="col-3" v-if="!viewMode">
<div class="row">
<div class="col-8">
<Upload v-if="termin && termin.allowedToUpload" accept=".pdf" v-model="termin.file"></Upload>
</div>
<div class="col-4">
<button class="btn btn-primary border-0" @click="upload(termin)" :disabled="!termin.allowedToUpload">
Upload
<i style="margin-left: 8px" class="fa-solid fa-upload"></i>
</button>
</div>
</div>
</div>
<div class="col-4">
<p>{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }}
<a :href="getMoodleLink" target="_blank">Moodle</a>
</p>
</div>
</div>
<Accordion :multiple="true">
<template v-for="termin in this.projektarbeit?.abgabetermine" :key="termin.paabgabe_id">
<AccordionTab :headerClass="termin.dateStyle + '-header'">
<template #header>
<div class="d-flex flex-nowrap align-items-center w-100">
<div class="flex-shrink-0 d-flex align-items-center justify-content-center" style="width: 36px; height: 36px; margin-left: -68px;">
<i v-if="termin.dateStyle == 'verspaetet'" v-tooltip.right="getTooltipVerspaetet" class="fa-solid fa-triangle-exclamation"></i>
<i v-else-if="termin.dateStyle == 'verpasst'" v-tooltip.right="getTooltipVerpasst" class="fa-solid fa-calendar-xmark"></i>
<i v-else-if="termin.dateStyle == 'abzugeben'" v-tooltip.right="getTooltipAbzugeben" class="fa-solid fa-hourglass-half"></i>
<i v-else-if="termin.dateStyle == 'standard'" v-tooltip.right="getTooltipStandard" class="fa-solid fa-clock"></i>
<i v-else-if="termin.dateStyle == 'abgegeben'" v-tooltip.right="getTooltipAbgegeben" class="fa-solid fa-paperclip"></i>
<i v-else-if="termin.dateStyle == 'beurteilungerforderlich'" v-tooltip.right="getTooltipBeurteilungerforderlich" class="fa-solid fa-list-check"></i>
<i v-else-if="termin.dateStyle == 'bestanden'" v-tooltip.right="getTooltipBestanden" class="fa-solid fa-check"></i>
<i v-else-if="termin.dateStyle == 'nichtbestanden'" v-tooltip.right="getTooltipNichtBestanden" class="fa-solid fa-circle-exclamation"></i>
</div>
<div class="text-start px-2" style="min-width: 150px; max-width: 300px; margin-left: 40px">
<span>{{ termin ? $p.t('abgabetool/c4paatyp' + termin.paabgabetyp_kurzbz) : '' }}</span>
</div>
<div class="text-start px-2" style="min-width: 100px;">
<span>{{ formatDate(termin.datum) }}</span>
</div>
<div class="px-1">
<i v-if="termin?.fixtermin" v-tooltip.right="getTooltipFixtermin" class="fa-solid fa-lock"></i>
<i v-if="termin?.abgabedatum && isMobile" v-tooltip.right="getTooltipAbgabeDetected" class="fa-solid fa-file"></i>
</div>
<div v-if="termin?.abgabedatum && !isMobile" class="px-1">
<i v-tooltip.right="getTooltipAbgabeDetected" class="fa-solid fa-file"></i>
</div>
<div class="flex-grow-1 text-end pe-2">
<span class="fw-bold">{{getNoteBezeichnung(termin)}}</span>
</div>
</div>
</template>
<div v-if="isMobile" class="row mt-2 align-items-center">
<Inplace
closable
:closeButtonProps="{
style: {
position: 'relative',
bottom: '100px',
left: '80%',
zIndex: 1
}
}"
>
<template #display>{{ $capitalize($p.t('abgabetool/c4tapForTooltipInfo'))}}</template>
<template #content>
<div class="col-auto">
<div class="row">
<div class="col-12 col-md-3 fw-bold align-content-center">{{ $capitalize($p.t('abgabetool/c4abgabeZeitstatus')) }}</div>
<div class="col-12 col-md-9">{{$p.t('abgabetool/c4tooltip' + $capitalize(termin?.dateStyle) )}}</div>
</div>
<div class="row">
<div class="col-12 col-md-3 fw-bold align-content-center">{{ $capitalize($p.t('abgabetool/c4fixterminv4')) }}</div>
<div class="col-12 col-md-9">{{!termin?.fixtermin}}</div>
</div>
<div class="row">
<div class="col-12 col-md-3 fw-bold align-content-center">{{ $capitalize($p.t('abgabetool/c4fileUploaded')) }}</div>
<div class="col-12 col-md-9">{{termin?.abgabedatum !== null}}</div>
</div>
</div>
</template>
</Inplace>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 align-content-center">
<div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatumv2') )}}</div>
<div class="row fw-light" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4abgabeuntil2359') )}}</div>
</div>
<div class="col-12 col-md-9">
<VueDatePicker
v-model="termin.datum"
:clearable="false"
:disabled="true"
:enable-time-picker="false"
locale="de"
format="dd.MM.yyyy"
:text-input="true"
auto-apply>
</VueDatePicker>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabetyp') )}}</div>
<div class="col-12 col-md-9">
{{ termin ? $p.t('abgabetool/c4paatyp' + termin.paabgabetyp_kurzbz) : '' }}
</div>
</div>
<div class="row mt-2" v-if="termin.note">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4note') )}}</div>
<div class="col-12 col-md-9">
<div class="col-auto d-flex justify-content-start align-items-start">
{{ getTerminNoteBezeichnung(termin) }}
</div>
</div>
</div>
<div class="row mt-2" v-if="termin.paabgabetyp_kurzbz === 'qualgate1' || termin.paabgabetyp_kurzbz === 'qualgate2'">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4notizQualGatev2') )}}</div>
<div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.beurteilungsnotiz" rows="1" class="w-100" disabled></Textarea>
</div>
</div>
<div v-if="termin.kurzbz && termin.kurzbz.length > 0" class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbzv2') )}}</div>
<div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="1" class="w-100" :disabled="true"></Textarea>
</div>
</div>
<div class="row mt-2" v-if="termin.upload_allowed">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabedatum') )}}</div>
<div class="col-12 col-md-9">
<template v-if="termin?.abgabedatum">
<div class="row">
<div style="width:100px; align-content: center;">
<h6>{{ termin.abgabedatum?.split("-").reverse().join(".") }}</h6>
</div>
<div class="col-auto">
<button v-if="termin?.abgabedatum" @click="downloadAbgabe(termin)" class="btn btn-primary">
<a> {{$capitalize($p.t('abgabetool/c4downloadAbgabe') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button>
</div>
<template v-if="termin.paabgabetyp_kurzbz == 'end'">
<div v-if="termin?.signatur !== undefined && termin?.signatur !== null" class="col-auto">
<Message v-if="termin?.signatur == true" severity="success" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4signaturGefunden') }} </Message>
<Message v-else-if="termin?.signatur == false" severity="error" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4keineSignatur') }} </Message>
<Message v-else-if="termin?.signatur == 'error'" severity="warn" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4signaturServerError') }} </Message>
</div>
<!-- <div v-else class="col-auto">-->
<!-- <Message severity="info" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4noFileFound') }} </Message>-->
<!-- </div>-->
</template>
</div>
</template>
<template v-else>
{{ $capitalize( $p.t('abgabetool/c4nochNichtsAbgegeben') )}}
</template>
</div>
</div>
<div class="row mt-2" v-if="termin.upload_allowed">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4fileupload') )}}</div>
<div class="col-12 col-md-9">
<div class="row" v-if="termin?.allowedToUpload">
<div class="col-12 col-sm-6 mb-2">
<Upload
accept=".pdf"
v-model="termin.file"
></Upload>
</div>
<div class="col-12 col-sm-6">
<button
class="btn btn-primary border-0 w-100"
@click="upload(termin)"
>
{{$capitalize( $p.t('abgabetool/c4upload') )}}
<i class="fa-solid fa-upload"></i>
</button>
</div>
</div>
<div class="row" v-else-if="!termin?.allowedToUpload || isViewMode" v-tooltip.right="getTooltipNotAllowedToUpload">
<div class="col-12 col-sm-6 mb-2">
<Upload
disabled
accept=".pdf"
v-model="termin.file"
></Upload>
</div>
<div class="col-12 col-sm-6">
<button
class="btn btn-primary border-0 w-100"
@click="upload(termin)"
disabled
>
{{$capitalize( $p.t('abgabetool/c4upload') )}}
<i class="fa-solid fa-upload"></i>
</button>
</div>
</div>
</div>
</div>
</AccordionTab>
</template>
</Accordion>
<div v-if="projektarbeit?.abgabetermine.length == 0" style="display:flex; justify-content: center; align-content: center;">
<h5>{{ $capitalize( $p.t('abgabetool/c4keineAbgabetermineGefunden') )}}</h5>
</div>
</div>
<bs-modal ref="modalContainerEnduploadZusatzdaten" class="bootstrap-prompt" dialogClass="modal-lg">
<bs-modal
ref="modalContainerEnduploadZusatzdaten"
class="bootstrap-prompt"
dialogClass="bordered-modal modal-lg">
<template v-slot:title>
<div>
{{$p.t('abgabetool/c4enduploadZusatzdaten')}}
{{$capitalize( $p.t('abgabetool/c4enduploadZusatzdaten') )}}
</div>
<div class="row mb-3 align-items-start">
@@ -285,13 +559,13 @@ export const AbgabeStudentDetail = {
</div>
<div class="row mb-3 align-items-start">
<p class="ml-4 mr-4">Titel: {{ projektarbeit?.titel }}</p>
<p class="ml-4 mr-4">{{$capitalize( $p.t('abgabetool/c4titel') )}}: {{ projektarbeit?.titel }}</p>
</div>
</template>
<template v-slot:default>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4Sprache')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4Sprache') )}}</div>
<div class="row">
<Dropdown
:style="{'width': '100%'}"
@@ -312,35 +586,37 @@ export const AbgabeStudentDetail = {
<!-- -->
<!-- </div>-->
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4schlagwoerterGer')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterGer') )}}</div>
<div class="row">
<Textarea v-model="form.schlagwoerter"></Textarea>
<Textarea v-model="form.schlagwoerter" class="w-100"></Textarea>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4schlagwoerterEng')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterEng') )}}</div>
<div class="row">
<Textarea v-model="form.schlagwoerter_en"></Textarea>
<Textarea v-model="form.schlagwoerter_en" class="w-100"></Textarea>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4abstractGer')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4abstractGer') )}}</div>
<div class="row">
<Textarea v-model="form.abstract" rows="10"></Textarea>
<Textarea v-model="form.abstract" rows="10" maxlength="5000" class="w-100"></Textarea>
<p>{{ form.abstract?.length ? form.abstract.length : 0 }} / 5000 characters</p>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4abstractEng')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4abstractEng') )}}</div>
<div class="row">
<Textarea v-model="form.abstract_en" rows="10"></Textarea>
<Textarea v-model="form.abstract_en" rows="10" maxlength="5000" class="w-100"></Textarea>
<p>{{ form.abstract_en?.length ? form.abstract_en.length : 0 }} / 5000 characters</p>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4seitenanzahl')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4seitenanzahl') )}}</div>
<div class="row">
<InputNumber
v-model="form.seitenanzahl"
@@ -353,7 +629,7 @@ export const AbgabeStudentDetail = {
<div v-html="getEid"></div>
<div class="row">
<div class="col-9"></div>
<div class="col-2"><p>{{ $p.t('abgabetool/c4gelesenUndAkzeptiert') }}</p></div>
<div class="col-2"><p>{{$capitalize( $p.t('abgabetool/c4gelesenUndAkzeptiert') )}}</p></div>
<div class="col-1">
<Checkbox
@@ -368,7 +644,8 @@ export const AbgabeStudentDetail = {
</template>
<template v-slot:footer>
<button class="btn btn-primary" :disabled="getEnduploadErlaubt" @click="triggerEndupload">{{$p.t('ui/hochladen')}}</button>
<div v-show="!allowedToSaveZusatzdaten">{{ $p.t('abgabetool/c4zusatzdatenausfuellen') }}</div>
<button class="btn btn-primary" :disabled="!getAllowedToSendEndupload" @click="triggerEndupload">{{$capitalize( $p.t('ui/hochladen') )}}</button>
</template>
</bs-modal>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,13 +1,24 @@
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
import AbgabeDetail from "./AbgabeStudentDetail.js";
import VerticalSplit from "../../verticalsplit/verticalsplit.js";
import ApiAbgabe from '../../../api/factory/abgabe.js'
import BsModal from "../../Bootstrap/Modal.js";
import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass} from "./getDateStyleClass.js";
export const AbgabetoolStudent = {
name: "AbgabetoolStudent",
components: {
CoreFilterCmpt,
Accordion: primevue.accordion,
AccordionTab: primevue.accordiontab,
BsModal,
AbgabeDetail,
VerticalSplit
FhcOverlay
},
provide() {
return {
notenOptions: Vue.computed(() => this.notenOptions),
isViewMode: Vue.computed(() => this.isViewMode),
moodle_link: Vue.computed(() => this.moodle_link)
}
},
props: {
student_uid_prop: {
@@ -24,147 +35,168 @@ export const AbgabetoolStudent = {
},
data() {
return {
tabulatorUuid: Vue.ref(0),
domain: '',
student_uid: null,
activeTabIndex: [0],
abgabeTypeOptions: null,
phrasenPromise: null,
phrasenResolved: false,
loading: false,
notenOptions: null,
detail: null,
projektarbeiten: null,
selectedProjektarbeit: null,
tableBuiltResolve: null,
tableBuiltPromise: null,
abgabeTableOptions: {
minHeight: 250,
index: 'projektarbeit_id',
layout: 'fitColumns',
placeholder: this.$p.t('global/noDataAvailable'),
columns: [
{title: Vue.computed(() => this.$p.t('abgabetool/c4details')), field: 'details', formatter: this.detailFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4beurteilung')), field: 'beurteilung', formatter: this.beurteilungFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4sem')), field: 'sem', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4stg')), field: 'stg', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4kontakt')), field: 'mail', formatter: this.mailFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4betreuer')), field: 'betreuer', formatter: this.centeredTextFormatter,widthGrow: 2},
{title: Vue.computed(() => this.$p.t('abgabetool/c4projekttyp')), field: 'typ', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4titel')), field: 'titel', formatter: this.centeredTextFormatter, widthGrow: 8}
],
persistence: false,
},
abgabeTableEventHandlers: [{
event: "tableBuilt",
handler: async () => {
this.tableBuiltResolve()
}
},
{
event: "cellClick",
handler: async (e, cell) => {
if(cell.getColumn().getField() === "details") {
const val = cell.getValue()
if(val.mode === 'detailTermine') {
this.setDetailComponent(cell.getValue())
} else if (val.mode === 'beurteilungDownload') {
const pdfExportLink = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cis/private/pdfExport.php?xml=projektarbeitsbeurteilung.xml.php&xsl=Projektbeurteilung&betreuerart_kurzbz='+val.betreuerart_kurzbz+'&projektarbeit_id='+val.projektarbeit_id+'&person_id=' + val.betreuer_person_id
// const pdfExportLink2 = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cis/private/lehre/projektbeurteilungDocumentExport.php?betreuerart_kurzbz='+val.betreuerart_kurzbz+'&projektarbeit_id='+val.projektarbeit_id+'&person_id=' + val.betreuer_person_id
window.open(pdfExportLink, '_blank')
}
} else if (cell.getColumn().getField() === "beurteilung") {
const val = cell.getValue()
if(val != '-') window.open(val, '_blank')
}
e.stopPropagation()
}
}
]};
moodle_link: null
};
},
methods: {
checkQualityGatesStrict(termine) {
let qgate1Passed = false
let qgate2Passed = false
termine.forEach(t => {
const noteOption = this.notenOptions?.find(opt => opt.note == t.note)
if(noteOption && noteOption.positiv) {
if(t.paabgabetyp_kurzbz == 'qualgate1') {
qgate1Passed = true
} else if(t.paabgabetyp_kurzbz == 'qualgate2') {
qgate2Passed = true
}
}
})
return qgate1Passed && qgate2Passed
},
checkQualityGatesOptional(termine) {
const qgate1found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate1')
const qgate2found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate2')
let qgate1positiv = true
if(qgate1found) {
qgate1positiv = false
termine.forEach(t => {
const noteOption = this.notenOptions?.find(opt => opt.note == t.note)
if(noteOption && noteOption.positiv) {
if (t.paabgabetyp_kurzbz == 'qualgate1') {
qgate1positiv = true
}
}
})
}
let qgate2positiv = true
if(qgate2found) {
qgate2positiv = false
termine.forEach(t => {
const noteOption = this.notenOptions?.find(opt => opt.note == t.note)
if(noteOption && noteOption.positiv) {
if (t.paabgabetyp_kurzbz == 'qualgate2') {
qgate2positiv = true
}
}
})
}
return qgate1positiv && qgate2positiv
},
isPastDate(date) {
return new Date(date) < new Date(Date.now())
const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day');
const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna');
return nowInVienna > deadline;
},
setDetailComponent(details){
this.loading = true
this.loadAbgaben(details).then((res)=> {
const pa = this.projektarbeiten?.retval?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
const pa = this.projektarbeiten?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
pa.abgabetermine = res.data[0].retval
const paIsBenotet = pa.note !== null
pa.abgabetermine.forEach(termin => {
termin.file = []
termin.allowedToUpload = true
termin.allowedToUpload = false
// TODO: fixtermin logic?
if(termin.bezeichnung == 'Endupload' && this.isPastDate(termin.datum)) {
if(termin.paabgabetyp_kurzbz == 'end') {
// old assumed production logic when qgates are required
// termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesStrict(pa.abgabetermine)
// termin.allowedToUpload = false
} else {
const inTime = termin.fixtermin ? !this.isPastDate(termin.datum) : true
termin.allowedToUpload = inTime && this.checkQualityGatesOptional(pa.abgabetermine)
// development purposes
// termin.allowedToUpload = this.checkQualityGatesStrict(pa.abgabetermine)
// termin.allowedToUpload = true
} else if(termin.fixtermin) {
termin.allowedToUpload = !this.isPastDate(termin.datum)
} else {
// this could confuse people since we should dont show people this flag
termin.allowedToUpload = termin.upload_allowed
}
// blocks client upload button if projektarbeitet is already beurteilt und thus further abgaben on any termin should be blocked
if(paIsBenotet) termin.allowedToUpload = false
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
})
pa.betreuer = this.buildBetreuer(pa)
pa.student_uid = this.student_uid
this.selectedProjektarbeit = pa
this.$refs.modalContainerAbgabeDetail.show()
this.$refs.verticalsplit.showBoth()
})
}).finally(()=>{this.loading=false})
},
centeredTextFormatter(cell) {
const val = cell.getValue()
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<p style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+val+'</p></div>'
},
detailFormatter(cell) {
const val = cell.getValue()
if(val.mode === 'detailTermine') {
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a><i class="fa fa-folder-open" style="color:#00649C"></i></a></div>'
} else if (val.mode === 'beurteilungDownload') {
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a><i class="fa fa-file-pdf" style="color:#00649C"></i></a></div>'
}
},
mailFormatter(cell) {
const val = cell.getValue()
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
},
beurteilungFormatter(cell) {
const val = cell.getValue()
if(val) {
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a><i class="fa fa-file-pdf" style="color:#00649C"></i></a></div>'
} else return '-'
},
tableResolve(resolve) {
this.tableBuiltResolve = resolve
},
buildMailToLink(abgabe) {
return 'mailto:' + abgabe.mitarbeiter_uid +'@'+ this.domain
buildMailToLink(projekt) {
// should always be "projekt.mitarbeiter_uid +'@'+ this.domain", built in backend
return 'mailto:' + projekt.email
},
buildBetreuer(abgabe) {
return abgabe.betreuerart_beschreibung + ': ' + (abgabe.btitelpre ? abgabe.btitelpre + ' ' : '') + abgabe.bvorname + ' ' + abgabe.bnachname + (abgabe.btitelpost ? ' ' + abgabe.btitelpost : '')
return (abgabe.btitelpre ? abgabe.btitelpre + ' ' : '') + abgabe.bvorname + ' ' + abgabe.bnachname + (abgabe.btitelpost ? ' ' + abgabe.btitelpost : '')
},
setupData(data){
this.projektarbeiten = data[0]
this.domain = data[1]
this.student_uid = data[2]
const d = data[0]?.retval?.map(projekt => {
async setupData(data){
// this.projektarbeiten = data[0]
const projektarbeiten = data[0] ?? null
if(!projektarbeiten) return
this.projektarbeiten = projektarbeiten.map(projekt => {
let mode = 'detailTermine'
if (projekt.babgeschickt || projekt.zweitbetreuer_abgeschickt) {
// mode = 'beurteilungDownload' // build dl link for both betreuer documents
projekt.beurteilungLink = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cis/private/pdfExport.php?xml=projektarbeitsbeurteilung.xml.php&xsl=Projektbeurteilung&betreuerart_kurzbz='+projekt.betreuerart_kurzbz+'&projektarbeit_id='+projekt.projektarbeit_id+'&person_id=' + projekt.bperson_id
}
return {
...projekt,
details: {
student_uid: this.student_uid,
projektarbeit_id: projekt.projektarbeit_id,
@@ -172,7 +204,8 @@ export const AbgabetoolStudent = {
betreuerart_kurzbz: projekt.betreuerart_kurzbz,
mode
},
beurteilung: projekt.beurteilungLink ?? null,
beurteilung1: projekt.downloadLink1 ?? null,
beurteilung2: projekt.downloadLink2 ?? null,
sem: projekt.studiensemester_kurzbz,
stg: projekt.kurzbzlang,
mail: this.buildMailToLink(projekt),
@@ -182,43 +215,49 @@ export const AbgabetoolStudent = {
}
})
this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns)
this.$refs.abgabeTable.tabulator.setData(d);
},
loadProjektarbeiten() {
this.$fhcApi.factory.lehre.getStudentProjektarbeiten(this.student_uid_prop || this.viewData?.uid || null)
this.$api.call(ApiAbgabe.getStudentProjektarbeiten(this.student_uid))
.then(res => {
if(res?.data) this.setupData(res.data)
})
},
loadAbgaben(details) {
return new Promise((resolve) => {
this.$fhcApi.factory.lehre.getStudentProjektabgaben(details)
this.$api.call(ApiAbgabe.getStudentProjektabgaben(details))
.then(res => {
resolve(res)
})
})
},
handleUuidDefined(uuid) {
this.tabulatorUuid = uuid
},
calcMaxTableHeight() {
const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : ''
const tableDataSet = document.getElementById('filterTableDataset' + tableID);
if(!tableDataSet) return
const rect = tableDataSet.getBoundingClientRect();
this.abgabeTableOptions.height = window.visualViewport.height - rect.top
this.$refs.abgabeTable.tabulator.setHeight(this.abgabeTableOptions.height)
},
async setupMounted() {
this.tableBuiltPromise = new Promise(this.tableResolve)
await this.tableBuiltPromise
this.loadProjektarbeiten()
this.$refs.verticalsplit.collapseBottom()
//this.calcMaxTableHeight()
},
getAccTabHeaderForProjektarbeit(projektarbeit) {
let title = ''
title += projektarbeit.titel ?? this.$p.t('abgabetool/keinTitel')
return title
},
getMailLink(projektarbeit) {
if(projektarbeit.email) {
return 'mailto:'+projektarbeit.email
} else return ''
},
getNoteBezeichnung(projektarbeit) {
if(projektarbeit.note && this.notenOptions) {
const noteOpt = this.notenOptions.find(opt => opt.note == projektarbeit.note)
return noteOpt?.bezeichnung
} else {
return ''
}
},
handleDownloadBeurteilung1(projektarbeit) {
window.open(projektarbeit.beurteilung1)
},
handleDownloadBeurteilung2(projektarbeit) {
window.open(projektarbeit.beurteilung2)
}
},
watch: {
@@ -227,37 +266,152 @@ export const AbgabetoolStudent = {
computed: {
isViewMode() {
return this.student_uid !== this.viewData.uid
},
student_uid() {
return this.student_uid_prop || this.viewData?.uid || null
}
},
created() {
async created() {
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
this.loading = true
//TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API
await this.$api.call(ApiAbgabe.getNoten()).then(res => {
if(res.meta.status == 'success') {
this.notenOptions = res.data[0]
this.allowedNotenOptions = this.notenOptions.filter(
opt => res.data[1].includes(opt.note)
)
}
}).finally(() => {
this.loading = false
})
// fetch abgabetypen options
this.$api.call(ApiAbgabe.getPaAbgabetypen()).then(res => {
this.abgabeTypeOptions = res.data
}).catch(e => {
this.loading = false
})
// fetch config to avoid hard coded links
this.$api.call(ApiAbgabe.getConfigStudent()).then(res => {
this.moodle_link = res.data?.moodle_link
}).catch(e => {
this.loading = false
})
},
mounted() {
this.setupMounted()
},
template: `
<vertical-split ref="verticalsplit">
<template #top>
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
<hr>
<core-filter-cmpt
@uuidDefined="handleUuidDefined"
:title="''"
ref="abgabeTable"
:tabulator-options="abgabeTableOptions"
:tabulator-events="abgabeTableEventHandlers"
tableOnly
:sideMenu="false"
/>
</template>
<template #bottom>
<div v-show="selectedProjektarbeit">
<AbgabeDetail :viewMode="isViewMode" :projektarbeit="selectedProjektarbeit"></AbgabeDetail>
</div>
<template v-if="phrasenResolved">
<FhcOverlay :active="loading"></FhcOverlay>
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
dialogClass="modal-xl" :allowFullscreenExpand="true">
<template v-slot:title>
<div>
{{$capitalize( $p.t('abgabetool/c4abgabeStudentDetailTitle') )}}
</div>
</template>
</vertical-split>
<template v-slot:default>
<AbgabeDetail :projektarbeit="selectedProjektarbeit"></AbgabeDetail>
</template>
</bs-modal>
<h2>{{$capitalize( $p.t('abgabetool/abgabetoolTitle') )}}</h2>
<hr>
<div v-if="projektarbeiten === null">
{{$capitalize( $p.t('abgabetool/c4abgabeStudentNoProjectsFound') )}}
</div>
<Accordion :multiple="true" :activeIndex="activeTabIndex">
<template v-for="projektarbeit in projektarbeiten" :key="projektarbeit.projektarbeit_id">
<AccordionTab>
<template #header>
<div class="d-flex row w-100">
<div class="text-start" :class="projektarbeit.note != null ? 'col-6' : 'col-12'">
<span>{{getAccTabHeaderForProjektarbeit(projektarbeit)}}</span>
</div>
<div class="col-6 text-end">
<span>{{getNoteBezeichnung(projektarbeit)}}</span>
</div>
</div>
</template>
<div class="row">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4details') )}}</div>
<div class="col-8 col-md-9">
<button @click="setDetailComponent(projektarbeit.details)" class="btn btn-primary">
{{$capitalize( $p.t('abgabetool/c4projektdetailsOeffnen') )}} <a><i class="fa fa-folder-open"></i></a>
</button>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4beurteilung') )}}</div>
<div class="col-8 col-md-9">
<button v-if="projektarbeit.beurteilung1" @click="handleDownloadBeurteilung1(projektarbeit)" class="btn btn-primary">
<a> {{$capitalize( $p.t('abgabetool/c4downloadBeurteilungErstbetreuerv2') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button>
<a v-else>{{$capitalize( $p.t('abgabetool/c4nobeurteilungVorhanden') )}}</a>
<button v-if="projektarbeit.beurteilung2" @click="handleDownloadBeurteilung2(projektarbeit)" class="btn btn-primary" style="margin-left: 4px;">
<a> {{$capitalize( $p.t('abgabetool/c4downloadBeurteilungZweitbetreuerv2') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4sem') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.sem }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4stg') )}}</div>
<div class="col-8 col-md-9">
<div class="col-1 d-flex justify-content-start align-items-start">
{{ projektarbeit.stg }}
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ projektarbeit?.betreuerart_kurzbz ? $capitalize( $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) ) : $capitalize( $p.t('abgabetool/c4betreuerv2') ) }}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.betreuerart_kurzbz ? projektarbeit.betreuer : '' }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4betreuerEmailKontaktv2') )}}</div>
<div class="col-8 col-md-9">
<a :href="getMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
</div>
</div>
<div v-if="projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer" class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ projektarbeit.zweitbetreuer_betreuerart_kurzbz ? $p.t('abgabetool/c4betrart' + projektarbeit.zweitbetreuer_betreuerart_kurzbz) : '' }}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.zweitbetreuer?.first }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4projekttyp') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.projekttypbezeichnung }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4titel') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.titel }}
</div>
</div>
</AccordionTab>
</template>
</Accordion>
</template>
`,
};
@@ -1,4 +1,5 @@
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
import ApiAbgabe from '../../../api/factory/abgabe.js'
export const DeadlineOverview = {
name: "DeadlineOverview",
@@ -25,19 +26,21 @@ export const DeadlineOverview = {
tabulatorUuid: Vue.ref(0),
tableBuiltResolve: null,
tableBuiltPromise: null,
phrasenPromise: null,
phrasenResolved: false,
deadlineTableOptions: {
height: 700,
index: 'projektarbeit_id',
layout: 'fitColumns',
placeholder: this.$p.t('global/noDataAvailable'),
placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')),
columns: [
{title: Vue.computed(() => this.$p.t('abgabetool/c4zieldatum')), field: 'datum', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4fixtermin')), field: 'fixterminstring', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4abgabetyp')), field: 'typ_bezeichnung', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4abgabekurzbz')), field: 'kurzbz', formatter: this.centeredTextFormatter, widthGrow: 3},
{title: Vue.computed(() => this.$p.t('person/studentIn')), field: 'student', formatter: this.centeredTextFormatter, widthGrow: 2},
{title: Vue.computed(() => this.$p.t('abgabetool/c4stg')), field: 'stg', formatter: this.centeredTextFormatter,widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4sem')), field: 'semester', formatter: this.centeredTextFormatter, widthGrow: 1}
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zieldatumv2'))), field: 'datum', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4fixterminv4'))), field: 'fixterminstring', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabetyp'))), field: 'typ_bezeichnung', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabekurzbzv2'))), field: 'kurzbz', formatter: this.centeredTextFormatter, widthGrow: 3},
{title: Vue.computed(() => this.$capitalize(this.$p.t('person/studentIn'))), field: 'student', formatter: this.centeredTextFormatter, widthGrow: 2},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', formatter: this.centeredTextFormatter,widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'semester', formatter: this.centeredTextFormatter, widthGrow: 1}
],
persistence: false,
},
@@ -84,7 +87,7 @@ export const DeadlineOverview = {
this.tableBuiltResolve = resolve
},
loadDeadlines() {
this.$fhcApi.factory.lehre.fetchDeadlines(this.person_uid_prop ?? null)
this.$api.call(ApiAbgabe.fetchDeadlines(this.person_uid_prop ?? null))
.then(res => {
if(res?.data) this.setupData(res.data)
})
@@ -109,7 +112,7 @@ export const DeadlineOverview = {
if(!tableDataSet) return
const rect = tableDataSet.getBoundingClientRect();
this.deadlineTableOptions.height = window.visualViewport.height - rect.top
this.deadlineTableOptions.height = window.visualViewport.height - rect.top - 30
this.$refs.deadlineTable.tabulator.setHeight(this.deadlineTableOptions.height)
},
async setupMounted() {
@@ -127,7 +130,8 @@ export const DeadlineOverview = {
},
created() {
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
},
mounted() {
this.setupMounted()
@@ -0,0 +1,83 @@
export const AbgabeterminStatusLegende = {
name: 'AbgabeterminStatusLegende',
template: `
<div class="text-center">
<div class="col" style="width: 80%; margin-left: 12px;">
<div class="row" style="margin-bottom: 2px">
<div class="col-auto verspaetet-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-triangle-exclamation"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipVerspaetet')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto verpasst-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-calendar-xmark"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipVerpasst')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto abzugeben-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-hourglass-half"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipAbzugeben')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto standard-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-clock"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipStandardv2')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto abgegeben-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-paperclip"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipAbgegeben')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto beurteilungerforderlich-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-list-check"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipBeurteilungerforderlich')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto bestanden-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-check"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipBestanden')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto nichtbestanden-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-circle-exclamation"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipNichtBestanden')) }}</h5>
</div>
</div>
</div>
</div>
`
};
export default AbgabeterminStatusLegende;
@@ -0,0 +1,37 @@
const zone = 'Europe/Vienna';
const today = luxon.DateTime.now().setZone(zone);
export function getDateStyleClass(termin, notenOptions) {
const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day');
const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null;
termin.diffindays = datum.diff(today, 'days').days;
const isLate = abgabedatum && abgabedatum > datum;
// GRADE STATUS
if (termin.note) {
const opt = typeof termin.note === 'object' ? termin.note : notenOptions.find(nopt => nopt.note == termin.note)
if (opt?.positiv === true) return 'bestanden';
else if (opt?.positiv === false) return 'nichtbestanden';
}
// ACTION REQUIRED FOR GRADE
if (termin.bezeichnung?.benotbar && datum <= today) {
return 'beurteilungerforderlich';
}
// SUBMISSION STATUS
if (termin.upload_allowed) {
if (termin.abgabedatum) {
return isLate ? 'verspaetet' : 'abgegeben';
}
// no submission yet
if (datum < today) return 'verpasst';
if (termin.diffindays <= 12) return 'abzugeben';
return 'standard';
}
// GENERIC STATUS
return datum < today ? 'verpasst' : 'standard';
}
@@ -64,7 +64,7 @@ export default {
<span>{{ start }}</span>
<span>{{ end }}</span>
</div>
<div class="event-text">
<div class="event-text" v-tooltip="tooltipString">
<span class="event-topic">{{ event.topic }}</span>
<span class="event-place">{{ event.ort_kurzbz }}</span>
<span
+66 -34
View File
@@ -3,15 +3,20 @@ import DashboardAdminEdit from "./Admin/Edit.js";
import DashboardAdminWidgets from "./Admin/Widgets.js";
import DashboardAdminPresets from "./Admin/Presets.js";
import ApiDashboardBoard from "../../api/factory/dashboard/board.js";
import ApiDashboardWidget from "../../api/factory/dashboard/widget.js";
export default {
name: 'DashboardAdmin',
components: {
DashboardAdminEdit,
DashboardAdminWidgets,
DashboardAdminPresets
DashboardAdminPresets,
},
provide() {
return {
adminMode: true
adminMode: true,
widgetsSetup: Vue.computed(() => this.dashboards[this.current] ? this.dashboards[this.current].widgetSetup : null)
};
},
data() {
@@ -22,9 +27,6 @@ export default {
};
},
computed: {
apiurl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard';
},
dashboard() {
return this.dashboards.find(el => el.dashboard_id == this.current);
}
@@ -35,33 +37,50 @@ export default {
BsPrompt.popup('New Dashboard name').then(
name => {
_name = name;
return axios.post(this.apiurl + '/Dashboard/create', {
const params = {
dashboard_kurzbz: name
})
}
).then(res => {
let newDashboard = {
dashboard_id: res.data.retval,
dashboard_kurzbz: _name,
beschreibung: ''
};
this.dashboards.push(newDashboard);
this.current = newDashboard.dashboard_id;
}).catch(err => err !== undefined ? console.error('ERROR:', err) : 0);
};
return this.$api
.call(ApiDashboardBoard.add(params))
.then(response =>{
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
let newDashboard = {
dashboard_id: response.data,
dashboard_kurzbz: _name,
beschreibung: ''
};
this.dashboards.push(newDashboard);
this.current = newDashboard.dashboard_id;
})
.catch(this.$fhcAlert.handleSystemError);
});
},
dashboardUpdate(dashboard) {
// TODO(chris): Loading or message
axios.post(this.apiurl + '/Dashboard/update', dashboard).then(() => {
let old = this.dashboards.find(el => el.dashboard_id == dashboard.dashboard_id);
old.dashboard_kurzbz = dashboard.dashboard_kurzbz;
old.beschreibung = dashboard.beschreibung;
}).catch(err => console.error('ERROR:', err));
return this.$api
.call(ApiDashboardBoard.update(dashboard))
.then(response =>{
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
let old = this.dashboards.find(el => el.dashboard_id == dashboard.dashboard_id);
old.dashboard_kurzbz = dashboard.dashboard_kurzbz;
old.beschreibung = dashboard.beschreibung;
})
.catch(this.$fhcAlert.handleSystemError);
},
dashboardDelete(dashboard_id) {
axios.post(this.apiurl + '/Dashboard/delete', {dashboard_id}).then(() => {
this.current = -1;
this.dashboards = this.dashboards.filter(el => el.dashboard_id != dashboard_id);
}).catch(err => console.error('ERROR:', err));
return this.$api
.call(ApiDashboardBoard.delete(dashboard_id))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => {
this.current = -1;
this.dashboards = this.dashboards.filter(el => el.dashboard_id != dashboard_id);
});
},
assignWidgets(widgets) {
this.widgets = widgets;
@@ -72,22 +91,35 @@ export default {
}
},
created() {
axios.get(this.apiurl + '/Dashboard').then(res => {
this.dashboards = res.data.retval;
}).catch(err => console.error('ERROR:', err));
this.$api
.call(ApiDashboardBoard.list())
.then(result => {
this.dashboards = result.data.retval;
for (const dashboard of this.dashboards) {
this.$api
.call(ApiDashboardWidget.list(dashboard.dashboard_id))
.then(res => {
dashboard.widgetSetup = res.data;
})
.catch(this.$fhcAlert.handleSystemError);
}
})
.catch(this.$fhcAlert.handleSystemError);
},
template: `<div class="dashboard-admin">
<div class="input-group">
<label for="dashbaord-select" class="input-group-text">Dashboard:</label>
<select id="dashbaord-select" class="form-select" v-model="current">
<label for="dashboard-select" class="input-group-text">Dashboard:</label>
<select id="dashboard-select" class="form-select" v-model="current">
<option v-for="dashboard in dashboards" :key="dashboard.dashboard_id" :value="dashboard.dashboard_id">{{dashboard.dashboard_kurzbz}}</option>
</select>
<button class="btn btn-outline-secondary" type="button" @click="dashboardAdd"><i class="fa-solid fa-plus"></i></button>
</div>
<div v-if="dashboard">
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link" id="edit-tab" data-bs-toggle="tab" data-bs-target="#edit" type="button" role="tab" aria-controls="edit" aria-selected="false">Edit</button>
<button class="nav-link" id="edit-tab" data-bs-toggle="tab" data-bs-target="#edit" type="button" role="tab" aria-controls="edit" aria-selected="false">{{this.$p.t('ui', 'bearbeiten')}}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link active" id="widgets-tab" data-bs-toggle="tab" data-bs-target="#widgets" type="button" role="tab" aria-controls="widgets" aria-selected="true">Widgets</button>
@@ -101,7 +133,7 @@ export default {
<dashboard-admin-edit v-bind="dashboard" :key="dashboard.dashboard_id" @change="dashboardUpdate($event)" @delete="dashboardDelete($event)"></dashboard-admin-edit>
</div>
<div class="tab-pane fade show active" id="widgets" role="tabpanel" aria-labelledby="widgets-tab">
<dashboard-admin-widgets :key="dashboard.dashboard_id" :dashboard_id="dashboard.dashboard_id" :widgets="widgets" @change="test" @assign-widgets="assignWidgets"></dashboard-admin-widgets>
<dashboard-admin-widgets :key="dashboard.dashboard_id" :dashboard_id="dashboard.dashboard_id" :widgets="widgets" @assign-widgets="assignWidgets"></dashboard-admin-widgets>
</div>
<div class="tab-pane fade" id="presets" role="tabpanel" aria-labelledby="presets-tab">
<dashboard-admin-presets :dashboard="dashboard.dashboard_kurzbz" :widgets="widgets"></dashboard-admin-presets>
+4 -3
View File
@@ -18,7 +18,8 @@ export default {
},
methods: {
sendDelete() {
BsConfirm.popup('Sure?').then(() => this.$emit('delete', this.dashboard_id)).catch();
BsConfirm.popup(this.$p.t('ui', 'confirm_delete') + " " + this.$p.t('ui', 'deleteInfo'))
.then(() => this.$emit('delete', this.dashboard_id)).catch();
}
},
template: `<div class="dashboard-admin-edit px-3">
@@ -31,8 +32,8 @@ export default {
<textarea id="dashboard-admin-edit-beschreibung" class="form-control" v-model="desc"></textarea>
</div>
<div>
<button class="btn btn-danger" @click="sendDelete">Delete</button>
<button class="btn btn-primary" @click="$emit('change', {dashboard_id,dashboard_kurzbz:kurzbz,beschreibung:desc})">Update</button>
<button class="btn btn-danger" @click="sendDelete">{{this.$p.t('ui', 'loeschen')}}</button>
<button class="btn btn-primary" @click="$emit('change', {dashboard_id,dashboard_kurzbz:kurzbz,beschreibung:desc})">{{this.$p.t('ui', 'btnAktualisieren')}}</button>
</div>
</div>`
}
+111 -89
View File
@@ -1,6 +1,7 @@
import DashboardSection from "../Section.js";
import DashboardWidgetPicker from "../Widget/Picker.js";
import ObjectUtils from "../../../helpers/ObjectUtils.js";
import ApiDashboardPreset from "../../../api/factory/dashboard/preset.js";
export default {
components: {
@@ -17,9 +18,6 @@ export default {
tmpLoading: ''
}),
computed: {
apiurl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard';
},
pickerWidgets() {
return this.widgets.filter(widget => widget.allowed);
}
@@ -28,6 +26,7 @@ export default {
widgetAdd(section_name, widget) {
this.$refs.widgetpicker.getWidget().then(widget_id => {
widget.widget = widget_id;
widget.id = 'loading_' + String((new Date()).valueOf());
delete widget.custom;
widget.preset = 1;
let loading = {...widget};
@@ -36,130 +35,153 @@ export default {
if (section.name == section_name)
section.widgets.push(loading);
});
axios.post(this.apiurl + '/Config/addWidgetsToPreset', {
db: this.dashboard,
const params = {
dashboard: this.dashboard,
funktion_kurzbz: section_name,
widgets: [widget]
}).then(result => {
let newId = Object.keys(result.data.retval.data[section_name].widgets).pop();
widget.id = newId;
widget.custom = 1;
this.sections.forEach(section => {
if (section.name == section_name) {
section.widgets.splice(section.widgets.indexOf(loading),1);
section.widgets.push(widget);
}
});
}).catch(error => {
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
}).catch(() => {});
widget
};
return this.$api
.call(ApiDashboardPreset.addWidget(params))
.then(result => {
let newId = result.data;
widget.id = newId;
widget.custom = 1;
this.sections.forEach(section => {
if (section.name == section_name) {
section.widgets.splice(section.widgets.indexOf(loading),1);
section.widgets.push(widget);
}
});
this.funktionen.forEach(funktion => {
if(funktion.funktion_kurzbz === section_name && funktion.has_preset < 1) {
funktion.has_preset = 1;
}
});
})
.catch(this.$fhcAlert.handleSystemError);
})
.catch(() => {});
},
widgetUpdate(section_name, payload) {
payload = payload[section_name];
for (var k in payload) {
for (var i in this.sections) {
if (this.sections[i].name == section_name) {
for (var wid in this.sections[i].widgets) {
if (this.sections[i].widgets[wid].id == k) {
payload[k] = ObjectUtils.mergeDeep(this.sections[i].widgets[wid], payload[k]);
// NOTE(chris): remove internal props
for (var prop in {_x:1,_y:1,_w:1,_h:1,index:1,id:1})
if (payload[k][prop])
delete payload[k][prop];
break;
}
}
const section = this.sections.find(section => section.name == section_name);
for (var wid in section.widgets) {
if (section.widgets[wid].id == k) {
payload[k] = ObjectUtils.mergeDeep(section.widgets[wid], payload[k]);
// NOTE(chris): remove internal props
for (var prop of ['_x', '_y', '_w', '_h', 'index', 'id'])
if (payload[k][prop])
delete payload[k][prop];
break;
}
}
payload[k].widgetid = k;
delete payload[k].custom;
}
axios.post(this.apiurl + '/Config/addWidgetsToPreset', {
db: this.dashboard,
funktion_kurzbz: section_name,
widgets: payload
}).then(() => {
this.sections.forEach(section => {
if (section.name == section_name) {
section.widgets.forEach((widget, i) => {
if (payload[widget.id]) {
payload[widget.id].id = widget.id;
payload[widget.id].index = widget.index;
section.widgets[i] = payload[widget.id];
section.widgets[i].custom = 1;
}
});
}
});
}).catch(error => {
// TODO(chris): revert placement on failure
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
this.$api
.call(Object.entries(payload).map(([key, widget]) => [
key,
ApiDashboardPreset.addWidget({
dashboard: this.dashboard,
funktion_kurzbz: section_name,
widget
})
]))
.then(result => {
this.sections.forEach(section => {
if (section.name == section_name) {
section.widgets.forEach((widget, i) => {
if (payload[widget.id]) {
payload[widget.id].id = widget.id;
payload[widget.id].index = widget.index;
section.widgets[i] = payload[widget.id];
section.widgets[i].custom = 1;
}
});
}
});
})
.catch(this.$fhcAlert.handleSystemError);
},
widgetRemove(section_name, id) {
axios.post(this.apiurl + '/Config/removeWidgetFromPreset', {
const params = {
db: this.dashboard,
funktion_kurzbz: section_name,
widgetid: id
}).then(() => {
this.sections.forEach(section => {
if (section.name == section_name)
section.widgets = section.widgets.filter(widget => widget.id != id);
});
}).catch(error => {
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
};
return this.$api
.call(ApiDashboardPreset.removeWidget(params))
.then(result => {
this.sections.forEach(section => {
if (section.name == section_name)
section.widgets = section.widgets.filter(widget => widget.id != id);
});
})
.catch(this.$fhcAlert.handleSystemError);
},
loadSections(evt) {
let funktionen = Array.from(evt.target.querySelectorAll("option:checked"),e=>e.value);
this.sections = [];
this.tmpLoading = funktionen.join('###');
axios.get(this.apiurl + '/Config/presetBatch', {params: {
const params = {
db: this.dashboard,
funktionen
}}).then(res => {
if (this.tmpLoading !== funktionen.join('###'))
return; // NOTE(chris): prevent race condition
for (var section in res.data.retval) {
let widgets = [];
for (var wid in res.data.retval[section]) {
res.data.retval[section][wid].id = wid;
res.data.retval[section][wid].custom = 1;
widgets.push(res.data.retval[section][wid]);
};
return this.$api
.call(ApiDashboardPreset.getBatch(params))
.then(result => {
if (this.tmpLoading !== funktionen.join('###'))
return; // NOTE(chris): prevent race condition
for (var section in result.data) {
let widgets = [];
for (var wid in result.data[section]) {
result.data[section][wid].id = wid;
result.data[section][wid].custom = 1;
widgets.push(result.data[section][wid]);
}
this.sections.push({
name: section,
widgets
});
}
this.sections.push({
name: section,
widgets
});
}
}).catch(err => console.error('ERROR:', err));
})
.catch(this.$fhcAlert.handleSystemError);
},
loadFunktionen() {
this.$api
.call(ApiDashboardPreset.list(this.dashboard))
.then(result => {
this.funktionen = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
}
},
created() {
axios.get(this.apiurl + '/Config/funktionen').then(res => {
this.funktionen = {general: 'GENERAL'};
res.data.retval.forEach(funktion => {
this.funktionen[funktion.funktion_kurzbz] = funktion.beschreibung;
});
}).catch(err => console.error('ERROR:', err));
this.loadFunktionen();
},
watch: {
dashboard() {
// TODO(chris): this should be done without a watcher
this.loadSections({target:this.$refs.funktionenList});
this.loadFunktionen();
}
},
template: `<div class="dashboard-admin-presets">
<div class="row">
<div class="col-3">
<select ref="funktionenList" style="height:30em" class="form-control" multiple @input="loadSections">
<option v-for="name,id in funktionen" :key="id" :value="id">{{ name }}</option>
<option
v-for="funktion in funktionen"
:key="funktion.funktion_kurzbz"
:value="funktion.funktion_kurzbz"
:class="(funktion.has_preset > 0) ? 'fw-bold' : ''"
>{{ funktion.beschreibung }}</option>
</select>
</div>
<div class="col-9">
+13 -20
View File
@@ -1,3 +1,5 @@
import ApiDashboardWidget from "../../../api/factory/dashboard/widget.js";
export default {
emits: [
"change",
@@ -7,34 +9,25 @@ export default {
dashboard_id: Number,
widgets: Array
},
computed: {
apiurl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard';
}
},
methods: {
sendChange(widget_id) {
let allow = !this.widgets.find(el => el.widget_id == widget_id).allowed;
axios.post(this.apiurl + '/Widget/setAllowed', {
dashboard_id: this.dashboard_id,
widget_id,
action: allow ? 'add' : 'delete'
}).catch(err => console.error('ERROR: ' + err));
this.$api
.call(ApiDashboardWidget.setAllowed(this.dashboard_id, widget_id, allow))
.catch(this.$fhcAlert.handleSystemError);
}
},
created() {
axios.get(this.apiurl + '/Widget/getAll', {
params:{
dashboard_id: this.dashboard_id
}
}).then(
result => {
this.$emit('assignWidgets', result.data.retval.map(el => ({
this.$api
.call(ApiDashboardWidget.list(this.dashboard_id))
.then(result => {
this.$emit('assignWidgets', result.data.map(el => ({
...el,
...{setup:JSON.parse(el.setup),arguments:JSON.parse(el.arguments),allowed:!!el.allowed}
allowed: !!el.allowed
})));
}
).catch(err => console.error('ERROR:', err));
})
.catch(this.$fhcAlert.handleSystemError);
},
template: `
<div class="dashboard-admin-widgets">
+105 -142
View File
@@ -2,7 +2,8 @@ import DashboardSection from "./Section.js";
import DashboardWidgetPicker from "./Widget/Picker.js";
import ObjectUtils from "../../helpers/ObjectUtils.js";
import ApiDashboard from '../../api/factory/cis/dashboard.js';
import ApiDashboardWidget from '../../api/factory/dashboard/widget.js';
import ApiDashboardUser from '../../api/factory/dashboard/user.js';
export default {
name: 'Dashboard',
@@ -20,185 +21,147 @@ export default {
type: Object,
required: true,
validator(value) {
return value && value.name && value.uid && value.timezone
return value && value.name && value.timezone
}
}
},
data() {
return {
sections: [],
widgets: null,
editMode: false,
viewDataInternal: this.viewData
widgets: [],
originalWidgets: {},
widgetsSetup: null,
editMode: false
}
},
provide() {
return {
editMode: Vue.computed(()=>this.editMode),
widgetsSetup: Vue.computed(() => this.widgets),
widgetsSetup: Vue.computed(() => this.widgetsSetup),
timezone: Vue.computed(() => this.viewData.timezone)
}
},
computed: {
apiurl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard';
}
},
methods: {
widgetAdd(section_name, widget) {
if (this.widgets === null) {
axios.get(this.apiurl + '/Widget/getWidgetsForDashboard', {params:{
db: this.dashboard
}}).then(res => {
res.data.retval.forEach(widget => {
widget.arguments = JSON.parse(widget.arguments);
widget.setup = JSON.parse(widget.setup);
});
this.widgets = res.data.retval;
}).catch(err => console.error('ERROR:', err));
}
this.$refs.widgetpicker.getWidget().then(widget_id => {
widget.widget = widget_id;
widget.id = 'loading_' + String((new Date()).valueOf());
let loading = {...widget};
loading.loading = true;
this.sections.forEach(section => {
if (section.name == section_name)
section.widgets.push(loading);
});
axios.post(this.apiurl + '/Config/addWidgetsToUserOverride', {
db: this.dashboard,
funktion_kurzbz: section_name,
widgets: [widget]
}).then(result => {
let newId = Object.keys(result.data.retval.data[section_name].widgets).pop();
widget.id = newId;
this.sections.forEach(section => {
if (section.name == section_name) {
section.widgets.splice(section.widgets.indexOf(loading),1);
section.widgets.push(widget);
}
});
}).catch(error => {
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
}).catch(() => {});
// TODO(chris): remove section_name? (change order of params => get rid of it)
this.$refs.widgetpicker
.getWidget()
.then(widget_id => {
widget.widget = widget_id;
widget.id = 'loading_' + String((new Date()).valueOf());
let loading = { ...widget };
loading.loading = true;
this.widgets.push(loading);
this.$api
.call(ApiDashboardUser.addWidget(this.dashboard, widget))
.then(result => {
widget.id = result.data;
this.widgets.splice(this.widgets.indexOf(loading), 1);
this.widgets.push(widget);
this.originalWidgets[widget.id] = structuredClone(ObjectUtils.deepToRaw(widget));
})
.catch(this.$fhcAlert.handleSystemError);
})
.catch(() => {});
},
widgetUpdate(section_name, payload) {
payload = payload[section_name];
for (var k in payload) {
for (var i in this.sections) {
if (this.sections[i].name == section_name) {
for (var wid in this.sections[i].widgets) {
if (this.sections[i].widgets[wid].id == k) {
payload[k] = ObjectUtils.mergeDeep(this.sections[i].widgets[wid], payload[k]);
// NOTE(chris): remove internal props
for (var prop in {_x:1,_y:1,_w:1,_h:1,index:1,id:1,preset:1})
if (payload[k][prop])
delete payload[k][prop];
break;
}
}
for (var wid in this.widgets) {
if (this.widgets[wid].id == k) {
payload[k] = ObjectUtils.mergeDeep(this.widgets[wid], payload[k]);
// NOTE(chris): remove internal props
for (var prop of ['_x','_y','_w','_h','index','id','preset'])
if (payload[k][prop])
delete payload[k][prop];
break;
}
}
payload[k].widgetid = k;
}
axios.post(this.apiurl + '/Config/addWidgetsToUserOverride', {
db: this.dashboard,
funktion_kurzbz: section_name,
widgets: payload
}).then(() => {
this.sections.forEach(section => {
if (section.name == section_name) {
section.widgets.forEach((widget, i) => {
if (payload[widget.id]) {
payload[widget.id].id = widget.id;
payload[widget.id].index = widget.index;
section.widgets[i] = payload[widget.id];
this.$api
.call(Object.entries(payload).map(([key, widget]) => [
key,
ApiDashboardUser.addWidget(this.dashboard, widget)
]))
.then(result => {
const failed = result
.filter(o => o.status == 'rejected')
.map(o => o.reason.config.errorHeader);
this.widgets.forEach((widget, i) => {
if (failed.includes(widget.id)) {
this.widgets[i] = structuredClone(ObjectUtils.deepToRaw(this.originalWidgets[widget.id]));
/** NOTE(chris): if you wanna hide or unhide a
* preset and it fails: switch around the hidden
* value to revert it properly (checkboxes can't
* really handle it otherwise)
*/
if (payload[widget.id].hidden !== undefined) {
this.widgets[i].hidden = payload[widget.id].hidden;
this.$nextTick(() => {
this.widgets[i] = structuredClone(ObjectUtils.deepToRaw(this.originalWidgets[widget.id]));
});
}
});
}
});
}).catch(error => {
// TODO(chris): revert placement on failure
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
} else if (payload[widget.id]) {
payload[widget.id].id = widget.id;
payload[widget.id].index = widget.index;
this.widgets[i] = payload[widget.id];
this.originalWidgets[widget.id] = structuredClone(ObjectUtils.deepToRaw(this.widgets[i]));
}
});
})
.catch(this.$fhcAlert.handleSystemError);
},
widgetRemove(section_name, id) {
axios.post(this.apiurl + '/Config/removeWidgetFromUserOverride', {
db: this.dashboard,
funktion_kurzbz: section_name,
widgetid: id
}).then(() => {
this.sections.forEach(section => {
if (section.name == section_name)
section.widgets = section.widgets.filter(widget => widget.id != id);
});
}).catch(error => {
console.error('ERROR: ', error);
alert('ERROR: ' + error.response.data.retval);
});
this.$api
.call(ApiDashboardUser.removeWidget(this.dashboard, id))
.then(() => {
this.widgets = this.widgets.filter(widget => widget.id != id);
})
.catch(this.$fhcAlert.handleSystemError);
}
},
created() {
this.$p.loadCategory('dashboard');
axios.get(this.apiurl + '/Widget/getWidgetsForDashboard', {
params: {
db: this.dashboard
}
}).then(res => {
res.data.retval.forEach(widget => {
widget.arguments = JSON.parse(widget.arguments);
widget.setup = JSON.parse(widget.setup);
});
this.widgets = res.data.retval;
}).catch(err => console.error('ERROR:', err));
axios.get(this.apiurl + '/Config', {params:{
db: this.dashboard
}}).then(res => {
for (var name in res.data.retval) {
let widgets = [];
let remove = [];
for (var wid in res.data.retval[name].widgets) {
res.data.retval[name].widgets[wid].id = wid;
if (res.data.retval[name].widgets[wid].custom || res.data.retval[name].widgets[wid].preset)
widgets.push(res.data.retval[name].widgets[wid]);
else
this.$api
.call(ApiDashboardWidget.listAllowed(this.dashboard))
.then(res => {
this.widgetsSetup = res.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api
.call(ApiDashboardUser.get(this.dashboard))
.then(res => {
const widgets = [];
const remove = [];
for (var wid in res.data.general.widgets) {
let widget = res.data.general.widgets[wid];
widget.id = wid;
if (widget.custom || widget.preset) {
widgets.push(widget);
this.originalWidgets[wid] = structuredClone(widget);
} else {
remove.push(wid);
}
}
this.sections.push({
name: name,
widgets: widgets
});
remove.forEach(wid => this.widgetRemove(name, wid));
}
this.sections = this.sections.sort((section1, section2) => {
if(section1.name == 'custom')
return 1;
if (section2.name == 'custom')
return -1;
return section2.widgets.length - section1.widgets.length;
});
}).catch(err => console.error('ERROR:', err));
},
async beforeMount() {
if (!this.viewData.name || !this.viewData.uid) {
const res = await this.$api.call(ApiDashboard.getViewData());
this.viewDataInternal = res.data
}
remove.forEach(wid => this.widgetRemove('general', wid));
this.widgets = widgets;
})
.catch(this.$fhcAlert.handleSystemError);
},
template: `
<div class="core-dashboard">
<h3 v-show="viewDataInternal?.name">
{{ $p.t('global/personalGreeting', [ viewDataInternal?.name ]) }}
<h3>
{{ $p.t('global/personalGreeting', [ viewData?.name ]) }}
<button style="margin-left: 8px;" class="btn" @click="editMode = !editMode" aria-label="edit dashboard" v-tooltip="{showDelay:1000,value:'edit dashboard'}"><i class="fa-solid fa-gear" aria-hidden="true"></i></button>
</h3>
<dashboard-section v-for="(section, index) in sections" :key="section.name" :seperator="index" :name="section.name" :widgets="section.widgets" @widgetAdd="widgetAdd" @widgetUpdate="widgetUpdate" @widgetRemove="widgetRemove"></dashboard-section>
<dashboard-widget-picker ref="widgetpicker" :widgets="widgets"></dashboard-widget-picker>
<dashboard-section :seperator="0" name="general" :widgets="widgets" @widgetAdd="widgetAdd" @widgetUpdate="widgetUpdate" @widgetRemove="widgetRemove"></dashboard-section>
<dashboard-widget-picker ref="widgetpicker" :widgets="widgetsSetup"></dashboard-widget-picker>
</div>`
}
+18 -4
View File
@@ -1,5 +1,5 @@
import BsModal from "../Bootstrap/Modal.js";
import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
import { useCachedWidgetLoader } from "../../composables/Dashboard/CachedWidgetLoader.js";
import HeightTransition from "../Tranistion/HeightTransition.js";
export default {
@@ -70,6 +70,14 @@ export default {
ready() {
return this.component && this.arguments !== null;
},
visible: {
get() {
return !this.hidden;
},
set(value) {
this.$emit('remove', this.hidden);
}
}
},
methods: {
unpin(){
@@ -142,9 +150,15 @@ export default {
this.isLoading = false;
},
},
setup() {
const { actions } = useCachedWidgetLoader();
return {
loadWidget: actions.load
};
},
async created() {
this.widget = await CachedWidgetLoader.loadWidget(this.id);
let component = (await import("../" + this.widget.setup.file)).default;
this.widget = await this.loadWidget(this.id);
let component = (await import(this.widget.setup.file)).default;
this.$options.components["widget" + this.widget.widget_id] = component;
this.component = "widget" + this.widget.widget_id;
this.arguments = { ...this.widget.arguments, ...this.config };
@@ -185,7 +199,7 @@ export default {
</a>
<Transition>
<div v-if="!custom && editMode" class="col-auto px-1 form-switch">
<input class="form-check-input ms-0" type="checkbox" role="switch" aria-label="toggle widget" id="flexSwitchCheckChecked" :checked="!hidden" @input="$emit('remove', hidden)">
<input class="form-check-input ms-0" type="checkbox" role="switch" aria-label="toggle widget" id="flexSwitchCheckChecked" v-model="visible" :value="true">
</div>
</Transition>
</div>
+14 -7
View File
@@ -1,7 +1,7 @@
import BsConfirm from "../Bootstrap/Confirm.js";
import DropGrid from '../Drop/Grid.js'
import DashboardItem from "./Item.js";
import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
import { useCachedWidgetLoader } from "../../composables/Dashboard/CachedWidgetLoader.js";
import WidgetIcon from "./Widget/WidgetIcon.js"
export default {
@@ -125,23 +125,23 @@ export default {
},
checkResizeLimit(item, w, h) {
// NOTE(chris): widgets needs to be loaded for this to work
let widget = CachedWidgetLoader.getWidget(item.widget);
let widget = this.widgetState[item.widget];
if (widget) {
let minmaxW = widget.setup.width;
let minmaxW = { ...widget.setup.width };
if (minmaxW.max)
minmaxW.min = minmaxW.min || 1;
else
minmaxW = {min:minmaxW,max:minmaxW};
minmaxW = { min: minmaxW, max: minmaxW };
if (w < minmaxW.min)
w = minmaxW.min;
if (w > minmaxW.max)
w = minmaxW.max;
let minmaxH = widget.setup.height;
let minmaxH = { ...widget.setup.height };
if (minmaxH.max)
minmaxH.min = minmaxH.min || 1;
else
minmaxH = {min:minmaxH,max:minmaxH};
minmaxH = { min: minmaxH, max: minmaxH };
if (h < minmaxH.min)
h = minmaxH.min;
if (h > minmaxH.max)
@@ -151,7 +151,7 @@ export default {
},
removeWidget(item, revert) {
if (item.custom) {
BsConfirm.popup('Are you sure you want to delete this widget?').then(() => this.$emit('widgetRemove', this.name, item.id));
BsConfirm.popup(this.$p.t('dashboard', 'alert_deleteWidget')).then(() => this.$emit('widgetRemove', this.name, item.id));
} else {
let update = {};
update[item.id] = { hidden: !revert };
@@ -199,6 +199,13 @@ export default {
this.$emit('widgetUpdate', this.name, payload);
}
},
setup() {
const { state: widgetState } = useCachedWidgetLoader();
return {
widgetState
};
},
mounted() {
let self = this;
let cont = self.$refs.container;
+243 -78
View File
@@ -1,12 +1,11 @@
import ApiDetailHeader from "../../api/factory/detailHeader.js";
import ApiHandleFoto from "../../api/factory/fotoHandling.js";
import ModalUploadFoto from "./Modal/UploadFoto.js";
export default {
name: 'DetailHeader',
inject: {
domain: {
from: 'configDomain',
default: 'technikum-wien.at'
},
components: {
ModalUploadFoto
},
props: {
headerData: {
@@ -17,6 +16,19 @@ export default {
type: Number,
required: false
},
mitarbeiter_uid: {
type: String,
required: false
},
fotoEditable: {
type: Boolean,
required: false,
default: false
},
domain: {
type: String,
required: false
},
typeHeader: {
type: String,
default: 'student',
@@ -36,31 +48,40 @@ export default {
if (this.typeHeader === 'student') return this.headerData;
if (this.typeHeader === 'mitarbeiter') return this.person_id;
return null;
}
},
hasTileAlphaSlot() {
return !!this.$slots.titleAlphaTile
},
hasTileBetaSlot() {
return !!this.$slots.titleBetaTile
},
hasTileGammaSlot() {
return !!this.$slots.titleGammaTile
},
hasTileUIDSlot() {
return !!this.$slots.uid
},
},
created(){
if(this.person_id) {
this.getHeader(this.person_id);
this.loadDepartmentData(this.mitarbeiter_uid)
.then(() => {
// Call getLeitungOrg only after departmentData is loaded
this.getLeitungOrg(this.departmentData.oe_kurzbz);
})
.catch((error) => {
console.error("Error loading department data:", error);
});
if (this.typeHeader === 'student') {
if (!this.headerData) {
throw new Error('[DetailHeader] "headerData" is required.')
}
} else if (this.typeHeader === 'mitarbeiter') {
if (!this.person_id || !this.mitarbeiter_uid || !this.domain) {
throw new Error(
'[DetailHeader] "person_id", "mitarbeiter_uid", and "domain" are requried.'
)
}
this.loadHeaderData(this.person_id, this.mitarbeiter_uid);
}
},
watch: {
person_id: {
handler(newVal) {
if (newVal) {
this.getHeader(this.person_id);
this.loadDepartmentData(this.person_id).
then(() => {
this.getLeitungOrg(this.departmentData.oe_kurzbz);
});
this.loadHeaderData(newVal, this.mitarbeiter_uid);
}
},
deep: true,
@@ -71,9 +92,21 @@ export default {
headerDataMa: {},
departmentData: {},
leitungData: {},
isFetchingIssues: false
};
},
methods: {
loadHeaderData(person_id, mitarbeiter_uid){
this.getHeader(person_id);
this.loadDepartmentData(mitarbeiter_uid)
.then(() => {
// Call getLeitungOrg only after departmentData is loaded
this.getLeitungOrg(this.departmentData.oe_kurzbz);
})
.catch((error) => {
console.error("Error loading header data: ", error);
});
},
getHeader(person_id) {
return this.$api
.call(ApiDetailHeader.getHeader(person_id))
@@ -99,9 +132,46 @@ export default {
})
.catch(this.$fhcAlert.handleSystemError);
},
redirectToLeitung(){
async goToLeitung() {
this.loadHeaderData(this.leitungData.person_id, this.leitungData.uid);
this.redirectToLeitung();
},
redirectToLeitung() {
this.$emit('redirectToLeitung', {
person_id: this.leitungData.person_id});
person_id: this.leitungData.person_id,
uid: this.leitungData.uid
});
},
showModal(person_id){
this.$refs.modalFoto.open(person_id);
},
showDeleteModal(person_id){
this.$fhcAlert
.confirmDelete()
.then(result => result
? person_id
: Promise.reject({handled: true}))
.then(this.deleteFoto)
.catch(this.$fhcAlert.handleSystemError);
},
deleteFoto(person_id){
return this.$api
.call(ApiHandleFoto.deleteFoto(person_id))
.then(result => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
})
.catch(this.$fhcAlert.handleSystemError)
.finally(()=> {
this.reload();
});
},
reload() {
if(this.person_id) {
this.loadHeaderData(this.person_id, this.mitarbeiter_uid);
}
else {
this.$emit('reload');
}
},
getFotoSrc(foto) {
if(foto === null) {
@@ -112,74 +182,127 @@ export default {
}
},
template: `
<div class="core-header d-flex justify-content-start align-items-center w-100 overflow-auto pb-3 gap-3" style="max-height:9rem; min-width: 37.5rem;">
<div class="core-header d-flex justify-content-start align-items-center w-100 overflow-auto pb-2 gap-3" style="max-height:9rem; min-width: 37.5rem;">
<modal-upload-foto
v-if="person_id"
ref="modalFoto"
:person_id="person_id"
@reload="reload"
>
</modal-upload-foto>
<modal-upload-foto
v-else
ref="modalFoto"
:person_id="headerData[0].person_id"
@reload="reload"
>
</modal-upload-foto>
<template v-if="typeHeader==='student'">
<div
v-for="person in headerData"
:key="person.person_id"
class="d-flex flex-column align-items-center h-100 position-relative d-inline-block"
>
<img
class="d-block rounded"
style="height: 84px;"
alt="Profilbild"
:src="getFotoSrc(person.foto)"
/>
<template v-if="person.foto_sperre">
<i
class=" fa fa-lock text-secondary bg-light rounded d-flex justify-content-center align-items-center position-absolute top-0 end-0"
style="z-index: 1; font-size: 1rem; width: 1.25rem; height: 1.25rem;"
></i>
</template>
<div
v-for="person in headerData"
:key="person.person_id"
class="foto-container d-flex flex-column align-items-center h-100 position-relative d-inline-block"
>
<img
class="d-block rounded"
style="height: 75px;"
alt="Profilbild"
:src="getFotoSrc(person.foto)"
/>
<template v-if="person.foto_sperre">
<i class="fa fa-lock text-secondary bg-light rounded d-flex justify-content-center align-items-center position-absolute top-0 end-0"
style="z-index: 1; font-size: 1rem; width: 1.25rem; height: 1.25rem;"
>
</i>
</template>
<template v-if="fotoEditable">
<button
type="button"
class="fotoedit buttonleft btn btn-outline-dark btn-sm d-flex justify-content-center align-items-center position-absolute start-0"
@click="showDeleteModal(headerData[0].person_id)">
<i class="fa fa-xmark"></i>
</button>
<button
type="button"
class="fotoedit buttonright btn btn-outline-dark btn-sm d-flex justify-content-center align-items-center position-absolute end-0"
@click="showModal(headerData[0].person_id)">
<i class="fa fa-pen"></i>
</button>
</template>
<small class="text-muted">{{person.uid}}</small>
</div>
</div>
<div v-if="headerData.length == 1">
<h2 class="h4">
{{headerData[0].titelpre}}
{{headerData[0].vorname}}
{{headerData[0].nachname}}<span v-if="headerData[0].titelpost">, </span>
{{headerData[0].titelpost}}
</h2>
<div class="d-flex align-items-center gap-3">
<h2 class="h4">
{{headerData[0].titelpre}}
{{headerData[0].vorname}}
{{headerData[0].nachname}}
<span v-if="headerData[0].titelpost">, </span>
{{headerData[0].titelpost}}
</h2>
<h6 v-if="headerData[0].unruly" class="badge" :class="'bg-unruly rounded-0'"><strong>unruly</strong></h6>
</div>
<h5 class="h6">
<strong class="text-muted">Person ID </strong>
{{headerData[0].person_id}}
<strong class="text-muted">| {{$p.t('lehre', 'studiengang')}} </strong>
{{headerData[0].stg_bezeichnung}} ({{headerData[0].studiengang}})
<strong v-if="headerData[0].semester" class="text-muted"> | {{$p.t('lehre', 'semester')}} </strong>
{{headerData[0].semester}}
<strong v-if="headerData[0].verband" class="text-muted"> | {{$p.t('lehre', 'verband')}}</strong>
{{headerData[0].verband}}
<strong v-if="headerData[0].gruppe" class="text-muted"> | {{$p.t('lehre', 'gruppe')}} </strong>
{{headerData[0].gruppe}}
</h5>
<h5 class="h6">
<strong class="text-muted">{{$p.t('lehre', 'studiengang')}} </strong>
{{headerData[0].stg_bezeichnung}} ({{headerData[0].studiengang}})
<strong v-if="headerData[0].semester" class="text-muted"> | {{$p.t('lehre', 'semester')}} </strong>
{{headerData[0].semester}}
<strong v-if="headerData[0].verband" class="text-muted"> | {{$p.t('lehre', 'verband')}}</strong>
{{headerData[0].verband}}
<strong v-if="headerData[0].gruppe" class="text-muted"> | {{$p.t('lehre', 'gruppe')}} </strong>
{{headerData[0].gruppe}}
</h5>
<h5 class="h6">
<h5 class="h6">
<strong class="text-muted">Email </strong>
<span>
<a :href="'mailto:'+headerData[0]?.mail_intern">{{headerData[0].mail_intern}}</a>
</span>
<strong v-if="headerData[0].statusofsemester" class="text-muted"> | Status </strong>
{{headerData[0].statusofsemester}}
<strong class="text-muted"> | {{$p.t('person', 'matrikelnummer')}} </strong>
{{headerData[0].matr_nr}}
</h5>
</div>
<div v-if="headerData.length == 1" class="col-md-1 d-flex flex-column align-items-end justify-content-start ms-auto">
<div class="d-flex py-1">
<div class="px-2" style="min-width: 100px;">
<slot name="issues"></slot>
</div>
<div v-if="hasTileGammaSlot" class="px-2" style="border-left: 1px solid #EEE">
<h4 class="mb-1 text-center"><slot name="titleGammaTile"></slot></h4>
<h6 class="text-muted text-center"><slot name="valueGammaTile"></slot></h6>
</div>
<div v-if="hasTileBetaSlot" class="px-2" style="border-left: 1px solid #EEE">
<h4 class="mb-1 text-center"><slot name="titleBetaTile"></slot></h4>
<h6 class="text-muted text-center"><slot name="valueBetaTile"></slot></h6>
</div>
<div v-if="hasTileAlphaSlot" class="px-2" style="border-left: 1px solid #EEE">
<h4 class="mb-1 text-center"><slot name="titleAlphaTile"></slot></h4>
<h6 class="text-muted text-center"><slot name="valueAlphaTile"></slot></h6>
</div>
<div v-if="hasTileUIDSlot" class="px-2" style="border-left: 1px solid #EEE">
<h4 class="mb-1 text-center">UID</h4>
<h6 class="text-muted text-center"><slot name="uid"></slot></h6>
</div>
</div>
</div>
</template>
<template v-if="typeHeader==='mitarbeiter'">
<div class="row">
<div class="col-md-2 d-flex justify-content-start align-items-center w-30 pb-3 gap-3 position-relative"
style="max-height: 8rem; max-width: 6rem; overflow: hidden;">
<div class="foto-container col-md-2 d-flex justify-content-start align-items-center w-30 pb-3 gap-3 mt-3 position-relative" style="max-height: 8rem; max-width: 6rem; overflow: hidden;">
<img
class="d-block rounded"
style="height: 84px;"
class="d-block w-100 h-100 rounded"
style="height: 84px; object-fit: contain;"
alt="Profilbild"
:src="'data:image/jpeg;base64,' + headerDataMa.foto"
:src="getFotoSrc(headerDataMa.foto)"
/>
<template v-if="headerDataMa.foto_sperre">
<i
@@ -187,31 +310,73 @@ export default {
style="z-index: 1; font-size: 1rem; width: 1.25rem; height: 1.25rem;"
></i>
</template>
<template v-if="fotoEditable">
<button
type="button"
class="fotoedit btn btn-outline-dark btn-sm d-flex justify-content-center align-items-center position-absolute start-0"
style="z-index: 104; font-size: 1rem; width: 2.5rem; height: 2.5rem; opacity:0; transition: opacity 0.2s; top:13%;"
@click="showDeleteModal(person_id)">
<i class="fa fa-xmark"></i>
</button>
<button
type="button"
class="fotoedit btn btn-outline-dark btn-sm d-flex justify-content-center align-items-center position-absolute end-0"
style="z-index: 104; font-size: 1rem; width: 2.5rem; height: 2.5rem; opacity:0; transition: opacity 0.2s; top:13%;"
@click="showModal(person_id)">
<i class="fa fa-pen"></i>
</button>
</template>
</div>
<!--show Ma-Details-->
<div class="col-md-10">
<h5>{{headerDataMa.titelpre}} {{headerDataMa.vorname}} {{headerDataMa.nachname}}<span v-if="headerDataMa?.titelpost">, </span> {{headerDataMa.titelpost}}
</h5>
<div class="col-md-9 text-nowrap mt-2">
<h4>{{headerDataMa.titelpre}} {{headerDataMa.vorname}} {{headerDataMa.nachname}}<span v-if="headerDataMa?.titelpost">, </span> {{headerDataMa.titelpost}}</h4>
<strong class="text-muted">{{departmentData.organisationseinheittyp_kurzbz}}</strong>
{{departmentData.bezeichnung}}
<span v-if="leitungData.uid"> | </span>
<strong v-if="leitungData.uid" class="text-muted">Vorgesetzte*r </strong>
<a href="#" @click.prevent="redirectToLeitung">
<a href="#" @click.prevent="goToLeitung">
{{leitungData.titelpre}} {{leitungData.vorname}} {{leitungData.nachname}}
</a>
<p>
<strong class="text-muted">Email </strong>
<span v-if="!headerDataMa?.alias">
<a :href="'mailto:'+headerDataMa?.uid+'@'+domain">{{headerDataMa.uid}}@{{domain}}</a>
<span v-if="headerDataMa && (headerDataMa.alias === undefined || headerDataMa.alias === null || headerDataMa.alias === '')">
<a :href="'mailto:' + mitarbeiter_uid + '@' + domain">
{{ mitarbeiter_uid }}@{{ domain }}
</a>
</span>
<span v-if="headerDataMa?.alias">
<span v-else>
<a :href="'mailto:'+headerDataMa?.alias+'@'+domain">{{headerDataMa.alias}}@{{domain}}</a>
</span>
<span v-if="headerDataMa?.telefonklappe" class="mb-2"> | <strong class="text-muted">DW </strong>{{headerDataMa?.telefonklappe}}</span>
</p>
<slot name="tag"></slot>
</div>
</div>
<div class="col-md-1 d-flex flex-column align-items-end justify-content-start ms-auto">
<div class="d-flex py-1">
<div class="px-2" style="min-width: 100px;">
<slot name="issues"></slot>
</div>
<div v-if="hasTileGammaSlot" class="px-2" style="border-left: 1px solid #EEE">
<h4 class="mb-1 text-center"><slot name="titleGammaTile"></slot></h4>
<h6 class="text-muted text-center"><slot name="valueGammaTile"></slot></h6>
</div>
<div v-if="hasTileBetaSlot" class="px-2" style="border-left: 1px solid #EEE">
<h4 class="mb-1 text-center"><slot name="titleBetaTile"></slot></h4>
<h6 class="text-muted text-center"><slot name="valueBetaTile" :valueBetaTile="valueBetaTile"></slot></h6>
</div>
<div v-if="hasTileAlphaSlot" class="px-2" style="border-left: 1px solid #EEE">
<h4 class="mb-1 text-center"><slot name="titleAlphaTile"></slot></h4>
<h6 class="text-muted text-center"><slot name="valueAlphaTile"></slot></h6>
</div>
<div v-if="hasTileUIDSlot" class="px-2" style="border-left: 1px solid #EEE">
<h4 class="mb-1 text-center">UID</h4>
<h6 class="text-muted text-center"><slot name="uid"></slot></h6>
</div>
</div>
</div>
</template>
</div>
`
@@ -0,0 +1,103 @@
import BsModal from "../../Bootstrap/Modal.js";
import FormForm from "../../Form/Form.js";
import FormInput from "../../Form/Input.js";
import ApiHandleFoto from "../../../../js/api/factory/fotoHandling.js";
export default {
name: "modalUploadFoto",
components: {
BsModal,
FormForm,
FormInput,
},
props: {
person_id: {
type: Number,
required: true
}
},
data(){
return{
base64Image: null,
file: null,
preview: null,
}
},
methods:{
open(person_id){
this.$refs.modalUploadFoto.show(person_id);
},
onFileChange(e) {
this.file = e.target.files[0];
if (!this.file) return;
// convert File in base64
const reader = new FileReader();
reader.onload = (event) => {
this.base64Image = event.target.result;
this.preview = this.base64Image;
};
reader.readAsDataURL(this.file);
},
async uploadImage(person_id) {
if (!this.base64Image) {
this.$fhcAlert.alertInfo(this.$p.t('header', 'alert_chooseFoto'));
return;
}
return this.$api
.call(ApiHandleFoto.uploadFoto(person_id, {
image: this.base64Image,
filename: this.file.name
}))
.then(result => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successFotoUpload'));
this.closeModal();
this.$emit('reload');
})
.catch(this.$fhcAlert.handleSystemError);
},
resetModal(){
this.base64Image = [];
this.file = null;
},
closeModal(){
this.resetModal();
this.$refs.modalUploadFoto.hide();
}
},
template: `
<bs-modal
ref="modalUploadFoto"
dialog-class="modal-dialog-scrollable"
>
<template #title>
Upload Foto
</template>
<form-form
ref="formUploadFoto"
>
<div>
<input class="form-control" type="file" @change="onFileChange" accept="image/*" />
<div class="mt-3">
<div>
<img :src="base64Image" style="width:100px"/>
</div>
</div>
</div>
</form-form>
<template #footer>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button class="btn btn-secondary" @click="closeModal()">{{$p.t('ui', 'cancel')}}</button>
<button type="button" class="btn btn-primary" @click="uploadImage(person_id)">{{$p.t('ui', 'hochladen')}}</button>
</div>
</template>
</bs-modal>
`,
}
+5 -3
View File
@@ -170,6 +170,7 @@ export default {
return this.$attrs.modelValue;
},
set(v) {
this.clearValidationForThisName()
if (!this.$attrs.hasOwnProperty('modelValue'))
this.modelValueDummy = v;
this.$emit('update:modelValue', v);
@@ -242,9 +243,9 @@ export default {
template: `
<component :is="!hasContainer ? 'FhcFragment' : 'div'" class="position-relative" :class="autoContainerClass">
<label v-if="label && lcType != 'radio' && lcType != 'checkbox'" :class="!noAutoClass && 'form-label'" :for="idCmp">{{label}}</label>
<input v-if="tag == 'input'" :type="lcType" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)">
<textarea v-else-if="tag == 'textarea'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)"></textarea>
<select v-else-if="tag == 'select'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="clearValidationForThisName(); $emit('input', $event)">
<input v-if="tag == 'input'" :type="lcType" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="$emit('input', $event)">
<textarea v-else-if="tag == 'textarea'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="$emit('input', $event)"></textarea>
<select v-else-if="tag == 'select'" ref="input" v-model="modelValueCmp" v-bind="$attrs" :id="idCmp" :name="name" :class="validationClass" :modelValue="undefined" @input="$emit('input', $event)">
<slot></slot>
</select>
<component
@@ -280,6 +281,7 @@ export default {
<template #chip="data"><slot name="chip" v-bind="data"></slot></template>
<template #header="data"><slot name="header" v-bind="data"></slot></template>
<template #footer="data"><slot name="footer" v-bind="data"></slot></template>
<template #selectedItem="data"><slot name="selectedItem" v-bind="data"></slot></template>
<template #option="data"><slot name="option" v-bind="data"></slot></template>
<template #optiongroup="data"><slot name="optiongroup" v-bind="data"></slot></template>
<template #content="data"><slot name="content" v-bind="data"></slot></template>
+1 -1
View File
@@ -27,7 +27,7 @@ export default {
<li class="form-upload-dms-item">
<span class="col-auto"><i class="fa fa-file me-1"></i></span>
<span class="col">{{ modelValue.name }}</span>
<a v-if="preview" :href="preview" target="_blank" class="col-auto btn btn-outline-secondary btn-p-0 me-1">
<a v-if="preview" :href="preview" target="_blank" class="col-auto btn btn-outline-secondary btn-p-0 me-2">
<i class="fa fa-download"></i>
</a>
<button class="col-auto btn btn-outline-secondary btn-p-0" @click="$emit('delete')">
+76 -77
View File
@@ -33,26 +33,71 @@ export default {
},
data(){
return {
tabulatorOptions: {
isFilterSet: true,
listOrgHeads: [],
listOrgUnits: [], //Old
listAllOrgUnits: [],
listOrgUnits_GST: [],
listOrgUnits_GMBH: [],
formData: {
head: 'gst',
oe_kurzbz: '',
funktion_kurzbz: null,
label:'',
//funktion_label: '',
funktion: null,
},
statusNew: true,
listAllFunctions: [],
abortController: {
oes: null,
functions: null
},
filteredOes: [],
filteredFunctions: [],
newBtnStyle: '',
selectedFunction: null,
selectedOe: null,
layout: 'fitDataFill',
layoutColumnsOnNewData: false,
height: '300'
}
},
computed: {
tabulatorOptions() {
const options = {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(
ApiCoreFunktion.getAllUserFunctions(this.personUID)
),
ajaxResponse: (url, params, response) => response.data,
persistenceID: 'core-functions-20260217',
columns: [
{
title: "dienstverhaeltnis_unternehmen",
field: "dienstverhaeltnis_unternehmen",
headerFilter: "list",
headerFilterParams: {valuesLookup: true, autocomplete: true, sort: "asc"},
headerFilterParams: {
valuesLookup: true, autocomplete: true, sort: "asc"
},
width: 140,
//Field Company: if visible show link to dv
visible: this.showDvCompany,
formatter: this.companyLinkFormatter
},
{
title: "funktion_beschreibung", field: "funktion_beschreibung", headerFilter: "list",
headerFilterParams: {valuesLookup: true, autocomplete: true, sort: "asc"},
headerFilterParams: {
valuesLookup: true, autocomplete: true, sort: "asc"
},
width: 140
},
{
title: "funktion_oebezeichnung", field: "funktion_oebezeichnung", headerFilter: "list",
headerFilterParams: {valuesLookup: true, autocomplete: true, sort: "asc"}
headerFilterParams: {
valuesLookup: true, autocomplete: true, sort: "asc"
},
width: 140
},
{title: "wochenstunden", field: "wochenstunden", headerFilter: true},
{
@@ -87,7 +132,7 @@ export default {
});
},
},
{title: "bezeichnung", field: "bezeichnung", headerFilter: true},
{title: "bezeichnung", field: "bezeichnung", headerFilter: true, width: 140},
{title: "aktiv", field: "aktiv", visible: false},
{title: "benutzerfunktion_id", field: "benutzerfunktion_id", visible: false},
{title: "uid", field: "uid", visible: false},
@@ -142,87 +187,41 @@ export default {
frozen: true
}
],
layout: 'fitDataFill',
layoutColumnsOnNewData: false,
height: '300',
persistenceID: 'core-functions',
},
tabulatorEvents: [
};
return options;
},
tabulatorEvents() {
const events = [
{
event: 'tableBuilt',
handler: async () => {
await this.$p.loadCategory(['global', 'lehre', 'person', 'ui']);
let cm = this.$refs.table.tabulator.columnManager;
//Field Company: if visible show link to dv
const column = cm.getColumnByField('dienstverhaeltnis_unternehmen');
const companyDv = {
title: this.$p.t('person', 'dv_unternehmen'),
width: 140,
visible: this.showDvCompany,
formatter: this.companyLinkFormatter
};
column.component.updateDefinition(companyDv);
const setHeader = (field, text) => {
const col = this.$refs.table.tabulator.getColumn(field);
if (!col) return;
cm.getColumnByField('funktion_beschreibung').component.updateDefinition({
title: this.$p.t('person', 'zuordnung_taetigkeit'),
width: 140
});
cm.getColumnByField('funktion_oebezeichnung').component.updateDefinition({
title: this.$p.t('lehre', 'organisationseinheit'),
width: 140
});
cm.getColumnByField('wochenstunden').component.updateDefinition({
title: this.$p.t('person', 'wochenstunden')
});
const el = col.getElement();
if (!el || !el.querySelector) return;
const columnDatumVon = cm.getColumnByField('datum_von');
const fieldVonDatum = {
title: this.$p.t('ui', 'from')
const titleEl = el.querySelector('.tabulator-col-title');
if (titleEl) {
titleEl.textContent = text;
}
};
columnDatumVon.component.updateDefinition(fieldVonDatum);
const columnDatumBis = cm.getColumnByField('datum_bis');
const fieldBisDatum = {
title: this.$p.t('global', 'bis'),
};
columnDatumBis.component.updateDefinition(fieldBisDatum);
cm.getColumnByField('bezeichnung').component.updateDefinition({
title: this.$p.t('ui', 'bezeichnung'),
width: 140
});
setHeader('dienstverhaeltnis_unternehmen', this.$p.t('person', 'dv_unternehmen'));
setHeader('funktion_beschreibung', this.$p.t('person', 'zuordnung_taetigkeit'));
setHeader('funktion_oebezeichnung', this.$p.t('lehre', 'organisationseinheit'));
setHeader('wochenstunden', this.$p.t('person', 'wochenstunden'));
setHeader('datum_von', this.$p.t('ui', 'from'));
setHeader('datum_bis', this.$p.t('global', 'bis'));
setHeader('bezeichnung', this.$p.t('ui', 'bezeichnung'));
}
}
],
isFilterSet: true,
listOrgHeads: [],
listOrgUnits: [], //Old
listAllOrgUnits: [],
listOrgUnits_GST: [],
listOrgUnits_GMBH: [],
formData: {
head: 'gst',
oe_kurzbz: '',
funktion_kurzbz: null,
label:'',
//funktion_label: '',
funktion: null,
},
statusNew: true,
listAllFunctions: [],
abortController: {
oes: null,
functions: null
},
filteredOes: [],
filteredFunctions: [],
newBtnStyle: '',
selectedFunction: null,
selectedOe: null
}
];
return events;
},
},
watch: {
selectedFunction(newVal) {
@@ -340,6 +339,8 @@ export default {
this.formData.head = 'gst';
this.formData.oe_kurzbz = '';
this.formData.funktion_kurzbz = '';
this.selectedFunction = null;
this.selectedOe= null;
},
filterFunctions(event) {
const query = event.query.toLowerCase();
@@ -460,14 +461,12 @@ export default {
</template>
<form-form class="row pt-3" ref="functionData">
<form-input
container-class="mb-3 col-8"
type="select"
name="companies"
:label="$p.t('core/unternehmen')"
v-model="formData.head"
@change="getOrgetsForCompanyOld"
>
<option
v-for="org in listOrgHeads"
+10 -161
View File
@@ -4,6 +4,8 @@ import DetailsForm from "../Details/Form.js";
import CoreTag from '../../Tag/Tag.js';
import { tagHeaderFilter } from '../../../../js/tabulator/filters/extendedHeaderFilter.js';
import { extendedHeaderFilter } from "../../../../js/tabulator/filters/extendedHeaderFilter.js";
import { tagFormatter } from "../../../../js/tabulator/formatter/tags.js";
import { addTagInTable, deleteTagInTable, updateTagInTable } from "../../../../js/helpers/TagHelper.js";
import ApiLv from "../../../api/lehrveranstaltung.js";
import ApiTag from "../../../api/lehrveranstaltung/tag.js";
@@ -223,72 +225,7 @@ export default {
headerFilter: "input",
headerFilterFunc: tagHeaderFilter,
headerFilterFuncParams: {field: 'tags'},
formatter: (cell) => {
let tags = cell.getValue();
if (!tags) return;
let container = document.createElement('div');
container.className = "d-flex gap-1";
let parsedTags = JSON.parse(tags);
let maxVisibleTags = 2;
const rowData = cell.getRow().getData();
if (rowData._tagExpanded === undefined) {
rowData._tagExpanded = false;
}
const renderTags = () => {
container.innerHTML = '';
parsedTags = parsedTags.filter(item => item !== null);
parsedTags.sort((a, b) => {
let adone = a.done ? 1 : 0;
let bbone = b.done ? 1 : 0;
if (adone !== bbone)
{
return adone - bbone;
}
return b.id - a.id;
});
const tagsToShow = rowData._tagExpanded ? parsedTags : parsedTags.slice(0, maxVisibleTags);
tagsToShow.forEach(tag => {
if (!tag) return;
let tagElement = document.createElement('span');
tagElement.innerText = tag.beschreibung;
tagElement.title = tag.notiz;
tagElement.className = "tag " + tag.style;
if (tag.done) tagElement.className += " tag_done";
tagElement.addEventListener('click', (event) => {
event.stopPropagation();
event.preventDefault();
this.$refs.tagComponent.editTag(tag.id);
});
container.appendChild(tagElement);
});
if (parsedTags.length > maxVisibleTags) {
let toggle = document.createElement('button');
toggle.innerText = (rowData._tagExpanded ? '- ' : '+ ') + (parsedTags.length - maxVisibleTags);
toggle.className = "display_all";
toggle.title = rowData._tagExpanded ? "Tags ausblenden" : "Tags einblenden";
toggle.addEventListener('click', () => {
rowData._tagExpanded = !rowData._tagExpanded;
renderTags();
});
container.appendChild(toggle);
}
};
renderTags();
return container;
},
formatter: (cell) => tagFormatter(cell, this.$refs.tagComponent),
width: 150,
},
{
@@ -444,104 +381,16 @@ export default {
},
addedTag(addedTag)
{
const table = this.$refs.table.tabulator;
this.selectedRows.forEach(row =>
{
if (Array.isArray(addedTag.response))
{
addedTag.response.forEach(tag => {
const targetRow = this.allRows.find(row => row.getData().lehreinheit_id === tag.lehreinheit_id);
if (targetRow)
{
const rowData = targetRow.getData();
let tags = [];
try {
tags = JSON.parse(rowData.tags || '[]');
} catch (e) {}
const tagExists = tags.some((t) => t.id === tag.id);
if (!tagExists)
{
addedTag.id = tag.id;
tags.unshift({ ...addedTag });
targetRow.update({ tags: JSON.stringify(tags) });
targetRow.reformat();
}
}
});
}
});
addTagInTable(addedTag, this.allRows, 'lehreinheit_id')
},
deletedTag(deletedTag) {
const targetRow = this.allRows.find(row => {
const rowData = row.getData();
let tags = [];
try {
tags = JSON.parse(rowData.tags || '[]');
} catch (e) {}
return tags.some(tag => tag.id === deletedTag);
});
if (targetRow) {
const rowData = targetRow.getData();
let tags = [];
try {
tags = JSON.parse(rowData.tags || '[]');
} catch (e) {}
const filteredTags = tags.filter(t => t.id !== deletedTag);
const updatedTags = JSON.stringify(filteredTags);
if (updatedTags !== rowData.tags) {
targetRow.update({
tags: updatedTags
});
targetRow.reformat();
}
}
deletedTag(deletedTag)
{
deleteTagInTable(deletedTag, this.allRows);
},
updatedTag(updatedTag) {
const targetRow = this.allRows.find(row => {
const rowData = row.getData();
let tags = [];
try {
tags = JSON.parse(rowData.tags || '[]');
} catch (e) {}
return tags.some(t => t?.id === updatedTag.id);
});
if (targetRow)
{
const rowData = targetRow.getData();
let tags = [];
try {
tags = JSON.parse(rowData.tags || '[]');
} catch (e) {}
let changed = false;
const tagIndex = tags.findIndex(tag => tag?.id === updatedTag.id);
if (tagIndex !== -1) {
tags[tagIndex] = { ...updatedTag };
changed = true;
}
if (changed)
{
targetRow.update({
tags: JSON.stringify(tags),
});
targetRow.reformat();
}
}
updatedTag(updatedTag)
{
updateTagInTable(updatedTag, this.allRows);
},
async copyLehreinheit(row, art)
{
@@ -47,6 +47,7 @@ export default {
<fhc-tabs
v-if="lv.length === 1"
ref="tabs"
:useprimevue="true"
:modelValue="lv[0]"
:config="configLVTabs"
:default="$route.params.tab"
@@ -25,246 +25,11 @@ export default {
},
data(){
return {
tabulatorOptions: {
ajaxURL: 'dummy',
ajaxRequestFunc: this.loadAjaxCall,
ajaxParams: () => {
return {
id: this.id,
type: this.typeId
};
},
ajaxResponse: (url, params, response) => this.buildTreemap(response),
columns: [
{title: "subject", field: "subject", headerFilter: true},
{title: "body", field: "body", formatter: "html", visible: false, headerFilter: true},
{title: "message_id", field: "message_id", visible: false, headerFilter: true},
{
title: "Datum",
field: "insertamum",
headerFilter: true,
formatter: function (cell) {
const dateStr = cell.getValue();
const date = new Date(dateStr); // Convert to Date object
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false
});
},
headerFilterFunc(headerValue, rowValue) {
const matches = headerValue.match(/^(([0-9]{2})\.)?([0-9]{2})\.([0-9]{4})?$/);
let comparestr = headerValue;
if(matches !== null) {
const year = (matches[4] !== undefined) ? matches[4] : '';
const month = matches[3];
const day = (matches[2] !== undefined) ? matches[2] : '';
comparestr = year + '-' + month + '-' + day;
}
return rowValue.match(comparestr);
}
},
{title: "sender", field: "sender", headerFilter: true},
{title: "recipient", field: "recipient", headerFilter: true},
{title: "senderId", field: "sender_id", headerFilter: true},
{title: "recipientId", field: "recipient_id", headerFilter: true},
{title: "Relationmessage ID", field: "relationmessage_id", headerFilter: true},
{
title: "Status",
field: "status",
headerFilter: true,
formatterParams: [
"unread",
"read",
"archived",
"deleted"
],
formatter: (cell, formatterParams) => {
return formatterParams[cell.getValue()];
},
},
{
title: "letzte Änderung",
field: "statusdatum",
headerFilter: true,
formatter: function (cell) {
const dateStr = cell.getValue();
const date = new Date(dateStr); // Convert to Date object
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false
});
},
headerFilterFunc(headerValue, rowValue) {
const matches = headerValue.match(/^(([0-9]{2})\.)?([0-9]{2})\.([0-9]{4})?$/);
let comparestr = headerValue;
if(matches !== null) {
const year = (matches[4] !== undefined) ? matches[4] : '';
const month = matches[3];
const day = (matches[2] !== undefined) ? matches[2] : '';
comparestr = year + '-' + month + '-' + day;
}
return rowValue.match(comparestr);
}
},
{
title: 'Aktionen', field: 'actions',
width: 100,
formatter: (cell, formatterParams, onRendered) => {
let container = document.createElement('div');
container.className = "d-flex gap-2";
let button = document.createElement('button');
if (this.personId != cell.getData().sender_id) {
button.disabled = true;
button.style = "visibility: hidden";
button.ariaHidden = true;
}
button.className = 'btn btn-outline-secondary btn-action';
button.title = this.$p.t('global', 'reply');
button.innerHTML = '<i class="fa fa-reply"></i>';
button.addEventListener(
'click',
(event) =>
this.actionReplyToMessage(cell.getData().message_id)
);
container.append(button);
button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.title = this.$p.t('ui', 'loeschen');
button.innerHTML = '<i class="fa fa-xmark"></i>';
button.addEventListener(
'click',
() =>
this.actionDeleteMessage(cell.getData().message_id)
);
container.append(button);
return container;
},
frozen: true
}
],
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: '400',
selectable: 1,
selectableRangeMode: 'click',
index: 'message_id',
pagination: true,
paginationMode: "remote",
paginationSize: 15,
paginationInitialPage: 1,
dataTree: true,
headerSort: true,
dataTreeChildField: "children",
dataTreeCollapseElement:"<i class='fas fa-minus-square'></i>",
dataTreeChildIndent: 15,
dataTreeStartExpanded: false,
persistenceID: 'core-message-2025112401',
locale: 'de',
"langs": {
"de":{ //German language definition
"data":{
"loading":"Lädt", //data loader text
"error":"Fehler", //data error text
},
"pagination":{
"first":"Erste",
"first_title":"Erste Seite",
"last":"Letzte",
"last_title":"Letzte Seite",
"prev":"Vorige",
"prev_title":"Vorige Seite",
"next":"Nächste",
"next_title":"Nächste Seite",
"all":"Alle"
},
},
}
},
tabulatorEvents: [
{
event: 'tableBuilt',
handler: async() => {
await this.$p.loadCategory(['global', 'person', 'stv', 'messages', 'ui', 'notiz']);
let cm = this.$refs.table.tabulator.columnManager;
cm.getColumnByField('subject').component.updateDefinition({
title: this.$p.t('global', 'betreff')
});
cm.getColumnByField('body').component.updateDefinition({
title: this.$p.t('messages', 'body')
});
cm.getColumnByField('message_id').component.updateDefinition({
title: this.$p.t('messages', 'message_id')
});
cm.getColumnByField('insertamum').component.updateDefinition({
title: this.$p.t('global', 'datum')
});
cm.getColumnByField('sender').component.updateDefinition({
title: this.$p.t('messages', 'sender')
});
cm.getColumnByField('recipient').component.updateDefinition({
title: this.$p.t('messages', 'recipient')
});
cm.getColumnByField('sender_id').component.updateDefinition({
title: this.$p.t('messages', 'senderId')
});
cm.getColumnByField('recipient_id').component.updateDefinition({
title: this.$p.t('messages', 'recipientId')
});
cm.getColumnByField('statusdatum').component.updateDefinition({
title: this.$p.t('notiz', 'letzte_aenderung')
});
cm.getColumnByField('status').component.updateDefinition({
formatterParams: [
this.$p.t('messages/unread'),
this.$p.t('messages/read'),
this.$p.t('messages/archived'),
this.$p.t('messages/deleted')
]
});
this.$refs.table.tabulator.rowManager.getDisplayRows();
/*
cm.getColumnByField('actions').component.updateDefinition({
title: this.$p.t('global', 'aktionen')
});
*/
}
},
{
event: 'rowClick',
handler: (e, row) => {
const selectedMessage = row.getData().message_id;
const body = row.getData().body;
this.previewBody = body;
}
},
/*
{
event: 'pageLoaded',
handler: (pageno) => {
this.pageNo = pageno+1;
}
}
*/
],
previewBody: "",
open: false,
personId: null,
layoutColumnsOnNewData: false,
height: '400',
}
},
methods: {
@@ -338,10 +103,10 @@ export default {
}
});
// to avoid endless loop
if (iteration > messages.length) break;
// to avoid endless loop
if (iteration > messages.length) break;
}
return {data: messageNested, last_page: last_page};
return {data: messageNested, last_page: last_page};
},
loadAjaxCall(url, config, params){
return this.$api.call(
@@ -350,24 +115,231 @@ export default {
}
},
computed: {
statusText(){
return {
0: this.$p.t('messsages', 'unread'),
1: this.$p.t('messsages', 'read'),
2: this.$p.t('messsages', 'archived'),
3: this.$p.t('messsages', 'deleted')
}
tabulatorOptions() {
const options = {
ajaxURL: 'dummy',
ajaxRequestFunc: this.loadAjaxCall,
ajaxParams: () => {
return {
id: this.id,
type: this.typeId
};
},
ajaxResponse: (url, params, response) => this.buildTreemap(response),
layout: 'fitDataStretchFrozen',
index: 'message_id',
persistenceID: 'core-message-20260217',
selectableRows: 1,
selectableRowsRangeMode: 'click',
columns: [
{title: "subject", field: "subject", headerFilter: true},
{title: "body", field: "body", formatter: "html", visible: false, headerFilter: true},
{title: "message_id", field: "message_id", visible: false, headerFilter: true},
{
title: "Datum",
field: "insertamum",
headerFilter: true,
formatter: function (cell) {
const dateStr = cell.getValue();
const date = new Date(dateStr); // Convert to Date object
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false
});
},
headerFilterFunc(headerValue, rowValue) {
const matches = headerValue.match(/^(([0-9]{2})\.)?([0-9]{2})\.([0-9]{4})?$/);
let comparestr = headerValue;
if(matches !== null) {
const year = (matches[4] !== undefined) ? matches[4] : '';
const month = matches[3];
const day = (matches[2] !== undefined) ? matches[2] : '';
comparestr = year + '-' + month + '-' + day;
}
return rowValue.match(comparestr);
}
},
{title: "sender", field: "sender", headerFilter: true},
{title: "recipient", field: "recipient", headerFilter: true},
{title: "senderId", field: "sender_id", headerFilter: true},
{title: "recipientId", field: "recipient_id", headerFilter: true},
{title: "Relationmessage ID", field: "relationmessage_id", headerFilter: true},
{
title: "Status",
field: "status",
headerFilter: true,
formatterParams: [
"unread",
"read",
"archived",
"deleted"
],
formatter: (cell, formatterParams) => {
const key = formatterParams[cell.getValue()];
return this.$p.t('messages', key);
},
},
{
title: "letzte Änderung",
field: "statusdatum",
headerFilter: true,
formatter: function (cell) {
const dateStr = cell.getValue();
const date = new Date(dateStr); // Convert to Date object
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false
});
},
headerFilterFunc(headerValue, rowValue) {
const matches = headerValue.match(/^(([0-9]{2})\.)?([0-9]{2})\.([0-9]{4})?$/);
let comparestr = headerValue;
if(matches !== null) {
const year = (matches[4] !== undefined) ? matches[4] : '';
const month = matches[3];
const day = (matches[2] !== undefined) ? matches[2] : '';
comparestr = year + '-' + month + '-' + day;
}
return rowValue.match(comparestr);
}
},
{
title: 'Aktionen', field: 'actions',
width: 100,
formatter: (cell, formatterParams, onRendered) => {
let container = document.createElement('div');
container.className = "d-flex gap-2";
let button = document.createElement('button');
if (this.personId != cell.getData().sender_id) {
button.disabled = true;
button.style = "visibility: hidden";
button.ariaHidden = true;
}
button.className = 'btn btn-outline-secondary btn-action';
button.title = this.$p.t('global', 'reply');
button.innerHTML = '<i class="fa fa-reply"></i>';
button.addEventListener(
'click',
(event) =>
this.actionReplyToMessage(cell.getData().message_id)
);
container.append(button);
button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.title = this.$p.t('ui', 'loeschen');
button.innerHTML = '<i class="fa fa-xmark"></i>';
button.addEventListener(
'click',
() =>
this.actionDeleteMessage(cell.getData().message_id)
);
container.append(button);
return container;
},
frozen: true
}
],
pagination: true,
paginationMode: "remote",
paginationSize: 15,
paginationInitialPage: 1,
dataTree: true,
headerSort: true,
dataTreeChildField: "children",
dataTreeCollapseElement:"<i class='fas fa-minus-square'></i>",
dataTreeChildIndent: 15,
dataTreeStartExpanded: false,
locale: 'de',
"langs": {
"de":{ //German language definition
"data":{
"loading":"Lädt", //data loader text
"error":"Fehler", //data error text
},
"pagination":{
"first":"Erste",
"first_title":"Erste Seite",
"last":"Letzte",
"last_title":"Letzte Seite",
"prev":"Vorige",
"prev_title":"Vorige Seite",
"next":"Nächste",
"next_title":"Nächste Seite",
"all":"Alle"
},
},
}
};
return options;
},
tabulatorEvents() {
const events = [
{
event: 'tableBuilt',
handler: async() => {
await this.$p.loadCategory(['global', 'person', 'stv', 'messages', 'ui', 'notiz']);
const setHeader = (field, text) => {
const col = this.$refs.table.tabulator.getColumn(field);
if (!col) return;
const el = col.getElement();
if (!el || !el.querySelector) return;
const titleEl = el.querySelector('.tabulator-col-title');
if (titleEl) {
titleEl.textContent = text;
}
};
setHeader('subject', this.$p.t('global', 'betreff'));
setHeader('body', this.$p.t('messages', 'body'));
setHeader('message_id', this.$p.t('messages', 'message_id'));
setHeader('insertamum', this.$p.t('global', 'datum'));
setHeader('sender', this.$p.t('messages', 'sender'));
setHeader('recipient', this.$p.t('messages', 'recipient'));
setHeader('sender_id', this.$p.t('messages', 'senderId'));
setHeader('recipient_id', this.$p.t('messages', 'recipientId'));
setHeader('statusdatum', this.$p.t('notiz', 'letzte_aenderung'));
this.$refs.table.tabulator.rowManager.getDisplayRows();
this.$emit('tabulator_tablebuilt');
}
},
{
event: 'rowClick',
handler: (e, row) => {
const selectedMessage = row.getData().message_id;
const body = row.getData().body;
this.previewBody = body;
}
},
];
return events;
},
},
mounted() {
// change to target="_blank"
/* this.$nextTick(() => {
const links = document.querySelectorAll('.preview a');
links.forEach(link => {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer'); // Sicherheitsmaßnahme
});
});*/
/* this.$nextTick(() => {
const links = document.querySelectorAll('.preview a');
links.forEach(link => {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer'); // Sicherheitsmaßnahme
});
});*/
},
created(){
if(this.typeId != 'person_id' && Array.isArray(this.id) && this.id.length === 1) {
@@ -382,6 +354,7 @@ export default {
})
.catch(this.$fhcAlert.handleSystemError);
}
},
template: `
<div class="messages-detail-table">
@@ -391,7 +364,7 @@ export default {
<div class="row">
<!--table-->
<div class="col-sm-6 pt-6">
<div class="col-sm-6 pt-1">
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
@@ -407,9 +380,9 @@ export default {
</div>
<!--preview wysiwyg-window-->
<div class="col-sm-6 pt-6">
<br><br><br><br>
<div ref="preview">
<div class="col-sm-6 pt-5">
<div class="msg-preview-spacer pt-2" aria-hidden="true"></div>
<div ref="preview" class="bg-white">
<div v-html="previewBody" class="p-3 border rounded overflow-scroll twoColumns"></div>
</div>
@@ -451,4 +424,4 @@ export default {
</div>
`
}
}
+8 -2
View File
@@ -56,6 +56,7 @@ export default {
},
data() {
return {
tablebuilt: false,
isVisibleDiv: false,
messageId: null
}
@@ -139,8 +140,10 @@ export default {
},
resetMessageId(){
this.messageId = null;
},
tableBuilt: function() {
this.tablebuilt = true;
}
},
template: `
<div class="core-messages h-100 pb-3">
@@ -155,6 +158,7 @@ export default {
</form>
<message-modal
v-if="tablebuilt || id.length > 1"
ref="modalMsg"
:type-id="typeId"
:id="id"
@@ -166,8 +170,9 @@ export default {
</message-modal>
<!--in same page-->
<div v-show="isVisibleDiv" class="overflow-auto m-3" style="max-height: 500px; border: 1px solid #ccc;">
<div v-if="isVisibleDiv" class="overflow-auto m-3" style="max-height: 500px; border: 1px solid #ccc;">
<form-only
v-if="tablebuilt || id.length > 1"
ref="templateNewDivMessage"
:type-id="typeId"
:id="id"
@@ -187,6 +192,7 @@ export default {
:openMode="openMode"
@newMessage="handleMessage"
@replyToMessage="handleMessage"
@tabulator_tablebuilt="tableBuilt"
>
</table-messages>
</div>
+139 -201
View File
@@ -1,5 +1,4 @@
import VueDatePicker from '../vueDatepicker.js.php';
import PvAutoComplete from "../../../../index.ci.php/public/js/components/primevue/autocomplete/autocomplete.esm.min.js";
import FormUploadDms from '../Form/Upload/Dms.js';
import {CoreFilterCmpt} from "../filter/Filter.js";
import BsModal from "../Bootstrap/Modal.js";
@@ -11,7 +10,6 @@ export default {
components: {
CoreFilterCmpt,
VueDatePicker,
PvAutoComplete,
FormUploadDms,
FormForm,
FormInput,
@@ -50,7 +48,49 @@ export default {
},
data() {
return {
tabulatorOptions: {
notizen: [],
multiupload: true,
mitarbeiter: [],
filteredMitarbeiter: [],
zwischenvar: '',
editorInitialized: false,
editor: null,
notizData: {
typeId: this.typeId,
id: this.id,
titel: null,
statusNew: true,
text: '',
lastUpdate: null,
von: null,
bis: null,
document: null,
erledigt: false,
verfasser: null,
bearbeiter: null,
anhang: []
},
showVariables: {
showTitel: false,
showText: false,
showVerfasser: false,
showBearbeiter: false,
showVon: false,
showBis: false,
showDokumente: false,
showErledigt: false,
showNotiz_id: false,
showNotizzuordnung_id: false,
showType_id: false,
showId: false,
showLastupdate: false
},
currentVerfasserUid: null
}
},
computed: {
tabulatorOptions: function() {
return {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(this.endpoint.getNotizen(this.id, this.typeId)),
ajaxParams: () => {
@@ -65,6 +105,7 @@ export default {
title: "Titel",
field: "titel",
width: 100,
visible: this.showVariables.showTitel,
tooltip:function(e, cell, onRendered){
var el = document.createElement("div");
el.style.backgroundColor = "white";
@@ -87,6 +128,7 @@ export default {
width: 250,
formatter: "html",
//clipContents: true,
visible: this.showVariables.showText,
tooltip:function(e, cell, onRendered){
var el = document.createElement("div");
el.style.backgroundColor = "white";
@@ -101,18 +143,18 @@ export default {
return el;
},
},
{title: "VerfasserIn", field: "verfasser", width: 124},
{title: "BearbeiterIn", field: "bearbeiter", width: 126},
{title: "VerfasserIn", field: "verfasser", width: 124, visible: this.showVariables.showVerfasser},
{title: "BearbeiterIn", field: "bearbeiter", width: 126, visible: this.showVariables.showBearbeiter},
{title: "Verfasser UID", field: "verfasser_uid", width: 124, visible: false},
{title: "Bearbeiter UID", field: "bearbeiter_uid", width: 126, visible: false},
{title: "Start", field: "start_format", width: 86, visible: false},
{title: "Ende", field: "ende_format", width: 86, visible: false},
{title: "Dokumente", field: "countdoc", width: 100, visible: false},
{title: "Start", field: "start_format", width: 86, visible: this.showVariables.showVon},
{title: "Ende", field: "ende_format", width: 86, visible: this.showVariables.showBis},
{title: "Dokumente", field: "countdoc", width: 100, visible: this.showVariables.showDokumente},
{
title: "Erledigt",
field: "erledigt",
width: 97,
visible: false,
visible: this.showVariables.showErledigt,
formatter:"tickCross",
hozAlign:"center",
formatterParams: {
@@ -120,15 +162,15 @@ export default {
crossElement: '<i class="fa fa-xmark text-danger"></i>'
}
},
{title: "Notiz_id", field: "notiz_id", width: 92, visible: false},
{title: "Notizzuordnung_id", field: "notizzuordnung_id", width: 164, visible: false},
{title: "type_id", field: "type_id", width: 164, visible: false},
{title: "extension_id", field: "id", width: 135, visible: false},
{title: "Notiz_id", field: "notiz_id", width: 92, visible: this.showVariables.showNotiz_id},
{title: "Notizzuordnung_id", field: "notizzuordnung_id", width: 164, visible: this.showVariables.showNotizzuordnung_id},
{title: "type_id", field: "type_id", width: 164, visible: this.showVariables.showType_id},
{title: "extension_id", field: "id", width: 135, visible: this.showVariables.showId},
{
title: "letzte Änderung",
title: "letzte Änderung",
field: "lastupdate",
width: 146,
visible: false,
visible: this.showVariables.showLastupdate,
formatter: function (cell) {
const dateStr = cell.getValue();
if (!dateStr) return "";
@@ -154,7 +196,7 @@ export default {
let button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.title = this.$p.t('ui', 'notiz_edit');
button.title = this.$p.t('notiz', 'notiz_edit');
button.innerHTML = '<i class="fa fa-edit"></i>';
button.addEventListener(
'click',
@@ -185,129 +227,60 @@ export default {
index: 'notiz_id',
persistenceID: this.tabulatorPersistenceId,
persistence: {
sort: false,
sort: true,
columns: ["width", "visible", "frozen"],
filter: false,
headerFilter: false,
group: false,
page: false,
}
},
tabulatorEvents: [
};
},
tabulatorEvents: function () {
return [
{
event: 'tableBuilt',
handler: async () => {
//to avoid js error
if (!this.$refs.table) return;
await this.$p.loadCategory(['notiz', 'global', 'ui']);
let cm = this.$refs.table.tabulator.columnManager;
const setHeader = (field, text) => {
const col = this.$refs.table.tabulator.getColumn(field);
if (!col) return;
cm.getColumnByField('verfasser').component.updateDefinition({
title: this.$p.t('notiz', 'verfasser'),
visible: this.showVariables.showVerfasser
});
cm.getColumnByField('verfasser_uid').component.updateDefinition({
title: this.$p.t('ui', 'verfasser_uid'),
});
cm.getColumnByField('titel').component.updateDefinition({
title: this.$p.t('global', 'titel'),
//visible: this.showVariables.showTitel
});
cm.getColumnByField('bearbeiter').component.updateDefinition({
title: this.$p.t('notiz', 'bearbeiter'),
visible: this.showVariables.showBearbeiter
});
cm.getColumnByField('bearbeiter_uid').component.updateDefinition({
title: this.$p.t('ui', 'bearbeiter_uid'),
});
cm.getColumnByField('start_format').component.updateDefinition({
title: this.$p.t('global', 'gueltigVon'),
visible: this.showVariables.showVon
});
cm.getColumnByField('ende_format').component.updateDefinition({
title: this.$p.t('global', 'gueltigBis'),
visible: this.showVariables.showBis
});
cm.getColumnByField('countdoc').component.updateDefinition({
title: this.$p.t('notiz', 'document'),
visible: this.showVariables.showDokumente
});
cm.getColumnByField('erledigt').component.updateDefinition({
title: this.$p.t('notiz', 'erledigt'),
visible: this.showVariables.showErledigt
});
cm.getColumnByField('lastupdate').component.updateDefinition({
title: this.$p.t('notiz', 'letzte_aenderung'),
visible: this.showVariables.showLastupdate
});
cm.getColumnByField('notiz_id').component.updateDefinition({
visible: this.showVariables.showNotiz_id,
title: this.$p.t('ui', 'notiz_id')
});
cm.getColumnByField('notizzuordnung_id').component.updateDefinition({
visible: this.showVariables.showNotizzuordnung_id,
title: this.$p.t('ui', 'notizzuordnung_id')
});
cm.getColumnByField('type_id').component.updateDefinition({
visible: this.showVariables.showType_id,
title: this.$p.t('ui', 'type_id')
});
cm.getColumnByField('id').component.updateDefinition({
visible: this.showVariables.showId,
title: this.$p.t('ui', 'extension_id')
});
cm.getColumnByField('actions').component.updateDefinition({
title: this.$p.t('global', 'aktionen')
});
const el = col.getElement();
if (!el || !el.querySelector) return;
cm.getColumnByField('text_stripped').component.updateDefinition({
title: this.$p.t('global', 'text'),
width: 250,
tooltip: true,
//clipContents: true,
});
const titleEl = el.querySelector('.tabulator-col-title');
if (titleEl) {
titleEl.textContent = text;
}
};
setHeader('verfasser', this.$p.t('notiz', 'verfasser'));
setHeader('verfasser_uid', this.$p.t('ui', 'verfasser_uid'));
setHeader('titel', this.$p.t('global', 'titel'));
setHeader('bearbeiter', this.$p.t('notiz', 'bearbeiter'));
setHeader('bearbeiter_uid', this.$p.t('ui', 'bearbeiter_uid'));
setHeader('start_format', this.$p.t('global', 'gueltigVon'));
setHeader('ende_format', this.$p.t('global', 'gueltigBis'));
setHeader('countdoc', this.$p.t('notiz', 'document'));
setHeader('erledigt', this.$p.t('notiz', 'erledigt'));
setHeader('lastupdate', this.$p.t('notiz', 'letzte_aenderung'));
setHeader('notiz_id', this.$p.t('ui', 'notiz_id'));
setHeader('notizzuordnung_id', this.$p.t('ui', 'notizzuordnung_id'));
setHeader('type_id', this.$p.t('ui', 'type_id'));
setHeader('id', this.$p.t('ui', 'extension_id'));
setHeader('text_stripped', this.$p.t('global', 'text'));
// Force layout recalculation for handling overflow text
this.$refs.table.tabulator.redraw(true);
}
}
],
notizen: [],
multiupload: true,
mitarbeiter: [],
filteredMitarbeiter: [],
zwischenvar: '',
editorInitialized: false,
editor: null,
notizData: {
typeId: this.typeId,
titel: null,
statusNew: true,
text: '',
lastUpdate: null,
von: null,
bis: null,
document: null,
erledigt: false,
verfasser: null,
bearbeiter: null,
anhang: []
},
showVariables: {
showTitel: false,
showText: false,
showVerfasser: false,
showBearbeiter: false,
showVon: false,
showBis: false,
showDokumente: false,
showErledigt: false,
showNotiz_id: false,
showNotizzuordnung_id: false,
showType_id: false,
showId: false,
showLastupdate: false
}
];
}
},
methods: {
@@ -335,8 +308,6 @@ export default {
this.notizData.bis = this.notizen.ende;
this.notizData.document = this.notizen.dms_id;
this.notizData.erledigt = this.notizen.erledigt;
this.notizData.verfasser = this.notizen.verfasser_uid;
this.notizData.intVerfasser = this.notizen.verfasser_uid;
this.notizData.intBearbeiter = this.notizen.bearbeiter_uid;
this.notizData.bearbeiter = this.notizen.bearbeiter_uid;
}
@@ -359,6 +330,7 @@ export default {
},
addNewNotiz() {
const formData = new FormData();
this.notizData.id = this.id;
formData.append('data', JSON.stringify(this.notizData));
Object.entries(this.notizData.anhang).forEach(([k, v]) => formData.append(k, v));
@@ -402,6 +374,7 @@ export default {
this.notizData = result.data;
this.notizData.typeId = this.typeId;
this.notizData.anhang = [];
this.currentVerfasserUid = result.data.verfasser_uid;
return result;
})
.catch(this.$fhcAlert.handleSystemError);
@@ -455,16 +428,17 @@ export default {
bis: null,
document: null,
erledigt: false,
verfasser: this.uid,
bearbeiter: null,
anhang: []
anhang: [],
};
this.currentVerfasserUid = this.uid
},
getUid() {
return this.$api
.call(this.endpoint.getUid())
.then(result => {
this.notizData.intVerfasser = result.data;
this.currentVerfasserUid = result.data;
this.uid = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
@@ -547,14 +521,7 @@ export default {
},
deep: true
},
'notizData.intVerfasser': {
handler(newVal) {
if (typeof newVal === 'object') {
this.notizData.verfasser = newVal.mitarbeiter_uid;
}
},
deep: true
},
id() {
this.reload();
}
@@ -654,24 +621,23 @@ export default {
<div class="row mb-3">
<label for="bis" class="form-label col-sm-2">{{$p.t('notiz','verfasser')}}</label>
<div v-if="notizData.verfasser_uid" class="col-sm-3">
<input type="text" :readonly="readonly" class="form-control" id="name" v-model="notizData.verfasser_uid">
</div>
<div v-else class="col-sm-3">
<PvAutoComplete v-model="notizData.intVerfasser" optionLabel="mitarbeiter" :suggestions="filteredMitarbeiter" @complete="search" minLength="3"/>
<div class="col-sm-3">
<input type="text" readonly="readonly" class="form-control" id="name" v-model="currentVerfasserUid">
</div>
<label for="von" class="form-label col-sm-1">{{$p.t('global','gueltigVon')}}</label>
<div class="col-sm-3">
<vue-date-picker
id="von"
v-model="notizData.start"
clearable="false"
<form-input
type="DatePicker"
v-model="notizData['start']"
name="von"
auto-apply
:enable-time-picker="false"
format="dd.MM.yyyy"
preview-format="dd.MM.yyyy"
:teleport="true"
preview-format="dd.MM.yyyy"></vue-date-picker>
>
</form-input>
</div>
</div>
@@ -679,26 +645,35 @@ export default {
<label for="bis" class="form-label col-sm-2">{{$p.t('notiz','bearbeiter')}}</label>
<div v-if="notizData.bearbeiter_uid" class="col-sm-3">
<input type="text" :readonly="readonly" class="form-control" id="name" v-model="notizData.bearbeiter_uid">
<input type="text" class="form-control" id="name" v-model="notizData.bearbeiter_uid">
</div>
<div v-else class="col-sm-3">
<PvAutoComplete v-model="notizData.intBearbeiter" optionLabel="mitarbeiter" :suggestions="filteredMitarbeiter" @complete="search" minlength="3"/>
<form-input
type="autocomplete"
v-model="notizData.intBearbeiter"
:suggestions="filteredMitarbeiter"
@complete="search"
optionLabel="mitarbeiter"
minlength="3"
>
</form-input>
</div>
<label for="bis" class="form-label col-sm-1">{{$p.t('global','gueltigBis')}}</label>
<div class="col-sm-3">
<vue-date-picker
id="bis"
<form-input
type="DatePicker"
v-model="notizData.ende"
clearable="false"
name="bis"
auto-apply
:enable-time-picker="false"
format="dd.MM.yyyy"
preview-format="dd.MM.yyyy"
:teleport="true"
preview-format="dd.MM.yyyy">
</vue-date-picker>
>
</form-input>
</div>
</div>
@@ -842,28 +817,15 @@ export default {
<div class="row mb-3">
<form-input
v-if="notizData.verfasser_uid"
container-class="col-6"
:label="$p.t('notiz', 'verfasser')"
type="text"
v-model="notizData.verfasser_uid"
name="titel"
readonly="readonly"
v-model="currentVerfasserUid"
name="verfasser"
>
</form-input>
<form-input
v-else
container-class="col-6"
:label="$p.t('notiz', 'verfasser')"
type="autocomplete"
v-model="notizData.intVerfasser"
:suggestions="filteredMitarbeiter"
@complete="search"
optionLabel="mitarbeiter"
minLength="3"
>
</form-input>
<form-input
v-if="notizData.bearbeiter_uid"
container-class="col-6"
@@ -1008,25 +970,12 @@ export default {
<div class="row mb-3">
<form-input
v-if="notizData.verfasser_uid"
container-class="col-6"
:label="$p.t('notiz', 'verfasser')"
type="text"
v-model="notizData.verfasser_uid"
name="titel"
>
</form-input>
<form-input
v-else
container-class="col-6"
:label="$p.t('notiz', 'verfasser')"
type="autocomplete"
v-model="notizData.intVerfasser"
:suggestions="filteredMitarbeiter"
@complete="search"
optionLabel="mitarbeiter"
minLength="3"
readonly="readonly"
v-model="currentVerfasserUid"
name="verfasser"
>
</form-input>
@@ -1195,33 +1144,21 @@ export default {
<div class="row mb-3">
<form-input
v-if="notizData.verfasser_uid"
container-class="col-6"
:label="$p.t('notiz', 'verfasser')"
type="text"
v-model="notizData.verfasser_uid"
name="titel"
readonly="readonly"
v-model="currentVerfasserUid"
name="verfasser"
>
</form-input>
<form-input
v-else
container-class="col-6"
:label="$p.t('notiz', 'verfasser')"
type="autocomplete"
v-model="notizData.intVerfasser"
:suggestions="filteredMitarbeiter"
@complete="search"
optionLabel="mitarbeiter"
minLength="3"
>
</form-input>
<form-input
v-if="notizData.bearbeiter_uid"
container-class="col-6"
:label="$p.t('notiz', 'bearbeiter')"
v-model="notizData.bearbeiter_uid"
name="bearbeiter"
minlength="3"
>
</form-input>
@@ -1235,6 +1172,7 @@ export default {
:suggestions="filteredMitarbeiter"
@complete="search"
optionLabel="mitarbeiter"
name="bearbeiter"
minlength="3"
>
</form-input>
@@ -1292,4 +1230,4 @@ export default {
<p v-else>Kein Layout übergeben</p>
</div>
</div>`,
}
}
@@ -0,0 +1,25 @@
export const FhcOverlay = {
name: 'FhcOverlay',
props: {
active: {
type: Boolean,
default: false
}
},
template: `
<div v-show="active"
style="
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,0.5);
z-index: 99999999999;
pointer-events: none;
">
<i class="fa-solid fa-spinner fa-pulse fa-5x"></i>
</div>
`
};
export default FhcOverlay;
@@ -84,6 +84,8 @@ export default {
configShowAufnahmegruppen: this.config.showAufnahmegruppen,
configAllowUebernahmePunkte: this.config.allowUebernahmePunkte,
configUseReihungstestPunkte: this.config.useReihungstestPunkte,
configHasExcludedAreas: this.config.hasExcludedAreas,
configStvTagsEnabled: this.config.stvTagsEnabled,
appConfig: Vue.computed(() => this.appconfig),
hasZGVBakkPermission: this.permissions['student/editBakkZgv'],
hasZGVMasterPermission: this.permissions['student/editMakkZgv'],
@@ -94,6 +96,7 @@ export default {
},
data() {
return {
sidebarCollapsed: false,
appconfig: {},
configEndpoints: ApiStvConfig,
selected: [],
@@ -146,7 +149,8 @@ export default {
sprachen: [],
geschlechter: []
},
verbandEndpoint: ApiStvVerband
verbandEndpoint: ApiStvVerband,
filter: []
}
},
computed: {
@@ -392,6 +396,7 @@ export default {
}
},
onSearch(e) {
this.deleteCustomFilter();
const searchsettings = { ...this.$refs.searchbar.searchsettings };
if (searchsettings.searchstr.length >= 2) {
this.blurSearchbar();
@@ -420,6 +425,12 @@ export default {
this.$refs.searchbar.$refs.input.blur();
this.$refs.searchbar.abort();
this.$refs.searchbar.hideresult();
},
handleCustomFilter(filter){
this.filter = filter;
},
deleteCustomFilter(){
this.$refs.stvList.resetFilter();
}
},
created() {
@@ -494,12 +505,10 @@ export default {
.catch(this.$fhcAlert.handleSystemError);
},
mounted() {
//Test manu Systemerror
//FHC_JS_DATA_STORAGE_OBJECT.systemerror_mailto = '[email protected]';this.$fhcAlert.handleSystemError(1);
this.handlePersonUrl();
},
template: /* html */`
<div class="stv">
<div class="stv" :class="{ 'sidebar-collapsed': sidebarCollapsed }">
<header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow">
<div class="col-md-4 col-lg-3 col-xl-2 d-flex align-items-center">
<button
@@ -514,6 +523,14 @@ export default {
<span class="svg-icon svg-icon-apps"></span>
</button>
<a class="navbar-brand me-0" :href="stvRoot">StudVw: {{studiensemesterKurzbz}} {{studiengangKuerzel}}</a>
<button
class="btn btn-outline-light border-0 d-none d-md-inline-flex m-1 ms-auto"
type="button"
@click="sidebarCollapsed = !sidebarCollapsed"
:aria-label="$p.t('ui/toggle_nav')"
>
<span class="fa-solid fa-list"></span>
</button>
</div>
<button
class="btn btn-outline-light border-0 d-md-none m-1 collapsed"
@@ -625,10 +642,10 @@ export default {
<main class="col-md-8 ms-sm-auto col-lg-9 col-xl-10">
<vertical-split>
<template #top>
<stv-list ref="stvList" v-model:selected="selected" :studiengang-kz="studiengangKz" :studiensemester-kurzbz="studiensemesterKurzbz"></stv-list>
<stv-list ref="stvList" v-model:selected="selected" :studiengang-kz="studiengangKz" :studiensemester-kurzbz="studiensemesterKurzbz" @filterActive="handleCustomFilter"></stv-list>
</template>
<template #bottom>
<stv-details ref="details" :students="selected"></stv-details>
<stv-details ref="details" :students="selected" @reload="reloadList"></stv-details>
</template>
</vertical-split>
</main>
@@ -39,7 +39,19 @@ export default {
return Object.fromEntries(Object.entries(this.configStudents).filter(([ , value ]) => !value.showOnlyWithUid));
}
return Object.fromEntries(Object.entries(this.configStudents).filter(([ , value ]) => !value.showOnlyWithUid && !value.showOnlyWithUid));
}
},
tile_PersId(){
let tile = this.students[0].person_id != null ? this.students[0].person_id : '-';
return tile;
},
tile_MatrNr(){
let tile = this.students[0].matr_nr != null ? this.students[0].matr_nr : '-';
return tile;
},
tile_PersKz(){
let tile = this.students[0].matrikelnr != null ? this.students[0].matrikelnr : '-';
return tile;
},
},
watch: {
'$p.user_language.value'(n, o) {
@@ -65,7 +77,10 @@ export default {
reload() {
if (this.$refs.tabs?.$refs?.current?.reload)
this.$refs.tabs.$refs.current.reload();
}
},
reloadList() {
this.$emit('reload');
},
},
created() {
this.loadConfig();
@@ -79,7 +94,16 @@ export default {
<fhc-header
:headerData="students"
typeHeader="student"
@reload="reloadList"
fotoEditable
>
<template #uid>{{students[0].uid}}</template>
<template #titleAlphaTile>PersID</template>
<template #valueAlphaTile>{{tile_PersId}}</template>
<template #titleBetaTile>MatrNr</template>
<template #valueBetaTile>{{tile_MatrNr}}</template>
<template #titleGammaTile>PersKz</template>
<template #valueGammaTile>{{tile_PersKz}}</template>
</fhc-header>
<fhc-tabs
v-if="students.length == 1"
@@ -34,43 +34,61 @@ export default {
default: false
}
},
computed: {
studentUids() {
if (this.student.uid)
{
return [this.student.uid];
}
return this.student.map(e => e.uid);
},
studentKzs(){
if (this.student.uid)
{
return [this.student.studiengang_kz];
}
return this.student.map(e => e.studiengang_kz);
},
stg_kz(){
return this.studentKzs[0];
},
showAllFormats() {
if( this.isBerechtigtDocAndOdt === false
|| !Array.isArray(this.isBerechtigtDocAndOdt) )
{
return false;
}
let retval = this.isBerechtigtDocAndOdt.includes(this.stgInfo.oe_kurzbz);
return retval;
}
},
props: {
student: Object
},
data() {
return {
tabulatorOptions: {
tabulatorData: [],
lastSelected: null,
formData: {
typStg: null,
pruefungstyp_kurzbz: null,
akadgrad_id: null,
vorsitz: null,
pruefungsantritt_kurzbz: null,
abschlussbeurteilung_kurzbz: null,
datum: null,
sponsion: null,
pruefer1: null,
pruefer2: null,
pruefer3: null,
anmerkung: null,
protokoll: null,
note: null,
link: null
},
statusNew: true,
arrTypen: [],
arrAntritte: [],
arrBeurteilungen: [],
arrAkadGrad: [],
arrNoten: [],
selectedVorsitz: null,
filteredMitarbeiter: [],
filteredPersons: [],
selectedPruefer1: null,
selectedPruefer2: null,
selectedPruefer3: null,
stgInfo: { typ: '', oe_kurzbz: '' },
abortController: {
mitarbeiter: null,
persons: null
},
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: 'auto',
minHeight: '200',
}
},
computed: {
tabulatorOptions() {
const options = {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(ApiStvAbschlusspruefung.getAbschlusspruefung(this.student.uid)),
ajaxResponse: (url, params, response) => response.data,
index: 'abschlusspruefung_id',
persistenceID: 'stv-details-finalexam-20260217',
columns: [
{title: "vorsitz", field: "vorsitz_nachname"},
{title: "abschlussbeurteilung", field: "beurteilung_bezeichnung"},
@@ -163,14 +181,11 @@ export default {
frozen: true
},
],
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: 'auto',
minHeight: '200',
index: 'abschlusspruefung_id',
persistenceID: 'stv-details-finalexam-2025112401'
},
tabulatorEvents: [
};
return options;
},
tabulatorEvents() {
const events = [
{
event: 'dataLoaded',
handler: data => this.tabulatorData = data.map(item => {
@@ -181,94 +196,66 @@ export default {
{
event: 'tableBuilt',
handler: async() => {
if (!this.$refs.table) return;
await this.$p.loadCategory(['global', 'person', 'stv', 'abschlusspruefung', 'ui']);
const setHeader = (field, text) => {
const col = this.$refs.table.tabulator.getColumn(field);
if (!col) return;
let cm = this.$refs.table.tabulator.columnManager;
const el = col.getElement();
if (!el || !el.querySelector) return;
cm.getColumnByField('vorsitz_nachname').component.updateDefinition({
title: this.$p.t('abschlusspruefung', 'vorsitz_header')
});
cm.getColumnByField('beurteilung_bezeichnung').component.updateDefinition({
title: this.$p.t('abschlusspruefung', 'abschlussbeurteilung')
});
cm.getColumnByField('p1_nachname').component.updateDefinition({
title: this.$p.t('abschlusspruefung', 'pruefer1')
});
cm.getColumnByField('p2_nachname').component.updateDefinition({
title: this.$p.t('abschlusspruefung', 'pruefer2')
});
cm.getColumnByField('p3_nachname').component.updateDefinition({
title: this.$p.t('abschlusspruefung', 'pruefer3')
});
cm.getColumnByField('datum').component.updateDefinition({
title: this.$p.t('global', 'datum')
});
cm.getColumnByField('uhrzeit').component.updateDefinition({
title: this.$p.t('global', 'uhrzeit')
});
cm.getColumnByField('freigabedatum').component.updateDefinition({
title: this.$p.t('abschlusspruefung', 'freigabe')
});
cm.getColumnByField('antritt_bezeichnung').component.updateDefinition({
title: this.$p.t('abschlusspruefung', 'pruefungsantritt')
});
cm.getColumnByField('sponsion').component.updateDefinition({
title: this.$p.t('abschlusspruefung', 'sponsion')
});
cm.getColumnByField('anmerkung').component.updateDefinition({
title: this.$p.t('global', 'anmerkung')
});
cm.getColumnByField('pruefungstyp_kurzbz').component.updateDefinition({
title: this.$p.t('global', 'typ')
});
cm.getColumnByField('abschlusspruefung_id').component.updateDefinition({
title: this.$p.t('abschlusspruefung', 'abschlusspruefung_id')
});
/*
cm.getColumnByField('actions').component.updateDefinition({
title: this.$p.t('global', 'aktionen')
});
*/
const titleEl = el.querySelector('.tabulator-col-title');
if (titleEl) {
titleEl.textContent = text;
}
};
setHeader('vorsitz_nachname', this.$p.t('abschlusspruefung', 'vorsitz_header'));
setHeader('beurteilung_bezeichnung', this.$p.t('abschlusspruefung', 'abschlussbeurteilung'));
setHeader('p1_nachname', this.$p.t('abschlusspruefung', 'pruefer1'));
setHeader('p2_nachname', this.$p.t('abschlusspruefung', 'pruefer2'));
setHeader('p3_nachname', this.$p.t('abschlusspruefung', 'pruefer3'));
setHeader('datum', this.$p.t('global', 'datum'));
setHeader('uhrzeit', this.$p.t('global', 'uhrzeit'));
setHeader('freigabedatum', this.$p.t('abschlusspruefung', 'freigabe'));
setHeader('antritt_bezeichnung', this.$p.t('abschlusspruefung', 'pruefungsantritt'));
setHeader('sponsion', this.$p.t('abschlusspruefung', 'sponsion'));
setHeader('anmerkung', this.$p.t('global', 'anmerkung'));
setHeader('pruefungstyp_kurzbz', this.$p.t('global', 'typ'));
setHeader('abschlusspruefung_id', this.$p.t('abschlusspruefung', 'abschlusspruefung_id'));
}
}
],
tabulatorData: [],
lastSelected: null,
formData: {
typStg: null,
pruefungstyp_kurzbz: null,
akadgrad_id: null,
vorsitz: null,
pruefungsantritt_kurzbz: null,
abschlussbeurteilung_kurzbz: null,
datum: null,
sponsion: null,
pruefer1: null,
pruefer2: null,
pruefer3: null,
anmerkung: null,
protokoll: null,
note: null,
link: null
},
statusNew: true,
arrTypen: [],
arrAntritte: [],
arrBeurteilungen: [],
arrAkadGrad: [],
arrNoten: [],
selectedVorsitz: null,
filteredMitarbeiter: [],
filteredPersons: [],
selectedPruefer1: null,
selectedPruefer2: null,
selectedPruefer3: null,
stgInfo: { typ: '', oe_kurzbz: '' },
abortController: {
mitarbeiter: null,
persons: null
},
];
return events;
},
studentUids() {
if (this.student.uid)
{
return [this.student.uid];
}
return this.student.map(e => e.uid);
},
studentKzs(){
if (this.student.uid)
{
return [this.student.studiengang_kz];
}
return this.student.map(e => e.studiengang_kz);
},
stg_kz(){
return this.studentKzs[0];
},
showAllFormats() {
if( this.isBerechtigtDocAndOdt === false
|| !Array.isArray(this.isBerechtigtDocAndOdt) )
{
return false;
}
let retval = this.isBerechtigtDocAndOdt.includes(this.stgInfo.oe_kurzbz);
return retval;
}
},
watch: {
@@ -339,7 +326,7 @@ export default {
},
getPersonLabel(titelpre, nachname, vorname, titelpost, uid) {
return nachname + ' ' + vorname + (titelpre ? ' ' + titelpre : '') + (titelpost ? ' ' + titelpost : '') + (uid ? ' (' + uid + ')' : '');
},
actionDeleteAbschlusspruefung(abschlusspruefung_id) {
this.$fhcAlert
@@ -841,4 +828,3 @@ export default {
</div>
`
}
@@ -31,12 +31,30 @@ export default {
},
data() {
return {
tabulatorOptions: {
formData: {},
listBegruendungen: [],
listNewLehrveranstaltungen: [],
listLektoren: [],
listKompatibleLehrveranstaltungen: [],
statusNew: true,
showNotizen: false,
currentAnrechnung_id: null,
endpoint: ApiNotizPerson,
}
},
computed: {
tabulatorOptions() {
const options = {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(
ApiStvExemptions.getAnrechnungen(this.student.prestudent_id)
),
ajaxResponse: (url, params, response) => response.data,
layout: 'fitDataStretchFrozen',
height: '500',
index: 'anrechnung_id',
persistenceID: 'stv-details-anrechnungen-20260217',
selectableRows: true,
columns: [
{title: "anrechnung_id", field: "anrechnung_id", visible: false},
{title: "lehrveranstaltung_id", field: "lehrveranstaltung_id", visible: false},
@@ -107,63 +125,43 @@ export default {
frozen: true
},
],
layout: 'fitDataStretchFrozen',
height: '500',
index: 'anrechnung_id',
persistenceID: 'stv-details-anrechnungen-2025112401'
},
tabulatorEvents: [
};
return options;
},
tabulatorEvents() {
const events = [
{
event: 'tableBuilt',
handler: async () => {
await this.$p.loadCategory(['anrechnungen', 'global', 'ui', 'lehre']);
let cm = this.$refs.table.tabulator.columnManager;
const setHeader = (field, text) => {
const col = this.$refs.table.tabulator.getColumn(field);
if (!col) return;
cm.getColumnByField('anrechnung_id').component.updateDefinition({
title: this.$p.t('ui', 'anrechnung_id'),
});
cm.getColumnByField('lehrveranstaltung_id').component.updateDefinition({
title: this.$p.t('lehre', 'lehrveranstaltung_id'),
});
cm.getColumnByField('bez_lehrveranstaltung').component.updateDefinition({
title: this.$p.t('lehre', 'lehrveranstaltung'),
});
cm.getColumnByField('begruendung').component.updateDefinition({
title: this.$p.t('global', 'begruendung'),
});
cm.getColumnByField('lehrveranstaltung_id_kompatibel').component.updateDefinition({
title: this.$p.t('anrechnung', 'lehrveranstaltung_id_kompatibel'),
});
cm.getColumnByField('lehrveranstaltung_bez_kompatibel').component.updateDefinition({
title: this.$p.t('anrechnung', 'lehrveranstaltung_bez_kompatibel'),
});
cm.getColumnByField('status').component.updateDefinition({
title: this.$p.t('global', 'status'),
});
cm.getColumnByField('genehmigt_von').component.updateDefinition({
title: this.$p.t('anrechnung', 'genehmigtVon'),
});
cm.getColumnByField('notizen_anzahl').component.updateDefinition({
title: this.$p.t('anrechnung', 'existingNotes'),
});
cm.getColumnByField('insertamum').component.updateDefinition({
title: this.$p.t('global', 'datum'),
});
const el = col.getElement();
if (!el || !el.querySelector) return;
const titleEl = el.querySelector('.tabulator-col-title');
if (titleEl) {
titleEl.textContent = text;
}
};
setHeader( 'anrechnung_id', this.$p.t('ui', 'anrechnung_id'));
setHeader( 'lehrveranstaltung_id', this.$p.t('lehre', 'lehrveranstaltung_id'));
setHeader( 'bez_lehrveranstaltung', this.$p.t('lehre', 'lehrveranstaltung'));
setHeader( 'begruendung', this.$p.t('global', 'begruendung'));
setHeader( 'lehrveranstaltung_id_kompatibel', this.$p.t('anrechnung', 'lehrveranstaltung_id_kompatibel'));
setHeader( 'lehrveranstaltung_bez_kompatibel', this.$p.t('anrechnung', 'lehrveranstaltung_bez_kompatibel'));
setHeader( 'status', this.$p.t('global', 'status'));
setHeader( 'genehmigt_von', this.$p.t('anrechnung', 'genehmigtVon'));
setHeader( 'notizen_anzahl', this.$p.t('anrechnung', 'existingNotes'));
setHeader( 'insertamum', this.$p.t('global', 'datum'));
}
}
],
formData: {},
listBegruendungen: [],
listNewLehrveranstaltungen: [],
listLektoren: [],
listKompatibleLehrveranstaltungen: [],
statusNew: true,
showNotizen: false,
currentAnrechnung_id: null,
endpoint: ApiNotizPerson
];
return events;
}
},
watch: {
@@ -74,6 +74,16 @@ export default {
],
'abschlussdokument_lehrgaenge.xml.php': [
'AbschlussdokumentLehrgaenge'
],
'microcredential.xml.php' : [
'microcredentialzertifikat_1',
'microcredentialzertifikat_2',
'microcredentialzertifikat_3',
'microcredentialzertifikat_4',
'microcredential_1',
'microcredential_2',
'microcredential_3',
'microcredential_4',
]
},
documentDropdownObject: {}
@@ -91,12 +101,24 @@ export default {
ajaxResponse: (url, params, response) => response.data,
layout:"fitDataTable",
index: 'akte_id',
persistenceID: 'stv-details-archiv',
persistenceID: 'stv-details-archiv-20260217',
columns: [
{title: "Akte Id", field: "akte_id", visible: false},
{title: this.$p.t('stv', 'archiv_title'), field: "titel"},
{title: this.$p.t('stv', 'archiv_description'), field: "bezeichnung"},
{title: this.$p.t('stv', 'archiv_creation_date'), field: "erstelltam"},
{title: this.$p.t('stv', 'archiv_creation_date'), field: "erstelltam",
formatter: function (cell) {
const dateStr = cell.getValue();
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour12: false
});
}},
{
title: this.$p.t('stv', 'archiv_signiert'),
field: "signiert",
@@ -117,7 +139,22 @@ export default {
crossElement: '<i class="fa fa-xmark text-danger"></i>'
},
},
{title: this.$p.t('stv', 'archiv_accepted_on_at'), field: "akzeptiertamum"},
{title: this.$p.t('stv', 'archiv_accepted_on_at'), field: "akzeptiertamum",
formatter: function (cell) {
const dateStr = cell.getValue();
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
});
}},
{
title: this.$p.t('stv', 'archiv_gedruckt'),
field: "gedruckt",
@@ -198,18 +235,32 @@ export default {
return events;
},
studentUids() {
if (this.modelValue.uid)
if(Array.isArray(this.modelValue))
{
return this.modelValue.map(e => e.uid);
}
else if (this.modelValue.uid)
{
return [this.modelValue.uid];
}
return this.modelValue.map(e => e.uid);
else
{
return [];
}
},
studentKzs(){
if (this.modelValue.uid)
if(Array.isArray(this.modelValue))
{
return this.modelValue.map(e => e.studiengang_kz);
}
else if (typeof this.modelValue.studiengang_kz !== 'undefined')
{
return [this.modelValue.studiengang_kz];
}
return this.modelValue.map(e => e.studiengang_kz);
else
{
return [];
}
},
stg_kz(){
return this.studentKzs[0];
@@ -23,6 +23,10 @@ export default {
from: 'configUseReihungstestPunkte',
default: true
},
hasExcludedAreas: {
from: 'configHasExcludedAreas',
default: false
},
$reloadList: {
from: '$reloadList',
required: true
@@ -35,159 +39,20 @@ export default {
student: Object
},
data() {
let self = this;
return {
tabulatorOptions: {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(
ApiStvAdmissionDates.getAufnahmetermine(this.student.person_id)
),
ajaxResponse: (url, params, response) => response.data,
columns: [
{title: "rt_id", field: "rt_id", visible: false},
{title: "rt_person_id", field: "rt_person_id", visible: false},
{title: "person_id", field: "person_id", visible: false},
{title: "datum", field: "datum",
formatter: function (cell) {
const dateStr = cell.getValue();
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
}
},
{title: "stufe", field: "stufe"},
{title: "studiensemester", field: "studiensemester"},
{title: "anmerkung", field: "anmerkung", visible: false},
{title: "anmeldedatum", field: "anmeldedatum", visible: false,
formatter: function (cell) {
const dateStr = cell.getValue();
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
}
},
{title: "punkte", field: "punkte"},
{
title: "teilgenommen", field: "teilgenommen",
formatter: "tickCross",
hozAlign: "center",
formatterParams: {
tickElement: '<i class="fa fa-check text-success"></i>',
crossElement: '<i class="fa fa-xmark text-danger"></i>'
}
},
{title: "ort", field: "ort", visible: false},
{title: "studienplan", field: "studienplan", visible: false},
{title: "studienplan_id", field: "studienplan_id", visible: false},
{title: "stg", field: "studiengangkurzbzlang"},
{title: "Stg", field: "stg_kuerzel"},
{
title: 'Aktionen', field: 'actions',
minWidth: 150, // Ensures Action-buttons will be always fully displayed
formatter: (cell, formatterParams, onRendered) => {
let container = document.createElement('div');
container.className = "d-flex gap-2";
let button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-edit"></i>';
button.title = this.$p.t('ui', 'bearbeiten');
button.addEventListener('click', (event) =>
this.actionEditPlacementTest(cell.getData().rt_person_id)
);
container.append(button);
button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-xmark"></i>';
button.title = this.$p.t('ui', 'loeschen');
button.addEventListener('click', () =>
this.actionDeletePlacementTest(cell.getData().rt_person_id)
);
container.append(button);
return container;
},
frozen: true
}
],
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: 'auto',
minHeight: 200,
index: 'aufnahmetermin_id',
persistenceID: 'stv-details-table_admission-dates-2025112401'
},
tabulatorEvents: [
{
event: 'tableBuilt',
handler: async () => {
await this.$p.loadCategory(['admission', 'global', 'person', 'ui', 'projektarbeitsbeurteilung']);
let cm = this.$refs.table.tabulator.columnManager;
cm.getColumnByField('rt_id').component.updateDefinition({
title: this.$p.t('ui', 'reihungstest_id')
});
cm.getColumnByField('rt_person_id').component.updateDefinition({
title: this.$p.t('ui', 'reihungstest_person_id')
});
cm.getColumnByField('person_id').component.updateDefinition({
title: this.$p.t('person', 'person_id')
});
cm.getColumnByField('datum').component.updateDefinition({
title: this.$p.t('global', 'datum')
});
cm.getColumnByField('stufe').component.updateDefinition({
title: this.$p.t('admission', 'stufe')
});
cm.getColumnByField('studiensemester').component.updateDefinition({
title: this.$p.t('lehre', 'studiensemester')
});
cm.getColumnByField('anmerkung').component.updateDefinition({
title: this.$p.t('global', 'anmerkung')
});
cm.getColumnByField('anmeldedatum').component.updateDefinition({
title: this.$p.t('admission', 'anmeldedatum')
});
cm.getColumnByField('punkte').component.updateDefinition({
title: this.$p.t('exam', 'punkte')
});
cm.getColumnByField('teilgenommen').component.updateDefinition({
title: this.$p.t('admission', 'teilgenommen')
});
cm.getColumnByField('ort').component.updateDefinition({
title: this.$p.t('person', 'ort')
});
cm.getColumnByField('studienplan').component.updateDefinition({
title: this.$p.t('lehre', 'studienplan')
});
cm.getColumnByField('studienplan_id').component.updateDefinition({
title: this.$p.t('ui', 'studienplan_id')
});
cm.getColumnByField('studiengangkurzbzlang').component.updateDefinition({
title: this.$p.t('projektarbeitsbeurteilung', 'studiengang')
});
cm.getColumnByField('stg_kuerzel').component.updateDefinition({
title: this.$p.t('admission', 'stg_kurz')
});
}
}
],
formData: {},
statusNew: true,
listPlacementTests: [],
listStudyPlans: [],
filterOnlyFutureTestsSet: false,
filteredPlacementTests: []
filteredPlacementTests: [],
youngestSemester: null,
stgRtPers: null,
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: 'auto',
minHeight: 200
}
},
methods: {
@@ -197,11 +62,12 @@ export default {
this.formData.anmeldedatum = new Date();
this.$refs.placementTestModal.show();
},
actionEditPlacementTest(rt_person_id) {
actionEditPlacementTest(rt_person_id, stg_kz) {
this.resetForm();
this.statusNew = false;
this.loadPlacementTest(rt_person_id);
this.$refs.placementTestModal.show();
this.stgRtPers = stg_kz;
},
actionDeletePlacementTest(rt_person_id) {
this.$fhcAlert
@@ -271,12 +137,13 @@ export default {
this.reload();
});
},
getResultReihungstest(reihungstest_id){
getResultReihungstest(reihungstest_id, stg_kz){
const paramsRt = {
reihungstest_id: reihungstest_id,
person_id: this.student.person_id,
punkte: this.useReihungstestPunkte,
studiengang_kz: this.student.studiengang_kz
studiengang_kz: stg_kz,
hasExcludedAreas: this.hasExcludedAreas
};
return this.$api
@@ -330,13 +197,188 @@ export default {
},
resetForm() {
this.formData = {};
this.stgRtPers = null;
},
parseSemester(semester) {
const type = semester.slice(0, 2).toUpperCase(); // "WS" or "SS"
const year = parseInt(semester.slice(2), 10);
// WS > SS
return year * 10 + (type === 'SS' ? 1 : 2);
}
},
computed: {
tabulatorOptions() {
const self = this;
const options = {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(
ApiStvAdmissionDates.getAufnahmetermine(this.student.person_id)
),
ajaxResponse: (url, params, response) => {
const data = response.data;
const filtered = data.filter(item =>
item.studiengang_kz_ber === this.student.studiengang_kz
);
if (filtered.length > 0) {
filtered.sort((a, b) =>
this.parseSemester(b.studiensemester) - this.parseSemester(a.studiensemester)
);
self.youngestSemester = filtered[0].studiensemester;
} else {
self.youngestSemester = null;
}
return data;
},
rowFormatter: function(row) {
let data = row.getData();
if (data.studiengang_kz_ber === self.student.studiengang_kz &&
data.studiensemester === self.youngestSemester) {
let cells = row.getCells();
cells.forEach((c) => {
c.getElement().classList.add("row-green");
}
);
}
},
dataLoaded: function() {
this.redraw(true);
},
index: 'aufnahmetermin_id',
persistenceID: 'stv-details-table_admission-dates-20260217',
columns: [
{title: "rt_id", field: "rt_id", visible: false},
{title: "rt_person_id", field: "rt_person_id", visible: false},
{title: "person_id", field: "person_id", visible: false},
{title: "datum", field: "datum",
formatter: function (cell) {
const dateStr = cell.getValue();
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
}
},
{title: "stufe", field: "stufe"},
{title: "studiensemester", field: "studiensemester"},
{title: "anmerkung", field: "anmerkung", visible: false},
{title: "anmeldedatum", field: "anmeldedatum", visible: false,
formatter: function (cell) {
const dateStr = cell.getValue();
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
}
},
{title: "punkte", field: "punkte"},
{
title: "teilgenommen", field: "teilgenommen",
formatter: "tickCross",
hozAlign: "center",
formatterParams: {
tickElement: '<i class="fa fa-check text-success"></i>',
crossElement: '<i class="fa fa-xmark text-danger"></i>'
}
},
{title: "ort", field: "ort", visible: false},
{title: "studienplan", field: "studienplan", visible: false},
{title: "studienplan_id", field: "studienplan_id", visible: false},
//{title: "stg", field: "studiengangkurzbzlang"},
{title: "stg_ber", field: "studiengangkurzbzlang_ber"},
{title: "Stg_Kz", field: "studiengang_kz_ber", visible: false},
{
title: 'Aktionen', field: 'actions',
minWidth: 150, // Ensures Action-buttons will be always fully displayed
formatter: (cell, formatterParams, onRendered) => {
let container = document.createElement('div');
container.className = "d-flex gap-2";
let button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-edit"></i>';
button.title = this.$p.t('ui', 'bearbeiten');
button.addEventListener('click', (event) =>
this.actionEditPlacementTest(cell.getData().rt_person_id, cell.getData().studiengang_kz_ber)
);
container.append(button);
button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-xmark"></i>';
button.title = this.$p.t('ui', 'loeschen');
button.addEventListener('click', () =>
this.actionDeletePlacementTest(cell.getData().rt_person_id)
);
container.append(button);
return container;
},
frozen: true
}
],
};
return options;
},
tabulatorEvents() {
const events = [
{
event: 'tableBuilt',
handler: async () => {
await this.$p.loadCategory(['admission', 'global', 'person', 'ui', 'projektarbeitsbeurteilung']);
const setHeader = (field, text) => {
const col = this.$refs.table.tabulator.getColumn(field);
if (!col) return;
const el = col.getElement();
if (!el || !el.querySelector) return;
const titleEl = el.querySelector('.tabulator-col-title');
if (titleEl) {
titleEl.textContent = text;
}
};
setHeader('rt_id', this.$p.t('ui', 'reihungstest_id'));
setHeader('rt_person_id', this.$p.t('ui', 'reihungstest_person_id'));
setHeader('person_id', this.$p.t('person', 'person_id'));
setHeader('datum', this.$p.t('global', 'datum'));
setHeader('stufe', this.$p.t('admission', 'stufe'));
setHeader('studiensemester', this.$p.t('lehre', 'studiensemester'));
setHeader('anmerkung', this.$p.t('global', 'anmerkung'));
setHeader('anmeldedatum', this.$p.t('admission', 'anmeldedatum'));
setHeader('punkte', this.$p.t('exam', 'punkte'));
setHeader('teilgenommen', this.$p.t('admission', 'teilgenommen'));
setHeader('ort', this.$p.t('person', 'ort'));
setHeader('studienplan', this.$p.t('lehre', 'studienplan'));
setHeader('studienplan_id', this.$p.t('ui', 'studienplan_id'));
setHeader('studiengangkurzbzlang_ber', this.$p.t('projektarbeitsbeurteilung', 'studiengang'));
setHeader('studiengang_kz_ber', this.$p.t('admission', 'stg_kurz'));
}
}
];
return events;
}
},
created() {
this.$api
.call(ApiStvAdmissionDates.getListPlacementTests(this.student.prestudent_id))
.then(result => {
this.listPlacementTests = this.filteredPlacementTests = result.data;
if(result.data)
this.listPlacementTests = this.filteredPlacementTests = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
@@ -488,7 +530,7 @@ export default {
<div v-if="allowUebernahmePunkte" class="col-4">
<label class="form-label" style="color:transparent;">getPunkte</label>
<button class="btn btn-outline-secondary w-100" @click="getResultReihungstest(formData.rt_id)">{{ $p.t('admission', 'getRTErgebnis') }}</button>
<button class="btn btn-outline-secondary w-100" @click="getResultReihungstest(formData.rt_id, stgRtPers)">{{ $p.t('admission', 'getRTErgebnis') }}</button>
</div>
</div>
@@ -50,11 +50,24 @@ export default {
this.listAufnahmetermine = result.data;
const listAufnahmetermineFiltered = this.listAufnahmetermine
.filter(item => item.studiengangkurzbzlang == this.student.studiengang)
.filter(item => item.studiengang_kz_ber == this.student.studiengang_kz)
.sort((a, b) => this.parseSemester(b.studiensemester) - this.parseSemester(a.studiensemester));
const elementSemYoungest = listAufnahmetermineFiltered[0];
this.formData.rt_gesamtpunkte = elementSemYoungest.punkte;
let pointsSemStg = 0;
if(listAufnahmetermineFiltered.length > 0){
const youngestSemester = listAufnahmetermineFiltered[0].studiensemester;
//sum of all rt-points of studiengang of youngest sem
pointsSemStg = listAufnahmetermineFiltered
.filter(item => item.studiensemester === youngestSemester)
.reduce((sum, item) => sum + (Number(item.punkte) || 0), 0);
}
else
pointsSemStg = 0;
this.formData.rt_gesamtpunkte = pointsSemStg;
})
.catch(this.$fhcAlert.handleSystemError);
@@ -91,8 +104,8 @@ export default {
},
template: `
<div class="stv-details-admission-header-placement h-100 pb-3">
<h4>{{ $p.t('lehre', 'studiengang') }}</h4>
<h4>{{ $p.t('lehre', 'studiengang') }}</h4> {{student.studiengang}}
<form-form class="mt-3" ref="formRtGesamtData" @submit.prevent>
<div v-if="showAufnahmegruppen" class="row mb-3">
<div class="col-1">

Some files were not shown because too many files have changed in this diff Show More