Merge branch 'master' into feature-68296/Vue_Router_via_Extensions_erweitern_ma0080

This commit is contained in:
Harald Bamberger
2026-02-26 16:05:32 +01:00
283 changed files with 27538 additions and 4538 deletions
+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 -1
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,6 +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;
FhcApps.router.makeExtendable(router);
FhcApps.makeExtendable(app);
+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,402 @@ export const AbgabeStudentDetail = {
for (let i = 0; i < termin.file.length; i++) {
formData.append('file', termin.file[i]);
}
this.$fhcApi.factory.lehre.postStudentProjektarbeitZwischenabgabe(formData)
this.loading = true
this.$api.call(ApiAbgabe.postStudentProjektarbeitZwischenabgabe(formData))
.then(res => {
this.handleUploadRes(res)
})
this.handleUploadRes(res, termin)
}).finally(()=> {
this.loading = false
})
}
},
handleUploadRes(res) {
handleUploadRes(res, termin) {
if(res.meta.status == "success") {
this.$fhcAlert.alertSuccess('File erfolgreich hochgeladen')
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4fileUploadSuccessv3')))
// update 'abgabedatum' for successful upload -> shows the pdf icon and date once set
termin.abgabedatum = new Date().toISOString().split('T')[0];
if(res?.data?.signatur !== undefined) {
termin.signatur = res.data.signatur
}
} else {
this.$fhcAlert.alertError('File upload error')
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4fileUploadErrorv3')))
}
if(res.meta.signaturInfo) {
this.$fhcAlert.alertInfo(res.meta.signaturInfo)
}
},
dateDiffInDays(datum, today){
const oneDayMs = 1000 * 60 * 60 * 24
return Math.round((new Date(datum) - new Date(today)) / oneDayMs)
},
getDateStyle(termin, mode) {
const datum = new Date(termin.datum)
const abgabedatum = new Date(termin.abgabedatum)
// todo: rework styling but keep the color pattern logic
// https://wiki.fhcomplete.info/doku.php?id=cis:abgabetool_fuer_studierende
let color = 'white'
let fontColor = 'black'
let icon = '';
if (termin.abgabedatum === null) {
if(datum < today) {
color = 'red'
fontColor = 'white'
icon = 'fa-triangle-exclamation'
} else if (datum > today && this.dateDiffInDays(datum, today) <= 12) {
color = 'yellow'
icon = 'fa-circle-exclamation'
}
} else if(abgabedatum > datum) {
color = 'pink' // aka "hellrot"
fontColor = 'white'
icon = 'fa-circle-question'
} else {
color = 'green'
icon = 'fa-square-check'
}
//return `font-color: ${fontColor} ; background-color: ${color}; border-radius: 50%;`
if( typeof mode !== 'undefined' || mode === 'icon') {
return icon;
} else {
return 'abgabe-zieldatum-border-' + color;
}
},
openBeurteilungLink(link) {
window.open(link, '_blank')
},
getOptionLabel(option) {
return option.sprache
}
},
getTerminNoteBezeichnung(termin) {
const noteOpt = this.notenOptions.find(opt => opt.note == termin.note)
return noteOpt ? noteOpt.bezeichnung : ''
},
},
watch: {
projektarbeit(newVal) {
// default select german if projektarbeit sprache was null
this.form.sprache = newVal.sprache ? this.allActiveLanguages.find(lang => lang.sprache == newVal.sprache) : this.allActiveLanguages.find(lang => lang.sprache == 'German')
this.form.abstract = newVal.abstract
this.form.abstract_en = newVal.abstract_en
this.form.schlagwoerter = newVal.schlagwoerter
this.form.schlagwoerter_en = newVal.schlagwoerter_en
this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter
this.form.seitenanzahl = newVal.seitenanzahl
this.form.abstract = newVal.abstract ?? ''
this.form.abstract_en = newVal.abstract_en ?? ''
this.form.schlagwoerter = newVal.schlagwoerter ?? ''
this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? ''
this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? ''
this.form.seitenanzahl = newVal.seitenanzahl ?? 1
}
},
computed: {
getMoodleLink() {
return this.moodle_link + this.projektarbeit.studiengang_kz
},
getMessagePtStyle() {
// adjust outer spacing and internal padding to appear similar to doenload button in size
return {
root: {
style: {
margin: '0px'
}
},
wrapper: {
style: {
padding: '6px'
}
}
}
},
getEid() {
return this.$p.t('abgabetool/c4eidesstattlicheErklaerung')
return this.$capitalize(this.$p.t('abgabetool/c4eidesstattlicheErklaerung'))
},
allowedToSaveZusatzdaten() {
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter_en.length > 0 && this.form.abstract.length > 0 && this.form.abstract_en.length > 0 && this.form.seitenanzahl > 0
},
getAllowedToSendEndupload() {
return this.eidAkzeptiert && this.allowedToSaveZusatzdaten
},
qualityGateTerminAvailable() {
let qgatefound = false
this.projektarbeit?.abgabetermine.forEach(abgabe => {
if(abgabe.paabgabetyp_kurzbz == 'qualgate1'
|| abgabe.paabgabetyp_kurzbz == 'qualgate2') {
qgatefound = true
}
})
return qgatefound
},
getTooltipVerspaetet() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')),
class: "custom-tooltip"
}
},
getTooltipVerpasst() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')),
class: "custom-tooltip"
}
},
getTooltipAbzugeben() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')),
class: "custom-tooltip"
}
},
getTooltipStandard() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')),
class: "custom-tooltip"
}
},
getTooltipAbgegeben() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')),
class: "custom-tooltip"
}
},
getTooltipFixtermin() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')),
class: "custom-tooltip"
}
},
getTooltipAbgabeDetected() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')),
class: "custom-tooltip"
}
},
getTooltipNotAllowedToUpload() {
if(this.isViewMode) {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')),
class: "custom-tooltip"
}
} else {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegular')),
class: "custom-tooltip"
}
}
},
getTooltipBeurteilungerforderlich() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')),
class: "custom-tooltip"
}
},
getTooltipBestanden() {
return {
value: this.$p.t('abgabetool/c4tooltipBestanden'),
class: "custom-tooltip"
}
},
getTooltipNichtBestanden() {
return {
value: this.$p.t('abgabetool/c4tooltipNichtBestanden'),
class: "custom-tooltip"
}
},
getEnduploadErlaubt() {
return !this.eidAkzeptiert
}
},
created() {
},
mounted() {
},
template: `
<FhcOverlay :active="loading"></FhcOverlay>
<div v-if="projektarbeit">
<h5>{{$p.t('abgabetool/c4abgabeStudentenbereich')}}</h5>
<h5>{{$capitalize( $p.t('abgabetool/c4abgabeStudentenbereich') )}}</h5>
<div class="row">
<p> {{projektarbeit?.betreuer}}</p>
<p> {{projektarbeit?.titel}}</p>
</div>
<div id="uploadWrapper">
<div class="row" style="margin-bottom: 12px;">
<div class="col-1 fw-bold text-center">{{$p.t('abgabetool/c4fixtermin')}}</div>
<div class="col-2 fw-bold">{{$p.t('abgabetool/c4zieldatum')}}</div>
<div class="col-2 fw-bold">{{$p.t('abgabetool/c4abgabetyp')}}</div>
<div class="col-3 fw-bold">{{$p.t('abgabetool/c4abgabekurzbz')}}</div>
<div class="col-1 fw-bold text-center">{{$p.t('abgabetool/c4abgabedatum')}}</div>
<div class="col-3 fw-bold">
{{$p.t('abgabetool/c4fileupload')}}
</div>
<div class="col-8">
<p> {{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}</p>
<p> {{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}</p>
<p> {{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}</p>
</div>
<div class="row" v-for="termin in projektarbeit.abgabetermine">
<div class="col-1 d-flex justify-content-center align-items-start">
<i v-if="termin.fixtermin" class="fa-solid fa-2x fa-circle-check fhc-bullet-red"></i>
<i v-else="" class="fa-solid fa-2x fa-circle-xmark fhc-bullet-green"></i>
<!--
<p class="fhc-bullet" :class="{ 'fhc-bullet-red': termin.fixtermin, 'fhc-bullet-green': !termin.fixtermin }"></p>
-->
</div>
<div class="col-2 d-flex justify-content-start align-items-start">
<div class="position-relative" :class="getDateStyle(termin)">
<VueDatePicker
v-model="termin.datum"
:clearable="false"
:disabled="true"
:enable-time-picker="false"
:format="formatDate"
:text-input="true"
auto-apply>
</VueDatePicker>
<i class="position-absolute abgabe-zieldatum-overlay fa-solid fa-2x" :class="getDateStyle(termin, 'icon')"></i>
</div>
</div>
<div class="col-2 d-flex justify-content-start align-items-start">{{ termin.bezeichnung }}</div>
<div class="col-3 d-flex justify-content-start align-items-start">
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="3" cols="45" :disabled="true"></Textarea>
</div>
<div class="col-1 d-flex flex-column justify-content-start align-items-center">
{{ termin.abgabedatum?.split("-").reverse().join(".") }}
<a v-if="termin?.abgabedatum" @click="downloadAbgabe(termin)" style="margin-left:4px; cursor: pointer;">
<i class="fa-solid fa-3x fa-file-pdf"></i>
</a>
</div>
<div class="col-3" v-if="!viewMode">
<div class="row">
<div class="col-8">
<Upload v-if="termin && termin.allowedToUpload" accept=".pdf" v-model="termin.file"></Upload>
</div>
<div class="col-4">
<button class="btn btn-primary border-0" @click="upload(termin)" :disabled="!termin.allowedToUpload">
Upload
<i style="margin-left: 8px" class="fa-solid fa-upload"></i>
</button>
</div>
</div>
</div>
<div class="col-4">
<p>{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }}
<a :href="getMoodleLink" target="_blank">Moodle</a>
</p>
</div>
</div>
<Accordion :multiple="true">
<template v-for="termin in this.projektarbeit?.abgabetermine" :key="termin.paabgabe_id">
<AccordionTab :headerClass="termin.dateStyle + '-header'">
<template #header>
<div class="d-flex flex-nowrap align-items-center w-100">
<div class="flex-shrink-0 d-flex align-items-center justify-content-center" style="width: 36px; height: 36px; margin-left: -68px;">
<i v-if="termin.dateStyle == 'verspaetet'" v-tooltip.right="getTooltipVerspaetet" class="fa-solid fa-triangle-exclamation"></i>
<i v-else-if="termin.dateStyle == 'verpasst'" v-tooltip.right="getTooltipVerpasst" class="fa-solid fa-calendar-xmark"></i>
<i v-else-if="termin.dateStyle == 'abzugeben'" v-tooltip.right="getTooltipAbzugeben" class="fa-solid fa-hourglass-half"></i>
<i v-else-if="termin.dateStyle == 'standard'" v-tooltip.right="getTooltipStandard" class="fa-solid fa-clock"></i>
<i v-else-if="termin.dateStyle == 'abgegeben'" v-tooltip.right="getTooltipAbgegeben" class="fa-solid fa-paperclip"></i>
<i v-else-if="termin.dateStyle == 'beurteilungerforderlich'" v-tooltip.right="getTooltipBeurteilungerforderlich" class="fa-solid fa-list-check"></i>
<i v-else-if="termin.dateStyle == 'bestanden'" v-tooltip.right="getTooltipBestanden" class="fa-solid fa-check"></i>
<i v-else-if="termin.dateStyle == 'nichtbestanden'" v-tooltip.right="getTooltipNichtBestanden" class="fa-solid fa-circle-exclamation"></i>
</div>
<div class="text-start px-2" style="min-width: 150px; max-width: 300px; margin-left: 40px">
<span>{{ termin ? $p.t('abgabetool/c4paatyp' + termin.paabgabetyp_kurzbz) : '' }}</span>
</div>
<div class="text-start px-2" style="min-width: 100px;">
<span>{{ formatDate(termin.datum) }}</span>
</div>
<div class="px-1">
<i v-if="termin?.fixtermin" v-tooltip.right="getTooltipFixtermin" class="fa-solid fa-lock"></i>
<i v-if="termin?.abgabedatum && isMobile" v-tooltip.right="getTooltipAbgabeDetected" class="fa-solid fa-file"></i>
</div>
<div v-if="termin?.abgabedatum && !isMobile" class="px-1">
<i v-tooltip.right="getTooltipAbgabeDetected" class="fa-solid fa-file"></i>
</div>
<div class="flex-grow-1 text-end pe-2">
<span class="fw-bold">{{getNoteBezeichnung(termin)}}</span>
</div>
</div>
</template>
<div v-if="isMobile" class="row mt-2 align-items-center">
<Inplace
closable
:closeButtonProps="{
style: {
position: 'relative',
bottom: '100px',
left: '80%',
zIndex: 1
}
}"
>
<template #display>{{ $capitalize($p.t('abgabetool/c4tapForTooltipInfo'))}}</template>
<template #content>
<div class="col-auto">
<div class="row">
<div class="col-12 col-md-3 fw-bold align-content-center">{{ $capitalize($p.t('abgabetool/c4abgabeZeitstatus')) }}</div>
<div class="col-12 col-md-9">{{$p.t('abgabetool/c4tooltip' + $capitalize(termin?.dateStyle) )}}</div>
</div>
<div class="row">
<div class="col-12 col-md-3 fw-bold align-content-center">{{ $capitalize($p.t('abgabetool/c4fixterminv4')) }}</div>
<div class="col-12 col-md-9">{{!termin?.fixtermin}}</div>
</div>
<div class="row">
<div class="col-12 col-md-3 fw-bold align-content-center">{{ $capitalize($p.t('abgabetool/c4fileUploaded')) }}</div>
<div class="col-12 col-md-9">{{termin?.abgabedatum !== null}}</div>
</div>
</div>
</template>
</Inplace>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 align-content-center">
<div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatumv2') )}}</div>
<div class="row fw-light" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4abgabeuntil2359') )}}</div>
</div>
<div class="col-12 col-md-9">
<VueDatePicker
v-model="termin.datum"
:clearable="false"
:disabled="true"
:enable-time-picker="false"
locale="de"
format="dd.MM.yyyy"
:text-input="true"
auto-apply>
</VueDatePicker>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabetyp') )}}</div>
<div class="col-12 col-md-9">
{{ termin ? $p.t('abgabetool/c4paatyp' + termin.paabgabetyp_kurzbz) : '' }}
</div>
</div>
<div class="row mt-2" v-if="termin.note">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4note') )}}</div>
<div class="col-12 col-md-9">
<div class="col-auto d-flex justify-content-start align-items-start">
{{ getTerminNoteBezeichnung(termin) }}
</div>
</div>
</div>
<div class="row mt-2" v-if="termin.paabgabetyp_kurzbz === 'qualgate1' || termin.paabgabetyp_kurzbz === 'qualgate2'">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4notizQualGatev2') )}}</div>
<div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.beurteilungsnotiz" rows="1" class="w-100" disabled></Textarea>
</div>
</div>
<div v-if="termin.kurzbz && termin.kurzbz.length > 0" class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbzv2') )}}</div>
<div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="1" class="w-100" :disabled="true"></Textarea>
</div>
</div>
<div class="row mt-2" v-if="termin.upload_allowed">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabedatum') )}}</div>
<div class="col-12 col-md-9">
<template v-if="termin?.abgabedatum">
<div class="row">
<div style="width:100px; align-content: center;">
<h6>{{ termin.abgabedatum?.split("-").reverse().join(".") }}</h6>
</div>
<div class="col-auto">
<button v-if="termin?.abgabedatum" @click="downloadAbgabe(termin)" class="btn btn-primary">
<a> {{$capitalize($p.t('abgabetool/c4downloadAbgabe') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button>
</div>
<template v-if="termin.paabgabetyp_kurzbz == 'end'">
<div v-if="termin?.signatur !== undefined && termin?.signatur !== null" class="col-auto">
<Message v-if="termin?.signatur == true" severity="success" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4signaturGefunden') }} </Message>
<Message v-else-if="termin?.signatur == false" severity="error" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4keineSignatur') }} </Message>
<Message v-else-if="termin?.signatur == 'error'" severity="warn" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4signaturServerError') }} </Message>
</div>
<!-- <div v-else class="col-auto">-->
<!-- <Message severity="info" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4noFileFound') }} </Message>-->
<!-- </div>-->
</template>
</div>
</template>
<template v-else>
{{ $capitalize( $p.t('abgabetool/c4nochNichtsAbgegeben') )}}
</template>
</div>
</div>
<div class="row mt-2" v-if="termin.upload_allowed">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4fileupload') )}}</div>
<div class="col-12 col-md-9">
<div class="row" v-if="termin?.allowedToUpload">
<div class="col-12 col-sm-6 mb-2">
<Upload
accept=".pdf"
v-model="termin.file"
></Upload>
</div>
<div class="col-12 col-sm-6">
<button
class="btn btn-primary border-0 w-100"
@click="upload(termin)"
>
{{$capitalize( $p.t('abgabetool/c4upload') )}}
<i class="fa-solid fa-upload"></i>
</button>
</div>
</div>
<div class="row" v-else-if="!termin?.allowedToUpload || isViewMode" v-tooltip.right="getTooltipNotAllowedToUpload">
<div class="col-12 col-sm-6 mb-2">
<Upload
disabled
accept=".pdf"
v-model="termin.file"
></Upload>
</div>
<div class="col-12 col-sm-6">
<button
class="btn btn-primary border-0 w-100"
@click="upload(termin)"
disabled
>
{{$capitalize( $p.t('abgabetool/c4upload') )}}
<i class="fa-solid fa-upload"></i>
</button>
</div>
</div>
</div>
</div>
</AccordionTab>
</template>
</Accordion>
<div v-if="projektarbeit?.abgabetermine.length == 0" style="display:flex; justify-content: center; align-content: center;">
<h5>{{ $capitalize( $p.t('abgabetool/c4keineAbgabetermineGefunden') )}}</h5>
</div>
</div>
<bs-modal ref="modalContainerEnduploadZusatzdaten" class="bootstrap-prompt" dialogClass="modal-lg">
<bs-modal
ref="modalContainerEnduploadZusatzdaten"
class="bootstrap-prompt"
dialogClass="bordered-modal modal-lg">
<template v-slot:title>
<div>
{{$p.t('abgabetool/c4enduploadZusatzdaten')}}
{{$capitalize( $p.t('abgabetool/c4enduploadZusatzdaten') )}}
</div>
<div class="row mb-3 align-items-start">
@@ -285,13 +559,13 @@ export const AbgabeStudentDetail = {
</div>
<div class="row mb-3 align-items-start">
<p class="ml-4 mr-4">Titel: {{ projektarbeit?.titel }}</p>
<p class="ml-4 mr-4">{{$capitalize( $p.t('abgabetool/c4titel') )}}: {{ projektarbeit?.titel }}</p>
</div>
</template>
<template v-slot:default>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4Sprache')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4Sprache') )}}</div>
<div class="row">
<Dropdown
:style="{'width': '100%'}"
@@ -312,35 +586,37 @@ export const AbgabeStudentDetail = {
<!-- -->
<!-- </div>-->
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4schlagwoerterGer')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterGer') )}}</div>
<div class="row">
<Textarea v-model="form.schlagwoerter"></Textarea>
<Textarea v-model="form.schlagwoerter" class="w-100"></Textarea>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4schlagwoerterEng')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterEng') )}}</div>
<div class="row">
<Textarea v-model="form.schlagwoerter_en"></Textarea>
<Textarea v-model="form.schlagwoerter_en" class="w-100"></Textarea>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4abstractGer')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4abstractGer') )}}</div>
<div class="row">
<Textarea v-model="form.abstract" rows="10"></Textarea>
<Textarea v-model="form.abstract" rows="10" maxlength="5000" class="w-100"></Textarea>
<p>{{ form.abstract?.length ? form.abstract.length : 0 }} / 5000 characters</p>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4abstractEng')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4abstractEng') )}}</div>
<div class="row">
<Textarea v-model="form.abstract_en" rows="10"></Textarea>
<Textarea v-model="form.abstract_en" rows="10" maxlength="5000" class="w-100"></Textarea>
<p>{{ form.abstract_en?.length ? form.abstract_en.length : 0 }} / 5000 characters</p>
</div>
</div>
<div class="row mb-3 align-items-start">
<div class="row">{{$p.t('abgabetool/c4seitenanzahl')}}</div>
<div class="row">{{$capitalize( $p.t('abgabetool/c4seitenanzahl') )}}</div>
<div class="row">
<InputNumber
v-model="form.seitenanzahl"
@@ -353,7 +629,7 @@ export const AbgabeStudentDetail = {
<div v-html="getEid"></div>
<div class="row">
<div class="col-9"></div>
<div class="col-2"><p>{{ $p.t('abgabetool/c4gelesenUndAkzeptiert') }}</p></div>
<div class="col-2"><p>{{$capitalize( $p.t('abgabetool/c4gelesenUndAkzeptiert') )}}</p></div>
<div class="col-1">
<Checkbox
@@ -368,7 +644,8 @@ export const AbgabeStudentDetail = {
</template>
<template v-slot:footer>
<button class="btn btn-primary" :disabled="getEnduploadErlaubt" @click="triggerEndupload">{{$p.t('ui/hochladen')}}</button>
<div v-show="!allowedToSaveZusatzdaten">{{ $p.t('abgabetool/c4zusatzdatenausfuellen') }}</div>
<button class="btn btn-primary" :disabled="!getAllowedToSendEndupload" @click="triggerEndupload">{{$capitalize( $p.t('ui/hochladen') )}}</button>
</template>
</bs-modal>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,13 +1,24 @@
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
import AbgabeDetail from "./AbgabeStudentDetail.js";
import VerticalSplit from "../../verticalsplit/verticalsplit.js";
import ApiAbgabe from '../../../api/factory/abgabe.js'
import BsModal from "../../Bootstrap/Modal.js";
import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass} from "./getDateStyleClass.js";
export const AbgabetoolStudent = {
name: "AbgabetoolStudent",
components: {
CoreFilterCmpt,
Accordion: primevue.accordion,
AccordionTab: primevue.accordiontab,
BsModal,
AbgabeDetail,
VerticalSplit
FhcOverlay
},
provide() {
return {
notenOptions: Vue.computed(() => this.notenOptions),
isViewMode: Vue.computed(() => this.isViewMode),
moodle_link: Vue.computed(() => this.moodle_link)
}
},
props: {
student_uid_prop: {
@@ -24,147 +35,168 @@ export const AbgabetoolStudent = {
},
data() {
return {
tabulatorUuid: Vue.ref(0),
domain: '',
student_uid: null,
activeTabIndex: [0],
abgabeTypeOptions: null,
phrasenPromise: null,
phrasenResolved: false,
loading: false,
notenOptions: null,
detail: null,
projektarbeiten: null,
selectedProjektarbeit: null,
tableBuiltResolve: null,
tableBuiltPromise: null,
abgabeTableOptions: {
minHeight: 250,
index: 'projektarbeit_id',
layout: 'fitColumns',
placeholder: this.$p.t('global/noDataAvailable'),
columns: [
{title: Vue.computed(() => this.$p.t('abgabetool/c4details')), field: 'details', formatter: this.detailFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4beurteilung')), field: 'beurteilung', formatter: this.beurteilungFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4sem')), field: 'sem', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4stg')), field: 'stg', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4kontakt')), field: 'mail', formatter: this.mailFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4betreuer')), field: 'betreuer', formatter: this.centeredTextFormatter,widthGrow: 2},
{title: Vue.computed(() => this.$p.t('abgabetool/c4projekttyp')), field: 'typ', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4titel')), field: 'titel', formatter: this.centeredTextFormatter, widthGrow: 8}
],
persistence: false,
},
abgabeTableEventHandlers: [{
event: "tableBuilt",
handler: async () => {
this.tableBuiltResolve()
}
},
{
event: "cellClick",
handler: async (e, cell) => {
if(cell.getColumn().getField() === "details") {
const val = cell.getValue()
if(val.mode === 'detailTermine') {
this.setDetailComponent(cell.getValue())
} else if (val.mode === 'beurteilungDownload') {
const pdfExportLink = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cis/private/pdfExport.php?xml=projektarbeitsbeurteilung.xml.php&xsl=Projektbeurteilung&betreuerart_kurzbz='+val.betreuerart_kurzbz+'&projektarbeit_id='+val.projektarbeit_id+'&person_id=' + val.betreuer_person_id
// const pdfExportLink2 = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cis/private/lehre/projektbeurteilungDocumentExport.php?betreuerart_kurzbz='+val.betreuerart_kurzbz+'&projektarbeit_id='+val.projektarbeit_id+'&person_id=' + val.betreuer_person_id
window.open(pdfExportLink, '_blank')
}
} else if (cell.getColumn().getField() === "beurteilung") {
const val = cell.getValue()
if(val != '-') window.open(val, '_blank')
}
e.stopPropagation()
}
}
]};
moodle_link: null
};
},
methods: {
checkQualityGatesStrict(termine) {
let qgate1Passed = false
let qgate2Passed = false
termine.forEach(t => {
const noteOption = this.notenOptions?.find(opt => opt.note == t.note)
if(noteOption && noteOption.positiv) {
if(t.paabgabetyp_kurzbz == 'qualgate1') {
qgate1Passed = true
} else if(t.paabgabetyp_kurzbz == 'qualgate2') {
qgate2Passed = true
}
}
})
return qgate1Passed && qgate2Passed
},
checkQualityGatesOptional(termine) {
const qgate1found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate1')
const qgate2found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate2')
let qgate1positiv = true
if(qgate1found) {
qgate1positiv = false
termine.forEach(t => {
const noteOption = this.notenOptions?.find(opt => opt.note == t.note)
if(noteOption && noteOption.positiv) {
if (t.paabgabetyp_kurzbz == 'qualgate1') {
qgate1positiv = true
}
}
})
}
let qgate2positiv = true
if(qgate2found) {
qgate2positiv = false
termine.forEach(t => {
const noteOption = this.notenOptions?.find(opt => opt.note == t.note)
if(noteOption && noteOption.positiv) {
if (t.paabgabetyp_kurzbz == 'qualgate2') {
qgate2positiv = true
}
}
})
}
return qgate1positiv && qgate2positiv
},
isPastDate(date) {
return new Date(date) < new Date(Date.now())
const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day');
const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna');
return nowInVienna > deadline;
},
setDetailComponent(details){
this.loading = true
this.loadAbgaben(details).then((res)=> {
const pa = this.projektarbeiten?.retval?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
const pa = this.projektarbeiten?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
pa.abgabetermine = res.data[0].retval
const paIsBenotet = pa.note !== null
pa.abgabetermine.forEach(termin => {
termin.file = []
termin.allowedToUpload = true
termin.allowedToUpload = false
// TODO: fixtermin logic?
if(termin.bezeichnung == 'Endupload' && this.isPastDate(termin.datum)) {
if(termin.paabgabetyp_kurzbz == 'end') {
// old assumed production logic when qgates are required
// termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesStrict(pa.abgabetermine)
// termin.allowedToUpload = false
} else {
const inTime = termin.fixtermin ? !this.isPastDate(termin.datum) : true
termin.allowedToUpload = inTime && this.checkQualityGatesOptional(pa.abgabetermine)
// development purposes
// termin.allowedToUpload = this.checkQualityGatesStrict(pa.abgabetermine)
// termin.allowedToUpload = true
} else if(termin.fixtermin) {
termin.allowedToUpload = !this.isPastDate(termin.datum)
} else {
// this could confuse people since we should dont show people this flag
termin.allowedToUpload = termin.upload_allowed
}
// blocks client upload button if projektarbeitet is already beurteilt und thus further abgaben on any termin should be blocked
if(paIsBenotet) termin.allowedToUpload = false
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
})
pa.betreuer = this.buildBetreuer(pa)
pa.student_uid = this.student_uid
this.selectedProjektarbeit = pa
this.$refs.modalContainerAbgabeDetail.show()
this.$refs.verticalsplit.showBoth()
})
}).finally(()=>{this.loading=false})
},
centeredTextFormatter(cell) {
const val = cell.getValue()
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<p style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+val+'</p></div>'
},
detailFormatter(cell) {
const val = cell.getValue()
if(val.mode === 'detailTermine') {
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a><i class="fa fa-folder-open" style="color:#00649C"></i></a></div>'
} else if (val.mode === 'beurteilungDownload') {
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a><i class="fa fa-file-pdf" style="color:#00649C"></i></a></div>'
}
},
mailFormatter(cell) {
const val = cell.getValue()
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
},
beurteilungFormatter(cell) {
const val = cell.getValue()
if(val) {
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a><i class="fa fa-file-pdf" style="color:#00649C"></i></a></div>'
} else return '-'
},
tableResolve(resolve) {
this.tableBuiltResolve = resolve
},
buildMailToLink(abgabe) {
return 'mailto:' + abgabe.mitarbeiter_uid +'@'+ this.domain
buildMailToLink(projekt) {
// should always be "projekt.mitarbeiter_uid +'@'+ this.domain", built in backend
return 'mailto:' + projekt.email
},
buildBetreuer(abgabe) {
return abgabe.betreuerart_beschreibung + ': ' + (abgabe.btitelpre ? abgabe.btitelpre + ' ' : '') + abgabe.bvorname + ' ' + abgabe.bnachname + (abgabe.btitelpost ? ' ' + abgabe.btitelpost : '')
return (abgabe.btitelpre ? abgabe.btitelpre + ' ' : '') + abgabe.bvorname + ' ' + abgabe.bnachname + (abgabe.btitelpost ? ' ' + abgabe.btitelpost : '')
},
setupData(data){
this.projektarbeiten = data[0]
this.domain = data[1]
this.student_uid = data[2]
const d = data[0]?.retval?.map(projekt => {
async setupData(data){
// this.projektarbeiten = data[0]
const projektarbeiten = data[0] ?? null
if(!projektarbeiten) return
this.projektarbeiten = projektarbeiten.map(projekt => {
let mode = 'detailTermine'
if (projekt.babgeschickt || projekt.zweitbetreuer_abgeschickt) {
// mode = 'beurteilungDownload' // build dl link for both betreuer documents
projekt.beurteilungLink = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cis/private/pdfExport.php?xml=projektarbeitsbeurteilung.xml.php&xsl=Projektbeurteilung&betreuerart_kurzbz='+projekt.betreuerart_kurzbz+'&projektarbeit_id='+projekt.projektarbeit_id+'&person_id=' + projekt.bperson_id
}
return {
...projekt,
details: {
student_uid: this.student_uid,
projektarbeit_id: projekt.projektarbeit_id,
@@ -172,7 +204,8 @@ export const AbgabetoolStudent = {
betreuerart_kurzbz: projekt.betreuerart_kurzbz,
mode
},
beurteilung: projekt.beurteilungLink ?? null,
beurteilung1: projekt.downloadLink1 ?? null,
beurteilung2: projekt.downloadLink2 ?? null,
sem: projekt.studiensemester_kurzbz,
stg: projekt.kurzbzlang,
mail: this.buildMailToLink(projekt),
@@ -182,43 +215,49 @@ export const AbgabetoolStudent = {
}
})
this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns)
this.$refs.abgabeTable.tabulator.setData(d);
},
loadProjektarbeiten() {
this.$fhcApi.factory.lehre.getStudentProjektarbeiten(this.student_uid_prop || this.viewData?.uid || null)
this.$api.call(ApiAbgabe.getStudentProjektarbeiten(this.student_uid))
.then(res => {
if(res?.data) this.setupData(res.data)
})
},
loadAbgaben(details) {
return new Promise((resolve) => {
this.$fhcApi.factory.lehre.getStudentProjektabgaben(details)
this.$api.call(ApiAbgabe.getStudentProjektabgaben(details))
.then(res => {
resolve(res)
})
})
},
handleUuidDefined(uuid) {
this.tabulatorUuid = uuid
},
calcMaxTableHeight() {
const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : ''
const tableDataSet = document.getElementById('filterTableDataset' + tableID);
if(!tableDataSet) return
const rect = tableDataSet.getBoundingClientRect();
this.abgabeTableOptions.height = window.visualViewport.height - rect.top
this.$refs.abgabeTable.tabulator.setHeight(this.abgabeTableOptions.height)
},
async setupMounted() {
this.tableBuiltPromise = new Promise(this.tableResolve)
await this.tableBuiltPromise
this.loadProjektarbeiten()
this.$refs.verticalsplit.collapseBottom()
//this.calcMaxTableHeight()
},
getAccTabHeaderForProjektarbeit(projektarbeit) {
let title = ''
title += projektarbeit.titel ?? this.$p.t('abgabetool/keinTitel')
return title
},
getMailLink(projektarbeit) {
if(projektarbeit.email) {
return 'mailto:'+projektarbeit.email
} else return ''
},
getNoteBezeichnung(projektarbeit) {
if(projektarbeit.note && this.notenOptions) {
const noteOpt = this.notenOptions.find(opt => opt.note == projektarbeit.note)
return noteOpt?.bezeichnung
} else {
return ''
}
},
handleDownloadBeurteilung1(projektarbeit) {
window.open(projektarbeit.beurteilung1)
},
handleDownloadBeurteilung2(projektarbeit) {
window.open(projektarbeit.beurteilung2)
}
},
watch: {
@@ -227,37 +266,152 @@ export const AbgabetoolStudent = {
computed: {
isViewMode() {
return this.student_uid !== this.viewData.uid
},
student_uid() {
return this.student_uid_prop || this.viewData?.uid || null
}
},
created() {
async created() {
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
this.loading = true
//TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API
await this.$api.call(ApiAbgabe.getNoten()).then(res => {
if(res.meta.status == 'success') {
this.notenOptions = res.data[0]
this.allowedNotenOptions = this.notenOptions.filter(
opt => res.data[1].includes(opt.note)
)
}
}).finally(() => {
this.loading = false
})
// fetch abgabetypen options
this.$api.call(ApiAbgabe.getPaAbgabetypen()).then(res => {
this.abgabeTypeOptions = res.data
}).catch(e => {
this.loading = false
})
// fetch config to avoid hard coded links
this.$api.call(ApiAbgabe.getConfigStudent()).then(res => {
this.moodle_link = res.data?.moodle_link
}).catch(e => {
this.loading = false
})
},
mounted() {
this.setupMounted()
},
template: `
<vertical-split ref="verticalsplit">
<template #top>
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
<hr>
<core-filter-cmpt
@uuidDefined="handleUuidDefined"
:title="''"
ref="abgabeTable"
:tabulator-options="abgabeTableOptions"
:tabulator-events="abgabeTableEventHandlers"
tableOnly
:sideMenu="false"
/>
</template>
<template #bottom>
<div v-show="selectedProjektarbeit">
<AbgabeDetail :viewMode="isViewMode" :projektarbeit="selectedProjektarbeit"></AbgabeDetail>
</div>
<template v-if="phrasenResolved">
<FhcOverlay :active="loading"></FhcOverlay>
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
dialogClass="modal-xl" :allowFullscreenExpand="true">
<template v-slot:title>
<div>
{{$capitalize( $p.t('abgabetool/c4abgabeStudentDetailTitle') )}}
</div>
</template>
</vertical-split>
<template v-slot:default>
<AbgabeDetail :projektarbeit="selectedProjektarbeit"></AbgabeDetail>
</template>
</bs-modal>
<h2>{{$capitalize( $p.t('abgabetool/abgabetoolTitle') )}}</h2>
<hr>
<div v-if="projektarbeiten === null">
{{$capitalize( $p.t('abgabetool/c4abgabeStudentNoProjectsFound') )}}
</div>
<Accordion :multiple="true" :activeIndex="activeTabIndex">
<template v-for="projektarbeit in projektarbeiten" :key="projektarbeit.projektarbeit_id">
<AccordionTab>
<template #header>
<div class="d-flex row w-100">
<div class="text-start" :class="projektarbeit.note != null ? 'col-6' : 'col-12'">
<span>{{getAccTabHeaderForProjektarbeit(projektarbeit)}}</span>
</div>
<div class="col-6 text-end">
<span>{{getNoteBezeichnung(projektarbeit)}}</span>
</div>
</div>
</template>
<div class="row">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4details') )}}</div>
<div class="col-8 col-md-9">
<button @click="setDetailComponent(projektarbeit.details)" class="btn btn-primary">
{{$capitalize( $p.t('abgabetool/c4projektdetailsOeffnen') )}} <a><i class="fa fa-folder-open"></i></a>
</button>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4beurteilung') )}}</div>
<div class="col-8 col-md-9">
<button v-if="projektarbeit.beurteilung1" @click="handleDownloadBeurteilung1(projektarbeit)" class="btn btn-primary">
<a> {{$capitalize( $p.t('abgabetool/c4downloadBeurteilungErstbetreuerv2') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button>
<a v-else>{{$capitalize( $p.t('abgabetool/c4nobeurteilungVorhanden') )}}</a>
<button v-if="projektarbeit.beurteilung2" @click="handleDownloadBeurteilung2(projektarbeit)" class="btn btn-primary" style="margin-left: 4px;">
<a> {{$capitalize( $p.t('abgabetool/c4downloadBeurteilungZweitbetreuerv2') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4sem') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.sem }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4stg') )}}</div>
<div class="col-8 col-md-9">
<div class="col-1 d-flex justify-content-start align-items-start">
{{ projektarbeit.stg }}
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ projektarbeit?.betreuerart_kurzbz ? $capitalize( $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) ) : $capitalize( $p.t('abgabetool/c4betreuerv2') ) }}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.betreuerart_kurzbz ? projektarbeit.betreuer : '' }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4betreuerEmailKontaktv2') )}}</div>
<div class="col-8 col-md-9">
<a :href="getMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
</div>
</div>
<div v-if="projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer" class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ projektarbeit.zweitbetreuer_betreuerart_kurzbz ? $p.t('abgabetool/c4betrart' + projektarbeit.zweitbetreuer_betreuerart_kurzbz) : '' }}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.zweitbetreuer?.first }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4projekttyp') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.projekttypbezeichnung }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4titel') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.titel }}
</div>
</div>
</AccordionTab>
</template>
</Accordion>
</template>
`,
};
@@ -1,4 +1,5 @@
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
import ApiAbgabe from '../../../api/factory/abgabe.js'
export const DeadlineOverview = {
name: "DeadlineOverview",
@@ -25,19 +26,21 @@ export const DeadlineOverview = {
tabulatorUuid: Vue.ref(0),
tableBuiltResolve: null,
tableBuiltPromise: null,
phrasenPromise: null,
phrasenResolved: false,
deadlineTableOptions: {
height: 700,
index: 'projektarbeit_id',
layout: 'fitColumns',
placeholder: this.$p.t('global/noDataAvailable'),
placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')),
columns: [
{title: Vue.computed(() => this.$p.t('abgabetool/c4zieldatum')), field: 'datum', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4fixtermin')), field: 'fixterminstring', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$p.t('abgabetool/c4abgabetyp')), field: 'typ_bezeichnung', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4abgabekurzbz')), field: 'kurzbz', formatter: this.centeredTextFormatter, widthGrow: 3},
{title: Vue.computed(() => this.$p.t('person/studentIn')), field: 'student', formatter: this.centeredTextFormatter, widthGrow: 2},
{title: Vue.computed(() => this.$p.t('abgabetool/c4stg')), field: 'stg', formatter: this.centeredTextFormatter,widthGrow: 1},
{title: Vue.computed(() => this.$p.t('abgabetool/c4sem')), field: 'semester', formatter: this.centeredTextFormatter, widthGrow: 1}
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zieldatumv2'))), field: 'datum', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4fixterminv4'))), field: 'fixterminstring', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabetyp'))), field: 'typ_bezeichnung', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabekurzbzv2'))), field: 'kurzbz', formatter: this.centeredTextFormatter, widthGrow: 3},
{title: Vue.computed(() => this.$capitalize(this.$p.t('person/studentIn'))), field: 'student', formatter: this.centeredTextFormatter, widthGrow: 2},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', formatter: this.centeredTextFormatter,widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'semester', formatter: this.centeredTextFormatter, widthGrow: 1}
],
persistence: false,
},
@@ -84,7 +87,7 @@ export const DeadlineOverview = {
this.tableBuiltResolve = resolve
},
loadDeadlines() {
this.$fhcApi.factory.lehre.fetchDeadlines(this.person_uid_prop ?? null)
this.$api.call(ApiAbgabe.fetchDeadlines(this.person_uid_prop ?? null))
.then(res => {
if(res?.data) this.setupData(res.data)
})
@@ -109,7 +112,7 @@ export const DeadlineOverview = {
if(!tableDataSet) return
const rect = tableDataSet.getBoundingClientRect();
this.deadlineTableOptions.height = window.visualViewport.height - rect.top
this.deadlineTableOptions.height = window.visualViewport.height - rect.top - 30
this.$refs.deadlineTable.tabulator.setHeight(this.deadlineTableOptions.height)
},
async setupMounted() {
@@ -127,7 +130,8 @@ export const DeadlineOverview = {
},
created() {
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
},
mounted() {
this.setupMounted()
@@ -0,0 +1,83 @@
export const AbgabeterminStatusLegende = {
name: 'AbgabeterminStatusLegende',
template: `
<div class="text-center">
<div class="col" style="width: 80%; margin-left: 12px;">
<div class="row" style="margin-bottom: 2px">
<div class="col-auto verspaetet-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-triangle-exclamation"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipVerspaetet')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto verpasst-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-calendar-xmark"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipVerpasst')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto abzugeben-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-hourglass-half"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipAbzugeben')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto standard-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-clock"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipStandardv2')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto abgegeben-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-paperclip"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipAbgegeben')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto beurteilungerforderlich-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-list-check"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipBeurteilungerforderlich')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto bestanden-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-check"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipBestanden')) }}</h5>
</div>
</div>
<div class="row" style="margin-bottom: 2px">
<div class="col-auto nichtbestanden-header" style="height: 36px; width:36px; padding: 0px; display: flex; align-items: center; justify-content: center;">
<i class="fa-solid fa-circle-exclamation"></i>
</div>
<div class="col-auto" style="display: flex; align-items: center;">
<h5>{{ $capitalize($p.t('abgabetool/c4tooltipNichtBestanden')) }}</h5>
</div>
</div>
</div>
</div>
`
};
export default AbgabeterminStatusLegende;
@@ -0,0 +1,37 @@
const zone = 'Europe/Vienna';
const today = luxon.DateTime.now().setZone(zone);
export function getDateStyleClass(termin, notenOptions) {
const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day');
const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null;
termin.diffindays = datum.diff(today, 'days').days;
const isLate = abgabedatum && abgabedatum > datum;
// GRADE STATUS
if (termin.note) {
const opt = typeof termin.note === 'object' ? termin.note : notenOptions.find(nopt => nopt.note == termin.note)
if (opt?.positiv === true) return 'bestanden';
else if (opt?.positiv === false) return 'nichtbestanden';
}
// ACTION REQUIRED FOR GRADE
if (termin.bezeichnung?.benotbar && datum <= today) {
return 'beurteilungerforderlich';
}
// SUBMISSION STATUS
if (termin.upload_allowed) {
if (termin.abgabedatum) {
return isLate ? 'verspaetet' : 'abgegeben';
}
// no submission yet
if (datum < today) return 'verpasst';
if (termin.diffindays <= 12) return 'abzugeben';
return 'standard';
}
// GENERIC STATUS
return datum < today ? 'verpasst' : 'standard';
}
@@ -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
{
@@ -113,7 +113,17 @@ export default {
},
template: /*html*/ `
<bs-modal v-show="!loading" ref="modalContainer" v-bind="$props" body-class="" dialog-class="modal-lg" class="bootstrap-alert" :backdrop="false" >
<bs-modal
v-show="!loading"
ref="modalContainer"
v-bind="$props"
header-class="bg-light"
footer-class="bg-light"
body-class=""
dialog-class="modal-lg"
class="bootstrap-alert"
:backdrop="false"
>
<template v-slot:title>
{{title}}
@@ -210,8 +220,8 @@ export default {
<template v-if="data.status === profilUpdateStates['Pending']" v-slot:footer>
<div class="form-underline flex-fill">
<div class="form-underline-titel">{{$p.t('global','nachricht')}}</div>
<div class="flex-fill">
<div>{{$p.t('global','nachricht')}}</div>
<div class="d-flex flex-row gap-2">
<input class="form-control " v-model="data.status_message" />
@@ -4,6 +4,7 @@ import Alert from "../../../components/Bootstrap/Alert.js";
import Loading from "../../../components/Loader.js";
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
import { dateFilter } from '../../../tabulator/filters/Dates.js';
const sortProfilUpdates = (ele1, ele2, thisPointer) => {
let result = 0;
@@ -45,7 +46,6 @@ export default {
loading: false,
filter: "Pending",
profil_update_id: Number(this.id),
};
},
computed: {
@@ -59,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));
@@ -204,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,
@@ -212,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,
@@ -221,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,
@@ -230,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,
@@ -239,15 +249,18 @@ export default {
headerFilterParams: {valuesLookup:true, listOnEmpty:true, autocomplete:true, sort:"asc"},
//responsive:0,
},
{
{
title: this.$p.t("profilUpdate", "insertamum"),
field: "insertamum",
field: "insertamum_iso",
resizable: true,
headerFilter: true,
headerFilterFunc: 'dates',
headerFilter: dateFilter,
minWidth: 200,
formatter:"datetime",
formatterParams: this.datetimeFormatterParams(),
//responsive:0,
},
{
{
title: this.$p.t("profilUpdate", "Status"),
field: "status_translated",
hozAlign: "center",
@@ -269,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,
@@ -305,7 +317,6 @@ export default {
],
};
}
},
methods: {
denyProfilUpdate: function (data) {
@@ -347,7 +358,6 @@ export default {
this.showModal = false;
this.modalData = null;
},
showAcceptDenyModal(value) {
this.modalData = value;
if (!this.modalData) {
@@ -360,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
@@ -377,6 +386,15 @@ export default {
this.showAcceptDenyModal(arrayRowData[0]);
}
}
},
datetimeFormatterParams: function() {
const params = {
inputFormat:"yyyy-MM-dd",
outputFormat:"dd.MM.yyyy",
invalidPlaceholder:"(invalid date)",
timezone:FHC_JS_DATA_STORAGE_OBJECT.timezone
};
return params;
}
},
watch: {
@@ -402,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 = '[email protected]';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>`
}
@@ -0,0 +1,424 @@
import {CoreFilterCmpt} from "../../../../filter/Filter.js";
import BsModal from "../../../../Bootstrap/Modal.js";
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';
import ProjektarbeitDetails from "./Details.js";
import Projektbetreuer from "./Projektbetreuer.js";
export default {
components: {
CoreFilterCmpt,
BsModal,
FormForm,
FormInput,
PvAutoComplete,
ProjektarbeitDetails,
Projektbetreuer
},
inject: {
cisRoot: {
from: 'cisRoot'
},
config: {
from: 'config',
required: true
},
$reloadList: {
from: '$reloadList',
required: true
}
},
props: {
student: Object
},
data() {
return {
tabulatorEvents: [
{
event: 'dataLoaded',
handler: data => this.tabulatorData = data.map(item => {
item.actionDiv = document.createElement('div');
return item;
}),
},
{
event: 'tableBuilt',
handler: async() => {
await this.$p.loadCategory(['global', 'person', 'lehre', 'stv', 'ui', 'projektarbeit']);
let cm = this.$refs.table.tabulator.columnManager;
cm.getColumnByField('projekttyp_kurzbz').component.updateDefinition({
title: this.$p.t('projektarbeit', 'typ_kurzbz')
});
cm.getColumnByField('bezeichnung').component.updateDefinition({
title: this.$p.t('projektarbeit', 'typ')
});
cm.getColumnByField('studiensemester_kurzbz').component.updateDefinition({
title: this.$p.t('lehre', 'studiensemester')
});
cm.getColumnByField('titel').component.updateDefinition({
title: this.$p.t('projektarbeit', 'titel')
});
cm.getColumnByField('note').component.updateDefinition({
title: this.$p.t('projektarbeit', 'gesamtnote')
});
cm.getColumnByField('beginn').component.updateDefinition({
title: this.$p.t('projektarbeit', 'beginn')
});
cm.getColumnByField('ende').component.updateDefinition({
title: this.$p.t('projektarbeit', 'ende')
});
cm.getColumnByField('freigegeben').component.updateDefinition({
title: this.$p.t('projektarbeit', 'freigegeben')
});
cm.getColumnByField('gesperrtbis').component.updateDefinition({
title: this.$p.t('projektarbeit', 'gesperrtBis')
});
cm.getColumnByField('themenbereich').component.updateDefinition({
title: this.$p.t('projektarbeit', 'themenbereich')
});
cm.getColumnByField('anmerkung').component.updateDefinition({
title: this.$p.t('projektarbeit', 'anmerkung')
});
cm.getColumnByField('firma_id').component.updateDefinition({
title: this.$p.t('projektarbeit', 'firmaId')
});
cm.getColumnByField('abgabedatum').component.updateDefinition({
title: this.$p.t('projektarbeit', 'abgabeEndupload')
});
cm.getColumnByField('actions').component.updateDefinition({
title: this.$p.t('global', 'aktionen')
});
}
},
],
tabulatorData: [],
editedProjektarbeit: null,
statusNew: true,
studiensemester_kurzbz: null,
lehrveranstaltung_id: null,
activeTab: 'details'
}
},
computed: {
tabulatorOptions() {
const options = {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(ApiStvProjektarbeit.getProjektarbeit(this.student.uid)),
ajaxResponse: (url, params, response) => response.data,
columns: [
{title: "Projektarbeit ID", field: "projektarbeit_id", visible: false},
{title: "Typ", field: "bezeichnung"},
{title: "Typ Kurzbz", field: "projekttyp_kurzbz", visible: false},
{title: "Studiensemester", field: "studiensemester_kurzbz"},
{title: "Titel", field: "titel"},
{title: "Gesamtnote", field: "note"},
{
title: "Abgabe Enduplad",
field: "abgabedatum",
formatter: function (cell) {
const dateStr = cell.getValue();
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour12: false
});
}
},
{
title: "Beginn",
field: "beginn",
formatter: function (cell) {
const dateStr = cell.getValue();
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour12: false
});
},
visible: false
},
{
title: "Ende",
field: "ende",
formatter: function (cell) {
const dateStr = cell.getValue();
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour12: false
});
},
visible: false
},
{
title:"Freigegeben",
field:"freigegeben",
formatter:"tickCross",
hozAlign:"center",
formatterParams: {
tickElement: '<i class="fa fa-check text-success"></i>',
crossElement: '<i class="fa fa-xmark text-danger"></i>'
},
visible: false
},
{
title: "Gesperrt bis",
field: "gesperrtbis",
formatter: function (cell) {
const dateStr = cell.getValue();
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour12: false
});
},
visible: false
},
{title: "Themenbereich", field: "themenbereich", visible: false},
{title: "Anmerkung", field: "anmerkung", visible: false},
{title: "Lehreinheit ID", field: "lehreinheit_id", visible: false},
{title: "Student UID", field: "student_uid", visible: false},
{title: "Projektbetreuer", field: "projektbetreuer"},
{
title:"Final",
field:"final",
formatter:"tickCross",
hozAlign:"center",
formatterParams: {
tickElement: '<i class="fa fa-check text-success"></i>',
crossElement: '<i class="fa fa-xmark text-danger"></i>'
},
visible: false
},
{title: "Firma ID", field: "firma_id", visible: false},
{
title: 'Aktionen', field: 'actions',
minWidth: 150, // Ensures Action-buttons will be always fully displayed
formatter: (cell, formatterParams, onRendered) => {
let container = document.createElement('div');
container.className = "d-flex gap-2";
let button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-edit"></i>';
button.title = this.$p.t('ui', 'bearbeiten');
button.addEventListener('click', (event) => {
let data = cell.getData();
this.editedProjektarbeit = data;
this.actionEditProjektarbeit();
});
container.append(button);
button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-users"></i>';
button.title = this.$p.t('projektarbeit', 'betreuerBearbeiten');
button.addEventListener('click', (event) => {
let data = cell.getData();
this.editedProjektarbeit = data;
this.actionEditBetreuer();
});
container.append(button);
button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-xmark"></i>';
button.title = this.$p.t('ui', 'loeschen');
button.addEventListener('click', () =>
this.actionDeleteProjektarbeit(cell.getData().projektarbeit_id)
);
container.append(button);
container.append(cell.getData().actionDiv);
return container;
},
frozen: true
},
],
layout: 'fitDataStretchFrozen',
height: 'auto',
minHeight: '200',
selectable: 1,
index: 'projektarbeit_id',
persistence:{
columns: true, //persist column layout
},
persistenceID: 'stv-details-projektarbeit-2025112401'
}
return options;
}
},
methods: {
actionNewProjektarbeit() {
this.statusNew = true;
this.editedProjektarbeit = null;
this.toggleMenu('details');
this.$refs.projektarbeitDetails.getFormData(this.statusNew, null, null);
this.$refs.projektarbeitModal.show();
},
actionEditProjektarbeit() {
this.statusNew = false;
this.toggleMenu('details');
this.$refs.projektarbeitModal.show();
},
actionEditBetreuer() {
this.statusNew = false;
this.toggleMenu('betreuer');
this.$refs.projektarbeitModal.show();
},
actionDeleteProjektarbeit(projektarbeit_id) {
this.$fhcAlert
.confirmDelete()
.then(result => result
? projektarbeit_id
: Promise.reject({handled: true}))
.then(this.deleteProjektarbeit)
.catch(this.$fhcAlert.handleSystemError);
},
addNewProjektarbeit() {
this.$refs.projektarbeitDetails.addNewProjektarbeit()
.then((result) => {
this.projektarbeitSaved();
})
.catch(this.$fhcAlert.handleSystemError);
},
updateProjektarbeit() {
this.$refs.projektarbeitDetails.updateProjektarbeit()
.then((result) => {
this.projektarbeitSaved();
})
.catch(this.$fhcAlert.handleSystemError);
},
deleteProjektarbeit(projektarbeit_id) {
return this.$api
.call(ApiStvProjektarbeit.deleteProjektarbeit(projektarbeit_id))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => {
this.reload();
});
},
projektarbeitSaved() {
this.reload();
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
this.hideModal('projektarbeitModal');
},
setDefaultStunden(projekttyp_kurzbz) {
this.$refs.projektbetreuer.setDefaultStunden(projekttyp_kurzbz);
},
hideModal(modalRef){
this.$refs[modalRef].hide();
},
reload() {
this.$refs.table.reloadTable();
},
toggleMenu(tabId) {
this.activeTab = tabId;
if (this.statusNew == false) {
switch(tabId) {
case 'details':
this.$refs.projektarbeitDetails.getFormData(
this.statusNew, this.editedProjektarbeit?.studiensemester_kurzbz, this.editedProjektarbeit?.lehrveranstaltung_id
);
this.$refs.projektarbeitDetails.loadProjektarbeit(this.editedProjektarbeit?.projektarbeit_id);
break;
case 'betreuer':
this.$refs.projektbetreuer.getFormData(
this.editedProjektarbeit ? this.editedProjektarbeit.projekttyp_kurzbz : null
);
this.$refs.projektbetreuer.getProjektbetreuer(this.editedProjektarbeit?.projektarbeit_id, this.editedProjektarbeit?.studiensemester_kurzbz);
break;
}
}
}
},
template: `
<div class="stv-details-projektarbeit h-100 pb-3">
<h4>{{this.$p.t('stv','tab_projektarbeit')}}</h4>
<core-filter-cmpt
v-if="!this.student.length"
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
reload
new-btn-show
:new-btn-label="this.$p.t('stv', 'tab_projektarbeit')"
@click:new="actionNewProjektarbeit"
>
</core-filter-cmpt>
<!--Modal: projektarbeitModal-->
<bs-modal ref="projektarbeitModal" dialog-class="modal-xl modal-dialog-scrollable" header-class="flex-wrap pb-0">
<template #title>
<p v-if="statusNew" class="fw-bold mt-3">{{$p.t('projektarbeit', 'projektarbeitAnlegen')}}</p>
<p v-else class="fw-bold mt-3">{{$p.t('projektarbeit', 'projektarbeitBearbeiten')}}</p>
</template>
<template #modal-header-content v-if="!statusNew">
<ul class="nav nav-tabs w-100 mt-3 msg_preview" id="pa_tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link" :class="activeTab == 'details' ? 'active' : ''" id="details-tab" data-bs-toggle="tab" data-bs-target="#details" type="button" role="tab" aria-controls="details" aria-selected="true" @click="toggleMenu('details')">Details</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" :class="activeTab == 'betreuer' ? 'active' : ''" id="betreuer-tab" data-bs-toggle="tab" data-bs-target="#betreuer" type="button" role="tab" aria-controls="betreuer" aria-selected="false" @click="toggleMenu('betreuer')">{{$p.t('projektarbeit', 'betreuerGross')}}</button>
</li>
</ul>
</template>
<div class="tab-content" id="pa_content">
<div class="tab-pane fade show" :class="activeTab == 'details' ? 'active' : ''" id="details" role="tabpanel" aria-labelledby="details-tab">
<div class="row">
<div class="col-12">
<projektarbeit-details ref="projektarbeitDetails" :student="student" @projekttyp-changed="setDefaultStunden">
</projektarbeit-details>
</div>
</div>
</div>
<div class="tab-pane fade show" :class="activeTab == 'betreuer' ? 'active' : ''" id="betreuer" role="tabpanel" aria-labelledby="betreuer-tab">
<div class="row">
<div class="col-12">
<projektbetreuer ref="projektbetreuer" :config="config" @betreuer-saved="reload"></projektbetreuer>
</div>
</div>
</div>
</div>
<template #footer>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{$p.t('ui', 'abbrechen')}}</button>
<button v-if="statusNew" class="btn btn-primary" @click="addNewProjektarbeit()"> {{$p.t('ui', 'speichern')}}</button>
<button v-if="!statusNew && activeTab == 'details'" class="btn btn-primary" @click="updateProjektarbeit()"> {{$p.t('ui', 'speichern')}}</button>
</template>
</bs-modal>
</div>
`
}
@@ -0,0 +1,600 @@
import {CoreFilterCmpt} from "../../../../filter/Filter.js";
import BsModal from "../../../../Bootstrap/Modal.js";
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 NewPerson from "../../List/New.js";
import Contact from "../Kontakt/Contact.js";
import Vertrag from "./Vertrag.js";
import ApiStvProjektbetreuer from '../../../../../api/factory/stv/projektbetreuer.js';
export default {
components: {
CoreFilterCmpt,
BsModal,
FormForm,
FormInput,
PvAutoComplete,
NewPerson,
Contact,
Vertrag
},
emits: ['betreuerSaved'],
provide() {
return {
configShowVertragsdetails: this.config.showVertragsdetails
}
},
computed: {
betreuerFormOpened() {
return this.newMode || this.editMode;
}
},
props: {
config: {
type: Object,
default: {}
}
},
data() {
return {
tabulatorOptions: {
columns: [
{title: "Nachname", field: "nachname"},
{title: "Vorname", field: "vorname"},
{title: "Note", field: "note"},
{title: "Punkte", field: "punkte"},
{title: "Stunden", field: "stunden"},
{title: "Stundensatz", field: "stundensatz", visible: false},
{title: "Art", field: "betreuerart_kurzbz", visible: false},
{title: "Person ID", field: "person_id", visible: false},
{title: "Vertrag ID", field: "vertrag_id", visible: false},
{title: "Projektarbeit ID", field: "projektarbeit_id", visible: false},
{
title: 'Aktionen',
field: 'actions',
minWidth: 150, // Ensures Action-buttons will be always fully displayed
formatter: (cell, formatterParams, onRendered) => {
let container = document.createElement('div');
container.className = "d-flex gap-2";
let button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-edit"></i>';
button.title = this.$p.t('ui', 'bearbeiten');
button.addEventListener('click', (event) => {
event.stopPropagation();
event.preventDefault();
let data = cell.getData();
this.actionEditProjektbetreuer(data.projektarbeit_id, data.person_id, data.betreuerart_kurzbz);
});
container.append(button);
button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-xmark"></i>';
button.title = this.$p.t('ui', 'loeschen');
button.addEventListener('click', (event) => {
event.stopPropagation();
event.preventDefault();
const data = cell.getData();
this.actionDeleteProjektbetreuer(data.betreuer_id, data.projektarbeit_id, data.person_id, data.betreuerart_kurzbz)
});
container.append(button);
let data = cell.getData();
if (data.beurteilungDownloadLink !== null) {
if (data.beurteilungDownloadLink == '') {
button = document.createElement('span');
button.title = this.$p.t('projektarbeit', 'projektarbeitNochNichtBeurteilt')
button.innerHTML = '<button class="btn btn-outline-secondary btn-action" disabled>'+
'<i class="fa-regular fa-file-pdf"></i></button>';
button.addEventListener('click', (event) => {
event.stopPropagation();
event.preventDefault();
});
}
else {
button = document.createElement('a');
button.setAttribute('href', data.beurteilungDownloadLink);
button.setAttribute('role', 'button');
button.innerHTML = '<i class="fa fa-file-pdf"></i>';
button.title = this.$p.t('projektarbeit', 'projektbeurteilungErstellen');
button.className = 'btn btn-outline-secondary btn-action';
button.addEventListener('click', (event) => {
event.stopPropagation();
event.preventDefault();
window.location.href = data.beurteilungDownloadLink;
});
}
container.append(button);
}
return container;
},
frozen: true
},
],
layout: 'fitData',
layoutColumnsOnNewData: false,
height: 'auto',
minHeight: '100',
selectable: true,
selectable: 1,
index: 'betreuer_id',
persistence:{
columns: true, //persist column layout
},
persistenceID: 'stv-details-projektbetreuer-2025121201'
},
tabulatorEvents: [
{
event: 'tableBuilt',
handler: async() => {
await this.$p.loadCategory(['global', 'person', 'lehre', 'stv', 'projektarbeit', 'ui']);
let cm = this.$refs.projektbetreuerTable.tabulator.columnManager;
cm.getColumnByField('nachname').component.updateDefinition({
title: this.$p.t('person', 'nachname')
});
cm.getColumnByField('vorname').component.updateDefinition({
title: this.$p.t('person', 'vorname')
});
cm.getColumnByField('note').component.updateDefinition({
title: this.$p.t('projektarbeit', 'note')
});
cm.getColumnByField('punkte').component.updateDefinition({
title: this.$p.t('projektarbeit', 'punkte')
});
cm.getColumnByField('stunden').component.updateDefinition({
title: this.$p.t('projektarbeit', 'stunden')
});
cm.getColumnByField('stundensatz').component.updateDefinition({
title: this.$p.t('projektarbeit', 'stundensatz')
});
cm.getColumnByField('betreuerart_kurzbz').component.updateDefinition({
title: this.$p.t('projektarbeit', 'betreuerart_kurzbz')
});
cm.getColumnByField('person_id').component.updateDefinition({
title: this.$p.t('person', 'person_id')
});
cm.getColumnByField('vertrag_id').component.updateDefinition({
title: this.$p.t('projektarbeit', 'vertrag_id')
});
cm.getColumnByField('projektarbeit_id').component.updateDefinition({
title: this.$p.t('projektarbeit', 'projektarbeit_id')
});
cm.getColumnByField('actions').component.updateDefinition({
title: this.$p.t('global', 'actions')
});
// Force layout recalculation for handling overflow text
this.$refs.projektbetreuerTable.tabulator.redraw(true);
}
},
{
event: 'rowSelected',
handler: row => {
let data = row.getData();
this.actionEditProjektbetreuer(data.projektarbeit_id, data.person_id, data.betreuerart_kurzbz);
}
}
],
formData: {
betreuerart_kurzbz: null,
note: null,
stunden: null,
stundensatz: null
},
defaultStundensatz: null,
newMode: false,
editMode: false,
initialFormData: null,
defaultFormDataValues: {stunden: null, stundensatz: null},
projektarbeit_id: null,
editedBetreuerIdx: -1,
arrBetreuerart: [],
arrNoten: [],
filteredBetreuer: [],
autocompleteSelectedBetreuer: null,
beurteilungDownloadLink: null,
vertragFieldsDisabled: false,
abortController: {
betreuer: null
}
}
},
methods: {
actionNewProjektbetreuer() {
this.newMode = !this.newMode;
this.editMode = false;
this.resetForm();
this.defaultStundensatz = this.config.defaultProjektbetreuerStundensatz;
this.captureFormData();
},
actionEditProjektbetreuer(projektarbeit_id, person_id, betreuerart_kurzbz) {
this.editMode = true;
this.newMode = false;
this.resetForm();
this.getDefaultStundensaetze(person_id).finally(() => {
// get betreuer from tabulator list
let projektbetreuerListe = this.$refs.projektbetreuerTable.tabulator.getData();
const idx = projektbetreuerListe.findIndex(
betr =>
betr.person_id === person_id &&
betr.projektarbeit_id === projektarbeit_id &&
betr.betreuerart_kurzbz === betreuerart_kurzbz
);
if (idx >= 0) { // if betreuer found
// set currently edited betreuer (deep copy)
this.formData = JSON.parse(JSON.stringify(projektbetreuerListe[idx]));
// set download link
if (this.formData.beurteilungDownloadLink !== null) this.beurteilungDownloadLink = this.formData.beurteilungDownloadLink;
// set betreuer for autocomplete field
this.autocompleteSelectedBetreuer = {
person_id: this.formData.person_id,
name: this.formData.name,
vorname: this.formData.vorname,
nachname: this.formData.nachname,
vertrag_id: this.formData.vertrag_id
};
}
// capture initial form data for detecting changes
this.captureFormData();
});
},
actionDeleteProjektbetreuer(betreuer_id, projektarbeit_id, person_id, betreuerart_kurzbz) {
this.$fhcAlert
.confirmDelete()
.then(result => result
? {projektarbeit_id, person_id, betreuerart_kurzbz}
: Promise.reject({handled: true}))
.then(result => {
return this.$api
.call(ApiStvProjektbetreuer.deleteProjektbetreuer(projektarbeit_id, person_id, betreuerart_kurzbz))
})
.then(result => {
this.$refs.projektbetreuerTable.tabulator.deleteRow(betreuer_id);
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
})
.catch(this.$fhcAlert.handleSystemError);
},
getFormData(projekttyp_kurzbz) {
// default Stundensätze from config
this.defaultFormDataValues.stunden = this.getDefaultStunden(projekttyp_kurzbz);
this.defaultFormDataValues.stundensatz = this.config.defaultProjektbetreuerStundensatz;
this.defaultStundensatz = this.config.defaultProjektbetreuerStundensatz;
// get other initial data
this.$api
.call(ApiStvProjektbetreuer.getBetreuerarten())
.then(result => {
this.arrBetreuerart = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api
.call(ApiStvProjektbetreuer.getNoten())
.then(result => {
this.arrNoten = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
getProjektbetreuer(projektarbeit_id, studiensemester_kurzbz) {
if (projektarbeit_id) {
// if projektarbeit changed, reset the form to hold new data
if (this.projektarbeit_id != projektarbeit_id) {
this.resetForm();
this.resetModes();
}
this.projektarbeit_id = projektarbeit_id;
this.studiensemester_kurzbz = studiensemester_kurzbz;
this.$api
.call(ApiStvProjektbetreuer.getProjektbetreuer(this.projektarbeit_id))
.then(result => {
this.$refs.projektbetreuerTable.tabulator.replaceData(this.addIds(result.data));
})
.catch(this.$fhcAlert.handleSystemError);
} else {
this.emptyBetreuerList();
}
},
saveProjektbetreuer() {
this.$refs.formProjektbetreuer.call(
ApiStvProjektbetreuer.saveProjektbetreuer(this.projektarbeit_id, this.getFormDataWithBetreuer())
)
.then(result => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
this.getProjektbetreuer(this.projektarbeit_id, this.studiensemester_kurzbz);
this.resetModes();
this.$emit('betreuerSaved');
})
.catch(this.$fhcAlert.handleSystemError);
},
searchBetreuer(event) {
if (this.abortController.betreuer) {
this.abortController.betreuer.abort();
}
this.abortController.betreuer = new AbortController();
return this.$api
.call(ApiStvProjektbetreuer.getProjektbetreuerBySearchQuery(event.query))
.then(result => {
this.filteredBetreuer = result.data;
});
},
emptyBetreuerList() {
this.$refs.projektbetreuerTable.tabulator.clearData();
},
resetForm() {
const defaultFormData = this.getDefaultFormData();
this.formData = defaultFormData;
if (this.beurteilungDownloadLink !== null) this.beurteilungDownloadLink = '';
this.autocompleteSelectedBetreuer = null;
this.initialFormData = null;
if (this.projekttyp_kurzbz) this.setDefaultStunden(this.projekttyp_kurzbz);
this.disableVertragFields(false);
this.$refs.formProjektbetreuer.clearValidation();
},
resetModes() {
this.newMode = false;
this.editMode = false;
},
getDefaultFormData() {
let formData = {betreuerart_kurzbz : null, note: null};
for (const name in this.defaultFormDataValues) {
formData[name] = this.defaultFormDataValues[name];
}
return formData;
},
captureFormData() {
this.initialFormData = JSON.parse(JSON.stringify(this.formData)); // deep copy
},
// add own betreuer ids to betreuer liste
addIds(betreuerListe) {
for (const idx in betreuerListe) {
let betreuer = betreuerListe[idx];
betreuer.person_id_old = betreuer.person_id;
betreuer.betreuerart_kurzbz_old = betreuer.betreuerart_kurzbz;
betreuer.betreuer_id = parseInt(idx);
}
return betreuerListe;
},
// add the betreuer selected in automomplete to betreuer liste
getFormDataWithBetreuer() {
let preparedFormData = this.formData;
preparedFormData.projektarbeit_id = this.projektarbeit_id;
if (this.autocompleteSelectedBetreuer) {
preparedFormData.person_id = this.autocompleteSelectedBetreuer.person_id;
preparedFormData.name = this.autocompleteSelectedBetreuer.name;
preparedFormData.vorname = this.autocompleteSelectedBetreuer.vorname;
preparedFormData.nachname = this.autocompleteSelectedBetreuer.nachname;
}
return preparedFormData;
},
// get default values for stunden
getDefaultStunden(projekttyp_kurzbz) {
let stunden = '0.0';
if (projekttyp_kurzbz == 'Bachelor') stunden = this.config.defaultProjektbetreuerStunden;
if (projekttyp_kurzbz == 'Diplom') stunden = this.config.defaultProjektbetreuerStundenDiplom;
return stunden;
},
setDefaultStunden(projekttyp_kurzbz) {
this.projekttyp_kurzbz = projekttyp_kurzbz;
// set default stunden if not in edit mode (so hours of editen person are not overwritten)
if (!this.editMode)
{
let defaultStunden = this.getDefaultStunden(projekttyp_kurzbz);
this.formData.stunden = defaultStunden;
}
},
getDefaultStundensaetze(person_id) {
return this.$api
.call(ApiStvProjektbetreuer.getDefaultStundensaetze(person_id, this.studiensemester_kurzbz))
.then(result => {
// set default stundensatz (if no other is set yet)
this.defaultStundensatz = result.data;
this.formData.stundensatz = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
actionNewPerson() {
this.$refs.newPersonModal.open();
},
actionKontaktdatenBearbeiten() {
if (!this.autocompleteSelectedBetreuer) return;
this.$refs.kontaktdatenModal.show();
},
// stuff to do after new person has been saved
personSaved(result) {
this.$api
.call(ApiStvProjektbetreuer.getPerson(result))
.then(response => {
// set the new person in Betreuer autocomplete field
this.autocompleteSelectedBetreuer = response.data;
this.getDefaultStundensaetze(this.autocompleteSelectedBetreuer.person_id);
})
.catch(this.$fhcAlert.handleSystemError)
},
// disable fields which are dependent on Vertrag status
disableVertragFields(statusAkzeptiert) {
this.vertragFieldsDisabled = statusAkzeptiert;
},
onBetreuerSelected(ev) {
this.getDefaultStundensaetze(ev.value.person_id);
}
},
template: `
<div class="stv-details-projektbetreuer h-100 pb-3 row">
<div :class="this.config.showVertragsdetails ? 'col-9' : 'col-12'">
<legend>{{this.$p.t('projektarbeit','betreuerGross')}}</legend>
<core-filter-cmpt
ref="projektbetreuerTable"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
new-btn-show
:new-btn-label="this.$p.t('projektarbeit', 'betreuerGross')"
@click:new="actionNewProjektbetreuer"
>
</core-filter-cmpt>
<form-form ref="formProjektbetreuer" v-show="betreuerFormOpened" class="mt-3" @submit.prevent >
<div class="row mb-3">
<form-input
container-class="stv-details-projektarbeit-betreuer"
:label="$p.t('projektarbeit', 'betreuer')"
type="autocomplete"
optionLabel="name"
v-model="autocompleteSelectedBetreuer"
name="person_id"
:suggestions="filteredBetreuer"
@complete="searchBetreuer"
@item-select="onBetreuerSelected"
@option-select="onBetreuerSelected"
:min-length="3"
:disabled="vertragFieldsDisabled"
>
</form-input>
</div>
<div class="row mb-3">
<div class="col-6">
<button class="btn btn-outline-secondary" @click="actionNewPerson">{{ $p.t('projektarbeit', 'neuePersonAnlegen') }}</button>
</div>
<div class="col-6">
<button class="btn btn-outline-secondary float-end" @click="actionKontaktdatenBearbeiten">{{ $p.t('projektarbeit', 'kontaktdatenBearbeiten') }}</button>
</div>
</div>
<div class="row mb-3">
<form-input
container-class="stv-details-projektbetreuer-betreuerart"
:label="$p.t('projektarbeit', 'betreuerart')"
type="select"
v-model="formData.betreuerart_kurzbz"
name="betreuerart_kurzbz"
>
<option
v-for="art in arrBetreuerart"
:key="art.betreuerart_kurzbz"
:value="art.betreuerart_kurzbz"
>
{{art.beschreibung}}
</option>
</form-input>
</div>
<div class="row mb-3">
<form-input
container-class="stv-details-projektbetreuer-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>
<div class="row mb-3">
<div class="col-6">
<form-input
container-class="stv-details-projektarbeit-stunden"
type="text"
name="stunden"
:label="$p.t('projektarbeit', 'stunden')"
:disabled="vertragFieldsDisabled"
v-model="formData.stunden"
>
</form-input>
</div>
<div class="col-6">
<form-input
container-class="stv-details-projektarbeit-stundensatz"
type="text"
name="stundensatz"
:label="$p.t('projektarbeit', 'stundensatz') + (defaultStundensatz ? ' (Default ' + defaultStundensatz + ')' : '')"
:disabled="vertragFieldsDisabled"
v-model="formData.stundensatz"
>
</form-input>
</div>
</div>
</form-form>
<button class="btn btn-primary" v-show="betreuerFormOpened" @click="saveProjektbetreuer">
{{ $p.t('projektarbeit', 'betreuerSpeichern') }}
</button>
<!-- <div class = "mt-5" v-if="beurteilungDownloadLink !== null">
<div class="mb-1">
<a :href="beurteilungDownloadLink" class="btn btn-primary d-block" :class="{ 'disabled' : beurteilungDownloadLink === ''}">
{{ $p.t('projektarbeit', 'projektbeurteilungErstellen') }}
</a>
</div>
{{ autocompleteSelectedBetreuer?.person_id && beurteilungDownloadLink === '' ? $p.t('projektarbeit', 'projektarbeitNochNichtBeurteilt') : ''}}
</div> -->
</div>
<div class="col-3">
<vertrag ref="vertrag"
:vertrag_id="autocompleteSelectedBetreuer?.vertrag_id"
:person_id="autocompleteSelectedBetreuer?.person_id"
:betreuerProjektarbeit="initialFormData"
@vertragsstatusChanged="disableVertragFields">
</vertrag>
</div>
</div>
<!--Modal: new Person modal -->
<new-person ref="newPersonModal" :personOnly="true" @saved="personSaved"></new-person>
<!--Modal: KontaktdatenModal -->
<bs-modal
ref="kontaktdatenModal"
dialog-class="modal-xl modal-dialog-scrollable"
v-if="autocompleteSelectedBetreuer && autocompleteSelectedBetreuer.person_id">
<template #title>
<p class="fw-bold mt-3">{{$p.t('projektarbeit', 'kontaktdatenBearbeiten')}}</p>
</template>
<div class="row">
<div class="col-12">
<contact ref="contact" :uid="autocompleteSelectedBetreuer.person_id">
</contact>
</div>
</div>
</bs-modal>
`
}
@@ -0,0 +1,160 @@
import CoreForm from '../../../../Form/Form.js';
import FormInput from '../../../../Form/Input.js';
import ApiVertrag from '../../../../../api/factory/stv/vertrag.js';
export default{
name: "ProjektarbeitVertrag",
components: {
CoreForm,
FormInput
},
emits: [
'canceledVertrag',
'vertragsstatusChanged'
],
props: {
vertrag_id: Number,
person_id: Number,
betreuerProjektarbeit: Object
},
inject: {
showVertragsdetails: {
from: 'configShowVertragsdetails',
default: false
}
},
data() {
return{
data: {
vertragsstatus: null,
vertragsstunden: null,
vertragsstunden_studiensemester_kurzbz: null
},
// status names for stages of Vertrag ("constants")
vertragsstatus_akzeptiert: 'Akzeptiert',
vertragsstatus_geaendert:'Geändert',
vertragsstatus_storniert: 'Storno'
}
},
watch: {
vertrag_id:
{
//deep: true,
handler(newVal, oldVal) {
this.resetForm();
if (newVal !== null && newVal !== undefined) this.getVertrag();
}
},
},
computed: {
vertragsstatus() {
// not show Vertragsstatus if no data
if (!this.data?.vertragsstatus || !this.betreuerProjektarbeit?.betreuerart_kurzbz) return;
const betragVertrag = Number(this.data.betrag) || 0;
const stundenVertrag = Number(this.data.vertragsstunden) || 0;
const semStunden = Number(this.betreuerProjektarbeit.stunden) || 0;
const stundensatz = Number(this.betreuerProjektarbeit.stundensatz) || 0;
const kostenAktuell = semStunden * stundensatz;
// Vertragsstunden amount should be same as Semesterstunden amount, otherwise there has been a change
let vertragsstatus = (stundenVertrag !== semStunden || betragVertrag !== kostenAktuell)
? this.vertragsstatus_geaendert
: (this.data.vertragsstatus || '');
// vertragsstatus changed to "akzeptiert"
this.$emit('vertragsstatusChanged', vertragsstatus == this.vertragsstatus_akzeptiert);
return vertragsstatus;
},
},
methods: {
getVertrag() {
if (this.showVertragsdetails === false)
return;
if (!this.vertrag_id)
return;
this.$api.call(ApiVertrag.getVertrag(this.vertrag_id))
.then(result => {
if (result.data.vertragsstatus != this.vertragsstatus_storniert) {
this.data = result.data;
}
})
.catch(this.$fhcAlert.handleSystemError);
},
cancelVertrag() {
this.$fhcAlert
.confirmDelete()
.then(result => result
? {vertrag_id: this.vertrag_id, person_id: this.person_id}
: Promise.reject({handled: true}))
.then(result => {
return this.$api.call(ApiVertrag.cancelVertrag({vertrag_id: this.vertrag_id, person_id: this.person_id}))
})
.then(result => {
this.resetForm();
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
this.$emit('canceledVertrag');
// vertragsstatus not "akzeptiert" anymore
this.$emit('vertragsstatusChanged', false);
})
.catch(this.$fhcAlert.handleSystemError);
},
resetForm() {
this.data = {
vertrag_id: null,
vertragsstatus: null,
vertragsstunden: null,
vertragsstunden_studiensemester_kurzbz: null
}
}
},
template: `
<core-form ref="form">
<fieldset class="overflow-hidden" v-if="showVertragsdetails">
<legend>
{{$p.t('lehre', 'vertragsdetails')}}
</legend>
<div class="mb-3">
{{ betreuerProjektarbeit?.betreuerart_kurzbz && betreuerProjektarbeit?.vertrag_id == null ? ' '+$p.t('lehre', 'nochKeinVertrag') : '' }}
</div>
<div class="row mb-3">
<div class="col-12">
{{ $p.t('lehre', 'vertragsstatus') }}: {{ vertragsstatus }}
</div>
</div>
<div class="row mb-3">
<div class="col-12">
{{$p.t('lehre', 'vertragurfassung')}}
</div>
</div>
<div class="row mb-3">
<div class="col-12">
{{ $p.t('lehre', 'semesterstunden') }}: {{ data.vertragsstunden }}
<br>
<span class="text-capitalize">{{ $p.t('lehre', 'studiensemester') }}</span>: {{ data.vertragsstunden_studiensemester_kurzbz }}
</div>
</div>
<div class="row mb-3" v-if="data?.vertragsstatus">
<div class="col-12">
<button
type="button"
class="btn btn-outline-secondary"
:disabled="vertragsstatus == vertragsstatus_storniert"
@click="cancelVertrag"
>
{{ $p.t('lehre', 'vertragStornieren') }}
</button>
</div>
</div>
</fieldset>
</core-form>
`
};
@@ -46,8 +46,6 @@ export default{
{title: "Punkte", field: "punkte", visible: false},
{
title: 'Aktionen', field: 'actions',
minWidth: 150,
maxWidth: 150,
formatter: (cell, formatterParams, onRendered) => {
let container = document.createElement('div');
container.className = "d-flex gap-2";
@@ -93,7 +91,15 @@ export default{
layoutColumnsOnNewData: false,
height: 'auto',
index: 'pruefung_id',
persistenceID: 'stv-details-pruefung-list'
persistenceID: 'stv-details-pruefung-list-2026012701',
persistence: {
sort: false,
columns: ["width", "visible", "frozen"],
filter: false,
headerFilter: false,
group: false,
page: false
}
},
tabulatorEvents: [
{
@@ -148,7 +154,6 @@ export default{
listMas: [],
listMarks: [],
zeugnisData: [],
checkData:[],
filter: false,
statusNew: true,
isStartDropDown: false,
@@ -181,7 +186,7 @@ export default{
this.pruefungData.student_uid = this.uid;
this.pruefungData.note = 9;
this.pruefungData.datum = new Date();
this.pruefungData.datum = luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
this.pruefungData.pruefungstyp_kurzbz = null;
if(lv_id){
this.pruefungData.lehrveranstaltung_id = lv_id;
@@ -193,7 +198,7 @@ export default{
this.isStartDropDown = false;
this.loadPruefung(pruefung_id).then(() => {
this.pruefungData.note = 9;
this.pruefungData.datum = new Date();
this.pruefungData.datum = luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
this.pruefungData.pruefungstyp_kurzbz = null;
this.pruefungData.anmerkung = null;
this.prepareDropdowns();
@@ -229,9 +234,8 @@ export default{
return this.$refs.examData
.call(ApiStvExam.addPruefung(this.pruefungData))
.then(response => {
this.checkData = response.data;
if (this.checkData === 2 || this.checkData === 5)
this.$fhcAlert.alertInfo(this.$p.t('exam', 'hinweis_changeAfterExamDate'));
if (response.data)
this.$fhcAlert.alertDefault('info', 'Info', response.data, true);
else
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
this.hideModal('pruefungModal');
@@ -243,12 +247,13 @@ export default{
});
},
updatePruefung(pruefung_id){
this.checkChangeAfterExamDate();
return this.$refs.examData
.call(ApiStvExam.updatePruefung(pruefung_id, this.pruefungData))
.then(response => {
this.checkData = response.data;
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
if (response.data)
this.$fhcAlert.alertDefault('info', 'Info', response.data, true);
else
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
this.hideModal('pruefungModal');
this.resetModal();
}).catch(this.$fhcAlert.handleSystemError)
@@ -266,27 +271,6 @@ export default{
else
this.showHint = false;
},
checkChangeAfterExamDate() {
const data = {
student_uid: this.pruefungData.student_uid,
studiensemester_kurzbz: this.pruefungData.studiensemester_kurzbz,
lehrveranstaltung_id: this.pruefungData.lehrveranstaltung_id
};
return this.$api
.call(ApiStvExam.checkZeugnisnoteLv(data))
.then(result => {
this.zeugnisData = result.data;
let checkDate = this.zeugnisData[0].uebernahmedatum === '' ||
this.zeugnisData[0].benotungsdatum > this.zeugnisData[0].uebernahmedatum
? this.zeugnisData[0].benotungsdatum
: this.zeugnisData[0].uebernahmedatum;
if (checkDate >= this.pruefungData.datum
&& this.pruefungData.note !== this.zeugnisData[0].note) {
this.$fhcAlert.alertInfo(this.$p.t('exam', 'hinweis_changeAfterExamDate'));
}
})
.catch(this.$fhcAlert.handleSystemError);
},
deletePruefung(pruefung_id) {
return this.$api
.call(ApiStvExam.deletePruefung(pruefung_id))
@@ -534,6 +518,7 @@ export default{
container-class="mb-3"
type="DatePicker"
v-model="pruefungData.datum"
model-type="yyyy-MM-dd"
name="datum"
:label="$p.t('global/datum')"
auto-apply
@@ -185,24 +185,10 @@ export default{
.then(result => this.statusgruende = result.data)
.catch(this.$fhcAlert.handleSystemError);
//TODO(Manu) check why it is/was hard coded
this.$api
.call(ApiStvStatus.getStati())
.then(result => this.stati = result.data)
.catch(this.$fhcAlert.handleSystemError);
/* this.stati = [
{ status_kurzbz: 'Interessent', bezeichnung: 'Interessent'},
{ status_kurzbz: 'Bewerber', bezeichnung: 'Bewerber'},
{ status_kurzbz: 'Aufgenommener', bezeichnung: 'Aufgenommener'},
{ status_kurzbz: 'Student', bezeichnung: 'Student'},
{ status_kurzbz: 'Unterbrecher', bezeichnung: 'Unterbrecher'},
{ status_kurzbz: 'Diplomand', bezeichnung: 'Diplomand'},
{ status_kurzbz: 'Incoming', bezeichnung: 'Incoming'},
{ status_kurzbz: 'Absolvent', bezeichnung: 'Absolvent'},
{ status_kurzbz: 'Abbrecher', bezeichnung: 'Abbrecher'},
{ status_kurzbz: 'Abgewiesener', bezeichnung: 'Abgewiesener'},
{ status_kurzbz: 'Wartender', bezeichnung: 'Wartender'}
];*/
},
template: `
<bs-modal class="stv-status-modal" ref="modal" dialog-class="modal-dialog-scrollable">
@@ -220,7 +206,7 @@ export default{
<p v-if="bisLocked && isStatusBeforeStudent">
<b>{{$p.t('bismeldestichtag', 'info_MeldestichtagStatusgrundSemester')}}</b>
</p>
<form-input
container-class="mb-3"
type="select"
@@ -353,7 +339,6 @@ export default{
v-model="formData.anmerkung"
name="anmerkung"
:label="$p.t('global/anmerkung')"
:disabled="bisLocked"
>
</form-input>
<form-input
@@ -1,18 +1,30 @@
import {CoreFilterCmpt} from "../../filter/Filter.js";
import ListNew from './List/New.js';
import ListFilter from './List/Filter.js';
import { capitalize } from '../../../helpers/StringHelpers.js';
import draggable from '../../../directives/draggable.js';
export default {
name: "ListPrestudents",
components: {
CoreFilterCmpt,
ListNew
ListNew,
ListFilter
},
directives: {
draggable
},
inject: {
'lists': {
lists: {
from: 'lists',
required: true
},
$reloadList: {
from: '$reloadList',
required: true
},
currentSemester: {
from: 'currentSemester',
required: true
@@ -51,8 +63,12 @@ export default {
{title:"Vornamen", field:"vornamen", visible:false, headerFilter: true},
{title:"TitelPost", field:"titelpost", headerFilter: "list", headerFilterParams: {valuesLookup:true, listOnEmpty:true, autocomplete:true, sort:"asc"}},
{title:"Ersatzkennzeichen", field:"ersatzkennzeichen", headerFilter: true},
{title:"Geburtsdatum", field:"gebdatum", formatter:dateFormatter,
headerFilter: true, headerFilterFunc: function(headerValue, rowValue, rowData, filterParams) {
{
title: "Geburtsdatum",
field: "gebdatum",
formatter: dateFormatter,
headerFilter: true,
headerFilterFunc(headerValue, rowValue) {
const matches = headerValue.match(/^(([0-9]{2})\.)?([0-9]{2})\.([0-9]{4})?$/);
let comparestr = headerValue;
if(matches !== null) {
@@ -119,7 +135,17 @@ export default {
{
return Promise.resolve({ data: []});
}
return this.$api.call({url, params});
/**
* NOTE(chris): Because of a bug in Tabulator
* we need to get the params from elsewhere.
* @see https://github.com/olifolkerd/tabulator/issues/4318
*/
const apiconfig = {
...this.tabulatorOptions.ajaxConfig,
url: this.tabulatorOptions.ajaxURL,
params: this.tabulatorOptions.ajaxParams
};
return this.$api.call(apiconfig);
},
ajaxResponse: (url, params, response) => {
return response?.data;
@@ -157,25 +183,160 @@ export default {
],
focusObj: null, // TODO(chris): this should be in the filter component
lastSelected: null,
filterKontoCount0: undefined,
filterKontoMissingCounter: undefined,
filter: [],
count: 0,
filteredcount: 0,
selectedcount: 0,
currentEndpointRawUrl: ''
currentEndpoint: null
}
},
computed: {
countsToHTML: function() {
return this.$p.t('global/ausgewaehlt')
+ ': <strong>' + (this.selectedcount || 0) + '</strong>'
+ ' | '
+ this.$p.t('global/gefiltert')
+ ': '
+ '<strong>' + (this.filteredcount || 0) + '</strong>'
+ ' | '
+ this.$p.t('global/gesamt')
+ ': <strong>' + (this.count || 0) + '</strong>';
},
selectedDragObject() {
return this.selected.map(item => {
let type, id;
if (item.uid) {
type = 'student';
id = item.uid;
} else if (item.prestudent_id) {
type = 'prestudent';
id = item.prestudent_id;
} else if (item.person_id) {
type = 'person';
id = item.person_id;
}
return {
...item,
type,
id
};
});
},
downloadConfig() {
return {
csv: {
formatter: 'csv',
file: this.fileString,
options: {
delimiter: ';',
bom: true,
}
}
};
},
fileString() {
let today = new Date().toLocaleDateString('en-GB')
.replace(/\//g, '_');
return "StudentList_" + today + ".csv";
}
},
watch: {
'$p.user_language.value'(n, o) {
if (n !== o && o !== undefined && this.$refs.table.tableBuilt) {
this.translateTabulator();
}
}
},
methods: {
translateTabulator() {
this.$p
.loadCategory(['global', 'person', 'lehre', 'ui', 'profilUpdate', 'admission', 'stv'])
.then(() => {
const translations = {
uid: capitalize(this.$p.t('person/uid')),
titelpre: capitalize(this.$p.t('person/titelpre')),
nachname: capitalize(this.$p.t('person/nachname')),
vorname: capitalize(this.$p.t('person/vorname')),
wahlname: capitalize(this.$p.t('person/wahlname')),
vornamen: capitalize(this.$p.t('person/vornamen')),
titelpost: capitalize(this.$p.t('person/titelpost')),
ersatzkennzeichen: capitalize(this.$p.t('person/ersatzkennzeichen')),
gebdatum: capitalize(this.$p.t('person/geburtsdatum')),
geschlecht: capitalize(this.$p.t('person/geschlecht')),
semester: capitalize(this.$p.t('lehre/sem')),
verband: capitalize(this.$p.t('lehre/verb')),
gruppe: capitalize(this.$p.t('lehre/grp')),
studiengang: capitalize(this.$p.t('lehre/studiengang')),
studiengang_kz: capitalize(this.$p.t('lehre/studiengang_kz')),
matrikelnr: capitalize(this.$p.t('person/personenkennzeichen')),
person_id: capitalize(this.$p.t('person/person_id')),
status: capitalize(this.$p.t('global/status')),
status_datum: capitalize(this.$p.t('profilUpdate/statusDate')),
status_bestaetigung: capitalize(this.$p.t('global/status_bestaetigung')),
mail_privat: capitalize(this.$p.t('person/email_private')),
mail_intern: capitalize(this.$p.t('person/email_intern')),
anmerkungen: capitalize(this.$p.t('stv/notes_person')),
anmerkung: capitalize(this.$p.t('stv/notes_prestudent')),
orgform_kurzbz: capitalize(this.$p.t('lehre/orgform')),
aufmerksamdurch_kurzbz: capitalize(this.$p.t('person/aufmerksamDurch')),
punkte: capitalize(this.$p.t('admission/gesamtpunkte')),
aufnahmegruppe_kurzbz: capitalize(this.$p.t('stv/aufnahmegruppe_kurzbz')),
dual: capitalize(this.$p.t('lehre/dual_short')),
matr_nr: capitalize(this.$p.t('person/matrikelnummer')),
studienplan_bezeichnung: capitalize(this.$p.t('lehre/studienplan')),
prestudent_id: capitalize(this.$p.t('ui/prestudent_id')),
priorisierung_relativ: capitalize(this.$p.t('lehre/prioritaet')),
mentor: capitalize(this.$p.t('stv/mentor')),
bnaktiv: capitalize(this.$p.t('person/aktiv'))
};
/** NOTE(chris):
* use this approach because updateDefinition
* on the Tabulator columns is way slower and
* freezes up the GUI.
*/
// Overwrite definition for column show/hide
this.$refs.table.tabulator.getColumns().forEach(col => {
const trans = translations[col.getField()];
if (!trans)
return;
col.getDefinition().title = trans;
});
// Overwrite node in dom
this.$refs.table.tabulator.element
.querySelectorAll('.tabulator-col[tabulator-field]')
.forEach(el => {
const field = el.getAttribute('tabulator-field');
if (!translations[field])
return;
const title = el.querySelector('.tabulator-col-title');
if (!title)
return;
title.innerText = translations[field];
});
});
},
reload() {
this.$refs.table.reloadTable();
},
actionNewPrestudent() {
this.$refs.new.open();
},
rowSelectionChanged(data) {
rowSelectionChanged(data, rows) {
this.selectedcount = data.length;
this.lastSelected = this.selected;
this.$emit('update:selected', data);
// set selected elements draggable
const tableEl = this.$refs.table?.$refs?.table;
if (tableEl) {
const oldDragables = tableEl.querySelectorAll('[draggable]');
for (const draggable of oldDragables)
draggable.removeAttribute('draggable');
}
rows.forEach(row => row.getElement().draggable = true);
},
autoSelectRows(data) {
if (this.lastSelected) {
@@ -194,19 +355,27 @@ export default {
}
}
},
updateFilter(filter) {
this.filter = filter;
this.updateUrl();
},
updateUrl(endpoint, first) {
this.lastSelected = first ? undefined : this.selected;
if( endpoint === undefined )
console.log('function param endpoint: ' + JSON.stringify(endpoint));
console.log('current endpoint: ' + JSON.stringify(this.currentEndpoint));
if( endpoint === undefined && this.currentEndpoint === null)
{
endpoint = {url: this.currentEndpointRawUrl};
}
else if( endpoint.url === undefined )
endpoint = { url: '' };
}
else if( endpoint === undefined )
{
endpoint.url = this.currentEndpointRawUrl;
} else
endpoint = JSON.parse(JSON.stringify(this.currentEndpoint));
}
else
{
this.currentEndpointRawUrl = endpoint.url;
this.currentEndpoint = JSON.parse(JSON.stringify(endpoint));
}
endpoint.url = endpoint.url.replace(
@@ -214,34 +383,42 @@ export default {
encodeURIComponent(this.currentSemester)
);
const params = {}, filter = {};
if (this.filterKontoCount0)
filter.konto_count_0 = this.filterKontoCount0;
if (this.filterKontoMissingCounter)
filter.konto_missing_counter = this.filterKontoMissingCounter;
if (filter.konto_count_0 || filter.konto_missing_counter)
params.filter = filter;
const params = (endpoint?.params !== undefined) ? endpoint.params : {};
let method = (endpoint?.method !== undefined) ? endpoint.method : 'get';
if (this.filter.length && !endpoint.url.match(/\/search\//))
{
params.filter = this.filter;
method = 'post';
}
this.tabulatorOptions.ajaxURL = endpoint.url;
this.tabulatorOptions.ajaxParams = { ...params };
this.tabulatorOptions.ajaxConfig = {method};
if (!this.$refs.table.tableBuilt) {
if (!this.$refs.table.tabulator) {
this.tabulatorOptions.ajaxURL = endpoint.url;
this.tabulatorOptions.ajaxParams = params;
} else
if (this.$refs.table.tabulator) {
this.$refs.table.tabulator.on("tableBuilt", () => {
this.$refs.table.tabulator.setData(endpoint.url, params);
this.$refs.table.tabulator.setData(endpoint.url, params, method);
});
}
} else
this.$refs.table.tabulator.setData(endpoint.url, params);
this.$refs.table.tabulator.setData(endpoint.url, params, method);
},
dragCleanup(evt) {
if (evt.dataTransfer.dropEffect == 'none')
return; // aborted or wrong target
this.$reloadList();
},
onKeydown(e) { // TODO(chris): this should be in the filter component
if (!this.focusObj)
return;
var next;
switch (e.code) {
case 'Enter':
case 'Space':
e.preventDefault();
const e2 = new Event('click', e);
var e2 = new Event('click', e);
e2.altKey = e.altKey;
e2.ctrlKey = e.ctrlKey;
e2.shiftKey = e.shiftKey;
@@ -250,13 +427,13 @@ export default {
break;
case 'ArrowUp':
e.preventDefault();
var next = this.focusObj.previousElementSibling;
next = this.focusObj.previousElementSibling;
if (next)
this.changeFocus(this.focusObj, next);
break;
case 'ArrowDown':
e.preventDefault();
var next = this.focusObj.nextElementSibling;
next = this.focusObj.nextElementSibling;
if (next)
this.changeFocus(this.focusObj, next);
break;
@@ -296,24 +473,19 @@ export default {
}
}
},
computed: {
countsToHTML: function() {
return this.$p.t('global/ausgewaehlt')
+ ': <strong>' + (this.selectedcount || 0) + '</strong>'
+ ' | '
+ this.$p.t('global/gefiltert')
+ ': '
+ '<strong>' + (this.filteredcount || 0) + '</strong>'
+ ' | '
+ this.$p.t('global/gesamt')
+ ': <strong>' + (this.count || 0) + '</strong>';
}
},
// TODO(chris): focusin, focusout, keydown and tabindex should be in the filter component
// TODO(chris): filter component column chooser has no accessibilty features
template: `
<div class="stv-list h-100 pt-3">
<div class="tabulator-container d-flex flex-column h-100" :class="{'has-filter': filterKontoCount0 || filterKontoMissingCounter}" tabindex="0" @focusin="onFocus" @keydown="onKeydown">
<div
class="tabulator-container d-flex flex-column h-100"
:class="{'has-filter': filter.length}"
tabindex="0"
@focusin="onFocus"
@keydown="onKeydown"
v-draggable:copyLink.capture="selectedDragObject"
@dragend="dragCleanup"
>
<core-filter-cmpt
ref="table"
:description="countsToHTML"
@@ -322,38 +494,16 @@ export default {
table-only
:side-menu="false"
reload
` + /* TODO(chris): Ausgeblendet für Testing
:download="downloadConfig"
new-btn-show
*/`
:new-btn-label="$p.t('stv/action_new')"
@click:new="actionNewPrestudent"
@table-built="translateTabulator"
>
<template #filter>
<div class="card">
<div class="card-body">
<div class="input-group mb-3">
<label class="input-group-text col-4" for="stv-list-filter-konto-count-0">{{ $p.t('stv/konto_filter_count_0') }}</label>
<select class="form-select" id="stv-list-filter-konto-count-0" v-model="filterKontoCount0" @input="$nextTick(updateUrl)">
<option v-for="typ in lists.buchungstypen" :key="typ.buchungstyp_kurzbz" :value="typ.buchungstyp_kurzbz">
{{ typ.beschreibung }}
</option>
</select>
<button v-if="filterKontoCount0" class="btn btn-outline-secondary" @click="filterKontoCount0 = undefined; updateUrl()">
<i class="fa fa-times"></i>
</button>
</div>
<div class="input-group">
<label class="input-group-text col-4" for="stv-list-filter-konto-missing-counter">{{ $p.t('stv/konto_filter_missing_counter') }}</label>
<select class="form-select" id="stv-list-filter-konto-missing-counter" v-model="filterKontoMissingCounter" @input="$nextTick(updateUrl)">
<option value="alle">{{ $p.t('stv/konto_all_types') }}</option>
<option v-for="typ in lists.buchungstypen" :key="typ.buchungstyp_kurzbz" :value="typ.buchungstyp_kurzbz">
{{ typ.beschreibung }}
</option>
</select>
<button v-if="filterKontoMissingCounter" class="btn btn-outline-secondary" @click="filterKontoMissingCounter = undefined; updateUrl()">
<i class="fa fa-times"></i>
</button>
</div>
<list-filter @change="updateFilter" />
</div>
</div>
</template>
@@ -0,0 +1,102 @@
import FilterItem from './Filter/Item.js';
import ApiStvApp from '../../../../api/factory/stv/app.js';
export default {
name: "ListPrestudentsFilter",
components: {
FilterItem
},
emits: [
'change'
],
data() {
return {
filters: [],
filterConfig: [// TODO(chris): get from BE!
{
name: 'stv/konto_filter_count_0',
type: 'konto',
fixed: {
missing: true,
usestdsem: true
},
dynamic: {
buchungstyp_kurzbz: {
type: 'select',
values: {
test1: 'Test1',
test2: 'Test2'
}
}
}
},
{
name: 'stv/konto_filter_missing_counter',
type: 'konto_counter',
dynamic: {
buchungstyp_kurzbz: {
type: 'select',
values: {
test1: 'Test1',
test2: 'Test2'
}
},
samestg: {
type: 'bool',
label: 'stv/konto',
default: true
}
}
}
]
}
},
computed: {
cleanFilters() {
return this.filters.filter(filter => {
if (!filter.type)
return false;
if (Object.values(filter).some(v => v === undefined))
return false;
return true;
});
}
},
watch: {
cleanFilters(n) {
this.$emit('change', n);
}
},
methods: {
add() {
this.filters.push({});
},
remove(index) {
this.filters.splice(index, 1);
}
},
created() {
this.$api
.call(ApiStvApp.configFilter())
.then(result => {
this.filterConfig = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
template: /* html */`
<div class="stv-list-filter h-100">
<button class="btn btn-outline-dark" type="button" @click="add">
<span class="fa-solid fa-plus" aria-hidden="true"></span>
{{ $p.t('filter/filter') }}
</button>
<filter-item
v-for="(filter, i) in filters"
:key="i"
v-model="filters[i]"
:filter-config="filterConfig"
class="mt-3"
@remove="remove(i)"
/>
</div>`
};

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