Merge branch 'master' into feature-52366/CI_LoginAsUID_verbessern

This commit is contained in:
Paolo
2026-02-23 14:17:36 +01:00
277 changed files with 26380 additions and 4474 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
View File
@@ -854,4 +854,10 @@ html {
#cis-main .modal-footer {
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);
}
+29 -3
View File
@@ -1,14 +1,38 @@
@import './Fhc.css';
@import './SvgIcons.css';
@import './components/searchbar/searchbar.css';
@import './components/verticalsplit.css';
@import './components/FilterComponent.css';
@import './components/Tabs.css';
@import './components/Notiz.css';
@import './components/Messages.css';
@import './components/AppMenu.css';
html {
font-size: .875em;
}
html.fs_xx-small {
font-size: .5em;
}
html.fs_x-small {
font-size: .625em;
}
html.fs_small {
font-size: .75em;
}
html.fs_normal {
font-size: .875em;
}
html.fs_big {
font-size: 1em;
}
html.fs_huge {
font-size: 1.125em;
}
#appMenu {
width: 300px;
}
.navbar-dark .navbar-brand:focus {
box-shadow: 0 0 0 .25rem rgba(13,110,253,.25);
@@ -37,8 +61,10 @@ html {
flex: 1 1 auto;
}
#sidebarMenu {
width: 0%;
#nav-user-btn img {
object-fit: contain;
height: 2.5rem;
width: 2.5rem;
}
.tabulator-row.disabled.tabulator-row-odd .tabulator-cell {
@@ -158,4 +184,4 @@ html {
.tiny-90 div.tox.tox-tinymce {
height: 90% !important;
}
}
+28
View File
@@ -0,0 +1,28 @@
/* Themable Variables */
:root {
--svg-icon-apps: var(--fhc-icon-apps, url('data:image/svg+xml,\
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">\
<circle cx="15" cy="15" r="15"/>\
<circle cx="15" cy="50" r="15"/>\
<circle cx="15" cy="85" r="15"/>\
<circle cx="50" cy="15" r="15"/>\
<circle cx="50" cy="50" r="15"/>\
<circle cx="50" cy="85" r="15"/>\
<circle cx="85" cy="15" r="15"/>\
<circle cx="85" cy="50" r="15"/>\
<circle cx="85" cy="85" r="15"/>\
</svg>'));
}
.svg-icon {
display: inline-block;
width: 1rem;
line-height: 1;
background-color: currentColor;
}
.svg-icon:before {
content: "\00a0";
}
.svg-icon.svg-icon-apps {
-webkit-mask-image: var(--svg-icon-apps);
mask-image: var(--svg-icon-apps);
}
+30
View File
@@ -0,0 +1,30 @@
.fhc-app-menu {
display: flex;
flex-direction: column;
padding-left: 0;
margin: calc(var(--bs-offcanvas-padding-y) * -1) calc(var(--bs-offcanvas-padding-x) * -1);
}
.fhc-app-menu li {
display: block;
border: var(--bs-border-width) solid var(--bs-border-color);
}
.fhc-app-menu li + li {
border-top-width: 0;
}
.fhc-app-menu li a {
display: block;
padding: .5rem 1rem;
text-decoration: none;
}
.fhc-app-menu li a.disabled {
--bs-link-opacity: .5;
}
.fhc-app-menu li a.active,
.fhc-app-menu li a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
background: var(--surface-hover);
}
.fhc-app-menu li a.disabled,
.fhc-app-menu li a.active {
pointer-events: none;
}
+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 */
}
+29
View File
@@ -5690,3 +5690,32 @@
outline: 0;
box-shadow: 0 0 0 .25rem rgba(13,110,253,.25);
}
/* input-group */
/* autocomplete */
.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) > .p-autocomplete-input,
.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control > .p-autocomplete-input,
.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select > .p-autocomplete-input,
.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating) > .p-autocomplete-input,
.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) > .p-autocomplete > .p-autocomplete-input,
.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control > .p-autocomplete > .p-autocomplete-input,
.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select > .p-autocomplete > .p-autocomplete-input,
.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating) > .p-autocomplete > .p-autocomplete-input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) > .p-autocomplete-input,
.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) > .p-autocomplete > .p-autocomplete-input {
margin-left: calc(var(--bs-border-width) * -1);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.p-inputtext.is-invalid:enabled:hover {
border-color: var(--bs-form-invalid-border-color);
}
.p-inputtext.is-invalid:enabled:focus,
.was-validated .p-inputtext:invalid:focus {
border-color: var(--bs-form-invalid-border-color);
box-shadow: 0 0 0 .25rem rgba(var(--bs-danger-rgb),.25);
}
+15 -1
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;
}
@@ -95,6 +103,12 @@
.modificationdate {
font-style: italic;
font-size: 0.7em;
font-size: 1em;
text-align: left;
}
.copy-btn {
float: right;
margin-top: 3px;
}
+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);
}
+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},
};
}
};
+19 -29
View File
@@ -38,16 +38,18 @@ export default {
url: 'api/frontend/v1/messages/messages/getMsgVarsLoggedInUser/'
};
},
getMessageVarsPerson(userParams){
getMessageVarsPerson(ids, type_id){
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/getMessageVarsPerson/' + userParams.id + '/' + userParams.type_id
url: 'api/frontend/v1/messages/messages/getMessageVarsPerson/' + type_id,
params: {ids}
};
},
getMsgVarsPrestudent(userParams){
getMsgVarsPrestudent(ids, type_id){
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/getMsgVarsPrestudent/' + userParams.id + '/' + userParams.type_id
url: 'api/frontend/v1/messages/messages/getMsgVarsPrestudent/' + type_id,
params: {ids}
};
},
getPersonId(params){
@@ -56,28 +58,23 @@ export default {
url: 'api/frontend/v1/messages/messages/getPersonId/' + params.id + '/' + params.type_id
};
},
getUid(userParams){
getDataVorlage(vorlage_kurzbz){
return {
method: 'get',
url: 'api/frontend/v1/messages/messages/getUid/' + userParams.id + '/' + userParams.type_id
url: 'api/frontend/v1/messages/messages/getDataVorlage/' + vorlage_kurzbz
};
},
getVorlagentext(vorlage_kurzbz){
return {
method: 'get',
url: 'api/frontend/v1/messages/messages/getVorlagentext/' + vorlage_kurzbz
};
},
getNameOfDefaultRecipient(params){
return {
method: 'get',
url: 'api/frontend/v1/messages/messages/getNameOfDefaultRecipient/' + params.id + '/' + params.type_id
};
},
getPreviewText(userParams, params){
getNameOfDefaultRecipients(ids, type_id){
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/getPreviewText/' + userParams.id + '/' + userParams.type_id,
url: 'api/frontend/v1/messages/messages/getNameOfDefaultRecipients/' + type_id,
params: {ids}
};
},
getPreviewText(type_id, params){
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/getPreviewText/' + type_id,
params
};
},
@@ -87,17 +84,10 @@ export default {
url: 'api/frontend/v1/messages/messages/getReplyData/' + messageId
};
},
sendMessageFromModalContext(id, params) {
sendMessage(type_id, params) {
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/sendMessage/' + id,
params
};
},
sendMessage(id, params) {
return {
method: 'post',
url: 'api/frontend/v1/messages/messages/sendMessage/' + id,
url: 'api/frontend/v1/messages/messages/sendMessage/' + type_id,
params
};
},
+11
View File
@@ -0,0 +1,11 @@
export default {
getStudienplaeneBySemester(studiengang_kz, studiensemester_kurzbz, ausbildungssemester, orgform_kurzbz)
{
return {
method: 'get',
url: 'api/frontend/v1/organisation/studienplan/getBySemester',
params: { studiengang_kz, studiensemester_kurzbz, ausbildungssemester, orgform_kurzbz },
};
}
}
+33
View File
@@ -0,0 +1,33 @@
/**
* 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 {
method: 'get',
url: 'api/frontend/v1/organisation/studiensemester/getAll',
params: { order, start }
};
}
};
+1 -1
View File
@@ -34,7 +34,7 @@ export default {
url: '/api/frontend/v1/studstatus/leitung/getHistory/' + antrag_id
};
},
getPrestudents(query, signal) {
getPrestudents(query) {
return {
method: 'post',
url: '/api/frontend/v1/studstatus/leitung/getPrestudents',
+8 -11
View File
@@ -86,13 +86,15 @@ export default {
getMitarbeiter(searchString) {
return {
method: 'get',
url: 'api/frontend/v1/stv/abschlusspruefung/getMitarbeiter/' + searchString
url: 'api/frontend/v1/stv/abschlusspruefung/getMitarbeiter',
params: { searchString }
};
},
getPruefer(searchString) {
return {
method: 'get',
url: 'api/frontend/v1/stv/abschlusspruefung/getPruefer/' + searchString
url: 'api/frontend/v1/stv/abschlusspruefung/getPruefer',
params: { searchString }
};
},
getNoten() {
@@ -108,16 +110,11 @@ export default {
params: { uids }
};
},
getAllMitarbeiter(){
searchPerson(searchString) {
return {
method: 'get',
url: 'api/frontend/v1/stv/abschlusspruefung/getAllMitarbeiter/'
url: 'api/frontend/v1/stv/abschlusspruefung/searchPerson/',
params: { id }
};
},
getAllPersons(){
return {
method: 'get',
url: 'api/frontend/v1/stv/abschlusspruefung/getAllPersons/'
};
}
};
};
+6
View File
@@ -16,6 +16,12 @@
*/
export default {
configFilter() {
return {
method: 'get',
url: 'api/frontend/v1/stv/config/filter'
};
},
configStudent() {
return {
method: 'get',
+1 -1
View File
@@ -39,5 +39,5 @@ export default {
url: 'api/frontend/v1/stv/archiv/delete',
params: {akte_id, studiengang_kz}
};
}
},
};
+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 {
get() {
return {
method: 'get',
url: 'api/frontend/v1/stv/config/get'
};
},
set(params) {
return {
method: 'post',
url: 'api/frontend/v1/stv/config/set',
params
};
}
};
+10
View File
@@ -35,4 +35,14 @@ export default {
params
};
},
saveStudent(student_uid, studiensemester_kurzbz, params) {
return {
method: 'post',
url: 'api/frontend/v1/stv/student/saveStudent/'
+ encodeURIComponent(student_uid)
+ '/'
+ encodeURIComponent(studiensemester_kurzbz),
params
};
}
};
+13
View File
@@ -73,4 +73,17 @@ export default {
params
};
},
getDocumentDropdown(params){
return {
method: 'get',
url: 'api/frontend/v1/stv/dokumente/getDocumentDropDown/' + params.prestudent_id + '/' + params.studiensemester_kurzbz + '/' + params.studiengang_kz,
};
},
getDocumentDropdownMulti(studentUids, params){
return {
method: 'get',
url: 'api/frontend/v1/stv/dokumente/getDocumentDropDownMulti/' + params.studiensemester_kurzbz + '/' + params.studiengang_kz,
params: {studentUids}
};
}
}
+21
View File
@@ -16,6 +16,27 @@
*/
export default {
add(uid, gruppe_kurzbz, studiensemester_kurzbz) {
return {
method: 'post',
url: 'api/frontend/v1/stv/gruppen/add/',
params: {
uid,
gruppe_kurzbz,
studiensemester_kurzbz
}
};
},
search(query, studiengang_kz) {
return {
method: 'post',
url: 'api/frontend/v1/stv/gruppen/search/',
params: {
query,
studiengang_kz
}
};
},
getGruppen(id) {
return {
method: 'get',
+31
View File
@@ -0,0 +1,31 @@
/**
* 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 {
hasOrgforms(studiengang_kz) {
return {
method: 'get',
url: 'api/frontend/v1/stv/lehrverband/hasOrgforms/' + studiengang_kz
};
},
getTree(studiengang_kz) {
return {
method: 'get',
url: 'api/frontend/v1/stv/lehrverband/getTree/' + studiengang_kz
};
}
};
+1 -1
View File
@@ -101,7 +101,7 @@ export default {
deleteMobilityPurpose(params) {
return {
method: 'post',
url: 'api/frontend/v1/stv/mobility/deleteMobilityPurpose/',
url: 'api/frontend/v1/stv/mobility/deleteMobilityPurpose/' + params.bisio_id,
params
};
},
@@ -0,0 +1,86 @@
/**
* 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 {
getProjektarbeit(uid) {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektarbeit/getProjektarbeit',
params: { uid }
};
},
getTypenProjektarbeit() {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektarbeit/getTypenProjektarbeit'
};
},
getFirmen(searchString) {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektarbeit/getFirmen',
params: {searchString}
};
},
getLehrveranstaltungen(student_uid, studiengang_kz, studiensemester_kurzbz, additional_lehrveranstaltung_id) {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektarbeit/getLehrveranstaltungen',
params: { student_uid, studiengang_kz, studiensemester_kurzbz, additional_lehrveranstaltung_id }
};
},
getNoten() {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektarbeit/getNoten'
};
},
getStudiensemester() {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektarbeit/getStudiensemester'
};
},
loadProjektarbeit(projektarbeit_id) {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektarbeit/loadProjektarbeit',
params: { projektarbeit_id }
};
},
addNewProjektarbeit(params) {
return {
method: 'post',
url: 'api/frontend/v1/stv/projektarbeit/insertProjektarbeit',
params
};
},
updateProjektarbeit(params) {
return {
method: 'post',
url: 'api/frontend/v1/stv/projektarbeit/updateProjektarbeit',
params
};
},
deleteProjektarbeit(projektarbeit_id) {
return {
method: 'post',
url: 'api/frontend/v1/stv/projektarbeit/deleteProjektarbeit',
params: { projektarbeit_id }
};
}
};
@@ -0,0 +1,80 @@
/**
* 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 {
getProjektbetreuer(projektarbeit_id ) {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektbetreuer/getProjektbetreuer',
params: { projektarbeit_id }
};
},
getBetreuerarten() {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektbetreuer/getBetreuerarten'
};
},
getDefaultStundensaetze(person_id, studiensemester_kurzbz) {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektbetreuer/getDefaultStundensaetze',
params: { person_id, studiensemester_kurzbz }
};
},
getNoten() {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektbetreuer/getNoten'
};
},
saveProjektbetreuer(projektarbeit_id, projektbetreuer) {
return {
method: 'post',
url: 'api/frontend/v1/stv/projektbetreuer/saveProjektbetreuer',
params: { projektarbeit_id, projektbetreuer }
};
},
deleteProjektbetreuer(projektarbeit_id, person_id, betreuerart_kurzbz) {
return {
method: 'post',
url: 'api/frontend/v1/stv/projektbetreuer/deleteProjektbetreuer',
params: { projektarbeit_id, person_id, betreuerart_kurzbz }
};
},
getProjektbetreuerBySearchQuery(searchString) {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektbetreuer/getProjektbetreuerBySearchQuery',
params: { searchString }
};
},
getPerson(person_id) {
return {
method: 'get',
url: 'api/frontend/v1/stv/projektbetreuer/getPerson',
params: { person_id }
};
},
validateProjektbetreuer(projektbetreuer) {
return {
method: 'post',
url: 'api/frontend/v1/stv/projektbetreuer/validateProjektbetreuer',
params: { projektbetreuer }
};
}
};
+18 -3
View File
@@ -46,17 +46,32 @@ export default {
url: url
};
},
search(params, studiensemester_kurzbz) {
return {
method: 'post',
url: 'api/frontend/v1/stv/students/search/'
+ encodeURIComponent(studiensemester_kurzbz),
params
};
},
verband(relative_path) {
return {
method: 'get',
url: 'api/frontend/v1/stv/students/' + relative_path
};
},
check(params) {
getPerson(params) {
return {
method: 'post',
url: 'api/frontend/v1/stv/student/check',
url: 'api/frontend/v1/stv/student/getPerson',
params
};
},
add(params) {
return {
method: 'post',
url: 'api/frontend/v1/stv/student/add',
params
};
}
};
};
+20
View File
@@ -0,0 +1,20 @@
export default {
getVertrag(vertrag_id)
{
return {
method: 'get',
url: 'api/frontend/v1/stv/vertrag/getVertrag',
params: { vertrag_id },
};
},
cancelVertrag(data)
{
return {
method: 'post',
url: '/api/frontend/v1/stv/vertrag/cancelVertrag/',
params: data
};
}
}
-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)
}
}
+5 -5
View File
@@ -37,20 +37,20 @@ export default {
/*------------- details -------- */
getAll()
getBenutzerSearch(query)
{
return {
method: 'get',
url: '/api/frontend/v1/lv/gruppe/getAll/'
url: `/api/frontend/v1/lv/gruppe/getBenutzerSearch?query=${encodeURIComponent(query)}`
};
},
getBenutzer()
getAllSearch(query)
{
return {
method: 'get',
url: '/api/frontend/v1/lv/gruppe/getBenutzer/'
url: `/api/frontend/v1/lv/gruppe/getAllSearch?query=${encodeURIComponent(query)}`
};
}
},
}
+2 -3
View File
@@ -8,12 +8,11 @@ export default {
};
},
getLektoren()
getLektorenSearch(query)
{
return {
method: 'get',
url: '/api/frontend/v1/lv/lektor/getLektoren/'
url: `/api/frontend/v1/lv/lektor/getLektorenSearch?query=${encodeURIComponent(query)}`
};
},
+2 -2
View File
@@ -20,8 +20,8 @@ export default {
getUid(params){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getUid/'+ params.id + '/' + params.type_id);
},
getVorlagentext(vorlage_kurzbz){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getVorlagentext/' + vorlage_kurzbz);
getDataVorlage(vorlage_kurzbz){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getDataVorlage/' + vorlage_kurzbz);
},
getNameOfDefaultRecipient(params){
return this.$fhcApi.get('api/frontend/v1/messages/messages/getNameOfDefaultRecipient/' + params.id + '/' + params.type_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');
+15 -2
View File
@@ -14,11 +14,13 @@ 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";
import ApiRenderers from '../../api/factory/renderers.js';
import ApiRouteInfo from '../../api/factory/routeinfo.js';
import {capitalize} from "../../helpers/StringHelpers.js";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
@@ -55,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',
@@ -227,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: {
@@ -272,6 +283,7 @@ const app = Vue.createApp({
}
},
async created(){
await this.$api
.call(ApiRenderers.loadRenderers())
.then(res => res.data)
@@ -312,6 +324,7 @@ const app = Vue.createApp({
},
mounted() {
document.addEventListener('click', this.handleClick);
},
beforeUnmount() {
document.removeEventListener('click', this.handleClick);
@@ -320,7 +333,7 @@ const app = Vue.createApp({
// kind of a bandaid for bad css on some pages to avoid horizontal scroll
setScrollbarWidth();
app.config.globalProperties.$capitalize = capitalize;
app.use(router);
app.use(primevue.config.default, {
zIndex: {
+4 -3
View File
@@ -1,14 +1,15 @@
import NewMessage from "../components/Messages/Details/NewMessage/NewDiv.js";
import Phrasen from "../plugin/Phrasen.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}/NeueNachricht/:id/:typeId`, component: NewMessage },
{ path: `/${ciPath}/NeueNachricht/:id/:typeId/:messageId`, component: NewMessage },
{ path: `/${ciPath}/NeueNachricht`, component: NewMessage, props: true },
{ path: `/${ciPath}/NeueNachricht/:id/:typeId`, component: NewMessage, props: true },
{ path: `/${ciPath}/NeueNachricht/:id/:typeId/:messageId`, component: NewMessage, props: true },
]
});
+38
View File
@@ -148,6 +148,44 @@ const router = VueRouter.createRouter({
next();
}
},
{
name: 'search',
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/search/:searchstr`,
component: FhcStudentenverwaltung,
props(route) {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_mode: 'search',
url_prestudent_id: route.params.searchstr
};
},
beforeEnter(to, from, next) {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
name: 'search_w_types',
path: `/${ciPath}/studentenverwaltung/:studiensemester_kurzbz/search/:types/:searchstr`,
component: FhcStudentenverwaltung,
props(route) {
return {
url_studiensemester_kurzbz: route.params.studiensemester_kurzbz,
url_mode: 'search',
url_prestudent_id: route.params.type + '/' + route.params.searchstr
};
},
beforeEnter(to, from, next) {
const isSemester = /^[WS]S\d{4}$/.test(to.params.studiensemester_kurzbz);
if (!isSemester) {
return next({name: 'index'});
}
next();
}
},
{
path: '/:pathMatch(.*)*',
redirect: {
+135
View File
@@ -0,0 +1,135 @@
/**
* 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 BsModal from "./Bootstrap/Modal.js";
import FhcForm from "./Form/Form.js";
import FormInput from "./Form/Input.js";
export default {
name: 'AppConfig',
components: {
BsModal,
FhcForm,
FormInput
},
emits: [
'update:modelValue'
],
props: {
modelValue: {
type: Object,
required: true
},
endpoints: {
type: Object,
required: true
}
},
data() {
return {
setup: {},
tempValues: {}
};
},
watch: {
'$p.user_language.value'(n, o) {
if (n !== o && o !== undefined && Object.keys(this.setup).length) {
this.$api
.call(this.endpoints.get())
.then(res => {
this.setup = {};
Object.keys(res.data).forEach(key => {
const binding = { ...res.data[key] };
delete binding.value;
delete binding.options;
const options = res.data[key].options;
this.setup[key] = {
binding,
options
};
});
})
.catch(this.$fhcAlert.handleSystemErrors);
}
}
},
methods: {
update() {
this.$refs.form
.call(this.endpoints.set(this.tempValues))
.then(() => {
this.$emit('update:modelValue', { ...this.tempValues });
this.$refs.modal.hide();
this.$fhcAlert.alertSuccess(this.$p.t('ui/settings_saved'));
})
.catch(this.$fhcAlert.handleSystemErrors);
}
},
created() {
this.$api
.call(this.endpoints.get())
.then(res => {
Object.keys(res.data).forEach(key => {
const binding = { ...res.data[key] };
delete binding.value;
delete binding.options;
const options = res.data[key].options;
this.tempValues[key] = res.data[key].value;
this.setup[key] = {
binding,
options
};
});
this.$emit('update:modelValue', { ...this.tempValues });
})
.catch(this.$fhcAlert.handleSystemErrors);
},
template: /* html */`
<fhc-form class="stv-config" ref="form" @submit.prevent="update">
<bs-modal
ref="modal"
class="fade"
id="configModal"
dialog-class="modal-lg"
@hidden-bs-modal="tempValues = { ...modelValue }"
>
<template #title>{{ $p.t('ui/settings') }}</template>
<template #default>
<div class="d-flex flex-column gap-5">
<form-input
v-for="(value, key) in setup"
v-model="tempValues[key]"
v-bind="value.binding"
>
<option
v-for="(label, val) in value.options"
:key="val"
:value="val"
>{{ label }}</option>
</form-input>
</div>
</template>
<template #footer>
<button class="btn btn-primary" type="submit">
{{ $p.t('ui/speichern') }}
</button>
</template>
</bs-modal>
</fhc-form>`
};
+69
View File
@@ -0,0 +1,69 @@
/**
* 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 ApiNavigation from '../api/factory/navigation.js';
export default {
name: 'AppMenu',
props: {
appIdentifier: {
type: String,
required: true
},
navigationPage: {
type: String,
default: 'apps'
}
},
data() {
return {
items: []
};
},
watch: {
navigationPage() {
this.getItems();
}
},
methods: {
getItems() {
this.$api
.call(ApiNavigation.getMenu(this.navigationPage))
.then(result => {
this.items = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
}
},
created() {
this.getItems();
},
template: /* html */`
<ul class="fhc-app-menu">
<li v-for="(menu, key) in items" :key="key">
<a
:href="menu.link"
@click="menu.onClickCall"
:class="{ active: key === appIdentifier }"
>
<i v-if="menu.icon" class="fa fa-fw" :class="'fa-'+ menu.icon" />
{{ menu.description }}
</a>
</li>
<slot />
</ul>`
};
+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>
`
}
@@ -96,8 +96,11 @@ export default {
},
methods: {
onDragstart(evt) {
DragAndDrop.setTransferData(evt.detail.originalEvent, evt.detail.item.orig);
this.draggedInternalEvent = evt.detail.item;
const data = DragAndDrop.convertToTransferData(evt.detail.item.orig);
if (DragAndDrop.isValidDragObject(data)) {
DragAndDrop.setTransferData(evt.detail.originalEvent, data);
this.draggedInternalEvent = evt.detail.item;
}
},
onDragend() {
this.draggedInternalEvent = null;
+1 -1
View File
@@ -316,7 +316,7 @@ export default {
template: /* html */`
<div
class="fhc-calendar-base-grid"
style="display:grid;width:100%;height:100%"
style="display:grid;width:100%;height:100%;overflow:auto"
:style="'grid-template-' + axisRow + 's:auto' + (allDayEvents ? ' auto ' : ' ') + '1fr;grid-template-' + axisCol + 's:auto ' + styleGridCols"
>
<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,401 @@ 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: {
getEid() {
return this.$p.t('abgabetool/c4eidesstattlicheErklaerung')
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.$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/c4betreuer') ) }}: {{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">
<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: -66px;">
<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/c4zieldatum') )}}</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"
:format="formatDate"
: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/c4abgabekurzbz') )}}</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 +558,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 +585,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 +628,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 +643,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
@@ -1,8 +1,9 @@
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
import AbgabeDetail from "./AbgabeMitarbeiterDetail.js";
import VerticalSplit from "../../verticalsplit/verticalsplit.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";
export const AbgabetoolMitarbeiter = {
name: "AbgabetoolMitarbeiter",
@@ -10,10 +11,21 @@ export const AbgabetoolMitarbeiter = {
BsModal,
CoreFilterCmpt,
AbgabeDetail,
VerticalSplit,
Checkbox: primevue.checkbox,
Dropdown: primevue.dropdown,
Textarea: primevue.textarea,
VueDatePicker
VueDatePicker,
FhcOverlay
},
provide() {
return {
abgabeTypeOptions: Vue.computed(() => this.abgabeTypeOptions),
abgabetypenBetreuer: Vue.computed(() => this.abgabetypenBetreuer),
allowedNotenOptions: Vue.computed(() => this.allowedNotenOptions),
notenOptionsNonFinal: Vue.computed(() => this.notenOptionsNonFinal),
turnitin_link: Vue.computed(() => this.turnitin_link),
old_abgabe_beurteilung_link: Vue.computed(() => this.old_abgabe_beurteilung_link)
}
},
props: {
viewData: {
@@ -21,44 +33,33 @@ export const AbgabetoolMitarbeiter = {
required: true,
default: () => ({name: '', uid: ''}),
validator(value) {
return value && value.name && value.uid
return value && value.uid // && value.name -> extensive viewData use only for cis4 onwards
}
}
},
data() {
return {
tableData: null,
abgabetypenBetreuer: null,
detailIsFullscreen: false,
phrasenPromise: null,
phrasenResolved: false,
turnitin_link: null,
old_abgabe_beurteilung_link: null,
saving: false,
loading: false,
// TODO: fetch types
allAbgabeTypes: [
{
paabgabetyp_kurzbz: 'abstract',
bezeichnung: 'Entwurf'
},
{
paabgabetyp_kurzbz: 'zwischen',
bezeichnung: 'Zwischenabgabe'
},
{
paabgabetyp_kurzbz: 'note',
bezeichnung: 'Benotung'
},
{
paabgabetyp_kurzbz: 'end',
bezeichnung: 'Endupload'
},
{
paabgabetyp_kurzbz: 'enda',
bezeichnung: 'Endabgabe im Sekretariat'
}
],
abgabeTypeOptions: null,
notenOptions: null,
allowedNotenOptions: null,
notenOptionsNonFinal: null,
serienTermin: Vue.reactive({
datum: new Date(),
bezeichnung: {
paabgabetyp_kurzbz: 'zwischen',
bezeichnung: 'Zwischenabgabe'
},
kurzbz: ''
kurzbz: '',
upload_allowed: false
}),
showAll: false,
tabulatorUuid: Vue.ref(0),
@@ -72,34 +73,78 @@ export const AbgabetoolMitarbeiter = {
tableBuiltResolve: null,
tableBuiltPromise: null,
abgabeTableOptions: {
height: 700,
minHeight: 250,
index: 'projektarbeit_id',
layout: 'fitDataStretch',
placeholder: this.$p.t('global/noDataAvailable'),
placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')),
selectable: true,
selectableCheck: this.selectionCheck,
rowHeight: 80,
columns: [
{
formatter: 'rowSelection',
titleFormatter: 'rowSelection',
titleFormatterParams: {
rowRange: "active" // Only toggle the values of the active filtered rows
formatter: function (cell, formatterParams, onRendered) {
// create the built-in checkbox
if(!cell.getRow().getData().selectable) return
let checkbox = document.createElement("input");
checkbox.type = "checkbox";
// Handle select manually
checkbox.addEventListener("click", (e) => {
e.stopPropagation();
// call our function
if (formatterParams && formatterParams.handleClick) {
formatterParams.handleClick(e, cell);
}
});
cell.getRow().getData().checkbox = checkbox
let wrapper = document.createElement("div");
wrapper.style.cssText = "display: flex; justify-content: center; align-items: center; height: 100%; width: 100%;";
wrapper.appendChild(checkbox);
return wrapper;
},
hozAlign:"center",
titleFormatter: function (cell, formatterParams, onRendered) {
// create the built-in checkbox
let checkbox = document.createElement("input");
checkbox.type = "checkbox";
// Handle "select all" manually
checkbox.addEventListener("click", (e) => {
e.stopPropagation();
// call our function
if (formatterParams && formatterParams.handleClick) {
formatterParams.handleClick(e, cell);
}
});
return checkbox;
},
hozAlign: "center",
headerSort: false,
frozen: true,
width: 70
formatterParams: {
handleClick: this.selectHandler
},
titleFormatterParams: {
handleClick: this.selectAllHandler
},
width: 50,
cssClass: 'sticky-col'
},
{title: Vue.computed(() => this.$p.t('abgabetool/c4details')), field: 'details', formatter: this.detailFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4personenkennzeichen')), field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4kontakt')), field: 'mail', formatter: this.mailFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4vorname')), field: 'vorname', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4nachname')), field: 'nachname', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4projekttyp')), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4stg')), field: 'stg', formatter: this.centeredTextFormatter, widthGrow: 2},
{title: Vue.computed(() => this.$p.t('abgabetool/c4sem')), field: 'studiensemester_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4titel')), field: 'titel', formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8},
{title: Vue.computed(() => this.$p.t('abgabetool/c4betreuerart')), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 8}
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, widthGrow: 1, tooltip: false, cssClass: 'sticky-col'},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4kontakt'))), field: 'mail', formatter: this.mailFormatter, widthGrow: 1, tooltip: false, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerart'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1}
],
persistence: false,
},
@@ -123,12 +168,54 @@ export const AbgabetoolMitarbeiter = {
{
event: "rowSelectionChanged",
handler: async(data) => {
this.selectedData.filter(sd => !data.includes(sd)).forEach(fsd => {
if(fsd.checkbox) fsd.checkbox.checked = false
})
data.forEach(d => {
if(d.checkbox) d.checkbox.checked = true
})
this.selectedData = data
}
}
]};
},
methods: {
selectHandler(e, cell) {
const row = cell.getRow();
if(row.isSelected()) {
row.deselect();
} else {
row.select();
}
// stop built-in handler
e.stopPropagation();
return false;
},
selectAllHandler(e, cell) {
const table = cell.getTable();
const rows = table.getRows();
// custom select all logic
const allowed = rows.filter(r => r.getData().selectable);
const selected = allowed.every(r => r.isSelected());
if(selected) {
allowed.forEach(r => r.deselect());
} else {
allowed.forEach(r => r.select());
}
// stop built-in handler
e.stopPropagation();
return false;
},
handleToggleFullscreenDetail() {
this.detailIsFullscreen = !this.detailIsFullscreen
},
getOptionLabelAbgabetyp(option){
return option.bezeichnung
},
@@ -176,17 +263,17 @@ export const AbgabetoolMitarbeiter = {
},
addSeries() {
this.saving = true
this.$fhcApi.factory.lehre.postSerientermin(
this.$api.call(ApiAbgabe.postSerientermin(
this.serienTermin.datum.toISOString(),
this.serienTermin.bezeichnung.paabgabetyp_kurzbz,
this.serienTermin.bezeichnung.bezeichnung,
this.serienTermin.kurzbz,
this.selectedData?.map(projekt => projekt.projektarbeit_id)
).then(res => {
this.serienTermin.upload_allowed,
this.selectedData?.map(projekt => projekt.projektarbeit_id),
false
)).then(res => {
if (res.meta.status === "success" && res.data) {
this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/serienTerminGespeichert'))
// TODO: sticky lifetime erhöhen um sinnvoll lesen zu können?
this.$fhcAlert.alertInfo(this.$p.t('abgabetool/serienTerminEmailSentInfo', [this.createInfoString(res.data)]));
} else {
this.$fhcAlert.alertError(this.$p.t('abgabetool/errorSerienterminSpeichern'))
}
@@ -210,44 +297,45 @@ export const AbgabetoolMitarbeiter = {
return new Date(date) < new Date(Date.now())
},
setDetailComponent(details){
this.loading=true
this.loadAbgaben(details).then((res)=> {
const pa = this.projektarbeiten?.retval?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
pa.abgabetermine = res.data[0].retval
pa.isCurrent = res.data[1]
pa.abgabetermine.push({ // new abgatermin row
'paabgabe_id': -1,
'projektarbeit_id': pa.projektarbeit_id,
'fixtermin': false,
'kurzbz': '',
'datum': new Date().toISOString().split('T')[0],
'paabgabetyp_kurzbz': '',
'bezeichnung': '',
'abgabedatum': null,
'insertvon': this.viewData?.uid ?? ''
})
let paIsBenotet = false
if(pa.note !== undefined && pa.note !== null) {
// check if the note is not defined as a non final projektarbeit note
const opt = this.notenOptionsNonFinal.find(opt => opt.note)
// if thats the case allow further work
if(opt) paIsBenotet = false
// else the PA is to be considered finished
paIsBenotet = true
}
pa.abgabetermine.forEach(termin => {
termin.note = this.allowedNotenOptions.find(opt => opt.note == termin.note)
termin.file = []
termin.allowedToSave = termin.insertvon == this.viewData?.uid && pa.betreuerart_kurzbz != 'Zweitbegutachter'
// update 08-01-2026: everybody is allowed to do everything in client, critical checks happen at backend level
// termin.allowedToSave = true
// update 21-01-2026: actually blocking operations on finished projektarbeiten seems like a decent idea
termin.allowedToSave = paIsBenotet ? false : true
// lektoren are not allowed to delete deadlines with existing submissions
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum
termin.bezeichnung = {
bezeichnung: termin.bezeichnung,
paabgabetyp_kurzbz: termin.paabgabetyp_kurzbz
}
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
})
pa.betreuer = this.buildBetreuer(pa)
pa.student_uid = details.student_uid
pa.student = `${pa.vorname} ${pa.nachname}`
this.selectedProjektarbeit = pa
this.$refs.verticalsplit.showBoth()
this.$refs.modalContainerAbgabeDetail.show()
})
}).finally(()=>{this.loading = false})
},
centeredTextFormatter(cell) {
const val = cell.getValue()
@@ -276,7 +364,7 @@ export const AbgabetoolMitarbeiter = {
const val = cell.getValue()
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>'
'<a style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+val+'</a></div>'
},
tableResolve(resolve) {
this.tableBuiltResolve = resolve
@@ -290,16 +378,13 @@ export const AbgabetoolMitarbeiter = {
buildStg(projekt) {
return (projekt.typ + projekt.kurzbz)?.toUpperCase()
},
buildBetreuer(abgabe) {
// TODO: preload and insert own titled name of betreuer somehow
return abgabe.betreuerart_beschreibung + ': ' + (abgabe.btitelpre ? abgabe.btitelpre + ' ' : '') + abgabe.bvorname + ' ' + abgabe.bnachname + (abgabe.btitelpost ? ' ' + abgabe.btitelpost : '')
},
setupData(data){
this.projektarbeiten = data[0]
this.domain = data[1]
const d = data[0]?.retval?.map(projekt => {
let mode = 'detailTermine'
this.tableData = data[0]?.retval?.map(projekt => {
projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter'
return {
...projekt,
@@ -316,12 +401,12 @@ export const AbgabetoolMitarbeiter = {
titel: projekt.titel
}
})
this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns)
this.$refs.abgabeTable.tabulator.setData(d);
this.$refs.abgabeTable.tabulator.setData(this.tableData);
},
loadProjektarbeiten(all = false, callback) {
this.$fhcApi.factory.lehre.getMitarbeiterProjektarbeiten(this.viewData?.uid ?? null, all)
this.$api.call(ApiAbgabe.getMitarbeiterProjektarbeiten(all))
.then(res => {
if(res?.data) this.setupData(res.data)
}).finally(() => {
@@ -332,7 +417,7 @@ export const AbgabetoolMitarbeiter = {
},
loadAbgaben(details) {
return new Promise((resolve) => {
this.$fhcApi.factory.lehre.getStudentProjektabgaben(details)
this.$api.call(ApiAbgabe.getStudentProjektabgaben(details))
.then(res => {
resolve(res)
})
@@ -347,7 +432,7 @@ export const AbgabetoolMitarbeiter = {
if(!tableDataSet) return
const rect = tableDataSet.getBoundingClientRect();
this.abgabeTableOptions.height = window.visualViewport.height - rect.top
this.abgabeTableOptions.height = window.visualViewport.height - rect.top - 80
this.$refs.abgabeTable.tabulator.setHeight(this.abgabeTableOptions.height)
},
async setupMounted() {
@@ -356,68 +441,125 @@ export const AbgabetoolMitarbeiter = {
this.loadProjektarbeiten()
this.$refs.verticalsplit.collapseBottom()
this.calcMaxTableHeight()
}
},
watch: {
'serienTermin.bezeichnung'(newVal) {
if(newVal?.paabgabetyp_kurzbz === 'qualgate1' || newVal?.paabgabetyp_kurzbz === 'qualgate2') {
this.serienTermin.kurzbz = newVal.bezeichnung
}
this.serienTermin.upload_allowed = newVal.upload_allowed_default
},
},
computed: {
getAllowedAbgabeTypeOptions() {
return this.abgabeTypeOptions.filter(opt => this.abgabetypenBetreuer.includes(opt.paabgabetyp_kurzbz))
}
},
created() {
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
// fetch config to avoid hard coded links
this.$api.call(ApiAbgabe.getConfig()).then(res => {
this.turnitin_link = res.data?.turnitin_link
this.old_abgabe_beurteilung_link = res.data?.old_abgabe_beurteilung_link
this.abgabetypenBetreuer = res.data?.abgabetypenBetreuer
}).catch(e => {
this.loading = false
})
// fetch noten options
//TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API
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)
)
this.notenOptionsNonFinal = this.notenOptions.filter(
opt => res.data[2].includes(opt.note)
)
}
}).catch(e => {
this.loading = false
})
// fetch abgabetypen options
this.$api.call(ApiAbgabe.getPaAbgabetypen()).then(res => {
this.abgabeTypeOptions = res.data
}).catch(e => {
this.loading = false
})
},
mounted() {
this.setupMounted()
},
template: `
<template v-if="phrasenResolved">
<FhcOverlay :active="loading || saving"></FhcOverlay>
<bs-modal ref="modalContainerAddSeries" class="bootstrap-prompt"
dialogClass="modal-lg">
dialogClass="modal-lg">
<template v-slot:title>
<div>
{{ $p.t('abgabetool/neueTerminserie') }}
</div>
</template>
<template v-slot:default>
<div class="row">
<div class="col-3 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4zieldatum')}}
<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/c4zieldatum') )}}</div>
</div>
<div class="col-3 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4abgabetyp')}}
</div>
<div class="col-6 d-flex justify-content-center align-items-center">
{{$p.t('abgabetool/c4abgabekurzbz')}}
<div class="col-12 col-md-9">
<VueDatePicker
style="width: 95%;"
v-model="serienTermin.datum"
:clearable="false"
:enable-time-picker="false"
:format="formatDate"
:text-input="true"
auto-apply>
</VueDatePicker>
</div>
</div>
<div class="row">
<div class="col-3 d-flex justify-content-center align-items-center">
<div>
<VueDatePicker
style="width: 95%;"
v-model="serienTermin.datum"
:clearable="false"
:enable-time-picker="false"
:format="formatDate"
:text-input="true"
auto-apply>
</VueDatePicker>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4upload_allowed') )}}</div>
<div class="col-12 col-md-9">
<Checkbox
v-model="serienTermin.upload_allowed"
:binary="true"
:pt="{ root: { class: 'ml-auto' }}"
>
</Checkbox>
</div>
<div class="col-3 d-flex justify-content-center align-items-center">
<Dropdown
</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"
v-if="abgabetypenBetreuer && abgabeTypeOptions"
>
<Dropdown
:style="{'width': '100%'}"
v-model="serienTermin.bezeichnung"
:options="allAbgabeTypes"
:options="getAllowedAbgabeTypeOptions"
:optionLabel="getOptionLabelAbgabetyp">
</Dropdown>
</div>
<div class="col-6 d-flex justify-content-center align-items-center">
<Textarea style="margin-bottom: 4px;" v-model="serienTermin.kurzbz" rows="3" cols="40"></Textarea>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}</div>
<div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="serienTermin.kurzbz" rows="1" class="w-100"></Textarea>
</div>
</div>
@@ -427,56 +569,56 @@ export const AbgabetoolMitarbeiter = {
</template>
</bs-modal>
<vertical-split ref="verticalsplit">
<template #top>
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
<hr>
<core-filter-cmpt
:title="''"
@uuidDefined="handleUuidDefined"
ref="abgabeTable"
:newBtnShow="true"
:newBtnLabel="$p.t('abgabetool/neueTerminserie')"
:newBtnDisabled="!selectedData.length"
@click:new=openAddSeriesModal
:tabulator-options="abgabeTableOptions"
:tabulator-events="abgabeTableEventHandlers"
tableOnly
:sideMenu="false"
:useSelectionSpan="false"
>
<template #actions>
<button @click="toggleShowAll(!showAll)" role="button" class="btn btn-secondary ml-2">
<i v-show="!showAll" class="fa fa-eye"></i>
<i v-show="showAll" class="fa fa-eye-slash"></i>
{{ $p.t('abgabetool/showAll') }}
</button>
<button @click="showDeadlines" role="button" class="btn btn-secondary ml-2">
<i class="fa fa-hourglass-end"></i>
{{ $p.t('abgabetool/showDeadlines') }}
</button>
<div v-show="saving">
{{ $p.t('abgabetool/currentlySaving') }} <i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<div v-show="loading">
{{ $p.t('abgabetool/currentlyLoading') }} <i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</template>
</core-filter-cmpt>
</template>
<template #bottom>
<div v-show="selectedProjektarbeit" ref="selProj">
<AbgabeDetail :projektarbeit="selectedProjektarbeit"></AbgabeDetail>
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
dialogClass="modal-xl" :allowFullscreenExpand="true"
@toggle-fullscreen="handleToggleFullscreenDetail">
<template v-slot:title>
<div>
{{$p.t('abgabetool/c4abgabeMitarbeiterDetailTitle')}}
</div>
</template>
</vertical-split>
<template v-slot:default>
<AbgabeDetail :projektarbeit="selectedProjektarbeit" :isFullscreen="detailIsFullscreen"></AbgabeDetail>
</template>
</bs-modal>
<!-- low max height on this vsplit wrapper to avoid padding scrolls, elements have their inherent height anyways -->
<div id="abgabetable" style="max-height:40vw;">
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
<hr>
<core-filter-cmpt
:title="''"
@uuidDefined="handleUuidDefined"
ref="abgabeTable"
:newBtnShow="true"
:newBtnLabel="$p.t('abgabetool/neueTerminserie')"
:newBtnDisabled="!selectedData.length"
@click:new=openAddSeriesModal
:tabulator-options="abgabeTableOptions"
:tabulator-events="abgabeTableEventHandlers"
tableOnly
:sideMenu="false"
:useSelectionSpan="false"
>
<template #actions>
<button @click="toggleShowAll(!showAll)" role="button" class="btn btn-secondary ml-2">
<i v-show="!showAll" class="fa fa-eye"></i>
<i v-show="showAll" class="fa fa-eye-slash"></i>
{{ $p.t('abgabetool/showAll') }}
</button>
<button @click="showDeadlines" role="button" class="btn btn-secondary ml-2">
<i class="fa fa-hourglass-end"></i>
{{ $p.t('abgabetool/showDeadlines') }}
</button>
</template>
</core-filter-cmpt>
</div>
</template>
`,
};
@@ -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";
const today = new Date()
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,221 @@ 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: {
dateDiffInDays(datumParam) {
let datum = datumParam
if(datumParam instanceof Date && !isNaN(datum.getTime()))
{
const year = datumParam.getFullYear();
const month = datumParam.getMonth() + 1; // getMonth() is 0-indexed
const day = datumParam.getDate();
const pad = (num) => String(num).padStart(2, '0');
datum = `${year}-${pad(month)}-${pad(day)}`
}
const dateToday = luxon.DateTime.now().startOf('day');
const dateDatum = luxon.DateTime.fromISO(datum).startOf('day');
const duration = dateDatum.diff(dateToday, 'days');
return duration.values.days;
},
getDateStyleClass(termin) {
const datum = new Date(termin.datum)
const abgabedatum = new Date(termin.abgabedatum)
termin.diffindays = this.dateDiffInDays(termin.datum)
const isLate = termin.abgabedatum && abgabedatum > datum;
// GRADE STATUS
if (termin.note) {
if(Number.isInteger(termin.note)) {
const opt = this.notenOptions.find(opt => opt.note == termin.note)
if(opt.positiv) return 'bestanden'
}
if (termin.note.positiv) return 'bestanden';
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';
},
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())
},
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 {
// new larifari we want qgates but they are optional fhtw mode
termin.allowedToUpload = !this.isPastDate(termin.datum) && 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 = this.getDateStyleClass(termin)
})
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 +257,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 +268,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 +319,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">
<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/c4downloadBeurteilungErstbetreuer') )}} <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/c4downloadBeurteilungZweitbetreuer') )}} <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/c4betreuer') ) }}</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/c4betreuerEmailKontakt') )}}</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/c4zieldatum'))), 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/c4abgabekurzbz'))), 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;
@@ -100,6 +100,11 @@ export default {
link_element.href = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/CisVue/Cms/getRoomInformation/" + room_name;
link_element.appendChild(title.cloneNode(true));
title.replaceWith(link_element);
let lvplanlinks = document.querySelectorAll('.menubox a[href*="stpl_week.php"]');
for(let lvplanlink of lvplanlinks) {
lvplanlink.href = link_element.href;
}
}
else
{
@@ -46,7 +46,6 @@ export default {
loading: false,
filter: "Pending",
profil_update_id: Number(this.id),
};
},
computed: {
@@ -60,6 +59,10 @@ export default {
},
profilUpdateOptions: function () {
return {
persistence: {
columns: ["width", "visible", "frozen"],
},
persistenceID: 'cis-profilupdate-2025121702',
ajaxURL: 'dummy',
ajaxRequestFunc: (url, config, params) => {
return this.$api.call(ApiProfilUpdate.getProfilUpdateWithPermission(params.filter));
@@ -205,7 +208,7 @@ export default {
//responsive:0,
},
{
title: this.$p.t("lehre", "studiengang") + ' (' + this.$p.t("profil", "studentIn") + ')',
title: this.$p.t("profil", "stg_short") + ' (' + this.$p.t("profil", "studentIn") + ')',
field: "studiengang",
minWidth: 50,
resizable: true,
@@ -213,8 +216,14 @@ export default {
headerFilterParams: {valuesLookup:true, listOnEmpty:true, autocomplete:true, sort:"asc"},
//responsive:0,
},
{
title: this.$p.t("lehre", "organisationsform") + ' (' + this.$p.t("profil", "studentIn") + ')',
{
title: this.$p.t("profil", "sem_short") + ' (' + this.$p.t("profil", "studentIn") + ')',
field: "semester",
headerFilter: "list",
headerFilterParams: {valuesLookup:true, listOnEmpty:true, autocomplete:true, sort:"asc"}
},
{
title: this.$p.t("profil", "orgform_short") + ' (' + this.$p.t("profil", "studentIn") + ')',
field: "orgform",
minWidth: 50,
resizable: true,
@@ -222,8 +231,8 @@ export default {
headerFilterParams: {valuesLookup:true, listOnEmpty:true, autocomplete:true, sort:"asc"},
//responsive:0,
},
{
title: this.$p.t("lehre", "organisationseinheit") + ' (' + this.$p.t("profil", "mitarbeiterIn") + ')',
{
title: this.$p.t("profil", "orgeinheit_short") + ' (' + this.$p.t("profil", "mitarbeiterIn") + ')',
field: "oezuordnung",
minWidth: 200,
resizable: true,
@@ -231,7 +240,7 @@ export default {
headerFilterParams: {valuesLookup:true, listOnEmpty:true, autocomplete:true, sort:"asc"},
//responsive:0,
},
{
{
title: this.$p.t("profilUpdate", "Topic"),
field: "topic",
resizable: true,
@@ -240,7 +249,7 @@ export default {
headerFilterParams: {valuesLookup:true, listOnEmpty:true, autocomplete:true, sort:"asc"},
//responsive:0,
},
{
{
title: this.$p.t("profilUpdate", "insertamum"),
field: "insertamum_iso",
resizable: true,
@@ -251,7 +260,7 @@ export default {
formatterParams: this.datetimeFormatterParams(),
//responsive:0,
},
{
{
title: this.$p.t("profilUpdate", "Status"),
field: "status_translated",
hozAlign: "center",
@@ -273,7 +282,6 @@ export default {
}
return `<div class='row justify-content-center'><div class='col-2'><i class='${iconClasses}'></i></div> <div class='col-4'><span>${cell.getValue()}</span></div></div>`;
},
resizable: true,
minWidth: 200,
//responsive:0,
@@ -309,7 +317,6 @@ export default {
],
};
}
},
methods: {
denyProfilUpdate: function (data) {
@@ -351,7 +358,6 @@ export default {
this.showModal = false;
this.modalData = null;
},
showAcceptDenyModal(value) {
this.modalData = value;
if (!this.modalData) {
@@ -364,7 +370,6 @@ export default {
this.$refs.AcceptDenyModal.show();
});
},
updateData: function (event) {
this.$refs.UpdatesTable.tabulator.setData();
//? store the selected view in the session storage of the browser
@@ -415,22 +420,30 @@ export default {
},
template: /*html*/ `
<div>
<accept-deny-update :title="$p.t('profilUpdate','profilUpdateRequest')" v-if="showModal" ref="AcceptDenyModal" @hideBsModal="hideAcceptDenyModal" :value="JSON.parse(JSON.stringify(modalData))" :setLoading="setLoading" ></accept-deny-update>
<div class="form-underline flex-fill ">
<div class="form-underline-titel">{{$p.t('ui','anzeigen')}} </div>
<select class="mb-4 form-select" v-model="filter" @change="updateData" aria-label="Profil updates display selection">
<option :selected="true" :value="profilUpdateStates['Pending']" >{{$p.t('profilUpdate','pendingRequests')}}</option>
<option :value="profilUpdateStates['Accepted']">{{$p.t('profilUpdate','acceptedRequests')}}</option>
<option :value="profilUpdateStates['Rejected']">{{$p.t('profilUpdate','rejectedRequests')}}</option>
<option :value="'Alle'">{{$p.t('profilUpdate','allRequests')}}</option>
</select>
</div>
<loading ref="loadingModalRef" :timeout="0"></loading>
<core-filter-cmpt v-if="profilUpdateStates && categoryLoaded" :title="$p.t('profilUpdate','profilUpdateRequests')" ref="UpdatesTable" :tabulatorEvents="profilUpdateEvents" :tabulator-options="profilUpdateOptions" tableOnly :sideMenu="false" />
<accept-deny-update :title="$p.t('profilUpdate','profilUpdateRequest')" v-if="showModal" ref="AcceptDenyModal" @hideBsModal="hideAcceptDenyModal" :value="JSON.parse(JSON.stringify(modalData))" :setLoading="setLoading" ></accept-deny-update>
<h3>{{$p.t('profilUpdate', 'profilUpdateRequests')}}</h3>
<loading ref="loadingModalRef" :timeout="0"></loading>
</div>`,
<core-filter-cmpt
v-if="profilUpdateStates && categoryLoaded"
ref="UpdatesTable"
:tabulatorEvents="profilUpdateEvents"
:tabulator-options="profilUpdateOptions"
table-only
:sideMenu="false">
<template #actions>
<div style="width: 94vw;" class="d-flex justify-content-end">
<div>
<select class="mb-4 form-select" v-model="filter" @change="updateData" aria-label="Profil updates display selection">
<option :selected="true" :value="profilUpdateStates['Pending']" >{{$p.t('profilUpdate','pendingRequests')}}</option>
<option :value="profilUpdateStates['Accepted']">{{$p.t('profilUpdate','acceptedRequests')}}</option>
<option :value="profilUpdateStates['Rejected']">{{$p.t('profilUpdate','rejectedRequests')}}</option>
<option :value="'Alle'">{{$p.t('profilUpdate','allRequests')}}</option>
</select>
</div>
</div>
</template>
</core-filter-cmpt>
</div>`,
};
@@ -31,7 +31,7 @@ export default {
this.event.lektor.slice(0, 3).map(lektor => lektor.kurzbz).join("\n")
+ "\n" + this.$p.t('lehre/weitereLektoren', [this.event.lektor.length - 3])
].join(": "));
} else {console.log(this.event.lektor);
} else {;
tooltipArray.push([
this.$p.t('lehre/lektor'),
this.event.lektor.map(lektor => lektor.kurzbz).join("\n")
@@ -151,10 +151,6 @@ export default {
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));
+1 -1
View File
@@ -144,7 +144,7 @@ export default {
},
async created() {
this.widget = await CachedWidgetLoader.loadWidget(this.id);
let component = (await import("../" + this.widget.setup.file)).default;
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 };
@@ -121,7 +121,8 @@ export default {
class="d-flex flex-column align-items-center h-100 position-relative d-inline-block"
>
<img
class="d-block h-100 rounded"
class="d-block rounded"
style="height: 84px;"
alt="Profilbild"
:src="getFotoSrc(person.foto)"
/>
@@ -175,7 +176,8 @@ export default {
<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;">
<img
class="d-block h-100 rounded"
class="d-block rounded"
style="height: 84px;"
alt="Profilbild"
:src="'data:image/jpeg;base64,' + headerDataMa.foto"
/>
+1
View File
@@ -280,6 +280,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')">
@@ -2,6 +2,7 @@ import {CoreFilterCmpt} from "../../filter/Filter.js";
import FormForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import ApiDirektGruppe from "../../../api/lehrveranstaltung/direktgruppe.js";
import ApiGruppe from "../../../api/lehrveranstaltung/gruppe.js";
export default{
name: "LVDirektGruppen",
components: {
@@ -61,11 +62,9 @@ export default{
lastSelected: null,
gruppen: [],
tabulatorEvents: [],
showAutocomplete: false,
selectedUser: null,
filteredUsers: [],
abortController: null,
searchTimeout: null,
}
},
watch: {
@@ -97,18 +96,36 @@ export default{
},
searchUser(event)
{
const query = event.query.toLowerCase().trim();
this.filteredUsers = this.dropdowns.benutzer_array.filter(user => {
const query = event.query.trim();
if (!query)
{
this.filteredUsers = [];
return;
}
const fullName = `${user.vorname.toLowerCase()} ${user.nachname.toLowerCase()}`;
const reverseFullName = `${user.nachname.toLowerCase()} ${user.vorname.toLowerCase()}`;
return fullName.includes(query) || reverseFullName.includes(query) || user.uid.toLowerCase().includes(query) || user.studiengang.toLowerCase().includes(query);
}).map(user => ({
label: user.studiengang
? `${user.nachname} ${user.vorname} ${user.uid} ${user.studiengang} ${user.semester}`
: `${user.nachname} ${user.vorname} ${user.uid}`,
uid: user.uid
}));
if (query.length < 2)
{
return;
}
if (this.abortController)
{
this.abortController.abort();
}
this.abortController = new AbortController();
const signal = this.abortController.signal;
this.$api.call(ApiGruppe.getBenutzerSearch(query), { signal })
.then(result => {
this.filteredUsers = result.data.map(user => ({
label: user.studiengang
? `${user.nachname} ${user.vorname} ${user.uid} ${user.studiengang} ${user.semester}`
: `${user.nachname} ${user.vorname} ${user.uid}`,
uid: user.uid
})
)})
.catch(this.$fhcAlert.handleSystemError)
},
addUser()
{
@@ -132,19 +149,15 @@ export default{
table-only
:side-menu="false"
:reload=true
:new-btn-label="$p.t('lehre', 'assignPerson')"
new-btn-show
@click:new="showAutocomplete = !showAutocomplete"
>
<template #search> <!--TODO (david) Slot prüfen -->
<form-input
v-if="showAutocomplete"
type="autocomplete"
:suggestions="filteredUsers"
:placeholder="$p.t('lehre', 'assignPerson')"
v-model="selectedUser"
field="label"
:minLength="3"
:minLength="2"
@item-select="addUser"
@complete="searchUser"
></form-input>
@@ -26,16 +26,6 @@ export default {
default: true
}
},
computed: {
formattedAnmerkung: {
get() {
return (this.data.anmerkung || '').replace(/\\n/g, '\n');
},
set(value) {
this.data.anmerkung = (value || '').replace(/\n/g, '\\n');
}
}
},
template: `
<div>
<div class="row mb-3">
@@ -74,7 +64,7 @@ export default {
:label="$p.t('lehre', 'detailanmerkung')"
type="textarea"
container-class="col-3"
v-model="formattedAnmerkung"
v-model="data.anmerkung"
name="anmerkung"
id="anmerkung"
rows="10"
@@ -88,9 +88,9 @@ export default{
data() {
return{
tabulatorEvents: [],
showAutocomplete: false,
filteredGroups: [],
selectedGroup: null
selectedGroup: null,
abortController: null
}
},
watch: {
@@ -99,19 +99,42 @@ export default{
}
},
methods:{
searchGroup(event)
async searchGroup(event)
{
const query = event.query.toLowerCase().trim();
this.filteredGroups = this.dropdowns.gruppen_array.filter(gruppe => {
return gruppe.gruppe_kurzbz.toLowerCase().includes(query) || gruppe?.bezeichnung?.toLowerCase().includes(query);
}).map(gruppe => ({
label: gruppe.bezeichnung
? `${gruppe.gruppe_kurzbz.trim()} (${gruppe.bezeichnung})`
: gruppe.gruppe_kurzbz.trim(),
gid: gruppe.gid,
gruppe_kurzbz: gruppe.gruppe_kurzbz.trim(),
lehrverband: gruppe.lehrverband,
}));
const query = event.query.trim();
if (!query)
{
this.filteredLektor = [];
return;
}
if (query.length < 2)
{
return;
}
if (this.abortController)
{
this.abortController.abort();
}
this.abortController = new AbortController();
const signal = this.abortController.signal;
this.$api.call(ApiGruppe.getAllSearch(query), { signal })
.then(result => {
this.filteredGroups = result.data.map(gruppe => ({
label: gruppe.bezeichnung
? `${gruppe.gruppe_kurzbz.trim()} (${gruppe.bezeichnung})`
: gruppe.gruppe_kurzbz.trim(),
gid: gruppe.gid,
gruppe_kurzbz: gruppe.gruppe_kurzbz.trim(),
lehrverband: gruppe.lehrverband,
})
)})
.catch(this.$fhcAlert.handleSystemError)
},
reload() {
this.$refs.table.reloadTable();
@@ -173,13 +196,9 @@ export default{
table-only
:side-menu="false"
:reload=true
:new-btn-label="$p.t('lehre', 'addGroup')"
new-btn-show
@click:new="showAutocomplete = !showAutocomplete"
>
<template #search> <!--TODO (david) Slot prüfen -->
<form-input
v-if="showAutocomplete"
type="autocomplete"
:suggestions="filteredGroups"
:placeholder="$p.t('lehre', 'addGroup')"
@@ -1,5 +1,6 @@
import CoreSearchbar from "../searchbar/searchbar.js";
import VerticalSplit from "../verticalsplit/verticalsplit.js";
import AppMenu from "../AppMenu.js";
import StvVerband from "../Stv/Studentenverwaltung/Verband.js";
import StvStudiensemester from "../Stv/Studentenverwaltung/Studiensemester.js";
import LvTable from "./Setup/Table.js";
@@ -17,6 +18,7 @@ export default {
components: {
CoreSearchbar,
VerticalSplit,
AppMenu,
StvVerband,
StvStudiensemester,
LvTable,
@@ -39,7 +41,7 @@ export default {
dropdowns: this.dropdowns,
configShowVertragsdetails: this.config.showVertragsdetails,
configShowGewichtung: this.config.showGewichtung,
lehreinheitAnmerkungDefault: this.config.lehreinheitAnmerkungDefault,
lehreinheitAnmerkungDefault: (this.config.lehreinheitAnmerkungDefault || '').replace(/\\n/g, '\n'),
lehreinheitRaumtypDefault: this.config.lehreinheitRaumtypDefault,
lehreinheitRaumtypAlternativeDefault: this.config.lehreinheitRaumtypAlternativeDefault,
@@ -80,9 +82,6 @@ export default {
sprachen_array: [],
lehrform_array: [],
raumtyp_array: [],
lektor_array: [],
gruppen_array: [],
benutzer_array: [],
},
selectedStudiengang: '',
searchbaroptions: {
@@ -125,6 +124,10 @@ export default {
? `${this.stg}/${this.semester}`
: this.stg;
}
else
{
this.selectedStudiengang = '';
}
this.filter = filter;
},
handleRowClicked(data)
@@ -164,6 +167,7 @@ export default {
const routeName = this.filter.emp ? 'byEmp' : 'byStg';
const params = { stg };
params.semester = '';
if (semester !== null)
params.semester = semester;
if (studiensemester_kurzbz)
@@ -185,6 +189,13 @@ export default {
this.$router.replace({ name: 'byStg', params: newParams });
}
},
resetStgFilter()
{
const newParams = { ...this.filter, activeFilter: 'emp' };
delete newParams.stg;
this.selectedStudiengang = '';
this.$router.replace({ name: 'byEmp', params: newParams });
},
searchfunction(params) {
return this.$api.call(ApiSearchbar.search(params));
},
@@ -227,34 +238,49 @@ export default {
this.dropdowns.lehrfunktion_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api.call(ApiLektor.getLektoren())
.then(result => {
this.dropdowns.lektor_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api.call(ApiGruppe.getAll())
.then(result => {
this.dropdowns.gruppen_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api.call(ApiGruppe.getBenutzer())
.then(result => {
this.dropdowns.benutzer_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
template: `
template: /* html */`
<div class="stv">
<header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-4 col-lg-3 col-xl-2 me-0 px-3">LV Verwaltung</a>
<div class="col-md-4 col-lg-3 col-xl-2 d-flex align-items-center">
<button
class="btn btn-outline-light border-0 m-1 collapsed"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#appMenu"
aria-controls="appMenu"
aria-expanded="false"
:aria-label="$p.t('ui/toggle_nav')"
>
<span class="svg-icon svg-icon-apps"></span>
</button>
<a class="navbar-brand me-0">LV Verwaltung</a>
</div>
<button
class="btn btn-outline-light border-0 d-md-none m-1 collapsed"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#sidebarMenu"
aria-controls="sidebarMenu"
aria-expanded="false"
:aria-label="$p.t('ui/toggle_nav')"
>
<span class="fa-solid fa-table-list"></span>
</button>
<core-searchbar :searchoptions="searchbaroptions" :searchfunction=searchfunction class="searchbar w-100"></core-searchbar>
</header>
<div class="container-fluid overflow-hidden">
<div class="row h-100">
<aside id="appMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100">
<div class="offcanvas-header">
LV Verwaltung
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
</div>
<div class="offcanvas-body">
<app-menu app-identifier="lvv" />
</div>
</aside>
<nav id="sidebarMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100">
<div class="offcanvas-header justify-content-end px-1 d-md-none">
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
@@ -272,12 +298,25 @@ export default {
:filter="filter"
>
<template #filterzuruecksetzen v-if="filter.activeFilter === 'employee'">
<button type="button"
class="btn btn-outline-secondary btn-action"
title="Mitarbeiter Filter entfernen"
@click="resetEmployeeFilter">
<span class="fw-bold small">
[{{ $p.t('lehre', 'lektor') }}: {{ filter.emp || '' }}
<button type="button"
class="btn btn-outline-secondary btn-action btn-sm ms-1"
:title="$p.t('ui', 'filterdelete')"
@click="resetEmployeeFilter">
<i class="fa fa-xmark"></i>
</button>
<template v-if="filter.stg">
| Stg: {{ filter.stg }}
<button type="button"
class="btn btn-outline-secondary btn-action btn-sm ms-1"
:title="$p.t('ui', 'filterdelete')"
@click="resetStgFilter">
<i class="fa fa-xmark"></i>
</button>
</template>
]
</span>
</template>
</lv-table>
</template>
@@ -31,6 +31,8 @@ export default{
changed: {},
internal_mitarbeiter_uid: null,
filteredLektor: [],
abortController: null,
selectedLektorLabel: ''
}
},
computed: {
@@ -63,7 +65,10 @@ export default{
this.internal_mitarbeiter_uid = newVal;
if (newVal === null)
{
this.data = null;
this.selectedLektorLabel = '';
}
else if (newVal !== undefined && this.lehreinheit_id !== undefined)
this.getLektorData();
}
@@ -99,10 +104,15 @@ export default{
return this.$api.call(ApiLektor.getLektorDaten(this.lehreinheit_id, this.internal_mitarbeiter_uid))
.then(result => {
this.data = result.data;
this.selectedLektorLabel = `${this.data.nachname} ${this.data.vorname} (${this.data.mitarbeiter_uid})`,
this.original = { ...this.data };
})
.catch(this.$fhcAlert.handleSystemError);
},
onLektorSelected(selectedLektor)
{
this.data.mitarbeiter_uid = selectedLektor.value.uid;
},
updateDaten()
{
if (!this.changedLength)
@@ -139,17 +149,37 @@ export default{
})
.catch(this.$fhcAlert.handleSystemError);
},
searchLektor(event)
async searchLektor(event)
{
const query = event.query.toLowerCase().trim();
this.filteredLektor = this.dropdowns.lektor_array.filter(lektor => {
const fullName = `${lektor.vorname.toLowerCase()} ${lektor.nachname.toLowerCase()}`;
const reverseFullName = `${lektor.nachname.toLowerCase()} ${lektor.vorname.toLowerCase()}`;
return fullName.includes(query) || reverseFullName.includes(query) || lektor.uid.toLowerCase().includes(query);
}).map(lektor => ({
label: `${lektor.nachname} ${lektor.vorname} (${lektor.uid})`,
uid: lektor.uid
}));
const query = event.query.trim();
if (!query)
{
this.filteredLektor = [];
return;
}
if (query.length < 2)
{
return;
}
if (this.abortController)
{
this.abortController.abort();
}
this.abortController = new AbortController();
const signal = this.abortController.signal;
this.$api.call(ApiLektor.getLektorenSearch(query), { signal })
.then(result => {
this.filteredLektor = result.data.map(lektor => ({
label: `${lektor.nachname} ${lektor.vorname} (${lektor.uid})`,
uid: lektor.uid
})
)})
.catch(this.$fhcAlert.handleSystemError)
},
},
@@ -187,11 +217,12 @@ export default{
:disabled="data.vertrag_id !== null"
:suggestions="filteredLektor"
placeholder="Mitarbeiter hinzufügen"
v-model="data.mitarbeiter_uid"
v-model="selectedLektorLabel"
field="label"
container-class="col-3"
dropdown
@complete="searchLektor"
@item-select="onLektorSelected"
name="lektorautocomplete"
></form-input>
@@ -231,7 +262,6 @@ export default{
</div>
<div class="row mb-3 d-flex align-items-end">
<form-input
:label="data.default_stundensatz !== null
? $p.t('lehre', 'stundensatz') + ' (' + $p.t('lehre', 'default') + ': ' + data.default_stundensatz + ')'
@@ -246,30 +276,23 @@ export default{
>
</form-input>
<form-input
:label="$p.t('lehre', 'bismelden')"
type="checkbox"
container-class="col-3"
v-model="data.bismelden"
name="bismelden"
>
</form-input>
<div class="col-3 d-flex align-items-end">
<form-input
:label="$p.t('lehre', 'bismelden')"
type="checkbox"
v-model="data.bismelden"
name="bismelden"
>
</form-input>
</div>
</div>
<div class="row mb-3">
<form-input
:label="$p.t('lehre', 'gesamtkosten')"
type="number"
name="gesamtkosten"
container-class="col-3"
readonly
v-model="berechneteGesamtkosten"
:style="{ color: berechneteGesamtkosten <= 0 ? 'red' : 'black' }"
>
</form-input>
<div class="d-flex mb-2 gap-2">
<span class="fw-bold">{{ $p.t('lehre', 'gesamtkosten') }}:</span>
<span :style="{ color: berechneteGesamtkosten <= 0 ? 'red' : 'black' }">
{{ berechneteGesamtkosten }}
</span>
</div>
</template>
</fieldset>
@@ -3,7 +3,6 @@ import FormForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import ApiLektor from "../../../api/lehrveranstaltung/lektor.js";
export default{
name: "LVLektorTable",
components: {
@@ -34,9 +33,9 @@ export default{
handler: this.lektorSelected
}
],
showAutocomplete: false,
filteredLektor: [],
selectedLektor: ''
selectedLektor: '',
abortController: null
}
},
computed: {
@@ -163,17 +162,37 @@ export default{
})
},
searchLektor(event)
async searchLektor(event)
{
const query = event.query.toLowerCase().trim();
this.filteredLektor = this.dropdowns.lektor_array.filter(lektor => {
const fullName = `${lektor.vorname.toLowerCase()} ${lektor.nachname.toLowerCase()}`;
const reverseFullName = `${lektor.nachname.toLowerCase()} ${lektor.vorname.toLowerCase()}`;
return fullName.includes(query) || reverseFullName.includes(query) || lektor.uid.toLowerCase().includes(query);
}).map(lektor => ({
label: `${lektor.nachname} ${lektor.vorname} (${lektor.uid})`,
uid: lektor.uid
}));
const query = event.query.trim();
if (!query)
{
this.filteredLektor = [];
return;
}
if (query.length < 2)
{
return;
}
if (this.abortController)
{
this.abortController.abort();
}
this.abortController = new AbortController();
const signal = this.abortController.signal;
this.$api.call(ApiLektor.getLektorenSearch(query), { signal })
.then(result => {
this.filteredLektor = result.data.map(lektor => ({
label: `${lektor.nachname} ${lektor.vorname} (${lektor.uid})`,
uid: lektor.uid
})
)})
.catch(this.$fhcAlert.handleSystemError)
},
addLektor()
{
@@ -199,13 +218,9 @@ export default{
table-only
:side-menu="false"
reload
:new-btn-label="$p.t('lehre', 'addLektor')"
new-btn-show
@click:new="showAutocomplete = !showAutocomplete"
>
<template #search> <!--TODO (david) Slot prüfen -->
<form-input
v-if="showAutocomplete"
type="autocomplete"
:suggestions="filteredLektor"
:placeholder="$p.t('lehre', 'addLektor')"
@@ -113,23 +113,16 @@ export default{
<core-form ref="form">
<fieldset class="overflow-hidden" v-if="showVertragsdetails">
<legend> {{$p.t('lehre', 'vertragsdetails')}}
{{ data === null ? ' Noch kein Vertrag' : '' }}
</legend>
{{ data?.vertrag === null ? ' ' + $p.t('lehre', 'keinvertrag') : '' }}
</legend>
<template v-if="data?.vertrag">
<div class="row align-items-end mb-3">
<form-input
:label="$p.t('lehre', 'vertragsstatus')"
type="text"
readonly
container-class="col-3"
v-model="vertragsstatus"
:style="{fontWeight: vertragsstatus === 'Geändert' ? 'bold' : 'normal'}"
name="vertragsstatus"
/>
<div class="col-3 d-flex align-items-end">
<button
type="button"
class="btn btn-outline-secondary w-100"
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
<div class="d-flex align-items-center flex-wrap gap-2">
<span class="fw-bold">{{ $p.t('lehre', 'vertragsstatus') }}:</span>
<span :class="{ 'fw-bold': vertragsstatus === 'Geändert' }">{{ vertragsstatus }}</span>
<button
type="button"
class="btn btn-outline-secondary btn-sm"
@click="cancelVertrag"
:title="$p.t('lehre', 'cancelvertrag')"
>
@@ -137,28 +130,18 @@ export default{
</button>
</div>
</div>
{{$p.t('lehre', 'vertragurfassung')}}
<div class="row mb-3">
<form-input
:label="$p.t('lehre', 'semesterstunden')"
type="text"
container-class="col-3"
readonly
v-model="data.vertrag.vertragsstunden"
name="vertragsstunden"
>
</form-input>
<div class="mb-2 fw-bold text-decoration-underline">
{{ $p.t('lehre', 'vertragurfassung') }}
</div>
<div class="row mb-3">
<form-input
:label="$p.t('lehre', 'studiensemester')"
type="text"
container-class="col-3"
readonly
v-model="data.vertrag.vertragsstunden_studiensemester_kurzbz"
name="vertragsstunden_studiensemester_kurzbz"
>
</form-input>
<div class="ps-4">
<div class="d-flex mb-2 gap-2">
<span class="fw-bold">{{ $p.t('lehre', 'semesterstunden') }}:</span>
<span>{{ data.vertrag.vertragsstunden }}</span>
</div>
<div class="d-flex mb-2 gap-2">
<span class="fw-bold">{{ $p.t('lehre', 'studiensemester') }}:</span>
<span>{{ data.vertrag.vertragsstunden_studiensemester_kurzbz }}</span>
</div>
</div>
</template>
</fieldset>
@@ -324,7 +324,7 @@ export default {
{title: this.$p.t('lehre', 'lehreinheit_id'), field: "lehreinheit_id", headerFilter: true, headerFilterFuncParams: {field: 'lehreinheit_id'}, visible: false},
{title: this.$p.t('lehre', 'studiensemester'), field: "studiensemester_kurzbz", headerFilter: true, headerFilterFuncParams: {field: 'studiensemester_kurzbz'}, visible: false},
{title: this.$p.t('lehre', 'unr'), field: "unr", headerFilter: true, headerFilterFuncParams: {field: 'unr'}, visible: false},
{title: this.$p.t('lehre', 'fachbereich'), field: "fachbereich", headerFilter: true, headerFilterFuncParams: {field: 'fachbereich'}, visible: false},
{title: this.$p.t('lehre', 'organisationseinheit'), field: "fachbereich", headerFilter: true, headerFilterFuncParams: {field: 'fachbereich'}, visible: false},
{title: this.$p.t('lehre', 'stundenblockung'), field: "stundenblockung", headerFilter: true, headerFilterFuncParams: {field: 'stundenblockung'}, visible: false},
{title: this.$p.t('lehre', 'wochenrhythmus'), field: "wochenrythmus", headerFilter: true, headerFilterFuncParams: {field: 'wochenrythmus'}, visible: false},
{title: this.$p.t('lehre', 'startkw'), field: "start_kw", headerFilter: true, headerFilterFuncParams: {field: 'startkw'}, visible: false},
@@ -563,36 +563,54 @@ export default {
this.allRows.forEach(row => {
if (row.getTreeChildren().length > 0 && row.isTreeExpanded())
{
this.expanded.push(row.getData().uniqueindex);
this.expanded.push(row.getData().lv_bezeichnung);
}
});
},
reexpandRows() {
this.allRows = this.getAllRows(this.$refs.table.tabulator.getRows());
const matchingRows = this.allRows.filter(row =>
this.expanded.includes(row.getData().uniqueindex)
);
let lastMatchingRow = null;
if (matchingRows.length === 0)
this.currentTreeLevel = 0;
matchingRows.forEach((row, index) => {
row._row.modules.dataTree.open = true;
if (index === matchingRows.length - 1)
this.allRows.forEach(row => {
if (this.expanded.includes(row.getData().lv_bezeichnung))
{
row.treeExpand();
if (row._row.modules.dataTree)
{
row._row.modules.dataTree.open = true;
}
if (row._row.data._children?.length > 0)
{
lastMatchingRow = row;
}
}
});
if (lastMatchingRow)
{
lastMatchingRow.treeExpand();
}
this.$refs.table.tabulator.redraw();
},
deleteLehreinheit(row)
{
let deleteData = {
lehreinheit_id: row.getData().lehreinheit_id,
}
let lehreinheit_id = row.getData().lehreinheit_id;
let is_selected = this.selectedColumnValues.length > 0 && this.selectedColumnValues.includes(lehreinheit_id);
let deleteData = is_selected ? {lehreinheit_id: [...new Set(this.selectedColumnValues)]} : {lehreinheit_id: lehreinheit_id};
return this.$api.call(ApiLehreinheit.delete(deleteData))
.then(result => {
if (result?.data?.errors)
{
result.data.errors.forEach(error => {
this.$fhcAlert.alertError(error)
})
}
this.reload()
})
.catch(this.$fhcAlert.handleSystemError);
@@ -624,7 +642,7 @@ export default {
},
expandTree()
{
this.currentTreeLevel = (this.currentTreeLevel || 0) + 1;
this.currentTreeLevel = (this.currentTreeLevel || 1);
let lastMatchingRow = null;
@@ -645,6 +663,7 @@ export default {
if (lastMatchingRow)
{
lastMatchingRow.treeExpand();
this.currentTreeLevel++;
}
this.$refs.table.tabulator.redraw();
},
@@ -663,6 +682,8 @@ export default {
@click:new="showLehreinheitModal">
<template #actions>
<button @click="expandTree" class="btn btn-outline-secondary" type="button" :title="$p.t('lehre', 'aufklappen')"><i class="fa-solid fa-maximize"></i></button>
<button @click="resetTree" class="btn btn-outline-secondary" type="button" :title="$p.t('lehre', 'zuklappen')"><i id="togglegroup" class="fa-solid fa-minimize"></i></button>
<core-tag ref="tagComponent"
:endpoint="tagEndpoint"
:values="selectedColumnValues"
@@ -671,8 +692,6 @@ export default {
@updated="updatedTag"
zuordnung_typ="lehreinheit_id"
></core-tag>
<button @click="expandTree" class="btn btn-outline-secondary" type="button"><i class="fa-solid fa-maximize"></i></button>
<button @click="resetTree" class="btn btn-outline-secondary" type="button"><i id="togglegroup" class="fa-solid fa-minimize"></i></button>
</template>
<template #search>
<slot name="filterzuruecksetzen"></slot>
@@ -4,6 +4,8 @@ import FormInput from '../../../Form/Input.js';
import ListBox from "../../../../../../index.ci.php/public/js/components/primevue/listbox/listbox.esm.min.js";
import DropdownComponent from "../../../VorlagenDropdown/VorlagenDropdown.js";
import ApiMessages from '../../../../api/factory/messages/messages.js';
export default {
name: "ModalNewMessages",
components: {
@@ -14,13 +16,9 @@ export default {
ListBox
},
props: {
endpoint: {
type: Object,
required: true
},
typeId: String,
id: {
type: [Number, String],
type: Array,
required: true
},
messageId: {
@@ -43,6 +41,8 @@ export default {
vorlagen: [],
recipientsArray: [],
defaultRecipient: null,
defaultRecipients: [],
defaultRecipientString: null,
editor: null,
fieldsUser: [],
fieldsPerson: [],
@@ -56,7 +56,6 @@ export default {
previewText: null,
previewBody: "",
replyData: null,
uid: null,
}
},
methods: {
@@ -111,56 +110,46 @@ export default {
},
sendMessage() {
const data = new FormData();
const params = {
id: this.id,
type_id: this.typeId
};
const merged = {
...this.formData,
...params
};
data.append('data', JSON.stringify(merged));
data.append('data', JSON.stringify(this.formData));
data.append('ids', JSON.stringify(this.id));
return this.$refs.formMessage
.call(this.endpoint.sendMessageFromModalContext(this.uid, data))
.call(ApiMessages.sendMessage(this.typeId, data))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSent'));
this.hideModal('modalNewMessage');
this.resetForm();
}).catch(this.$fhcAlert.handleSystemError)
.finally(() => {
//this.resetForm();
//closeModal
//closewindwo
//just emit if no multitasking
if(this.id.length == 1){
this.$emit('reloadTable');
}
}
);
},
getVorlagentext(vorlage_kurzbz){
getDataVorlage(vorlage_kurzbz){
return this.$api
.call(this.endpoint.getVorlagentext(vorlage_kurzbz))
.call(ApiMessages.getDataVorlage(vorlage_kurzbz))
.then(response => {
this.formData.body = response.data;
}).catch(this.$fhcAlert.handleSystemError)
.finally(() => {
//this.resetForm();
//closeModal
//closewindwo
});
this.formData.body = response.data.text;
this.formData.subject = response.data.subject;
}).catch(this.$fhcAlert.handleSystemError);
},
getPreviewText(){
const data = new FormData();
data.append('data', JSON.stringify(this.formData.body));
data.append('ids', JSON.stringify(this.id));
return this.$api
.call(this.endpoint.getPreviewText({
id: this.id,
type_id: this.typeId}, data))
.call(ApiMessages.getPreviewText(
this.typeId, data))
.then(response => {
this.previewText = response.data;
const previews = response.data;
this.previewText = previews[this.defaultRecipient];
}).catch(this.$fhcAlert.handleSystemError)
.finally(() => {
//this.resetForm();
//closeModal
//closewindwo
});
@@ -175,7 +164,7 @@ export default {
this.editor.save();
} else {
console.error("Editor instance is not available.");
console.error(this.$p.t('messages', 'errorEditorNotAvailable'));
}
},
resetForm(){
@@ -184,6 +173,7 @@ export default {
body: null,
subject: null,
};
this.$emit('resetMessageId');
if (this.editor) {
@@ -197,8 +187,7 @@ export default {
},
handleSelectedVorlage(vorlage_kurzbz) {
if (typeof vorlage_kurzbz === "string") {
this.getVorlagentext(vorlage_kurzbz);
this.formData.subject = vorlage_kurzbz;
this.getDataVorlage(vorlage_kurzbz);
}
},
showPreview(){
@@ -206,18 +195,6 @@ export default {
this.previewBody = this.previewText;
});
},
getUid(id, typeId){
const params = {
id: id,
type_id: typeId
};
this.$api
.call(this.endpoint.getUid(params))
.then(result => {
this.uid = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
show(){
this.$refs.modalNewMessage.show();
},
@@ -240,7 +217,7 @@ export default {
handler(newVal){
if (newVal && newVal != null) {
this.formData.subject = newVal;
return this.getVorlagentext(newVal);
return this.getDataVorlage(newVal);
}
}
},
@@ -250,7 +227,7 @@ export default {
if (!newMessageId) return;
try {
const result = await this.$api.call(this.endpoint.getReplyData(newMessageId));
const result = await this.$api.call(ApiMessages.getReplyData(newMessageId));
this.replyData = result.data;
if (this.replyData.length > 0) {
@@ -265,15 +242,9 @@ export default {
}
},
created(){
this.getUid(this.id, this.typeId);
if(this.typeId == 'person_id' || this.typeId == 'mitarbeiter_uid'){
const params = {
id: this.id,
type_id: this.typeId
};
this.$api
.call(this.endpoint.getMessageVarsPerson(params))
.call(ApiMessages.getMessageVarsPerson(this.id, this.typeId))
.then(result => {
this.fieldsPerson = result.data;
const person = this.fieldsPerson[0];
@@ -286,12 +257,8 @@ export default {
}
if(this.typeId == 'prestudent_id' || this.typeId == 'uid'){
const params = {
id: this.id,
type_id: this.typeId
};
this.$api
.call(this.endpoint.getMsgVarsPrestudent(params))
.call(ApiMessages.getMsgVarsPrestudent(this.id, this.typeId))
.then(result => {
this.fieldsPrestudent = result.data;
const prestudent = this.fieldsPrestudent[0];
@@ -304,7 +271,7 @@ export default {
}
this.$api
.call(this.endpoint.getMsgVarsLoggedInUser())
.call(ApiMessages.getMsgVarsLoggedInUser())
.then(result => {
this.fieldsUser = result.data;
const user = this.fieldsUser;
@@ -316,21 +283,18 @@ export default {
.catch(this.$fhcAlert.handleSystemError);
this.$api
.call(this.endpoint.getNameOfDefaultRecipient({
id: this.id,
type_id: this.typeId}))
.call(ApiMessages.getNameOfDefaultRecipients(this.id, this.typeId))
.then(result => {
this.defaultRecipient = result.data;
this.recipientsArray.push({
'uid': this.uid,
'details': this.defaultRecipient});
this.defaultRecipients = result.data;
this.defaultRecipientString = Object.values(this.defaultRecipients).join("; ");
})
.catch(this.$fhcAlert.handleSystemError);
//case of reply
if(this.messageId) {
this.$api
.call(this.endpoint.getReplyData(this.messageId))
.call(ApiMessages.getReplyData(this.messageId))
.then(result => {
this.replyData = result.data;
this.formData.subject = this.replyData[0].replySubject;
@@ -386,7 +350,7 @@ export default {
type="text"
name="recipient"
:label="$p.t('messages/recipient')"
v-model="defaultRecipient"
v-model="defaultRecipientString"
disabled
>
</form-input>
@@ -507,17 +471,17 @@ export default {
>
<option :value="null">{{ $p.t('messages', 'recipient') }}...</option>
<option
v-for="recipient in recipientsArray"
:key="recipient.uid"
:value="recipient.uid"
>{{recipient.details}}
v-for="(name, id) in defaultRecipients"
:key="id"
:value="Number(id)"
> {{name}}
</option>
</form-input>
</div>
<div class="col-md-2 mt-4">
<br>
<button type="button" class="btn btn-secondary" @click="showPreview()">{{ $p.t('ui', 'btnAktualisieren') }}</button>
<button type="button" class="btn btn-secondary" @click="showPreview(defaultRecipient)">{{ $p.t('ui', 'btnAktualisieren') }}</button>
</div>
</form-form>
@@ -2,6 +2,7 @@ import FormForm from '../../../Form/Form.js';
import FormInput from '../../../Form/Input.js';
import ListBox from "../../../../../../index.ci.php/public/js/components/primevue/listbox/listbox.esm.min.js";
import DropdownComponent from '../../../VorlagenDropdown/VorlagenDropdown.js';
import ApiMessages from "../../../../api/factory/messages/messages.js"; //props not working with route
export default {
name: "ComponentNewMessages",
@@ -12,33 +13,17 @@ export default {
DropdownComponent,
},
props: {
endpoint: {
type: Object,
required: true
},
openMode: String,
tempTypeId: String,
tempId: {
type: [Number, String],
typeId: String,
id: {
type: Array,
required: false
},
tempMessageId: {
messageId: {
type: Number,
required: false,
}
},
computed: {
//params with routes for new tab and new window AND props for inSamePage
id(){
return this.$props.tempId || this.$route.params.id;
},
typeId(){
return this.$props.tempTypeId || this.$route.params.typeId;
},
messageId(){
return this.$props.tempMessageId ||this.$route.params.messageId;
}
},
data(){
return {
formData: {
@@ -53,6 +38,8 @@ export default {
vorlagen: [],
recipientsArray: [],
defaultRecipient: null,
defaultRecipients: [],
defaultRecipientString: null,
editor: null,
isVisible: false,
fieldsUser: [],
@@ -67,8 +54,7 @@ export default {
previewText: null,
previewBody: "",
replyData: null,
uid: null,
messageSent: false
messageSent: false,
}
},
methods: {
@@ -106,19 +92,11 @@ export default {
},
sendMessage() {
const data = new FormData();
data.append('data', JSON.stringify(this.formData));
data.append('ids', JSON.stringify(this.id));
const params = {
id: this.id,
type_id: this.typeId
};
const merged = {
...this.formData,
...params
};
data.append('data', JSON.stringify(merged));
return this.$api
.call(this.endpoint.sendMessage(this.uid, data))
.call(ApiMessages.sendMessage(this.typeId, data))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSent'));
this.hideTemplate();
@@ -126,61 +104,50 @@ export default {
this.messageSent = true;
}).catch(this.$fhcAlert.handleSystemError)
.finally(() => {
//TODO(Manu) hier route definieren für openmode in Tab, Page?
// ist kein child sondern mit route aufgerufen
//würde allerdings neues fenster aktualisiert öffnen, altes bleibt ohne reload gleich
//Reload vorheriges tab???
if(this.openMode == "inSamePage"){
if(this.openMode == "inSamePage" && this.id.length == 1 ){
this.$emit('reloadTable');
}
}
);
},
getVorlagentext(vorlage_kurzbz){
getDataVorlage(vorlage_kurzbz){
return this.$api
.call(this.endpoint.getVorlagentext(vorlage_kurzbz))
.call(ApiMessages.getDataVorlage(vorlage_kurzbz))
.then(response => {
this.formData.body = response.data;
}).catch(this.$fhcAlert.handleSystemError)
.finally(() => {
//this.resetForm();
});
this.formData.body = response.data.text;
this.formData.subject = response.data.subject;
}).catch(this.$fhcAlert.handleSystemError);
},
getPreviewText(id, typeId){
getPreviewText(){
console.log("subj" + this.formData.subject);
const data = new FormData();
data.append('data', JSON.stringify(this.formData.body));
data.append('ids', JSON.stringify(this.id));
console.log("subj" + this.formData.subject);
return this.$api
.call(this.endpoint.getPreviewText({
id: this.id,
type_id: this.typeId}, data))
.call(ApiMessages.getPreviewText(
this.typeId, data))
.then(response => {
this.previewText = response.data;
const previews = response.data;
this.previewText = previews[this.defaultRecipient];
}).catch(this.$fhcAlert.handleSystemError)
.finally(() => {
//this.resetForm();
});
},
insertVariable(selectedItem){
if (this.editor) {
this.editor.insertContent(selectedItem.value + " ");
//TODO(Manu) check: Laden von Variblen geht nicht wenn kein Zeichen danach kommt
// nicht mal mit Punkt adden gehts ohne eintrag nach vars
//this.editor.focus();
// this.editor.setDirty(true);
this.editor.setDirty(true);//seting dirty true if changes appear
// console.log(tinyMCE.activeEditor.isDirty());//dirty output = true
//this.editor.undoManager.add();
//this.editor.insertContent(selectedItem.value + "\u00A0");
//this.editor.insertContent(`<span>${selectedItem.value}&nbsp;</span>`);
//this.editor.selection.setCursorLocation(this.editor.getBody(), 1);
this.editor.fire('input');
this.editor.fire('change');
this.editor.setDirty(true);
this.editor.save();
} else {
console.error("Editor instance is not available.");
console.error(this.$p.t('messages', 'errorEditorNotAvailable'));
}
},
resetForm(){
@@ -188,11 +155,13 @@ export default {
vorlage_kurzbz: null,
body: null,
subject: null,
recipient: null,
selectedValue: null
};
if (this.editor) {
this.editor.setContent("");
}
this.$refs.dropdownComp.setValue(null);
// this.$refs.dropdownComp.setValue(null);
this.previewBody = null;
@@ -202,8 +171,7 @@ export default {
},
handleSelectedVorlage(vorlage_kurzbz) {
if (typeof vorlage_kurzbz === "string") {
this.getVorlagentext(vorlage_kurzbz);
this.formData.subject = vorlage_kurzbz;
this.getDataVorlage(vorlage_kurzbz);
}
},
hideTemplate(){
@@ -211,23 +179,25 @@ export default {
this.isVisible = false;
},
showTemplate(){
if (this.openMode == "inSamePage")
if (this.openMode == "inSamePage") {
this.isVisible = true;
//to enable send newMessage after sentMessage
this.messageSent = false;
}
},
showPreview(id, typeId){
this.getPreviewText(id, typeId).then(() => {
showPreview(){
this.getPreviewText().then(() => {
this.previewBody = this.previewText;
});
},
getUid(id, typeId){
const params = {
id: id,
type_id: typeId
};
loadReplyData(messageId){
this.$api
.call(this.endpoint.getUid(params))
.call(ApiMessages.getReplyData(messageId))
.then(result => {
this.uid = result.data;
this.replyData = result.data;
this.formData.subject = this.replyData[0].replySubject;
this.formData.body = this.replyData[0].replyBody;
this.formData.relationmessage_id = messageId;
})
.catch(this.$fhcAlert.handleSystemError);
}
@@ -248,44 +218,49 @@ export default {
if (newVal && newVal != null) {
this.formData.subject = newVal;
return this.getVorlagentext(newVal);
return this.getDataVorlage(newVal);
}
}
},
},
created(){
this.getUid(this.id, this.typeId);
const missingparamsmsgs = [];
if(!this.typeId)
{
missingparamsmsgs.push(this.$p.t('messages', 'errorMissingOrInvalidParameterRecipientTypeId'));
}
if (['person_id', 'mitarbeiter_uid'].includes(this.typeId)){
const params = {
id: this.id,
type_id: this.typeId
};
if(!this.id || this.id.length < 1)
{
missingparamsmsgs.push(this.$p.t('messages', 'errorMissingOrInvalidParameterRecipientIds'));
}
this.$api
.call(this.endpoint.getMessageVarsPerson(params))
.then(result => {
this.fieldsPerson = result.data;
const person = this.fieldsPerson[0];
this.itemsPerson = Object.entries(person).map(([key, value]) => ({
label: key.toLowerCase(),
value: '{' + key.toLowerCase() + '}'
}));
})
.catch(this.$fhcAlert.handleSystemError);
}
if(missingparamsmsgs.length > 0)
{
this.$fhcAlert.alertMultiple(missingparamsmsgs, 'warn', 'Warning', true);
return;
}
if (['prestudent_id', 'uid'].includes(this.typeId)){
const params = {
id: this.id,
type_id: this.typeId
};
if(this.typeId == 'person_id' || this.typeId == 'mitarbeiter_uid'){
this.$api
.call(this.endpoint.getMsgVarsPrestudent(params))
.call(ApiMessages.getMessageVarsPerson(this.id, this.typeId))
.then(result => {
this.fieldsPerson = result.data;
const person = this.fieldsPerson[0];
this.itemsPerson = Object.entries(person).map(([key, value]) => ({
label: key.toLowerCase(),
value: '{' + key.toLowerCase() + '}'
}));
})
.catch(this.$fhcAlert.handleSystemError);
}
if(this.typeId == 'prestudent_id' || this.typeId == 'uid'){
this.$api
.call(ApiMessages.getMsgVarsPrestudent(this.id, this.typeId))
.then(result => {
this.fieldsPrestudent = result.data;
const prestudent = this.fieldsPrestudent[0];
this.itemsPrestudent = Object.entries(prestudent).map(([key, value]) => ({
label: key.toLowerCase(),
value: '{' + key.toLowerCase() + '}'
@@ -295,7 +270,7 @@ export default {
}
this.$api
.call(this.endpoint.getMsgVarsLoggedInUser())
.call(ApiMessages.getMsgVarsLoggedInUser())
.then(result => {
this.fieldsUser = result.data;
const user = this.fieldsUser;
@@ -307,28 +282,26 @@ export default {
.catch(this.$fhcAlert.handleSystemError);
this.$api
.call(this.endpoint.getNameOfDefaultRecipient({
id: this.id,
type_id: this.typeId}))
.call(ApiMessages.getNameOfDefaultRecipients(this.id, this.typeId))
.then(result => {
this.defaultRecipient = result.data;
this.recipientsArray.push({
'uid': this.uid,
'details': this.defaultRecipient});
this.defaultRecipients = result.data;
this.defaultRecipientString = Object.values(this.defaultRecipients).join("; ");
})
.catch(this.$fhcAlert.handleSystemError);
//case of reply
if(this.messageId != null) {
this.$api
.call(this.endpoint.getReplyData(this.messageId))
this.loadReplyData(this.messageId);
/* this.$api
.call(ApiMessages.getReplyData(this.messageId))
.then(result => {
this.replyData = result.data;
this.formData.subject = this.replyData[0].replySubject;
this.formData.body = this.replyData[0].replyBody;
this.formData.relationmessage_id = this.messageId;
})
.catch(this.$fhcAlert.handleSystemError);
.catch(this.$fhcAlert.handleSystemError);*/
}
},
@@ -342,7 +315,7 @@ export default {
<div class="messages-detail-newmessage-newdiv">
<!--new page-->
<div v-if="!messageSent" class="overflow-auto m-3">
<div v-if="!messageSent" ref="divNewMessage" class="overflow-auto m-3">
<h4>{{ $p.t('messages', 'neueNachricht') }}</h4>
<div class="row">
@@ -355,7 +328,7 @@ export default {
type="text"
name="recipient"
:label="$p.t('messages/recipient')"
v-model="defaultRecipient"
v-model="defaultRecipientString"
disabled
>
</form-input>
@@ -484,18 +457,18 @@ export default {
v-model="defaultRecipient"
>
<option :value="null">{{ $p.t('messages', 'recipient') }}...</option>
<option
v-for="recipient in recipientsArray"
:key="recipient.uid"
:value="recipient.uid"
>{{recipient.details}}
<option
v-for="(name, id) in defaultRecipients"
:key="id"
:value="Number(id)"
> {{name}}
</option>
</form-input>
</div>
<div class="col-md-2 mt-4">
<br>
<button type="button" class="btn btn-secondary" @click="showPreview(id, typeId)">{{ $p.t('ui', 'btnAktualisieren') }}</button>
<button type="button" class="btn btn-secondary" @click="showPreview(defaultRecipient)">{{ $p.t('ui', 'btnAktualisieren') }}</button>
</div>
</form-form>
@@ -1,6 +1,8 @@
import {CoreFilterCmpt} from "../../filter/Filter.js";
import FormForm from '../../Form/Form.js';
import ApiMessages from "../../../api/factory/messages/messages.js"
export default {
name: "TableMessages",
components: {
@@ -13,13 +15,9 @@ export default {
},
},
props: {
endpoint: {
type: Object,
required: true
},
typeId: String,
id: {
type: [Number, String],
type: Array,
required: true
},
messageLayout: String,
@@ -38,12 +36,13 @@ export default {
},
ajaxResponse: (url, params, response) => this.buildTreemap(response),
columns: [
{title: "subject", field: "subject"},
{title: "body", field: "body", formatter: "html", visible: false},
{title: "message_id", field: "message_id", visible: false},
{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
@@ -55,16 +54,28 @@ export default {
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"},
{title: "recipient", field: "recipient"},
{title: "senderId", field: "sender_id"},
{title: "recipientId", field: "recipient_id"},
{title: "Relationmessage ID", field: "relationmessage_id"},
{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",
@@ -73,11 +84,12 @@ export default {
],
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
@@ -89,6 +101,17 @@ export default {
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);
}
},
{
@@ -132,9 +155,10 @@ export default {
frozen: true
}
],
layout: 'fitDataFill',
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: '400',
selectable: 1,
selectableRangeMode: 'click',
index: 'message_id',
pagination: true,
@@ -147,7 +171,7 @@ export default {
dataTreeCollapseElement:"<i class='fas fa-minus-square'></i>",
dataTreeChildIndent: 15,
dataTreeStartExpanded: false,
persistenceID: 'core-message',
persistenceID: 'core-message-2025112401',
locale: 'de',
"langs": {
"de":{ //German language definition
@@ -219,6 +243,7 @@ export default {
title: this.$p.t('global', 'aktionen')
});
*/
this.$emit('tabulator_tablebuilt');
}
},
{
@@ -255,7 +280,7 @@ export default {
},
deleteMessage(message_id){
return this.$api
.call(this.endpoint.deleteMessage(message_id))
.call(ApiMessages.deleteMessage(message_id))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
}).catch(this.$fhcAlert.handleSystemError)
@@ -274,6 +299,11 @@ export default {
this.$refs.table.reloadTable();
},
buildTreemap(messages) {
if (!messages || !messages.data || messages.data.length === 0)
{
return {data: [], last_page: 0};
}
const last_page = messages.meta.count;
messages = messages.data;
const messageMap = new Map();
@@ -316,7 +346,7 @@ export default {
},
loadAjaxCall(url, config, params){
return this.$api.call(
this.endpoint.getMessages(params)
ApiMessages.getMessages(params)
);
}
},
@@ -341,13 +371,13 @@ export default {
});*/
},
created(){
if(this.typeId != 'person_id') {
if(this.typeId != 'person_id' && Array.isArray(this.id) && this.id.length === 1) {
const params = {
id: this.id,
type_id: this.typeId
};
this.$api
.call(this.endpoint.getPersonId(params))
.call(ApiMessages.getPersonId(params))
.then(result => {
this.personId = result.data;
})
+61 -30
View File
@@ -14,10 +14,6 @@ export default {
}
},
props: {
endpoint: {
type: Object,
required: true
},
typeId: {
type: String,
required: true,
@@ -31,7 +27,7 @@ export default {
}
},
id: {
type: [Number, String],
type: Array,
required: true
},
showTable: Boolean,
@@ -60,11 +56,15 @@ export default {
},
data() {
return {
tablebuilt: false,
isVisibleDiv: false,
messageId: null
}
},
methods: {
getControllerUrl() {
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/NeueNachricht';
},
reloadTable(){
this.$refs.templateTableMessage.reload();
},
@@ -77,60 +77,92 @@ export default {
this.openInNewTab(id, typeId, messageId);
}
else if (this.openMode == "modal"){
if(!messageId)
this.$refs.modalMsg.resetForm();
this.$refs.modalMsg.show();
}
else if (this.openMode == "inSamePage"){
console.log("in same Page");
this.isVisibleDiv = true;
if(messageId)
this.$refs.templateNewDivMessage.loadReplyData(messageId);
else
this.$refs.templateNewDivMessage.resetForm();
this.$refs.templateNewDivMessage.showTemplate();
}
else
console.log("no valid openMode");
},
openInNewTab(id, typeId, messageId= null){
openInNewTab(id, typeId, messageId=null){
if(id.length > 1)
{
this.$refs['newMsgForm'].submit();
return;
}
let path = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
let path = this.getControllerUrl();
if (messageId){
path += "/NeueNachricht/" + id + "/" + typeId + "/" + messageId;
path += "/" + encodeURIComponent(id) + "/" + encodeURIComponent(typeId) + "/" + encodeURIComponent(messageId);
}
else {
path += "/NeueNachricht/" + id + "/" + typeId;
path += "/" + encodeURIComponent(id) + "/" + encodeURIComponent(typeId);
}
const newTab = window.open(path, "_blank");
},
openInNewWindow(id, typeId, messageId){
let path = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
if (messageId){
path += "/NeueNachricht/" + id + "/" + typeId + "/" + messageId;
}
else {
path += "/NeueNachricht/" + id + "/" + typeId;
}
const width = Math.round(window.innerWidth * 0.75);
const height = Math.round(window.innerHeight * 0.75);
const left = Math.round((window.innerWidth - width) / 2);
const top = Math.round((window.innerHeight - height) / 2);
if(id.length > 1)
{
const newWindow = window.open('', "NewMsgWindow", `width=${width},height=${height},left=${left},top=${top}`);
this.$refs['newMsgForm'].submit();
return;
}
let path = this.getControllerUrl();
if (messageId){
path += "/" + encodeURIComponent(id) + "/" + encodeURIComponent(typeId) + "/" + encodeURIComponent(messageId);
}
else {
path += "/" + encodeURIComponent(id) + "/" + encodeURIComponent(typeId);
}
const newWindow = window.open(path, "_blank", `width=${width},height=${height},left=${left},top=${top}`);
},
resetMessageId(){
this.messageId = null;
},
tableBuilt: function() {
this.tablebuilt = true;
}
},
template: `
<div class="core-messages h-100 pb-3">
<!-- TODO(bh) set target _self for debugging post but _blank for newTab -->
<form ref="newMsgForm"
method="post"
:action="getControllerUrl()"
:target="(openMode === 'window') ? 'NewMsgWindow' : '_blank'"
>
<input type="hidden" name="typeid" :value="typeId">
<input type="hidden" name="ids" :value="id">
</form>
<message-modal
v-if="tablebuilt || id.length > 1"
ref="modalMsg"
:type-id="typeId"
:id="id"
:message-id="messageId"
:endpoint="endpoint"
:openMode="openMode"
@reloadTable="reloadTable"
@resetMessageId="resetMessageId"
@@ -140,28 +172,27 @@ export default {
<!--in same page-->
<div v-if="isVisibleDiv" class="overflow-auto m-3" style="max-height: 500px; border: 1px solid #ccc;">
<form-only
ref="templateNewMessage"
:temp-type-id="typeId"
:temp-id="id"
:temp-message-id="messageId"
:endpoint="endpoint"
v-if="tablebuilt || id.length > 1"
ref="templateNewDivMessage"
:type-id="typeId"
:id="id"
:message-id="messageId"
:openMode="openMode"
@reloadTable="reloadTable"
>
</form-only>
</div>
<div v-if="showTable">
<div v-if="showTable && id.length==1">
<table-messages
ref="templateTableMessage"
:type-id="typeId"
:id="id"
:endpoint="endpoint"
:messageLayout="messageLayout"
:openMode="openMode"
@newMessage="handleMessage"
@replyToMessage="handleMessage"
@tabulator_tablebuilt="tableBuilt"
>
</table-messages>
</div>
+15 -3
View File
@@ -42,7 +42,11 @@ export default {
showErweitert: Boolean,
showDocument: Boolean,
showTinyMce: Boolean,
visibleColumns: Array
visibleColumns: Array,
tabulatorPersistenceId: {
type: String,
default: 'core-notiz'
}
},
data() {
return {
@@ -150,7 +154,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',
@@ -179,7 +183,15 @@ export default {
//responsiveLayout: "collapse",
maxHeight: '200px',
index: 'notiz_id',
persistenceID: 'core-notiz'
persistenceID: this.tabulatorPersistenceId,
persistence: {
sort: false,
columns: ["width", "visible", "frozen"],
filter: false,
headerFilter: false,
group: false,
page: false,
}
},
tabulatorEvents: [
{
@@ -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;
@@ -192,7 +192,15 @@ export default {
<option value="textLong_plageat">{{$p.t('studierendenantrag', 'dropdown_plageat')}}
</option>
<option value="textLong_MissingZgv">{{$p.t('studierendenantrag', 'dropdown_MissingZgv')}}
</option>
</option>
<option value="textLong_Studienwechsel">{{$p.t('studierendenantrag', 'dropdown_Studienwechsel')}}
</option>
<option value="textLong_Studienabbruch_allgemein">{{$p.t('studierendenantrag', 'dropdown_Studienabbruch_allgemein')}}
</option>
<option value="textLong_vsCodeOfConduct">{{$p.t('studierendenantrag', 'dropdown_vsCodeOfConduct')}}
</option>
<option value="textLong_additionalReason">{{$p.t('studierendenantrag', 'dropdown_additionalReason')}}
</option>
<!--
<option value="textLong_unruly">{{$p.t('studierendenantrag', 'dropdown_unruly')}}
</option>
@@ -37,16 +37,16 @@ export default {
});
},
loadData(evt) {
if( evt.query.length < 2 )
{
return false;
}
if (evt.query.length < 2)
{
return false;
}
if (this.abortController)
{
this.abortController.abort();
}
if (this.abortController instanceof AbortController
&& this.abortController.signal.aborted === false)
{
this.abortController.abort();
}
this.abortController = new AbortController();
this.$api
@@ -56,16 +56,8 @@ export default {
})
.then(result => {
this.data = result.data;
this.abortController = null;
})
.catch(error => {
if (this.abortController instanceof AbortController
&& this.abortController.signal.aborted === false)
{
this.abortController.abort();
}
this.$fhcAlert.handleSystemError(error);
});
.catch(this.$fhcAlert.handleSystemError);
}
},
template: `
+272 -14
View File
@@ -16,7 +16,10 @@
*/
import CoreSearchbar from "../searchbar/searchbar.js";
import NavLanguage from "../navigation/Language.js";
import VerticalSplit from "../verticalsplit/verticalsplit.js";
import AppMenu from "../AppMenu.js";
import AppConfig from "../AppConfig.js";
import StvVerband from "./Studentenverwaltung/Verband.js";
import StvList from "./Studentenverwaltung/List.js";
import StvDetails from "./Studentenverwaltung/Details.js";
@@ -25,13 +28,17 @@ import StvStudiensemester from "./Studentenverwaltung/Studiensemester.js";
import ApiSearchbar from "../../api/factory/searchbar.js";
import ApiStv from "../../api/factory/stv.js";
import ApiStvVerband from '../../api/factory/stv/verband.js';
import ApiStvConfig from '../../api/factory/stv/config.js';
export default {
name: 'Studentenverwaltung',
components: {
CoreSearchbar,
NavLanguage,
VerticalSplit,
AppMenu,
AppConfig,
StvVerband,
StvList,
StvDetails,
@@ -43,6 +50,8 @@ export default {
permissions: Object,
stvRoot: String,
cisRoot: String,
avatarUrl: String,
logoutUrl: String,
activeAddons: String, // semicolon separated list of active addons
url_studiensemester_kurzbz: String,
url_mode: String,
@@ -74,15 +83,24 @@ export default {
},
configShowAufnahmegruppen: this.config.showAufnahmegruppen,
configAllowUebernahmePunkte: this.config.allowUebernahmePunkte,
configUseReihungstestPunkte: this.config.useReihungstestPunkte
configUseReihungstestPunkte: this.config.useReihungstestPunkte,
appConfig: Vue.computed(() => this.appconfig),
hasZGVBakkPermission: this.permissions['student/editBakkZgv'],
hasZGVMasterPermission: this.permissions['student/editMakkZgv'],
hasZGVDoctorPermission: this.permissions['student/editDokZgv'],
hasBismeldenPermission: this.permissions['student/editBismelden'],
}
},
data() {
return {
appconfig: {},
configEndpoints: ApiStvConfig,
selected: [],
searchbaroptions: {
origin: 'stv',
calcheightonly: true,
nolivesearch: true,
types: {
student: Vue.computed(() => this.$p.t('search/type_student')),
prestudent: Vue.computed(() => this.$p.t('search/type_prestudent'))
@@ -121,6 +139,8 @@ export default {
studiengangKz: undefined,
studiengangKuerzel: '',
studiensemesterKurzbz: this.defaultSemester,
selected_semester: undefined,
selected_orgform: undefined,
lists: {
nations: [],
sprachen: [],
@@ -129,12 +149,60 @@ export default {
verbandEndpoint: ApiStvVerband
}
},
computed: {
appMenuExtraItems() {
const extraItems = [];
if (this.studiengangKz !== undefined && this.selected_semester !== undefined) {
const studiengang_kz = String(this.studiengangKz);
const semester = String(this.selected_semester);
const orgform = this.selected_orgform || '';
extraItems.push({
link: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'content/statistik/notenspiegel.php?typ=xls'
+ '&studiengang_kz=' + studiengang_kz
+ '&semester=' + semester
+ '&studiensemester=' + this.studiensemesterKurzbz
+ '&orgform=' + orgform,
description: 'stv/grade_report_xls'
});
extraItems.push({
link: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'content/statistik/notenspiegel_erweitert.php?typ=xls'
+ '&studiengang_kz=' + studiengang_kz
+ '&semester=' + semester
+ '&studiensemester=' + this.studiensemesterKurzbz
+ '&orgform=' + orgform,
description: 'stv/grade_report_xls_extended'
});
extraItems.push({
link: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'content/statistik/notenspiegel.php?typ=html'
+ '&studiengang_kz=' + studiengang_kz
+ '&semester=' + semester
+ '&studiensemester=' + this.studiensemesterKurzbz
+ '&orgform=' + orgform,
description: 'stv/grade_report_html'
});
}
return extraItems;
}
},
watch: {
'url_studiensemester_kurzbz': function (newVal, oldVal) {
if (newVal !== oldVal) {
this.studiensemesterKurzbz = newVal;
this.$refs.stvList.updateUrl();
this.$refs.details.reload();
if(this.$route.name === 'search')
{
this.handleSearchUrl();
}
else
{
this.$refs.stvList.updateUrl();
this.$refs.details.reload();
}
}
},
'url_studiengang': function (newVal, oldVal) {
@@ -144,6 +212,25 @@ export default {
},
'url_mode': function () {
this.handlePersonUrl();
},
url_prestudent_id() {
this.handlePersonUrl();
},
'appconfig.font_size'() {
// add to html class
const classList = Object.keys(this.$refs.config.setup.font_size.options);
classList.forEach(cn => document.documentElement.classList.remove(cn));
document.documentElement.classList.add(this.appconfig.font_size);
// recalc Tabulator heights
if (this.$el) {
const tabulatorEls = this.$el.querySelectorAll('.tabulator');
for (const el of tabulatorEls) {
const tabulators = Tabulator.findTable(el);
if (tabulators) {
tabulators[0].searchRows().forEach(row => row.normalizeHeight());
}
}
}
}
},
methods: {
@@ -157,7 +244,7 @@ export default {
}
},
buildPrestudentSearchResultLink(data) {
return this.$fhcApi.getUri(
return this.$api.getUri(
'/studentenverwaltung'
+ '/' + this.studiensemesterKurzbz
+ '/prestudent/'
@@ -165,7 +252,7 @@ export default {
);
},
buildStudentSearchResultLink(data) {
return this.$fhcApi.getUri(
return this.$api.getUri(
'/studentenverwaltung'
+ '/' + this.studiensemesterKurzbz
+ '/student/'
@@ -173,14 +260,14 @@ export default {
);
},
buildPersonSearchResultLink(data) {
return this.$fhcApi.getUri(
return this.$api.getUri(
'/studentenverwaltung'
+ '/' + this.studiensemesterKurzbz
+ '/person/'
+ data.person_id
);
},
onSelectVerband( {link, studiengang_kz}) {
onSelectVerband({ link, studiengang_kz, semester, orgform_kurzbz }) {
let urlpath = String(link);
if (!urlpath.match(/\/prestudent/))
{
@@ -189,6 +276,8 @@ export default {
this.$refs.stvList.updateUrl(ApiStv.students.verband(urlpath));
this.studiengangKz = studiengang_kz;
this.selected_semester = semester;
this.selected_orgform = orgform_kurzbz;
const stg = this.lists.stgs.find((element) => {
return (element.studiengang_kz === this.studiengangKz);
});
@@ -221,9 +310,6 @@ export default {
studiensemester_kurzbz: v
}
});
this.$refs.stvList.updateUrl();
this.$refs.details.reload();
},
reloadList() {
this.$refs.stvList.reload();
@@ -247,6 +333,37 @@ export default {
ApiStv.students.person(this.$route.params.person_id, 'CURRENT_SEMESTER'),
true
);
} else if (this.$route.params.searchstr) {
this.handleSearchUrl();
}
else
{
this.clearTabulator();
}
},
handleSearchUrl() {
const searchsettings = {
searchstr: this.$route.params.searchstr,
types: this.$route.params.types?.split('+') || []
};
// init into student list
this.$refs.stvList.updateUrl(
ApiStv.students.search(searchsettings, this.studiensemesterKurzbz)
);
// init into searchbar
this.$refs.searchbar.searchsettings.searchstr = searchsettings.searchstr;
this.$refs.searchbar.searchsettings.types = searchsettings.types;
this.$nextTick(this.blurSearchbar);
},
clearTabulator() {
if(['index', 'studiensemester'].includes(this.$route.name))
{
if(this.$refs?.stvList?.$refs?.table?.tabulator)
{
this.$refs.stvList.$refs.table.tabulator.setData([]);
}
}
},
checkUrlStudiengang() {
@@ -267,6 +384,42 @@ export default {
});
}
}
else
{
this.studiengangKz = undefined;
this.studiengangKuerzel = '';
this.clearTabulator();
}
},
onSearch(e) {
const searchsettings = { ...this.$refs.searchbar.searchsettings };
if (searchsettings.searchstr.length >= 2) {
this.blurSearchbar();
if (!searchsettings.types.length || searchsettings.types.length == this.$refs.searchbar.types.length) {
this.$router.push({
name: 'search',
params: {
studiensemester_kurzbz: this.studiensemesterKurzbz,
searchstr: searchsettings.searchstr
}
});
} else {
this.$router.push({
name: 'search_w_types',
params: {
studiensemester_kurzbz: this.studiensemesterKurzbz,
searchstr: searchsettings.searchstr,
types: searchsettings.types.join('+')
}
});
}
}
},
blurSearchbar() {
this.$refs.searchbar.$refs.input.blur();
this.$refs.searchbar.abort();
this.$refs.searchbar.hideresult();
}
},
created() {
@@ -345,24 +498,128 @@ export default {
//FHC_JS_DATA_STORAGE_OBJECT.systemerror_mailto = 'ma0068@technikum-wien.at';this.$fhcAlert.handleSystemError(1);
this.handlePersonUrl();
},
template: `
template: /* html */`
<div class="stv">
<header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-4 col-lg-3 col-xl-2 me-0 px-3" :href="stvRoot">StudVw: {{studiensemesterKurzbz}} {{studiengangKuerzel}}</a>
<button class="navbar-toggler d-md-none m-1 collapsed" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" :aria-label="$p.t('ui/toggle_nav')"><span class="navbar-toggler-icon"></span></button>
<div class="col-md-4 col-lg-3 col-xl-2 d-flex align-items-center">
<button
class="btn btn-outline-light border-0 m-1 collapsed"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#appMenu"
aria-controls="appMenu"
aria-expanded="false"
:aria-label="$p.t('ui/toggle_nav')"
>
<span class="svg-icon svg-icon-apps"></span>
</button>
<a class="navbar-brand me-0" :href="stvRoot">StudVw: {{studiensemesterKurzbz}} {{studiengangKuerzel}}</a>
</div>
<button
class="btn btn-outline-light border-0 d-md-none m-1 collapsed"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#sidebarMenu"
aria-controls="sidebarMenu"
aria-expanded="false"
:aria-label="$p.t('ui/toggle_nav')"
>
<span class="fa-solid fa-table-list"></span>
</button>
<core-searchbar
ref="searchbar"
:searchoptions="searchbaroptions"
:searchfunction="searchfunction"
class="searchbar position-relative w-100"
show-btn-submit
@submit.prevent="onSearch"
></core-searchbar>
<div id="nav-user" class="dropdown">
<button
id="nav-user-btn"
class="btn btn-link rounded-0 py-0"
type="button"
data-bs-toggle="dropdown"
data-bs-target="#nav-user-menu"
aria-expanded="false"
aria-controls="nav-user-menu"
>
<img
:src="avatarUrl"
:alt="$p.t('profilUpdate/profilBild')"
class="bg-light avatar rounded-circle border border-light"
/>
</button>
<ul
ref="navUserDropdown"
class="dropdown-menu dropdown-menu-dark dropdown-menu-end rounded-0 text-center m-0"
aria-labelledby="nav-user-btn"
>
<li>
<button
type="button"
class="dropdown-item"
data-bs-toggle="modal"
data-bs-target="#configModal"
>
{{ $p.t('ui/settings') }}
</button>
</li>
<li><hr class="dropdown-divider m-0"/></li>
<li>
<nav-language
item-class="dropdown-item border-left-dark"
/>
</li>
<li><hr class="dropdown-divider m-0"/></li>
<li>
<a class="dropdown-item" :href="logoutUrl">
{{ $p.t('ui/logout') }}
</a>
</li>
</ul>
</div>
</header>
<div class="container-fluid overflow-hidden">
<div class="row h-100">
<aside id="appMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100">
<div class="offcanvas-header">
StudVw: {{studiensemesterKurzbz}} {{studiengangKuerzel}}
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
</div>
<div class="offcanvas-body">
<app-menu app-identifier="stv">
<li class="dropend">
<a
class="dropdown-toggle"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
:class="{ disabled: !appMenuExtraItems.length }"
data-bs-popper-config='{"strategy":"fixed"}'
>
{{ $p.t('stv/grade_report') }}
</a>
<ul class="dropdown-menu p-0">
<li
v-for="(item, key) in appMenuExtraItems"
:key="key"
>
<a class="dropdown-item" :href="item.link" target="_blank">
{{ $p.t(item.description) }}
</a>
</li>
</ul>
</li>
</app-menu>
</div>
</aside>
<nav id="sidebarMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100">
<div class="offcanvas-header justify-content-end px-1 d-md-none">
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
</div>
<stv-verband :preselectedKey="'' + studiengangKz" :endpoint="verbandEndpoint" @select-verband="onSelectVerband" class="col" style="height:0%"></stv-verband>
<stv-verband :preselectedKey="studiengangKz ? '' + studiengangKz : null" :endpoint="verbandEndpoint" @select-verband="onSelectVerband" class="col" style="height:0%"></stv-verband>
<stv-studiensemester v-model:studiensemester-kurzbz="studiensemesterKurzbz" @update:studiensemester-kurzbz="studiensemesterChanged"></stv-studiensemester>
</nav>
<main class="col-md-8 ms-sm-auto col-lg-9 col-xl-10">
@@ -377,5 +634,6 @@ export default {
</main>
</div>
</div>
<app-config ref="config" v-model="appconfig" :endpoints="configEndpoints"></app-config>
</div>`
};
@@ -31,38 +31,51 @@ export default {
if (this.students.length == 1) {
const student = this.students[0];
if (student.uid)
return Object.fromEntries(Object.entries(this.configStudent).filter(([key, value]) => !value.showOnlyWithoutUid));
return Object.fromEntries(Object.entries(this.configStudent).filter(([key, value]) => !value.showOnlyWithUid));
return Object.fromEntries(Object.entries(this.configStudent).filter(([ , value ]) => !value.showOnlyWithoutUid));
return Object.fromEntries(Object.entries(this.configStudent).filter(([ , value ]) => !value.showOnlyWithUid));
} else if (this.students.every(student => student.uid)) {
return Object.fromEntries(Object.entries(this.configStudents).filter(([ , value ]) => !value.showOnlyWithoutUid));
} else if (this.students.every(student => !student.uid)) {
return Object.fromEntries(Object.entries(this.configStudents).filter(([ , value ]) => !value.showOnlyWithUid));
}
return this.configStudents;
return Object.fromEntries(Object.entries(this.configStudents).filter(([ , value ]) => !value.showOnlyWithUid && !value.showOnlyWithUid));
}
},
watch: {
'$p.user_language.value'(n, o) {
if (n !== o && o !== undefined)
this.loadConfig();
}
},
methods: {
loadConfig() {
this.$api
.call(ApiStvApp.configStudent())
.then(result => {
this.configStudent = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api
.call(ApiStvApp.configStudents())
.then(result => {
this.configStudents = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
reload() {
if (this.$refs.tabs?.$refs?.current?.reload)
this.$refs.tabs.$refs.current.reload();
}
},
created() {
this.$api
.call(ApiStvApp.configStudent())
.then(result => {
this.configStudent = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api
.call(ApiStvApp.configStudents())
.then(result => {
this.configStudents = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.loadConfig();
},
template: `
<div class="stv-details h-100 pb-3 d-flex flex-column">
<div class="stv-details h-100 d-flex flex-column">
<div v-if="!students?.length" class="justify-content-center d-flex h-100 align-items-center">
{{$p.t('ui', 'chooseStudent')}}
</div>
<div v-else-if="configStudent && configStudents" class="d-flex flex-column h-100 pb-3">
<div v-else-if="configStudent && configStudents" class="d-flex flex-column h-100">
<fhc-header
:headerData="students"
typeHeader="student"
@@ -7,7 +7,6 @@ import AbschlusspruefungDropdown from "./AbschlusspruefungDropdown.js";
import ApiStudiengang from '../../../../../api/factory/studiengang.js';
import ApiStvAbschlusspruefung from '../../../../../api/factory/stv/abschlusspruefung.js';
import ApiStvAddress from "../../../../../api/factory/stv/kontakt/address.js";
export default {
components: {
@@ -164,12 +163,12 @@ export default {
frozen: true
},
],
layout: 'fitDataFill',
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: 'auto',
minHeight: '200',
index: 'abschlusspruefung_id',
persistenceID: 'stv-details-finalexam'
persistenceID: 'stv-details-finalexam-2025112401'
},
tabulatorEvents: [
{
@@ -260,14 +259,16 @@ export default {
arrAkadGrad: [],
arrNoten: [],
selectedVorsitz: null,
listeFilteredMitarbeiter: [],
listeAllMitarbeiter: [],
listeAllPersons: [],
filteredMitarbeiter: [],
filteredPersons: [],
selectedPruefer1: null,
selectedPruefer2: null,
selectedPruefer3: null,
listeFilteredPersons: [],
stgInfo: { typ: '', oe_kurzbz: '' }
stgInfo: { typ: '', oe_kurzbz: '' },
abortController: {
mitarbeiter: null,
persons: null
},
}
},
watch: {
@@ -307,23 +308,39 @@ export default {
actionEditAbschlusspruefung(abschlusspruefung_id) {
this.resetForm();
this.statusNew = false;
this.loadAbschlusspruefung(abschlusspruefung_id).then(() => {
this.loadAbschlusspruefung(abschlusspruefung_id).then((result) => {
//set selectedData to enable viewing label in primevue autocomplete fields
this.selectedVorsitz = this.listeAllMitarbeiter.find(
item => item.mitarbeiter_uid === this.formData.vorsitz
);
this.selectedPruefer1 = this.listeAllPersons.find(
item => item.person_id === this.formData.pruefer1
);
this.selectedPruefer2= this.listeAllPersons.find(
item => item.person_id === this.formData.pruefer2
);
this.selectedPruefer3= this.listeAllPersons.find(
item => item.person_id === this.formData.pruefer3
);
const data = result.data;
this.selectedVorsitz = {
label: this.getPersonLabel(data.pv_titelpre, data.pv_nachname, data.pv_vorname, data.pv_titelpost, data.pv_uid),
person_id: data.pv_person_id,
mitarbeiter_uid: data.pv_uid
};
if (data.p1_person_id) {
this.selectedPruefer1 = {
label: this.getPersonLabel(data.p1_titelpre, data.p1_nachname, data.p1_vorname, data.p1_titelpost),
person_id: data.p1_person_id
};
}
if (data.p2_person_id) {
this.selectedPruefer2 = {
label: this.getPersonLabel(data.p2_titelpre, data.p2_nachname, data.p2_vorname, data.p2_titelpost),
person_id: data.p2_person_id
}
};
if (data.p3_person_id) {
this.selectedPruefer3= {
label: this.getPersonLabel(data.p3_titelpre, data.p3_nachname, data.p3_vorname, data.p3_titelpost),
person_id: data.p3_person_id
};
}
});
this.$refs.finalexamModal.show();
},
getPersonLabel(titelpre, nachname, vorname, titelpost, uid) {
return nachname + ' ' + vorname + (titelpre ? ' ' + titelpre : '') + (titelpost ? ' ' + titelpost : '') + (uid ? ' (' + uid + ')' : '');
},
actionDeleteAbschlusspruefung(abschlusspruefung_id) {
this.$fhcAlert
.confirmDelete()
@@ -362,8 +379,7 @@ export default {
.call(ApiStvAbschlusspruefung.loadAbschlusspruefung(abschlusspruefung_id))
.then(result => {
this.formData = result.data;
//TODO(Manu) check if cisRoot is okay
this.formData.link = this.cisRoot + 'index.ci.php/lehre/Pruefungsprotokoll/showProtokoll?abschlusspruefung_id=' + this.formData.abschlusspruefung_id + '&fhc_controller_id=67481e5ed5490';
this.formData.link = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'index.ci.php/lehre/Pruefungsprotokoll/showProtokoll?abschlusspruefung_id=' + this.formData.abschlusspruefung_id;
return result;
})
.catch(this.$fhcAlert.handleSystemError);
@@ -422,11 +438,9 @@ export default {
if (this.stgInfo.typ === 'b') {
this.formData.pruefungstyp_kurzbz = 'Bachelor';
this.formData.protokoll = this.$p.t('abschlusspruefung', 'pruefungsnotizenMaster');
}
if (this.stgInfo.typ === 'd' || this.stgInfo === 'm') {
this.formData.pruefungstyp_kurzbz = 'Diplom';
this.formData.protokoll = this.$p.t('abschlusspruefung', 'pruefungsnotizenMaster');
}
if (this.stgInfo.typ === 'lg') {
this.formData.pruefungstyp_kurzbz = 'lgabschluss';
@@ -439,22 +453,61 @@ export default {
printDocument(link) {
window.open(link, '_blank');
},
filterMitarbeiter(event){
const query = event?.query?.toLowerCase()?.trim() || "";
searchMitarbeiter(event) {
if (this.abortController.mitarbeiter) {
this.abortController.mitarbeiter.abort();
}
this.listeFilteredMitarbeiter = this.listeAllMitarbeiter.filter(item => {
const label = (item.label || "").toLowerCase();
return label.includes(query);
});
this.abortController.mitarbeiter = new AbortController();
return this.$api
.call(ApiStvAbschlusspruefung.getMitarbeiter(event.query))
.then(result => {
this.filteredMitarbeiter = [];
for (let mitarbeiter of result.data.retval) {
this.filteredMitarbeiter.push(
{
label: this.getPersonLabel(
mitarbeiter.titelpre,
mitarbeiter.nachname,
mitarbeiter.vorname,
mitarbeiter.titelpost,
mitarbeiter.mitarbeiter_uid
),
person_id: mitarbeiter.person_id,
mitarbeiter_uid: mitarbeiter.mitarbeiter_uid
}
);
}
});
},
filterPersons(event){
const query = event?.query?.toLowerCase()?.trim() || "";
searchPerson(event) {
if (this.abortController.persons) {
this.abortController.persons.abort();
}
this.listeFilteredPersons = this.listeAllPersons.filter(item => {
const label = (item.label || "").toLowerCase();
return label.includes(query);
});
}
this.abortController.persons = new AbortController();
return this.$api
.call(ApiStvAbschlusspruefung.getPruefer(event.query))
.then(result => {
this.filteredPersons = [];
for (let person of result.data.retval) {
this.filteredPersons.push(
{
label: this.getPersonLabel(
person.titelpre,
person.nachname,
person.vorname,
person.titelpost,
person.person_uid
),
person_id: person.person_id
}
);
}
});
},
},
created() {
this.$api
@@ -492,20 +545,6 @@ export default {
})
.catch(this.$fhcAlert.handleSystemError);
this.$api
.call(ApiStvAbschlusspruefung.getAllMitarbeiter())
.then(result => {
this.listeAllMitarbeiter = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api
.call(ApiStvAbschlusspruefung.getAllPersons())
.then(result => {
this.listeAllPersons = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
if (!this.student.length) {
this.$api
.call(ApiStudiengang.getStudiengangByKz(this.student.studiengang_kz))
@@ -623,8 +662,8 @@ export default {
optionValue="mitarbeiter_uid"
dropdown
forceSelection
:suggestions="listeFilteredMitarbeiter"
@complete="filterMitarbeiter"
:suggestions="filteredMitarbeiter"
@complete="searchMitarbeiter"
:min-length="3"
>
</form-input>
@@ -636,9 +675,10 @@ export default {
v-model="selectedPruefer1"
optionLabel="label"
optionValue="person_id"
dropdown
forceSelection
:suggestions="listeFilteredPersons"
@complete="filterPersons"
:suggestions="filteredPersons"
@complete="searchPerson"
:min-length="3"
>
</form-input>
@@ -669,9 +709,10 @@ export default {
v-model="selectedPruefer2"
optionLabel="label"
optionValue="person_id"
dropdown
forceSelection
:suggestions="listeFilteredPersons"
@complete="filterPersons"
:suggestions="filteredPersons"
@complete="searchPerson"
:min-length="3"
>
</form-input>
@@ -701,9 +742,10 @@ export default {
v-model="selectedPruefer3"
optionLabel="label"
optionValue="person_id"
dropdown
forceSelection
:suggestions="listeFilteredPersons"
@complete="filterPersons"
:suggestions="filteredPersons"
@complete="searchPerson"
:min-length="3"
>
</form-input>
@@ -715,6 +757,7 @@ export default {
:label="$p.t('global', 'datum')"
type="DatePicker"
v-model="formData.datum"
model-type="yyyy-MM-dd"
auto-apply
:enable-time-picker="false"
text-input
@@ -739,6 +782,7 @@ export default {
:label="$p.t('abschlusspruefung', 'sponsion')"
type="DatePicker"
v-model="formData.sponsion"
model-type="yyyy-MM-dd"
auto-apply
:enable-time-picker="false"
text-input
@@ -102,9 +102,9 @@ export default {
uids = !Array.isArray(this.studentUids) ? this.studentUids : this.studentUids.join(";");
let linkToPdf = this.showDropDownMulti
? this.cisRoot +
? FHC_JS_DATA_STORAGE_OBJECT.app_root +
'content/pdfExport.php?xml=abschlusspruefung.rdf.php&xsl=' + xsl + '&uid=' + uids + '&xsl_stg_kz=' + this.stgKz + '&output=' + output
: this.cisRoot +
: FHC_JS_DATA_STORAGE_OBJECT.app_root +
'content/pdfExport.php?xml=abschlusspruefung.rdf.php&xsl=' + xsl + '&abschlusspruefung_id=' + this.abschlusspruefung_id + '&uid=' + uids + '&xsl_stg_kz=' + this.stgKz + '&output=' + output;
this.$emit('linkGenerated', linkToPdf);
@@ -107,10 +107,10 @@ export default {
frozen: true
},
],
layout: 'fitDataFill',
layout: 'fitDataStretchFrozen',
height: '500',
index: 'anrechnung_id',
persistenceID: 'stv-details-anrechnungen'
persistenceID: 'stv-details-anrechnungen-2025112401'
},
tabulatorEvents: [
{
@@ -3,18 +3,29 @@ import FormInput from "../../../Form/Input.js";
import AkteEdit from "./Archiv/Edit.js";
import ApiStvArchiv from '../../../../api/factory/stv/archiv.js';
import ApiStvDocuments from '../../../../api/factory/stv/documents.js';
import DocumentDropdown from "../Details/Archiv/DocumentDropdown.js";
export default {
name: 'Archiv',
components: {
CoreFilterCmpt,
FormInput,
AkteEdit
AkteEdit,
DocumentDropdown
},
inject: {
currentSemester: {
from: 'currentSemester'
}
},
/* isBerechtigtDocAndOdt: {
from: 'hasPermissionOutputformat',
default: false
},*/
cisRoot: {
from: 'cisRoot'
},
},
props: {
modelValue: Object,
@@ -64,7 +75,8 @@ export default {
'abschlussdokument_lehrgaenge.xml.php': [
'AbschlussdokumentLehrgaenge'
]
}
},
documentDropdownObject: {}
};
},
computed: {
@@ -184,6 +196,38 @@ export default {
];
return events;
},
studentUids() {
if (this.modelValue.uid)
{
return [this.modelValue.uid];
}
return this.modelValue.map(e => e.uid);
},
studentKzs(){
if (this.modelValue.uid)
{
return [this.modelValue.studiengang_kz];
}
return this.modelValue.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;
},
showDropDownMulti(){
if (this.modelValue.length) {
return true;
}
return false;
}
},
watch: {
@@ -252,14 +296,43 @@ export default {
}
},
created() {
this.$api
.call(ApiStvArchiv.getArchivVorlagen())
.then(result => {
this.vorlagenArchiv = result.data;
this.selectedVorlage = result.data.filter(o => o.vorlage_kurzbz == 'Zeugnis')[0];
})
.catch(this.$fhcAlert.handleSystemError);
if (this.modelValue.length) {
const params = {
studiensemester_kurzbz: this.currentSemester,
studiengang_kz: this.stg_kz
};
this.$api
.call(ApiStvArchiv.getArchivVorlagen())
.then(result => {this.vorlagenArchiv = result.data; this.selectedVorlage = result.data.filter(o => o.vorlage_kurzbz == 'Zeugnis')[0];})
.call(ApiStvDocuments.getDocumentDropdownMulti(this.studentUids, params))
.then(result => {
this.documentDropdownObject = result;
})
.catch(this.$fhcAlert.handleSystemError);
} else {
const params = {
prestudent_id: this.modelValue.prestudent_id,
studiensemester_kurzbz: this.currentSemester,
studiengang_kz: this.modelValue.studiengang_kz
};
this.$api
.call(ApiStvDocuments.getDocumentDropdown(params))
.then(result => {
this.documentDropdownObject = result;
})
.catch(this.$fhcAlert.handleSystemError);
}
},
template: `
<div class="stv-details-archiv h-100 d-flex flex-column">
<core-filter-cmpt
ref="table"
table-only
@@ -270,6 +343,7 @@ export default {
:reload-btn-infotext="this.$p.t('table', 'reload')"
>
<template #actions>
<div class="input-group w-auto">
<select class="form-select" v-model="selectedVorlage">
<option v-for="vorlage in vorlagenArchiv" :key="vorlage.vorlage_kurzbz" :value="vorlage">
@@ -285,6 +359,17 @@ export default {
{{ $p.t('stv/archiv_dokument_archivieren') }}
</button>
</div>
<document-dropdown
v-if="documentDropdownObject.data"
:documents="documentDropdownObject.data"
:showAllFormats='true'
:studentUids="studentUids"
:showDropDownMulti="showDropDownMulti"
:cisRoot="cisRoot"
:stgKz="stg_kz"
></document-dropdown>
</template>
</core-filter-cmpt>
<akte-edit ref="edit" :config="config" @saved="updateData"></akte-edit>
@@ -0,0 +1,99 @@
export default {
name: "DocumentDropdown",
props: {
documents: {
type: [Object, Array],
required: true,
},
studentUids: {
type: [Array, String],
required: true,
default: () => []
},
showDropDownMulti: {
type: Boolean,
required: true
},
cisRoot: {
type: String,
required: true
},
stgKz: {
type: Number,
required: true
},
showAllFormats: {
type: Boolean,
required: true
}
},
data() {
return {};
},
methods: {
printDokument(url, scope){
//TODO Manu(check if logic not in content (Zutrittkarte also in content folder))
let linkToPdf = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'content/' + url;
window.open(linkToPdf, '_blank');
}
},
template: `
<div class="stv-document-dropdown btn-group">
<button
ref="toolbarButton"
type="button"
class="btn btn-secondary dropdown-toggle px-5 ms-5"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
>
{{this.$p.t('dokumente','dokument_erstellen')}}
</button>
<ul class="dropdown-menu dropdown-menu-right">
<template v-for="doc in documents" :key="doc.id">
<li v-if="doc.type === 'documenturl'">
<button class="dropdown-item" type="button" @click="printDokument(doc.url, doc.scope)">
{{ doc.name }}
</button>
</li>
<li v-else-if="doc.type === 'submenu'" class="dropend">
<a
class="dropdown-item dropdown-toggle"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{{ doc.name }}
</a>
<ul class="dropdown-menu">
<template v-for="child in doc.data" :key="child.id">
<li v-if="child.type === 'documenturl'">
<button class="dropdown-item" type="button" @click="printDokument(child.url, child.scope)">
{{ child.name }}
</button>
</li>
<li v-else-if="child.type === 'submenu'" class="dropend">
<a
class="dropdown-item dropdown-toggle"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{{ child.name }}
</a>
</li>
</template>
</ul>
</li>
</template>
</ul>
</div>`
};
@@ -120,12 +120,12 @@ export default {
frozen: true
}
],
layout: 'fitDataFill',
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: 'auto',
minHeight: 200,
index: 'aufnahmetermin_id',
persistenceID: 'stv-details-table_admission-dates'
persistenceID: 'stv-details-table_admission-dates-2025112401'
},
tabulatorEvents: [
{
@@ -322,7 +322,7 @@ export default {
this.$refs.filterButton.title = this.$p.t('admission', 'loadZukuenftigeRT');}
},
openAdministrationPlacementTest(reihungstest_id){
let link = this.cisRoot + 'vilesci/stammdaten/reihungstestverwaltung.php';
let link = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'vilesci/stammdaten/reihungstestverwaltung.php';
if(reihungstest_id){
link += '?reihungstest_id=' + reihungstest_id;
}
@@ -0,0 +1,83 @@
export default {
name: "TabCombinePeople",
inject: {
cisRoot: {
from: 'cisRoot'
},
},
props: {
modelValue: Object,
},
data(){
return {
iframeUrl: null,
viewLoaded: false
}
},
computed: {
personIds() {
return Array.isArray(this.modelValue)
? this.modelValue.map(e => e.person_id)
: [this.modelValue.person_id];
},
detailStringPerson1(){
let person1 = this.modelValue[0];
return person1.vorname + " " + person1.nachname + "(" + person1.person_id + ")";
},
detailStringPerson2(){
let person2 = this.modelValue[1];
return person2.vorname + " " + person2.nachname + "(" + person2.person_id+ ")";
},
},
methods: {
combinePeople(){
this.viewLoaded = true;
let person1_id = this.personIds[0];
let person2_id = this.personIds[1];
if(person1_id == person2_id) {
return this.$fhcAlert.alertError(this.$p.t('stv', 'error_combinePeople_samePerson'));
}
let linkCombinePeople = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'vilesci/stammdaten/personen_wartung.php?person_id_1=' + person1_id + '&person_id_2='+ person2_id;
this.openLink(linkCombinePeople);
},
openLink(url) {
this.iframeUrl = url;
},
goBack(){
this.viewLoaded = false;
this.iframeUrl = null;
}
},
template: /*html*/ `
<div class="stv-details-combine-people h-100 pb-3">
<div v-if="!this.viewLoaded">
<h4>Personen zusammenlegen</h4>
<div v-if="this.modelValue.length">
<div v-if="this.modelValue.length == 2">
<p>{{$p.t('stv', 'question_combine_people', { person1: detailStringPerson1, person2: detailStringPerson2 })}}</p>
<button class="btn btn-primary" @click="combinePeople">{{$p.t('ui', 'ok')}}</button>
</div>
<div v-else>
ungültige Anzahl: {{this.modelValue.length}} <!-- should not be seen anymore-->
</div>
</div>
</div>
<div v-else>
<button class="btn btn-secondary" @click="goBack">{{$p.t('ui', 'cancel')}}</button>
</div>
<!-- Iframe-Section -->
<iframe
v-if="iframeUrl"
:src="iframeUrl"
class="w-100 mt-4 border-0"
style="height: 600px;"
></iframe>
</div>
`
};
@@ -186,7 +186,7 @@ export default {
<label>{{$p.t('global', 'zugangscode')}}</label>
<div class="align-self-center">
<span class="form-text">
<a :href="cisRoot + 'addons/bewerbung/cis/registration.php?code=' + data.zugangscode + '&emailAdresse=' + data.email_privat + '&keepEmailUnverified=true'" target="_blank">{{data.zugangscode}}</a>
<a :href="cisRoot + 'addons/bewerbung/cis/registration.php?code=' + data.zugangscode + '&emailAdresse=' + (data.email_privat == null ? data.email_privat_unverified : data.email_privat) + '&keepEmailUnverified=true'" target="_blank">{{data.zugangscode}}</a>
</span>
</div>
</div>
@@ -0,0 +1,216 @@
import GroupsLvb from './Groups/Lvb.js';
import GroupsSpecial from './Groups/Special.js';
import GroupsList from './Groups/List.js';
import ApiStvGroups from '../../../../api/factory/stv/group.js';
import ApiStvDetails from '../../../../api/factory/stv/details.js';
export default {
name: 'TabGroups',
components: {
GroupsLvb,
GroupsSpecial,
GroupsList
},
inject: {
$reloadList: {
from: '$reloadList',
required: true
},
currentSemester: {
form: 'currentSemester',
required: true
}
},
props: {
modelValue: [Object, Array]
},
data() {
return {
hasOrgforms: false,
lvbList: [],
specialGroups: [],
selectedOrgform: false,
selectedSemester: false,
selectedVerband: false,
selectedGruppe: false,
multiFormHandler: (form, errors) => {
function _split_errors(result, [uid, errors]) {
const gruppe_kurzbz = [];
const studiensemester_kurzbz = [];
const others = {};
errors.forEach(error => {
_split_messages(error.messages, gruppe_kurzbz, studiensemester_kurzbz, others);
});
if (gruppe_kurzbz.length) {
if (!result.formFeedback.gruppe_kurzbz)
result.formFeedback.gruppe_kurzbz = [];
result.formFeedback.gruppe_kurzbz.push(...gruppe_kurzbz);
}
if (studiensemester_kurzbz.length) {
if (!result.formFeedback.studiensemester_kurzbz)
result.formFeedback.studiensemester_kurzbz = [];
result.formFeedback.studiensemester_kurzbz.push(...studiensemester_kurzbz);
}
if (Object.keys(others).length) {
result.toast[uid] = [
{ type: 'validation', messages: others }
];
}
return result;
}
function _split_messages(messages, gruppe_kurzbz, studiensemester_kurzbz, others) {
Object.entries(messages).forEach(([field, msg]) => {
if (field == 'gruppe_kurzbz') {
gruppe_kurzbz.push(msg);
} else if (field == 'studiensemester_kurzbz') {
studiensemester_kurzbz.push(msg);
} else {
if (!others[field])
others[field] = [];
others[field].push(msg);
}
});
}
const { formFeedback, toast } = Object.entries(errors)
.reduce(_split_errors, { formFeedback: {}, toast: {} });
if (formFeedback.gruppe_kurzbz)
formFeedback.gruppe_kurzbz = formFeedback.gruppe_kurzbz
.filter((v,k,a) => a.indexOf(v) == k);
if (formFeedback.studiensemester_kurzbz)
formFeedback.studiensemester_kurzbz = formFeedback.studiensemester_kurzbz
.filter((v,k,a) => a.indexOf(v) == k);
form.clearValidation();
if (Object.keys(formFeedback)) {
form.setFeedback(false, formFeedback);
}
if (Object.keys(toast).length) {
console.log(toast);
this.$api.getErrorHandler().handler.toast(toast);
}
}
};
},
computed: {
allAreStudents() {
if (Array.isArray(this.modelValue))
return this.modelValue.every(ps => ps.uid);
return this.modelValue.uid;
},
sharedStg() {
if (Array.isArray(this.modelValue)) {
const first = this.modelValue.find(Boolean);
if (this.modelValue.every(ps => ps.studiengang_kz === first.studiengang_kz))
return first.studiengang_kz;
return undefined;
}
return this.modelValue.studiengang_kz;
}
},
methods: {
showNewGroupModal() {
this.$refs.newGroupModal.show()
},
changeLvb(params) {
let data = { semester: params.semester };
if (params.verband && params.verband != " ") {
data.verband = params.verband;
if (params.gruppe && params.gruppe != " ")
data.gruppe = params.gruppe;
}
let endpoint;
if (Array.isArray(this.modelValue)) {
endpoint = this.modelValue.map(student => [
student.uid + ' (' + student.vorname + ' ' + student.nachname + ')',
ApiStvDetails.save(
student.prestudent_id,
this.currentSemester,
data
)
]);
} else {
endpoint = ApiStvDetails.save(
this.modelValue.prestudent_id,
this.currentSemester,
data
);
}
this.$api
.call(endpoint)
.then(() => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
this.$reloadList();
})
.catch(this.$fhcAlert.handleSystemError);
},
addSpecialGroup() {
const gruppe_kurzbz = this.$refs.newGroupModal.value.gruppe_kurzbz || this.$refs.newGroupModal.value;
if (Array.isArray(this.modelValue)) {
this.$refs.newGroupModal.$refs.form
.call(
this.modelValue.map(student => [
student.uid + ' (' + student.vorname + ' ' + student.nachname + ')',
ApiStvGroups.add(
student.uid,
gruppe_kurzbz,
this.currentSemester
)
]),
{
errorHandling: {
combine: { form: ['validation'] },
handler: { form: this.multiFormHandler }
}
}
)
.then(result => {
const successes = result.filter(res => res.status == 'fulfilled');
if (result.length == successes.length) {
this.$refs.newGroupModal.hide();
}
if (successes.length) {
this.$fhcAlert.alertSuccess(this.$p.t('gruppenmanagement/groups_added', { n: successes.length }));
this.$refs.list.reload();
}
})
.catch(this.$fhcAlert.handleSystemError);
} else {
this.$refs.newGroupModal.$refs.form
.call(ApiStvGroups.add(this.modelValue.uid, gruppe_kurzbz, this.currentSemester))
.then(() => {
this.$refs.newGroupModal.hide();
this.$fhcAlert.alertSuccess(this.$p.t('gruppenmanagement/groups_added', { n: 1 }));
this.$refs.list.reload();
})
.catch(this.$fhcAlert.handleSystemError);
}
}
},
template: /* html */`
<div class="stv-details-groups h-100 d-flex flex-column">
<h3>{{ $p.t('gruppenmanagement/special_groups') }}</h3>
<groups-special
ref="newGroupModal"
:default-stg="sharedStg"
@submit.capture.prevent="addSpecialGroup"
/>
<groups-list
ref="list"
class="mb-3"
:students="modelValue"
@new="showNewGroupModal"
/>
<h3>{{ $p.t('lehre/lehrverband') }}</h3>
<groups-lvb
:students="modelValue"
@submit="changeLvb"
/>
</div>`
};
@@ -0,0 +1,171 @@
import { CoreFilterCmpt } from "../../../../filter/Filter.js";
import ApiStvGroups from '../../../../../api/factory/stv/group.js';
export default {
name: 'TabGroupsList',
components: {
CoreFilterCmpt
},
inject: [
"currentSemester"
],
props: {
students: Object
},
emits: [
"new"
],
data() {
return {
phrasenLoaded: false,
optionsReady: true
};
},
computed: {
tabulatorOptions() {
let ajaxRequestFunc, ajaxResponse, initialFilter;
if (Array.isArray(this.students)) {
ajaxRequestFunc = () => {
return this.$api.call(
this.students.map(student => ApiStvGroups.getGruppen(student.uid))
);
};
ajaxResponse = (url, params, response) => {
return response.reduce((data, result) => [
...data,
...result.value.data
], []);
};
initialFilter = [
this.students.map(student => {
return { field: "uid", type: "=", value: student.uid };
}),
[
{ field: "studiensemester_kurzbz", type: "=", value: null },
{ field: "studiensemester_kurzbz", type: "=", value: this.currentSemester }
]
];
} else {
ajaxRequestFunc = () => {
return this.$api.call(
ApiStvGroups.getGruppen(this.students.uid)
);
};
ajaxResponse = (url, params, response) => {
return response.data;
};
initialFilter = [
{ field: "uid", type: "=", value: this.students.uid },
[
{ field: "studiensemester_kurzbz", type: "=", value: null },
{ field: "studiensemester_kurzbz", type: "=", value: this.currentSemester }
]
];
}
return {
ajaxURL: 'dummy',
ajaxRequestFunc,
ajaxResponse,
initialFilter,
columns: [
{ title: this.$p.t('gruppenmanagement/gruppe'), field: "gruppe_kurzbz" },
{ title: this.$p.t('ui/bezeichnung'), field: "bezeichnung" },
{ title: this.$p.t('lehre/studiensemester'), field: "studiensemester_kurzbz" },
{
title: this.$p.t('gruppenmanagement/automatisch_generiert'),
field: "generiert",
formatter: "tickCross",
hozAlign: "center",
formatterParams: {
tickElement: '<i class="fa fa-check text-success"></i>',
crossElement: '<i class="fa fa-xmark text-danger"></i>'
}
},
{ title: this.$p.t('ui/student_uid'), field: "uid" },
{
title: this.$p.t('global/actions'),
field: 'actions',
minWidth: 150, // Ensures Action-buttons will be always fully displayed
formatter: (cell, formatterParams, onRendered) => {
const container = document.createElement('div');
container.className = "d-flex gap-2";
const data = cell.getData();
const 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.actionDeleteGroup(data)
);
if (data.generiert)
button.disabled = true;
container.append(button);
return container;
},
frozen: true
}
],
height: 'auto',
index: 'group_id',
persistenceID: 'stv-details-group-list'
};
}
},
watch: {
tabulatorOptions() {
// Refresh Tabulator if options have changed
this.optionsReady = false;
this.$nextTick(() => this.optionsReady = true);
}
},
methods: {
actionDeleteGroup(data) {
this.$fhcAlert
.confirmDelete()
.then(result => result
? data
: Promise.reject({handled: true}))
.then(this.deleteGroup)
.catch(this.$fhcAlert.handleSystemError);
},
deleteGroup(params) {
return this.$api
.call(ApiStvGroups.deleteGroup(params))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
this.reload();
})
.catch(this.$fhcAlert.handleSystemError);
},
reload() {
this.$refs.table.reloadTable();
}
},
mounted() {
this.$p
.loadCategory(['global', 'lehre', 'ui', 'gruppenmanagement'])
.then(() => {
this.phrasenLoaded = true;
});
},
template: /* html */`
<div class="stv-details-groups-list">
<core-filter-cmpt
v-if="phrasenLoaded && optionsReady"
ref="table"
:tabulator-options="tabulatorOptions"
table-only
:side-menu="false"
reload
:reload-btn-infotext="$p.t('table/reload')"
new-btn-show
:new-btn-label="$p.t('lehre/gruppe')"
@click:new="$emit('new')"
>
</core-filter-cmpt>
</div>`
};
@@ -0,0 +1,241 @@
import FhcForm from "../../../../Form/Form.js";
import ApiStvLvb from '../../../../../api/factory/stv/lehrverband.js';
export default {
name: 'TabGroupsLvb',
components: {
FhcForm
},
props: {
students: Object
},
emits: [
"submit"
],
data() {
return {
lvbList: [],
selectedSemester: false,
selectedVerband: false,
selectedGruppe: false
};
},
computed: {
allAreStudents() {
if (Array.isArray(this.students))
return this.students.every(ps => ps.uid);
return this.students.uid;
},
studiengang_kz() {
if (Array.isArray(this.students)) {
const first = this.students.find(Boolean);
if (this.students.every(ps => ps.studiengang_kz === first.studiengang_kz))
return first.studiengang_kz;
return false;
}
return this.students.studiengang_kz;
},
semester: {
get() {
if (this.selectedSemester !== false) {
if (this.lvbList.some(item => item.semester == this.selectedSemester))
return this.selectedSemester;
return false;
}
if (Array.isArray(this.students)) {
const first = this.students.find(Boolean);
if (this.lvbList.every(item => item.semester != first.semester))
return false;
if (this.students.every(ps => ps.semester === first.semester))
return first.semester;
return false;
}
if (this.lvbList.some(item => item.semester == this.students.semester))
return this.students.semester;
return false;
},
set(value) {
this.selectedSemester = value;
}
},
verband: {
get() {
if (this.semester === false)
return false;
if (this.selectedVerband !== false) {
if (this.lvbListVerband.some(item => item.verband == this.selectedVerband))
return this.selectedVerband;
return false;
}
if (Array.isArray(this.students)) {
const first = this.students.find(Boolean);
if (this.lvbListVerband.every(item => item.verband != first.verband))
return false;
if (this.students.every(ps => ps.verband === first.verband))
return first.verband;
return false;
}
if (this.lvbListVerband.some(item => item.verband == this.students.verband))
return this.students.verband;
return false;
},
set(value) {
this.selectedVerband = value;
}
},
gruppe: {
get() {
if (this.verband === false)
return false;
if (this.selectedGruppe !== false) {
if (this.lvbListGruppe.some(item => item.gruppe == this.selectedGruppe))
return this.selectedGruppe;
return false;
}
if (Array.isArray(this.students)) {
const first = this.students.find(Boolean);
if (this.lvbListGruppe.every(item => item.gruppe != first.gruppe))
return false;
if (this.students.every(ps => ps.gruppe === first.gruppe))
return first.gruppe;
return false;
}
if (this.lvbListGruppe.some(item => item.gruppe == this.students.gruppe))
return this.students.gruppe;
return false;
},
set(value) {
this.selectedGruppe = value;
}
},
stgSemester() {
if (!this.lvbList.length)
return [];
const semester = new Set(this.lvbList.map(lvb => lvb.semester));
return Array.from(semester).sort((a, b) => a - b);
},
lvbListVerband() {
if (!this.lvbList.length)
return [];
if (this.semester === false)
return [];
return this.lvbList.filter(lvb => this.semester == lvb.semester);
},
semesterVerband() {
if (!this.lvbListVerband.length)
return [];
const verband = new Set(this.lvbListVerband.map(lvb => lvb.verband.replace(/ /g, '')));
return Array.from(verband).filter(Boolean).sort();
},
lvbListGruppe() {
if (!this.lvbListVerband.length)
return [];
if (this.verband === false)
return [];
return this.lvbListVerband.filter(lvb => this.verband == lvb.verband);
},
verbandGruppe() {
if (!this.lvbListGruppe.length)
return [];
const gruppe = new Set(this.lvbListGruppe.map(lvb => lvb.gruppe.replace(/ /g, '')));
return Array.from(gruppe).filter(Boolean).sort();
}
},
watch: {
studiengang_kz() {
this.loadGroupsForStg();
}
},
methods: {
loadGroupsForStg() {
this.lvbList = [];
if (this.studiengang_kz === false)
return;
this.$api
.call(ApiStvLvb.getTree(this.studiengang_kz))
.then(result => this.lvbList = result.data)
.catch(this.$fhcAlert.handleSystemError)
},
onSubmit() {
let params = {
studiengang_kz: this.studiengang_kz,
semester: this.semester,
verband: this.verband,
gruppe: this.gruppe
};
this.$emit("submit", params);
}
},
created() {
this.loadGroupsForStg();
},
template: /* html */`
<div class="stv-details-groups-lvb">
<fhc-form
v-if="allAreStudents && studiengang_kz"
ref="form"
class="input-group"
@submit.prevent="onSubmit"
>
<span class="input-group-text">
{{ $p.t('lehre/semester') }}
</span>
<select
v-model="semester"
class="form-select"
>
<option v-for="semester in stgSemester">{{ semester }}</option>
</select>
<span
class="input-group-text"
:class="{'text-muted': semester === false}"
>
{{ $p.t('lehre/verband') }}
</span>
<select
v-model="verband"
class="form-select"
:disabled="semester === false"
>
<option v-for="verband in semesterVerband">{{ verband }}</option>
</select>
<span
class="input-group-text"
:class="{'text-muted': verband === false}"
>
{{ $p.t('lehre/gruppe') }}
</span>
<select
v-model="gruppe"
class="form-select"
:disabled="verband === false"
>
<option v-for="gruppe in verbandGruppe">{{ gruppe }}</option>
</select>
<button
type="submit"
class="btn btn-primary"
:disabled="gruppe === false && verband === false"
>
{{ $p.t('ui/change') }}
</button>
</fhc-form>
<div v-if="!allAreStudents" class="alert alert-danger">
{{ $p.t('stv/groups_error_notallstudents') }}
</div>
<div v-if="!studiengang_kz" class="alert alert-danger">
{{ $p.t('stv/groups_error_notsamestg') }}
</div>
</div>`
};
@@ -0,0 +1,97 @@
import BsModal from "../../../../Bootstrap/Modal.js";
import FhcForm from "../../../../Form/Form.js";
import FormValidation from "../../../../Form/Validation.js";
import FormInput from "../../../../Form/Input.js";
import ApiStvGroups from '../../../../../api/factory/stv/group.js';
export default {
name: 'TabGroupsSpecial',
components: {
BsModal,
FhcForm,
FormValidation,
FormInput,
PvAutocomplete: primevue.autocomplete
},
props: {
defaultStg: Number
},
emits: [
"chosen"
],
data() {
return {
value: '',
groupSuggestions: []
};
},
methods: {
show() {
this.$refs.popup.show();
},
hide() {
this.$refs.popup.hide();
},
getGroupSuggestions({ query }) {
this.$api
.call(ApiStvGroups.search(query, this.defaultStg))
.then(result => this.groupSuggestions = result.data)
.catch(this.$fhcAlert.handleSystemError);
},
onSubmit(evt) {
if (!evt.defaultPrevented) {
evt.preventDefault();
this.hide();
}
},
onEnter() {
/**
* NOTE(chris): PrimeVue: AutoComplete: Enter does not submit form #5618
* @see https://github.com/primefaces/primevue/issues/5618
*
* this is fixed in 3.52.0
* until then this function fill fix it
*/
if (!this.$refs.autocomplete.$refs.input.overlayVisible) {
this.$refs.form.$el.requestSubmit();
}
},
modalOpened() {
this.$refs.autocomplete.$refs.input.$refs.focusInput.focus();
},
modalClosed() {
this.value = '';
this.$refs.form.clearValidation();
}
},
template: /* html */`
<bs-modal
ref="popup"
class="stv-details-groups-special"
@hide-bs-modal="modalClosed"
@shown-bs-modal="modalOpened"
>
<template #title>
{{ $p.t('gruppenmanagement/add_group') }}
</template>
<fhc-form ref="form" @submit="onSubmit">
<form-validation />
<div class="input-group">
<form-input
ref="autocomplete"
type="autocomplete"
name="gruppe_kurzbz"
v-model="value"
container-class="flex-grow-1"
input-class="w-100"
:suggestions="groupSuggestions"
:option-label="el => el.gruppe_kurzbz + (el.bezeichnung ? ' (' + el.bezeichnung + ')' : '')"
@complete="getGroupSuggestions"
@keydown.enter.capture="onEnter"
/>
<button type="submit" class="btn btn-primary">{{ $p.t('ui/hinzufuegen') }}</button>
</div>
</fhc-form>
</bs-modal>`
};
@@ -1,187 +0,0 @@
import {CoreFilterCmpt} from "../../../../filter/Filter.js";
import ApiStvGroups from '../../../../../api/factory/stv/group.js';
export default {
name: 'TblGroups',
components: {
CoreFilterCmpt,
},
inject: {
currentSemester: {
from: 'currentSemester',
},
},
props: {
student: Object
},
data() {
return {
tabulatorOptions: {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(
ApiStvGroups.getGruppen(this.student.uid)
),
ajaxResponse: (url, params, response) => response.data,
initialFilter: {
logic: "and",
filters: [
{ field: "uid", operator: "eq", value: this.student.uid },
{
logic: "or",
filters: [
{ field: "studiensemester_kurzbz", operator: "eq", value: null },
{ field: "studiensemester_kurzbz", operator: "eq", value: this.currentSemester }
]
}
]
},
columns: [
{title: "Gruppe", field: "gruppe_kurzbz"},
{title: "Bezeichnung", field: "bezeichnung"},
{title: "Semester", field: "studiensemester_kurzbz"},
{
title: "automatisch generiert",
field: "generiert",
formatter: "tickCross",
hozAlign: "center",
formatterParams: {
tickElement: '<i class="fa fa-check text-success"></i>',
crossElement: '<i class="fa fa-xmark text-danger"></i>'
}
},
{title: "UID", field: "uid"},
{
title: 'Aktionen', field: 'actions',
minWidth: 150, // Ensures Action-buttons will be always fully displayed
formatter: (cell, formatterParams, onRendered) => {
const container = document.createElement('div');
container.className = "d-flex gap-2";
const data = cell.getData();
const 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.actionDeleteGroup(data.gruppe_kurzbz)
);
if (data.generiert)
button.disabled = true;
container.append(button);
return container;
},
frozen: true
},
],
layout: 'fitDataFill',
height: 'auto',
index: 'group_id',
persistenceID: 'stv-details-gruppe'
},
tabulatorEvents: [
{
event: 'tableBuilt',
handler: async () => {
await this.$p.loadCategory(['global', 'person', 'stv', 'ui', 'gruppenmanagement']);
let cm = this.$refs.table.tabulator.columnManager;
cm.getColumnByField('gruppe_kurzbz').component.updateDefinition({
title: this.$p.t('gruppenmanagement', 'gruppe')
});
cm.getColumnByField('bezeichnung').component.updateDefinition({
title: this.$p.t('ui', 'bezeichnung')
});
cm.getColumnByField('generiert').component.updateDefinition({
title: this.$p.t('gruppenmanagement', 'automatisch_generiert')
});
cm.getColumnByField('uid').component.updateDefinition({
title: this.$p.t('ui', 'student_uid')
});
//Interference with Filter if not commented out
/*
cm.getColumnByField('studiensemester_kurzbz').component.updateDefinition({
title: this.$p.t('lehre', 'studiensemester')
});*/
}
}
],
}
},
methods: {
actionDeleteGroup(gruppe_kurzbz) {
this.$fhcAlert
.confirmDelete()
.then(result => result
? gruppe_kurzbz
: Promise.reject({handled: true}))
.then(this.deleteGroup)
.catch(this.$fhcAlert.handleSystemError);
},
deleteGroup(gruppe_kurzbz) {
const group_id = {
id: this.student.uid,
gruppe_kurzbz: gruppe_kurzbz
};
return this.$api
.call(ApiStvGroups.deleteGroup(group_id))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
}).catch(this.$fhcAlert.handleSystemError)
.finally(() => {
window.scrollTo(0, 0);
this.reload();
});
},
reload() {
this.$refs.table.reloadTable();
},
},
watch: {
currentSemester(newVal) {
if (newVal) {
this.$refs.table.tabulator.clearFilter(); // Clear old filters
this.$refs.table.tabulator.setFilter((data) => {
return (
data.uid === this.student.uid &&
(
data.studiensemester_kurzbz === newVal ||
data.studiensemester_kurzbz === null
)
);
});
}
},
student() {
this.$refs.table.reloadTable();
}
},
template: `
<div class="stv-details-gruppen h-100 pb-3">
<h5>{{$p.t('stv', 'tab_groups')}}</h5>
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
reload
:reload-btn-infotext="this.$p.t('table', 'reload')"
>
</core-filter-cmpt>
</div>
`
}
@@ -0,0 +1,58 @@
import { splitMailsHelper } from "../../../../helpers/EmailHelpers.js"
export default {
name: "Kontaktieren",
computed: {
internMails() {
if (this.modelValue.mail_intern)
{
return [this.modelValue.mail_intern];
}
return this.modelValue.map(e => e.mail_intern);
},
privateMails()
{
if (this.modelValue.mail_privat)
{
return [this.modelValue.mail_privat];
}
return this.modelValue.map(e => e.mail_privat);
},
},
props: {
modelValue: Object
},
methods: {
internMail(event) {
if (this.internMails.length)
{
splitMailsHelper(this.privateMails, event, null, this.$fhcAlert, this.$p)
}
},
privateMail(event) {
if (this.privateMails.length)
{
splitMailsHelper(this.privateMails, event, null, this.$fhcAlert, this.$p)
}
}
},
template: `
<div>
<div id="elementID"></div>
<div class="row">
<div class="col-lg-2">
<button class="btn btn-primary mb-2" @click="internMail($event)" :title="$p.t('stv', 'bccEMail')">
<i class="fa-solid fa-mail"></i> {{$p.t('stv', 'internEMail')}}
</button>
</div>
</div>
<div class="row">
<div class="col-lg-2">
<button class="btn btn-primary mb-2" @click="privateMail($event)" :title="$p.t('stv', 'bccEMail')">
<i class="fa-solid fa-mail"></i> {{$p.t('stv', 'privateEMail')}}
</button>
</div>
</div>
</div>`
};
@@ -1,5 +1,5 @@
import CoreMessages from "../../../Messages/Messages.js";
import ApiMessages from "../../../../api/factory/messages/messages.js";
//import ApiMessages from "../../../../api/factory/messages/messages.js";
export default {
name: "TabMessages",
@@ -11,29 +11,50 @@ export default {
},
data(){
return {
endpoint: ApiMessages
//endpoint: ApiMessages
};
},
computed: {
prestudent_ids() {
if (this.modelValue.prestudent_id)
{
return [this.modelValue.prestudent_id];
}
return this.modelValue.map(e => e.prestudent_id);
},
person_ids() {
if (this.modelValue.person_id)
{
return [this.modelValue.person_id];
}
return this.modelValue.map(e => e.person_id);
},
},
template: `
<div class="stv-details-messages h-100 pb-3 overflow-hidden">
<template v-if="modelValue.prestudent_id">
<core-messages
ref="formc"
:endpoint="endpoint"
type-id="prestudent_id"
:id="modelValue.prestudent_id"
messageLayout="twoColumnsTableLeft"
open-mode="modal"
show-table
>
</core-messages>
</template>
<template v-else>
<h3><strong>No valid prestudent_id!</strong></h3>
<p>{{modelValue.anmerkungen}}</p>
</template>
<template v-if="prestudent_ids">
<core-messages
ref="formc"
type-id="prestudent_id"
:id="prestudent_ids"
messageLayout="twoColumnsTableLeft"
open-mode="modal"
show-table
>
</core-messages>
</template>
<template v-else >
<core-messages
ref="formc"
type-id="person_id"
:id="person_ids"
messageLayout="twoColumnsTableLeft"
open-mode="modal"
show-table
>
</core-messages>
</template>
</div>
`
};
@@ -71,10 +71,10 @@ export default {
frozen: true
},
],
layout: 'fitColumns',
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: 200,
persistenceID: 'core-mobility-purpose'
persistenceID: 'core-mobility-purpose-2025112401'
},
tabulatorEvents: [
{
@@ -169,6 +169,11 @@ export default {
{
this.localData.push(newEntry);
// reload tabulator mit tabulator method
if (this.$refs.table?.tabulator) {
this.$refs.table.tabulator.replaceData(this.localData);
}
this.$emit('setMobilityPurposeToNewMobility', {
zweck_code: this.formData.zweck_code,
});
@@ -198,7 +203,6 @@ export default {
<br>
<div class="override_filtercmpt_actions_style">
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
@@ -69,10 +69,10 @@ export default {
frozen: true
},
],
layout: 'fitColumns',
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: 200,
persistenceID: 'core-mobility-support'
persistenceID: 'core-mobility-support-2025112401'
},
tabulatorEvents: [
{
@@ -157,6 +157,11 @@ export default {
{
this.localData.push(newEntry);
// reload tabulator mit tabulator method
if (this.$refs.table?.tabulator) {
this.$refs.table.tabulator.replaceData(this.localData);
}
this.$emit('setMobilitySupportToNewMobility', {
aufenthaltfoerderung_code: this.formData.aufenthaltfoerderung_code,
});
@@ -109,12 +109,12 @@ export default {
frozen: true
},
],
layout: 'fitDataFill',
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: 'auto',
minHeight: 200,
index: 'bisio_id',
persistenceID: 'stv-details-table_mobiliy'
persistenceID: 'stv-details-table_mobiliy-2025112401'
},
tabulatorEvents: [
{
@@ -7,10 +7,14 @@ export default {
},
props: {
modelValue: Object,
config: {
type: Object,
default: {}
}
},
template: `
<div class="stv-details-multistatus h-100">
<tbl-multi-status :model-value="modelValue"></tbl-multi-status>
<tbl-multi-status :model-value="modelValue" :config="config"></tbl-multi-status>
</div>
`
}
@@ -217,9 +217,13 @@ export default {
},
columns,
height: '100%',
layout: 'fitDataStretchFrozen',
selectable: 1,
selectableRangeMode: 'click',
persistenceID: 'stv-details-noten-zeugnis'
persistenceID: 'stv-details-noten-zeugnis-2025112401',
persistence:{
columns: ["width", "visible", "frozen"]
}
};
}
},
@@ -287,4 +291,4 @@ export default {
<zeugnis-documents :data="grade" :list="config.documentslist"></zeugnis-documents>
</Teleport>
</div>`
};
};
@@ -31,6 +31,7 @@ export default {
show-tiny-mce
:visibleColumns="['titel','text','verfasser','bearbeiter','dokumente']"
@reload="$emit('update:suffix')"
tabulator-persistence-id="stv-notiz-2026011301"
>
</core-notiz>
@@ -30,6 +30,22 @@ export default {
from: 'hasAdminPermission',
default: false
},
hasZGVBakkPermission: {
from: 'hasZGVBakkPermission',
default: []
},
hasZGVMasterPermission: {
from: 'hasZGVMasterPermission',
default: []
},
hasZGVDoctorPermission: {
from: 'hasZGVDoctorPermission',
default: []
},
hasBismeldenPermission: {
from: 'hasBismeldenPermission',
default: false
},
currentSemester: {
from: 'currentSemester',
required: true
@@ -73,6 +89,15 @@ export default {
computed: {
deltaLength() {
return Object.keys(this.deltaArray).length;
},
disableZgvBakk: function() {
return !this.hasZGVBakkPermission.includes(this.modelValue.studiengang_kz.toString());
},
disableZgvMaster: function() {
return !this.hasZGVMasterPermission.includes(this.modelValue.studiengang_kz.toString());
},
disableZgvDoctor: function() {
return !this.hasZGVDoctorPermission.includes(this.modelValue.studiengang_kz.toString());
}
},
watch: {
@@ -106,7 +131,7 @@ export default {
},
methods: {
loadPrestudent() {
async loadPrestudent() {
return this.$api
.call(ApiStvPrestudent.get(this.modelValue.prestudent_id, this.currentSemester))
.then(result => result.data)
@@ -154,8 +179,8 @@ export default {
)
},
},
created() {
this.loadPrestudent();
async created() {
await this.loadPrestudent();
this.$api
.call(ApiStvPrestudent.getBezeichnungZGV())
.then(result => result.data)
@@ -273,6 +298,7 @@ export default {
dropdown
name="zgv_code"
@complete="filterZgvs"
:disabled="disableZgvBakk"
>
<template #option="slotProps">
<div
@@ -336,6 +362,7 @@ export default {
dropdown
name="zgvmas_code"
@complete="filterMasterZgvs"
:disabled="disableZgvMaster"
>
<template #option="slotProps">
<div
@@ -400,6 +427,7 @@ export default {
dropdown
name="zgvdoktor_code"
@complete="filterDoktorZgvs"
:disabled="disableZgvDoctor"
>
<template #option="slotProps">
<div
@@ -601,6 +629,7 @@ export default {
type="checkbox"
v-model="data.bismelden"
name="bismelden"
:disabled="!hasBismeldenPermission"
>
</form-input>
</div>
@@ -51,7 +51,8 @@ export default{
}
},
props: {
modelValue: Object
modelValue: Object,
config: Object,
},
data() {
return {
@@ -175,15 +176,18 @@ export default{
const data = cell.getData();
let button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-forward"></i>';
button.title = this.$p.t('ui', 'btn_statusVorruecken');
button.addEventListener('click', () =>
this.actionAdvanceStatus(data.status_kurzbz, data.studiensemester_kurzbz, data.ausbildungssemester)
);
if (!['Student', 'Diplomand', 'Unterbrecher'].includes(data.status_kurzbz))
button.disabled = true;
container.append(button);
if (this.config?.showStatusVorruecken !== false)
{
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-forward"></i>';
button.title = this.$p.t('ui', 'btn_statusVorruecken');
button.addEventListener('click', () =>
this.actionAdvanceStatus(data.status_kurzbz, data.studiensemester_kurzbz, data.ausbildungssemester)
);
if (!['Student', 'Diplomand', 'Unterbrecher'].includes(data.status_kurzbz))
button.disabled = true;
container.append(button);
}
button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
@@ -203,8 +207,8 @@ export default{
button.addEventListener('click', () =>
this.actionEditStatus(data.status_kurzbz, data.studiensemester_kurzbz, data.ausbildungssemester)
);
if (this.dataMeldestichtag && this.dataMeldestichtag > data.datum && !this.hasPermissionToSkipStatusCheck)
button.disabled = true;
/* if (this.dataMeldestichtag && this.dataMeldestichtag > data.datum && !this.hasPermissionToSkipStatusCheck)
button.disabled = true;*/
container.append(button);
button = document.createElement('button');
@@ -227,15 +231,15 @@ export default{
const rowData = row.getData();
if (this.dataMeldestichtag && this.dataMeldestichtag > rowData.datum)
{
row.getElement().classList.add('disabled');
row.getElement().classList.add('text-black','text-opacity-50','fst-italic');
}
},
layout: 'fitDataFill',
layout: 'fitDataStretchFrozen',
layoutColumnsOnNewData: false,
height: 'auto',
selectable: false,
index: 'statusId',
persistenceID: 'stv-multistatus'
persistenceID: 'stv-multistatus-2025112401'
},
tabulatorEvents: [
{
@@ -401,7 +405,7 @@ export default{
this.$api
.call(ApiStvPrestudent.getLastBismeldestichtag())
.then(result => {
this.dataMeldestichtag = result.data[0].meldestichtag;
this.dataMeldestichtag = result.data[0]?.meldestichtag;
if (this.$refs.table && this.$refs.table.tableBuilt)
this.$refs.table.tabulator.redraw(true);
})
@@ -0,0 +1,24 @@
import Projektarbeit from './Projektarbeit/Projektarbeit.js';
export default {
name: "TabProjektarbeit",
components: {
Projektarbeit
},
provide() {
return {
config: this.config
};
},
props: {
modelValue: Object,
config: Object
},
data(){
return {}
},
template: `
<div class="stv-details-projektarbeit h-100 d-flex flex-column">
<projektarbeit ref="projektarbeit" :student="modelValue"></projektarbeit>
</div>`
};
@@ -0,0 +1,463 @@
import FormForm from '../../../../Form/Form.js';
import FormInput from '../../../../Form/Input.js';
import PvAutoComplete from "../../../../../../../index.ci.php/public/js/components/primevue/autocomplete/autocomplete.esm.min.js";
import ApiStvProjektarbeit from '../../../../../api/factory/stv/projektarbeit.js';
export default {
components: {
FormForm,
FormInput,
PvAutoComplete
},
emits: ['projekttypChanged'],
inject: {
defaultSemester: {
from: 'defaultSemester'
}
},
computed: {
// prepared Lehreinheiten (with compound Bezeichnung)
arrLes() {
let lehreinheiten = [];
if (this.formData.lehrveranstaltung_id) {
let lv = this.arrLvs.find(lv => {return lv.lehrveranstaltung_id == this.formData.lehrveranstaltung_id});
if (lv) lehreinheiten = lv.lehreinheiten
}
for (let le of lehreinheiten)
{
let bezeichnung = le.lehrfach_kurzbz + '-' + le.lehrform_kurzbz + ' ' + le.lehrfach_bezeichnung + ' ';
for (let grp of le.lehreinheitgruppen)
{
bezeichnung += grp.gruppe_kurzbz ? grp.gruppe_kurzbz : '' + grp.semester ?? '' + grp.verband ?? '' + grp.gruppe ?? '';
}
bezeichnung += ' (' + le.lektoren.join(' ') + ') ID: ' + le.lehreinheit_id;
le.bezeichnung = bezeichnung;
}
return lehreinheiten;
},
firmenverwaltungLink(){
return FHC_JS_DATA_STORAGE_OBJECT.app_root + 'vilesci/stammdaten/firma_frameset.html';
}
},
props: {
statusNew: Boolean,
student: Object,
projektarbeit: Object
},
data() {
return {
formData: {
projektarbeit_id: null,
titel: null,
titel_english: null,
themenbereich: null,
projekttyp_kurzbz: null,
firma: null,
lehrveranstaltung_id: null,
lehreinheit_id: null,
beginn: null,
ende: null,
freigegeben: true,
gesperrtbis: null,
note: null,
final: true,
anmerkung: null
},
studiensemester: null,
arrTypen: [],
arrFirmen: [],
arrLvs: [],
arrNoten: [],
arrStudiensemester: [],
additional_lehrveranstaltung_id: null,
filteredFirmen: [],
abortController: {
firma: null
}
}
},
watch: {
'formData.projekttyp_kurzbz'(newValue, oldValue) {
this.$emit('projekttypChanged', newValue);
}
},
methods: {
resetForm() {
this.formData.projektarbeit_id = null;
this.formData.titel = null;
this.formData.titel_english = null;
this.formData.themenbereich = null;
this.formData.projekttyp_kurzbz = null;
this.formData.firma = null;
this.formData.lehrveranstaltung_id = null;
this.formData.lehreinheit_id = null;
this.formData.beginn = null;
this.formData.ende = null;
this.formData.freigegeben = true;
this.formData.gesperrtbis = null;
this.formData.note = null;
this.formData.final = true;
this.formData.anmerkung = null;
this.$refs.formDetails.clearValidation();
},
getFormData(newProjektarbeit, studiensemester_kurzbz, additional_lehrveranstaltung_id) {
this.additional_lehrveranstaltung_id = additional_lehrveranstaltung_id;
this.studiensemester = studiensemester_kurzbz || this.defaultSemester;
this.newProjektarbeit = newProjektarbeit;
this.$api
.call(ApiStvProjektarbeit.getTypenProjektarbeit())
.then(result => {
this.arrTypen = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.getLehrveranstaltungen();
this.$api
.call(ApiStvProjektarbeit.getNoten())
.then(result => {
this.arrNoten = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api
.call(ApiStvProjektarbeit.getStudiensemester())
.then(result => {
this.arrStudiensemester = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
if (this.newProjektarbeit) this.resetForm();
},
loadProjektarbeit(projektarbeit_id) {
if (this?.formData?.projektarbeit_id == projektarbeit_id) return;
this.resetForm();
return this.$api
.call(ApiStvProjektarbeit.loadProjektarbeit(projektarbeit_id))
.then(result => {
this.formData = result.data;
if (this.formData.firma_id) this.formData.firma = {firma_id: this.formData.firma_id, name: this.formData.firma_name};
return result;
})
.catch(this.$fhcAlert.handleSystemError)
},
addNewProjektarbeit() {
let dataToSend = {
uid: this.student.uid,
formData: this.getPreparedFormData()
};
return this.$refs.formDetails
.call(ApiStvProjektarbeit.addNewProjektarbeit(dataToSend));
},
updateProjektarbeit() {
let dataToSend = {
projektarbeit_id: this.formData.projektarbeit_id,
formData: this.getPreparedFormData()
};
return this.$refs.formDetails
.call(ApiStvProjektarbeit.updateProjektarbeit(dataToSend));
},
searchFirma(event) {
if (this.abortController.firma) {
this.abortController.firma.abort();
}
this.abortController.firma = new AbortController();
return this.$api
.call(ApiStvProjektarbeit.getFirmen(event.query))
.then(result => {
this.filteredFirmen = result.data;
});
},
getLehrveranstaltungen() {
return this.$api
.call(ApiStvProjektarbeit.getLehrveranstaltungen(
this.student.uid,
this.newProjektarbeit ? this.student.studiengang_kz : null,
this.studiensemester,
this.additional_lehrveranstaltung_id
))
.then(result => {
this.arrLvs = result.data
}
)
.catch(this.$fhcAlert.handleSystemError);
},
lvChanged(event) {
this.formData.lehreinheit_id = null;
},
studiensemesterChanged() {
this.formData.lehreinheit_id = null;
this.getLehrveranstaltungen();
},
gesperrtBisChanged(newSperrdatum) {
this.formData.freigegeben = newSperrdatum == null || newSperrdatum == '';
},
// enrich and modify data before sending
getPreparedFormData() {
let preparedFormData = JSON.parse(JSON.stringify(this.formData)); // deep copy
// set firma Id
if (preparedFormData.firma)
preparedFormData.firma_id = preparedFormData.firma.firma_id;
else
preparedFormData.firma_id = null;
// delete "helper" fields
if (preparedFormData.projektarbeit_id == null) delete(preparedFormData.projektarbeit_id);
delete(preparedFormData.firma);
delete(preparedFormData.firma_name);
delete(preparedFormData.lehrveranstaltung_id);
return preparedFormData;
}
},
template: `
<form-form v-if="!this.student.length" ref="formDetails" @submit.prevent>
<legend>Details</legend>
<p v-if="statusNew">[{{$p.t('ui', 'neu')}}]</p>
<div class="row mb-3">
<form-input
container-class="stv-details-projektarbeit-titel"
type="text"
name="titel"
:label="$p.t('projektarbeit', 'titel')"
v-model="formData.titel">
</form-input>
</div>
<div class="row mb-3">
<form-input
container-class="stv-details-projektarbeit-titel_english"
type="text"
name="titel_english"
:label="$p.t('projektarbeit', 'titelEnglisch')"
v-model="formData.titel_english"
>
</form-input>
</div>
<div class="row mb-3">
<form-input
container-class="stv-details-projektarbeit-themenbereich"
type="text"
name="themenbereich"
:label="$p.t('projektarbeit', 'themenbereich')"
v-model="formData.themenbereich"
>
</form-input>
</div>
<div class="row mb-3">
<form-input
container-class="stv-details-projektarbeit-typ"
:label="$p.t('projektarbeit', 'typ')"
type="select"
v-model="formData.projekttyp_kurzbz"
name="projekttyp_kurzbz"
>
<option
v-for="typ in arrTypen"
:key="typ.projekttyp_kurzbz"
:value="typ.projekttyp_kurzbz"
>
{{typ.bezeichnung}}
</option>
</form-input>
</div>
<div class="row mb-3">
<div class="col-10">
<form-input
container-class="stv-details-projektarbeit-firma"
:label="$p.t('projektarbeit', 'firma')"
type="autocomplete"
optionLabel="name"
v-model="formData.firma"
name="firma"
:suggestions="filteredFirmen"
@complete="searchFirma"
:min-length="3"
>
</form-input>
</div>
<div class="col-2 align-content-center">
<a :href="firmenverwaltungLink" target="_blank">
{{ $p.t('projektarbeit', 'zurFirmenverwaltung') }}
</a>
</div>
</div>
<div class="row mb-3">
<form-input
container-class="stv-details-projektarbeit-lv col-10"
:label="$p.t('projektarbeit', 'lehrveranstaltung')"
type="select"
v-model="formData.lehrveranstaltung_id"
name="lehrveranstaltung_id"
@change="lvChanged($event)"
>
<option :value="null"> -- {{$p.t('fehlermonitoring', 'keineAuswahl')}} -- </option>
<option
v-for="lv in arrLvs"
:key="lv.lehrveranstaltung_id"
:value="lv.lehrveranstaltung_id"
>
{{lv.bezeichnung + (lv.orgform_kurzbz ? ' ' + lv.orgform_kurzbz : '') + ' (' + lv.semester + ' Sem) ID: ' + lv.lehrveranstaltung_id}}
</option>
</form-input>
<form-input
container-class="col-2"
:label="$p.t('lehre', 'studiensemester')"
type="select"
v-model="studiensemester"
name="studiensemester"
@change="studiensemesterChanged"
>
<option :value="null"> -- {{$p.t('fehlermonitoring', 'keineAuswahl')}} -- </option>
<option
v-for="sem in arrStudiensemester"
:key="sem.studiensemester_kurzbz"
:value="sem.studiensemester_kurzbz"
>
{{sem.studiensemester_kurzbz}}
</option>
</form-input>
</div>
<div class="row mb-3">
<form-input
container-class="stv-details-projektarbeit-le"
:label="$p.t('projektarbeit', 'lvTeil')"
type="select"
v-model="formData.lehreinheit_id"
name="lehreinheit_id"
>
<option :value="null"> -- {{$p.t('fehlermonitoring', 'keineAuswahl')}} -- </option>
<option
v-for="le in arrLes"
:key="le.lehreinheit_id"
:value="le.lehreinheit_id"
>
{{le.bezeichnung}}
</option>
</form-input>
</div>
<div class="row mb-3">
<form-input
container-class="col-6 stv-details-projektarbeit-beginn"
:label="$p.t('projektarbeit', 'beginn')"
type="DatePicker"
v-model="formData.beginn"
auto-apply
:enable-time-picker="false"
text-input
locale="de"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
name="beginn"
>
</form-input>
<form-input
container-class="col-6 stv-details-projektarbeit-ende"
:label="$p.t('projektarbeit', 'ende')"
type="DatePicker"
v-model="formData.ende"
auto-apply
:enable-time-picker="false"
text-input
locale="de"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
name="ende"
>
</form-input>
</div>
<div class="row mb-3 align-items-center">
<form-input
container-class="col-8 stv-details-projektarbeit-gesperrtbis"
:label="$p.t('projektarbeit', 'gesperrtBis')"
type="DatePicker"
v-model="formData.gesperrtbis"
auto-apply
:enable-time-picker="false"
text-input
locale="de"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
name="gesperrtbis"
@update:model-value="gesperrtBisChanged"
>
</form-input>
<div class="col-4">
<form-input
container-class="form-check stv-details-projektarbeit-freigegeben"
type="checkbox"
name="freigegeben"
:label="$p.t('projektarbeit','freigegeben')"
v-model="formData.freigegeben"
>
</form-input>
</div>
</div>
<div class="row mb-3 align-items-center">
<form-input
container-class="col-8 stv-details-projektarbeit-note"
:label="$p.t('projektarbeit', 'note')"
type="select"
v-model="formData.note"
name="note"
>
<option :value="null"> -- {{$p.t('fehlermonitoring', 'keineAuswahl')}} -- </option>
<option
v-for="note in arrNoten"
:key="note.note"
:value="note.note"
>
{{note.bezeichnung}}
</option>
</form-input>
<div class="col-4">
<form-input
container-class="form-check stv-details-projektarbeit-final"
type="checkbox"
name="final"
label="final"
v-model="formData.final"
>
</form-input>
</div>
</div>
<div class="row mb-3">
<form-input
container-class="col-12 stv-details-abschlusspruefung-anmerkung"
:label="$p.t('projektarbeit', 'anmerkung')"
type="textarea"
v-model="formData.anmerkung"
name="anmerkung"
:rows= 3
>
</form-input>
</div>
</form-form>`
}

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