From 06788bafc8a59579fa4a55c94bbb3f2a8d96b039 Mon Sep 17 00:00:00 2001 From: kindlm Date: Tue, 7 Oct 2025 18:42:50 +0200 Subject: [PATCH 01/51] =?UTF-8?q?ParseHTML=20bei=20statistik.class=20erg?= =?UTF-8?q?=C3=A4nzt.=20SVNR=20aus=20BIS-Checks=20entfernt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/statistik.class.php | 9 ++++++++- vilesci/bis/lehrgangsmeldung.php | 32 +++++++++++++++++++++++--------- vilesci/bis/studentenmeldung.php | 16 +++++++++++----- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/include/statistik.class.php b/include/statistik.class.php index fd48e7d28..364c6d1a0 100644 --- a/include/statistik.class.php +++ b/include/statistik.class.php @@ -513,6 +513,7 @@ class statistik extends basis_db $this->json=array(); $this->countRows=0; set_time_limit(600); + $parseHtml = (strpos($this->preferences, 'parseHTML: true') !== false); // In case a decryption function is used then perform password substitution $this->sql = $this->replaceSQLDecryptionPassword($this->sql); @@ -565,7 +566,13 @@ class statistik extends basis_db for($spalte=0;$spalte<$anzahl_spalten;$spalte++) { $name = $this->db_field_name($this->data,$spalte); - $this->html.= ''.$this->convert_html_chars($row->$name).''; + if ($parseHtml) { + // HTML direkt rendern + $this->html .= ''.($row->$name).''; + } else { + // wie bisher escapen + $this->html .= ''.$this->convert_html_chars($row->$name).''; + } // Umwandeln von Punkt in Komma bei Float-Werten if (is_numeric($row->$name)) { diff --git a/vilesci/bis/lehrgangsmeldung.php b/vilesci/bis/lehrgangsmeldung.php index dd7597689..f16dafc91 100644 --- a/vilesci/bis/lehrgangsmeldung.php +++ b/vilesci/bis/lehrgangsmeldung.php @@ -390,30 +390,34 @@ if($result = $db->db_query($qry)) $error_log.=(!empty($error_log)?', ':'')."Matrikelnummer ('".trim($row->matr_nr)."') ist nicht 8 Zeichen lang"; } //SVNR mu߸ 10-stellig sein + /* Alle SVNR Checks entfernt if($row->svnr!='' && $row->svnr!=null && mb_strlen(trim($row->svnr))!=10) { $error_log.=(!empty($error_log)?', ':'')."SVNR ('".trim($row->svnr)."') ist nicht 10 Zeichen lang"; - } + }*/ //Ersatzkennzeichen muß 10-stellig sein if($row->ersatzkennzeichen!='' && $row->ersatzkennzeichen!=null && mb_strlen(trim($row->ersatzkennzeichen))!=10) { $error_log.=(!empty($error_log)?', ':'')."Ersatzkennzeichen ('".trim($row->ersatzkennzeichen)."') ist nicht 10 Zeichen lang"; } + //Vergleich der letzten 6 Stellen der SVNR mit Geburtsdatum - ausser bei 01.01. und 01.07. + /* Alle SVNR Checks entfernt if($row->svnr!='' && $row->svnr!=null && substr($row->svnr,4,6)!=$row->vdat && substr($row->vdat,0,4)!='0101' && substr($row->vdat,0,4)!='0107') { $error_log_hinweis.=(!empty($error_log_hinweis)?', ':'')."SVNR ('".$row->svnr."') enthält Geburtsdatum (".$row->gebdatum.") nicht"; - } + }*/ //Vergleich der letzten 6 Stellen des Ersatzkennzeichen mit Geburtsdatum if($row->ersatzkennzeichen!='' && $row->ersatzkennzeichen!=null && substr($row->ersatzkennzeichen,4,6)!=$row->vdat) { $error_log.=(!empty($error_log)?', ':'')."Ersatzkennzeichen ('".$row->ersatzkennzeichen."') enthält Geburtsdatum (".$row->gebdatum.") nicht"; } // Wenn SVNR fehlt, darf Ersatzkennzeichen nicht fehlen (und umgekehrt) + /* Alle SVNR Checks entfernt if(($row->svnr=='' || $row->svnr==null)&&($row->ersatzkennzeichen=='' || $row->ersatzkennzeichen==null)) { $error_log.=(!empty($error_log)?', ':'')."SVNR ('".$row->svnr."') bzw. ErsKz ('".$row->ersatzkennzeichen."') fehlt"; - } + }*/ if($row->staatsbuergerschaft=='' || $row->staatsbuergerschaft==null) { $error_log.=(!empty($error_log)?', ':'')."Staatsbürgerschaft ('".$row->staatsbuergerschaft."')"; @@ -714,7 +718,7 @@ if($result = $db->db_query($qry)) $qry_ap="SELECT * FROM lehre.tbl_abschlusspruefung WHERE student_uid=".$db->db_add_param($row->student_uid)." AND abschlussbeurteilung_kurzbz!='nicht' AND abschlussbeurteilung_kurzbz IS NOT NULL"; if($result_ap = $db->db_query($qry_ap)) { - $ap=0; + $ap = array(); while($row_ap = $db->db_fetch_object($result_ap)) { if($row_ap->datum=='' || $row_ap->datum==null) @@ -725,12 +729,19 @@ if($result = $db->db_query($qry)) { $error_log.=(!empty($error_log)?', ':'')."Datum der Sponsion ('".$row_ap->sponsion."')"; } - $ap++; + if (!isset($ap[$row_ap->pruefungstyp_kurzbz])) + { + $ap[$row_ap->pruefungstyp_kurzbz] = 0; + } + $ap[$row_ap->pruefungstyp_kurzbz]++; $sponsion=$row_ap->sponsion; } - if($ap!=1) + foreach ($ap as $typ => $count) { - $error_log.=(!empty($error_log)?', ':'').$ap." bestandene Abschlussprüfungen"; + if ($count > 1) + { + $error_log.=(!empty($error_log)?', ':'').$count." bestandene Abschlussprüfungen desselben Typs"; + } } } } @@ -815,13 +826,16 @@ if($result = $db->db_query($qry)) ".$row->vorname." ".$row->nachname.""; + /* Alle SVNR Checks entfernt if($row->svnr!='') { $datei.=" ".$row->svnr.""; - } + }*/ // Ersatzkennzeichen nur inkludieren wenn svnr nicht gesetzt - if($row->ersatzkennzeichen!='' && $row->svnr == null) + // Alle SVNR Checks entfernt + // if($row->ersatzkennzeichen!='' && $row->svnr == null) + if($row->ersatzkennzeichen!='') { $datei.=" ".$row->ersatzkennzeichen.""; diff --git a/vilesci/bis/studentenmeldung.php b/vilesci/bis/studentenmeldung.php index 8c7e31d36..7e06726ec 100644 --- a/vilesci/bis/studentenmeldung.php +++ b/vilesci/bis/studentenmeldung.php @@ -930,26 +930,29 @@ function GenerateXMLStudentBlock($row) { $error_log.=(!empty($error_log)?', ':'')."Matrikelnummer ('".trim($row->matr_nr)."') ist nicht 8 Zeichen lang"; } + /* Alle SVNR Checks entfernt if($row->svnr!='' && $row->svnr!=null && mb_strlen(trim($row->svnr))!=10) { $error_log.=(!empty($error_log)?', ':'')."SVNR ('".trim($row->svnr)."') ist nicht 10 Zeichen lang"; - } + }*/ if($row->ersatzkennzeichen!='' && $row->ersatzkennzeichen!=null && mb_strlen(trim($row->ersatzkennzeichen))!=10) { $error_log.=(!empty($error_log)?', ':'')."Ersatzkennzeichen ('".trim($row->ersatzkennzeichen)."') ist nicht 10 Zeichen lang"; } + /* Alle SVNR Checks entfernt if($row->svnr!='' && $row->svnr!=null && substr($row->svnr,4,6)!=$row->vdat && substr($row->vdat,0,4)!='0101' && substr($row->vdat,0,4)!='0107') { $error_log_hinweis.=(!empty($error_log_hinweis)?', ':'')."SVNR ('".$row->svnr."') enthält Geburtsdatum (".$datum_obj->formatDatum($row->gebdatum,'d.m.Y').") nicht (Nicht BIS-Relevant)"; - } + }*/ if($row->ersatzkennzeichen!='' && $row->ersatzkennzeichen!=null && substr($row->ersatzkennzeichen,4,6)!=$row->vdat) { $error_log.=(!empty($error_log)?', ':'')."Ersatzkennzeichen ('".$row->ersatzkennzeichen."') enthält Geburtsdatum (".$datum_obj->formatDatum($row->gebdatum,'d.m.Y').") nicht"; } + /* Alle SVNR Checks entfernt if(($row->svnr=='' || $row->svnr==null)&&($row->ersatzkennzeichen=='' || $row->ersatzkennzeichen==null)) { $error_log.=(!empty($error_log)?', ':'')."SVNR ('".$row->svnr."') bzw. ErsKz ('".$row->ersatzkennzeichen."') fehlt"; - } + }*/ if($row->staatsbuergerschaft=='' || $row->staatsbuergerschaft==null) { $error_log.=(!empty($error_log)?', ':'')."Staatsbürgerschaft ('".$row->staatsbuergerschaft."')"; @@ -1510,14 +1513,17 @@ function GenerateXMLStudentBlock($row) " . $row->vorname . " " . $row->nachname . ""; + /* Alle SVNR Checks entfernt if ($row->svnr != '') { $datei .= " " . $row->svnr . ""; - } + }*/ // Ersatzkennzeichen nur inkludieren wenn svnr nicht gesetzt - if ($row->ersatzkennzeichen != '' && $row->svnr == null) + // Alle SVNR Checks entfernt + // if ($row->ersatzkennzeichen != '' && $row->svnr == null) + if ($row->ersatzkennzeichen != '') { $datei .= " " . $row->ersatzkennzeichen . ""; From c724c6a20f1dabaf4f0cb2c5c5924b4bb9fecc01 Mon Sep 17 00:00:00 2001 From: kindlm Date: Tue, 27 Jan 2026 16:53:33 +0100 Subject: [PATCH 02/51] =?UTF-8?q?MathML=20auf=20Darstellungs-Testseite=20?= =?UTF-8?q?=C3=BCberarbeitet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cis/public/testtool_test/MathML_Beispiel.png | Bin 0 -> 4923 bytes cis/public/testtool_test/testseite.php | 129 +++++++++++-------- 2 files changed, 75 insertions(+), 54 deletions(-) create mode 100644 cis/public/testtool_test/MathML_Beispiel.png diff --git a/cis/public/testtool_test/MathML_Beispiel.png b/cis/public/testtool_test/MathML_Beispiel.png new file mode 100644 index 0000000000000000000000000000000000000000..d6ef2403f2a84eccaa333fdbfe306fdf6b08ba2a GIT binary patch literal 4923 zcmb_gXH-*Lw+%KFRK%cIX%dQ*XrxNJO6U+kq(ivUJJM^wLYHC^I-xfK2}o}N>4eaG zN9hn+=%K#cckdYQdq3}Z@5d=W_Bm^=J@%Sw&K05tleBWP^*TUyKE;KC=NPAR{%wy-Hw|R&P|1F0;-{XBbTk?t4Z+wqz};s*)Q= zxUEk!VP9o%!$y_G72B#I0&z%iYB2E;V`X!G|F&q*Ak92{qh++%lyD@L;JMg$u+tH< zw-g&vx6>)U)45Zt#>H?IV6dTz;RgVCKd@ii0w`ShMTNTl^CJ8G57hDy0Nqzp022!y zApQr6%Hv-wF!%_$(>;&jzmQ5l<5Uf3L{%nYO$wDNT#&b`R zq&3HECbslsd9Cob-i_m-%Jam%yUbfD(U!U~%v^XX{fXDY_7b!0fp^xF*dR&-OSpQe)$I>)xYwW*Z1g= zXQlJEWM_Iiyi4pB(+ji*y}FW;O-)#N10h**{pJ?m(THS9IG5%-IBasmhe&y}_Lm7Y zPZl8g!snZp@tXP$k#_{$y-*>+LK<@|C9`Ev+xYfA4PM#AC=6C3%SK58iF}vdHF%Oh zE~Oyq+mdfi%dh00@PeAEcYzsRB0#&S1{WxF{-I%qP`ygD|2?#Rbwn0>bKKfs?*h$D z2j+paf=uO*9}69R)@x0Q)V@MZ;rcxZ2}o0$vgG-QVJDOrV+8g;PH(HBbE9~Z?XSz% zU1^`Q#9m8L6)Yy}#Nkq}k5dkNs>-52XM$!1jPW;I(4om3E@u>QTjs|R@U5oAzC#RC zWZ7GViNWuA*-y=4;h(T_dxXL3_CL$*aT)JjZv6ZlVUL~wJdxJ>gk)7|@`P~qx=-ES zHM|I*?d5R3fEeL(^qUd*c`YQJizxvU4C2*pce0a-xjQ!K zBz)rG%gKJVv0+?11$Oll%T=IUtW+_&s0(Sf&FmImjtRRI%+Q1}uW?t|DjV`v}8$yN+4~3qs z;IbaX$|A-Mdef8`y@^bUCDc6KN}NQ%e1(d%~H-|T2-Fz#t=f`%hZ7$G}Yn1AjbbYwf?(5EbAz~W{^?84!#G|Hl|{~ zFJi&#-;&OqH4i?^Oo(Y`ytg|@mfW*|#}|83e`LSd6UF_!QZPNi9HbWk8df~R47hH4 zdGnsO8*uQ3{xDOTDY+>R+3J)k$QwAr8C|(>8+o>ynk&GBwM3l4iIUwqSHFx{%Bm<0 z<1CWCnl2k0#Vn!&#Y;!NhNkNb8w+;>yYk$hxIX@_zpfAS;rehfEWoo0T*tt7MMYPa zW7C$T&0WB88f~e!H+iFl;zSKOjegdga|(Kz z0`JAEJuA~al2^k_oc{pV&8zL`>wcsnqSIO|V2pk7l1aBNy_?nWmRuvx%Dk$qJe*8c z<=Iyj9NHdjNvQOz6-;VLaMd#w4n{06IW3i*3rs6{(J~s}?^N@GX zF?hi((fF@ip$&r^>V+d4Znk`^)2lNMQqj$40bB^BjJiWqHN)l;^2aypk0${V@W(1M z{olaPOxMlgq@Ci6L}0>i%OoVKJEF!OvG12yZV2wX3sfn4g!Q*H{VKcTdG5+Xn5S&j z{C$*9X>IyfREmYriLBKztrj2v-3CVKJIRg!nD;;7Mb! zlwy3Z$duX;4uM8i#qCnlX6Fu}bnD)AFu$qI?r&ekRVy zuoN6PKpopKevJZ_x1zdOK@u-sF1bAAs7@W{r?&^Bzj68KO)j5de5jp)I+{A@UUK`C z$v#`wF(d7P;QFMUW>5z4X10SRrN+!HC6GL~=@m-y^ol~>p;@WPmr+p)2rP!P!t1aEBdc|I%ooS_&s`9 zhg};!zhHC%->*!Dx+68cHygS{#Zb+N$Ww#19R?}F*PZh#& zuIA@Uv=!$X1@k4x75rq_pzn*nn=vk4(YEaL(EM=H|HOEA|CA0}XRR7`o|*~wtJ*(x zNAbhJ|N={m7vh5^Y=dcj#)XFwYzeQp )<~E|0>W8a?1}e_ZTw-seRMeb$u)1v+ z@b1ir?bXfKNTZoEv#@QsDI8Jj4|)XK{4ZURFomm48R**mnMn=od>Gkg)heE_WfXBN z9}z9yub!|`vk%Rd^Z6?l%k%ThZFKc#`rKMwp}UR7TpHC)Z<*Sm>`7+Fsmp#+b;-lu zjvG6BPKBC1I4h1|%ad86pqZcC*Xik9$vGUE*jkK-cbQkr9$)yrL)RW=`?eJi-3>Mn zYq^~Z6gCoKsZ^0ywF@oD)9W9Yrc{IV*psBpfPA$aeiht1d&r#Ib~&6R=HAFG;y&Nd zHRwV5Wx+?U&$0R?@|lF1sT_G}FvIXAg4{LWiUF6Jx*wAYl%%}Y1J9al#scy!9-LRA zFG`Uon8xc;^u)~%6rqb(IUn3yTbHDkrR;UrWf@1%pNNqR(Pc%3E>>V-E{mM=#U z^$EX|lU5#YlV0fxKO+*i(b>eES6sMPvlQA%i|V@Al}K8sH=@7vn|dzg#OPRxn6H}c z=QW46n4t*L*>Y?tn;P0wBF3-(^`lr#_{AUDaHv$?*=9jHqq z8( z9USadHV6$QT+Hqnl7r48@NFG}ISF1czxpqh+Uj-ht8W)65inC-_)9kIRCnMC=_DLQ%=mRS+ zZ0@lwre+TlysY~8*t95KMsCE2XXN~M`Ku{LdvN%2arHYz-xk=HAllhlE6wqEzfce2#v^N|gawYWmt#&9ep>?cxg8mW ze1+>CMoY}6M^n*u6k8#@>3i5biap+Ft*yoa`%-tcZI!Jq2wC#sYrM?avew;{UToGL z;jX|f+5`w3N*wxU(=)ZAlQN}8zl$FiY)_B1{j5rv@+lSW;HW=>35Xmz;RU*fESXK2 zfv!e}HLN4o!3Ej)&)mbX*|313(K<2N+%oSpUq3O5IORbTjWj*`{iaTSu!Iq^Di*|z zC~68#Ebwue8|L)CE4e#nyiEORt0TPU+|o+jZfj_@Ir7r0!?tMab8dTeyB~>rA;sRS z98XV1AQf7$E^rz-GH=%*H;XLfxl}^bwjZt|!@ap4LOIXBXL>%%(cR#KmGDB!Hot%8 zgaCKc&zeX^gP8Cp^@)z~iiLH1Zi$7ncWxA?sf?{{Z_s-!kaLNL-x21tCjVvRM8Uv> zurTO%w5XbAWhLr%qCGV+V>=>SqA;zM?MHGv^9P?z51BaB%}n77acB`em(K&djCAO{ zqIck5*QUGN?M>k+-e{E)UU{MfcZ>_EG_ZkDBo4ArZ~Uj z-2P~_37c|ay&@&~i?tQnnm4KDocEn~;MLBb#s}=(1{(HtZ%Wok`$AO*R+Jtt{NV8u zQWEwvft8}9>9al=hf>7Zi;aghTXvhhqczeduvqoreb`Q{hZ~HvT^B#@VjufUOm_MHIujs_ahbhth^n=Q+VFxj z9I5=JCm|$>K~N6ytu4$(SLxDHX$HNSSHUBStu{T)eRVMgVYLod$u>bpm)`%hr{yjOh(R6VS#X`g;^#rO6BG9&Q*P(8!7?n8Ncx}bm(R(!>1(@s+x3Vgg zeNIh5&n+T}R#U$reO&9ZlEnc*CVe=08j zWO48_8gjK5Uerij?3?`f3!_zA6G19-ieUE1W{3W$`aLEOdG@U}-FZ>ZxXr|;;E{>u zu%$=yxQV~kNGiZG(v7d&AhvwpqX5+W9M=&#+w}_dQB!jx_5E`YYwNC%L2qN9V39RT zZnxrgT%1n+sX^?hSg&Q9t5qrf=SwriFuRu6jp2K=?II?uk1#vg6GE03{+jpvFL(U^ gF#P!shjPX&a}T4$KiJtunT7!rWMEM2>pwsJ7YZ=8c>n+a literal 0 HcmV?d00001 diff --git a/cis/public/testtool_test/testseite.php b/cis/public/testtool_test/testseite.php index a200b95b2..34ea12818 100644 --- a/cis/public/testtool_test/testseite.php +++ b/cis/public/testtool_test/testseite.php @@ -86,67 +86,88 @@ echo '';

Formel / Formula

- - 5 - 3 - - - + - - 7 - 6 - - = - - - 10 - 6 - - + - - - 7 - 6 - - = - - 17 - - 6 - + + + 5 + 3 + + + + + 7 + 6 + + = + + 10 + 6 + + + + + 7 + 6 + + = + + 17 + 6 + +

- - - - k=1 - 5 - - - - (-1) - k+1 - - - - - - x - 2k + 1 - - - - (2k+1)! - - - - + + + + + k + = + 1 + + 5 + + + + + ( + - + 1 + ) + + + k + + + 1 + + + + + + x + + 2 + k + + + 1 + + + + + ( + 2 + k + + + 1 + ) + ! + + + +

Bild / Picture

- Beispielbild + Beispielbild
From fbe10cc2a1301626b9d553bd9096e7b6a28fd7f7 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Thu, 29 Jan 2026 15:08:24 +0100 Subject: [PATCH 03/51] PersonModel loadAllStudentUIDSForPersonID used in anw extension AdministrationApi aka Entschuldigungsmanagement --- application/models/person/Person_model.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/application/models/person/Person_model.php b/application/models/person/Person_model.php index 233cfc751..e72b24de4 100644 --- a/application/models/person/Person_model.php +++ b/application/models/person/Person_model.php @@ -420,4 +420,17 @@ class Person_model extends DB_Model return success($result); } } + + public function loadAllStudentUIDSForPersonID($person_id) { + $qry = "SELECT + CONCAT(tp.vorname, ' ', tp.nachname) AS name, + ARRAY_AGG(DISTINCT b.uid ORDER BY b.uid) AS uids + FROM public.tbl_student s + JOIN public.tbl_benutzer b ON s.student_uid = b.uid + JOIN public.tbl_person tp ON b.person_id = tp.person_id + GROUP BY tp.vorname, tp.nachname, b.aktiv, b.person_id + HAVING b.person_id = ? AND b.aktiv IS TRUE;"; + + return $this->execReadOnlyQuery($qry, [$person_id]); + } } From 366cb16b618308af2cc2199d59f5008956f8871c Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Fri, 30 Jan 2026 14:10:28 +0100 Subject: [PATCH 04/51] anw phrasen "studentByLVATitle" & "kontrolliertVon", slight adjustment of fullscreen modal button so it looks similar --- public/js/components/Bootstrap/Modal.js | 14 ++++++--- system/phrasesupdate.php | 40 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/public/js/components/Bootstrap/Modal.js b/public/js/components/Bootstrap/Modal.js index a84d9d8d7..f1df2a6b1 100644 --- a/public/js/components/Bootstrap/Modal.js +++ b/public/js/components/Bootstrap/Modal.js @@ -135,10 +135,16 @@ export default {
- - - diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 0a4028a27..7fa78f7d1 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -38,6 +38,7 @@ export const AbgabetoolAssistenz = { return { abgabeTypeOptions: Vue.computed(() => this.abgabeTypeOptions), allowedNotenOptions: Vue.computed(() => this.allowedNotenOptions), + notenOptionsNonFinal: Vue.computed(() => this.notenOptionsNonFinal), turnitin_link: Vue.computed(() => this.turnitin_link), old_abgabe_beurteilung_link: Vue.computed(() => this.old_abgabe_beurteilung_link), abgabetypenBetreuer: Vue.computed(() => this.abgabeTypeOptions) @@ -86,6 +87,7 @@ export const AbgabetoolAssistenz = { notenOptions: null, allowedNotenFilterOptions: null, allowedNotenOptions: null, + notenOptionsNonFinal: null, serienTermin: Vue.reactive({ datum: new Date(), bezeichnung: { @@ -1089,6 +1091,10 @@ export const AbgabetoolAssistenz = { this.allowedNotenOptions = this.notenOptions.filter( opt => res.data[1].includes(opt.note) ); + + this.notenOptionsNonFinal = this.notenOptions.filter( + opt => res.data[2].includes(opt.note) + ) } this.allowedNotenFilterOptions = [ diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index df521d52d..67e1d09af 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -22,6 +22,7 @@ export const AbgabetoolMitarbeiter = { abgabeTypeOptions: Vue.computed(() => this.abgabeTypeOptions), abgabetypenBetreuer: Vue.computed(() => this.abgabetypenBetreuer), allowedNotenOptions: Vue.computed(() => this.allowedNotenOptions), + notenOptionsNonFinal: Vue.computed(() => this.notenOptionsNonFinal), turnitin_link: Vue.computed(() => this.turnitin_link), old_abgabe_beurteilung_link: Vue.computed(() => this.old_abgabe_beurteilung_link) } @@ -50,6 +51,7 @@ export const AbgabetoolMitarbeiter = { abgabeTypeOptions: null, notenOptions: null, allowedNotenOptions: null, + notenOptionsNonFinal: null, serienTermin: Vue.reactive({ datum: new Date(), bezeichnung: { @@ -301,7 +303,15 @@ export const AbgabetoolMitarbeiter = { pa.abgabetermine = res.data[0].retval pa.isCurrent = res.data[1] - const paIsBenotet = pa.note !== null + let paIsBenotet = false + if(pa.note !== undefined && pa !== null) { + // check if the note is not defined as a non final projektarbeit note + const opt = this.notenOptionsNonFinal.find(opt => opt.note) + // if thats the case allow further work + if(opt) paIsBenotet = false + // else the PA is to be considered finished + paIsBenotet = true + } pa.abgabetermine.forEach(termin => { termin.note = this.allowedNotenOptions.find(opt => opt.note == termin.note) @@ -471,6 +481,10 @@ export const AbgabetoolMitarbeiter = { this.allowedNotenOptions = this.notenOptions.filter( opt => res.data[1].includes(opt.note) ) + + this.notenOptionsNonFinal = this.notenOptions.filter( + opt => res.data[2].includes(opt.note) + ) } }).catch(e => { From 136d6f9f286c3d231560e9005c724cb528087544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96sterreicher?= Date: Wed, 11 Feb 2026 16:08:15 +0100 Subject: [PATCH 12/51] =?UTF-8?q?Fix=20von=20BFI=20=C3=BCbernommen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/anwesenheit.class.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/include/anwesenheit.class.php b/include/anwesenheit.class.php index f601d6b95..9493a3d4b 100644 --- a/include/anwesenheit.class.php +++ b/include/anwesenheit.class.php @@ -489,7 +489,7 @@ class anwesenheit extends basis_db gesamt AS gesamtstunden, anwesend, nichtanwesend, - trunc(100-(nichtanwesend/gesamt)*100,2) AS prozent + CASE WHEN gesamt = 0 THEN 100.00 ELSE trunc(100-(nichtanwesend/(gesamt))*100,2) END AS prozent FROM( SELECT @@ -499,9 +499,10 @@ class anwesenheit extends basis_db lehrveranstaltung_id, bezeichnung, student_uid, - COUNT(stundenplan_id) AS gesamt, - CASE WHEN anwesend.summe IS NULL THEN 0 ELSE anwesend.summe END AS anwesend, - CASE WHEN nichtanwesend.summe IS NULL THEN 0 ELSE nichtanwesend.summe END AS nichtanwesend + --COUNT(stundenplan_id) AS gesamts, + COALESCE(anwesend.summe, 0) + COALESCE(nichtanwesend.summe, 0) AS gesamt, + COALESCE(anwesend.summe, 0) AS anwesend, + COALESCE(nichtanwesend.summe, 0) AS nichtanwesend FROM ( From 3465e299f7ec81bd373d9806d14c31e1a437d19f Mon Sep 17 00:00:00 2001 From: ma0048 Date: Thu, 12 Feb 2026 08:16:31 +0100 Subject: [PATCH 13/51] tag - helper and formatter --- public/js/helpers/TagHelper.js | 124 ++++++++++++++++++ .../tabulator/filters/extendedHeaderFilter.js | 2 +- public/js/tabulator/formatter/tags.js | 67 ++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 public/js/helpers/TagHelper.js create mode 100644 public/js/tabulator/formatter/tags.js diff --git a/public/js/helpers/TagHelper.js b/public/js/helpers/TagHelper.js new file mode 100644 index 000000000..9282aa167 --- /dev/null +++ b/public/js/helpers/TagHelper.js @@ -0,0 +1,124 @@ +export function addTagInTable(addedTag, rows, matchKey, tagsKey = "tags") +{ + if (!addedTag || !Array.isArray(addedTag.response)) + return; + + rows.forEach(row => + { + const rowData = row.getData(); + let updated = false; + + addedTag.response.forEach(tag => + { + if (rowData[matchKey] !== tag[matchKey]) + return; + + let tags; + try { + tags = JSON.parse(rowData[tagsKey] || "[]"); + } catch (e) { + tags = []; + } + + if (!Array.isArray(tags)) + tags = []; + + if (tags.some(t => t?.id === tag.id)) + return; + + let newTag = { ...addedTag, id: tag.id }; + + tags.unshift(newTag); + + rowData[tagsKey] = JSON.stringify(tags); + updated = true; + }); + + if (updated) + row.update(rowData); + }); +} + +export function deleteTagInTable(deletedTag, rows, tagsKeys = ['tags']) +{ + if (!Array.isArray(tagsKeys)) + tagsKeys = [tagsKeys]; + + rows.forEach(row => { + let rowData = row.getData(); + let updates = {}; + let changed = false; + + tagsKeys.forEach(key => { + let tags; + + try { + tags = JSON.parse(rowData[key] || "[]"); + } catch (e) { + tags = []; + } + + if (!Array.isArray(tags)) + return; + + let filtered = tags.filter(tag => tag?.id !== deletedTag); + + if (filtered.length !== tags.length) + { + updates[key] = JSON.stringify(filtered); + changed = true; + } + }); + + if (changed) { + row.update(updates); + row.reformat(); + } + }); +} + + +export function updateTagInTable(updatedTag, rows, fields = ['tags']) +{ + if (!Array.isArray(fields)) + fields = [fields]; + + rows.forEach(row => + { + const rowData = row.getData(); + let updated = false; + + fields.forEach(field => + { + if (!rowData[field]) + return; + + let fieldData; + try { + fieldData = JSON.parse(rowData[field] || "[]"); + } catch (e) { + return; + } + + if (!Array.isArray(fieldData)) + return; + + let index = fieldData.findIndex(tag => tag?.id === updatedTag.id); + + if (index !== -1) + { + fieldData[index] = { ...updatedTag }; + let updatedFieldData = JSON.stringify(fieldData); + + if (updatedFieldData !== rowData[field]) + { + rowData[field] = updatedFieldData; + updated = true; + } + } + }); + + if (updated) + row.update(rowData); + }); +} diff --git a/public/js/tabulator/filters/extendedHeaderFilter.js b/public/js/tabulator/filters/extendedHeaderFilter.js index 7bf86c119..35b66dc1c 100644 --- a/public/js/tabulator/filters/extendedHeaderFilter.js +++ b/public/js/tabulator/filters/extendedHeaderFilter.js @@ -146,7 +146,7 @@ export function tagHeaderFilter(headerValue, rowValue, rowData, filterParams) if (Array.isArray(data)) { combinedText = data - .filter(item => item?.done === false) + .filter(item => item?.done !== true) .map(item => `${item?.beschreibung} ${item?.notiz}`) .join(' '); } diff --git a/public/js/tabulator/formatter/tags.js b/public/js/tabulator/formatter/tags.js new file mode 100644 index 000000000..0d2f5004c --- /dev/null +++ b/public/js/tabulator/formatter/tags.js @@ -0,0 +1,67 @@ +export function tagFormatter(cell, tagComponent) +{ + let tags = cell.getValue(); + if (!tags) return; + + let container = document.createElement('div'); + container.className = "d-flex gap-1"; + + let parsedTags = JSON.parse(tags); + let maxVisibleTags = 2; + + const rowData = cell.getRow().getData(); + if (rowData._tagExpanded === undefined) { + rowData._tagExpanded = false; + } + + const renderTags = () => { + container.innerHTML = ''; + parsedTags = parsedTags.filter(item => item !== null); + + parsedTags.sort((a, b) => { + let adone = a.done ? 1 : 0; + let bbone = b.done ? 1 : 0; + + if (adone !== bbone) + { + return adone - bbone; + } + return b.id - a.id; + }); + const tagsToShow = rowData._tagExpanded ? parsedTags : parsedTags.slice(0, maxVisibleTags); + + tagsToShow.forEach(tag => { + if (!tag) return; + let tagElement = document.createElement('span'); + tagElement.innerText = tag.beschreibung; + tagElement.title = tag.notiz; + tagElement.className = "tag " + tag.style; + if (tag.done) tagElement.className += " tag_done"; + + tagElement.addEventListener('click', (event) => { + event.stopPropagation(); + event.preventDefault(); + tagComponent.editTag(tag.id); + }); + + container.appendChild(tagElement); + }); + + if (parsedTags.length > maxVisibleTags) { + let toggle = document.createElement('button'); + toggle.innerText = (rowData._tagExpanded ? '- ' : '+ ') + (parsedTags.length - maxVisibleTags); + toggle.className = "display_all"; + toggle.title = rowData._tagExpanded ? "Tags ausblenden" : "Tags einblenden"; + + toggle.addEventListener('click', () => { + rowData._tagExpanded = !rowData._tagExpanded; + renderTags(); + }); + + container.appendChild(toggle); + } + }; + + renderTags(); + return container; +} \ No newline at end of file From 0a97e5781ec21c60241cca768b46d06bd8a0e2eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96sterreicher?= Date: Thu, 12 Feb 2026 11:02:16 +0100 Subject: [PATCH 14/51] Nicht beurteilt aus Default Config entfernt --- application/config/abgabe.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/config/abgabe.php b/application/config/abgabe.php index f806e1ef8..82782b043 100644 --- a/application/config/abgabe.php +++ b/application/config/abgabe.php @@ -28,7 +28,7 @@ $config['RELEVANT_PAABGABETYPEN_SAMMELMAIL_STUDENT'] = ['qualgate1', 'qualgate2' $config['ALLOWED_NOTEN_ABGABETOOL'] = [10, 14]; // tbl_note pk // benotete projektarbeiten sperren weitere terminanlage & bearbeitung, diese noten sind ausnahmen dieser Regel // wie zB "Nicht beurteilt" & "Noch nicht eingetragen" -$config['NONFINAL_NOTEN_ABGABETOOL'] = [7, 9]; +$config['NONFINAL_NOTEN_ABGABETOOL'] = [9]; $config['beurteilung_link_fallback'] = 'addons/fhtw/content/projektbeurteilung/projektbeurteilungDocumentExport.php?projektarbeit_id=?&betreuerart_kurzbz=?&person_id=?'; $config['PROJEKTARBEITSBEURTEILUNG_MAIL_BASELINK_ERSTBEGUTACHTER'] = 'index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/ProjektarbeitsbeurteilungErstbegutachter'; From ed170645df97af47d341db6f132f220f5240c829 Mon Sep 17 00:00:00 2001 From: Harald Bamberger Date: Thu, 12 Feb 2026 11:27:50 +0100 Subject: [PATCH 15/51] use plsql function public.get_rolle_prestudent instead of local sql --- .../api/frontend/v1/stv/Student.php | 11 +++------- .../api/frontend/v1/stv/Students.php | 22 +++++-------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/application/controllers/api/frontend/v1/stv/Student.php b/application/controllers/api/frontend/v1/stv/Student.php index 943577bb3..2721bbd6f 100644 --- a/application/controllers/api/frontend/v1/stv/Student.php +++ b/application/controllers/api/frontend/v1/stv/Student.php @@ -136,14 +136,9 @@ class Student extends FHCAPI_Controller ); } $this->PrestudentModel->addSelect( - "( - SELECT status_kurzbz - FROM public.tbl_prestudentstatus pss - WHERE pss.prestudent_id = public.tbl_prestudent.prestudent_id - AND pss.studiensemester_kurzbz = " . $this->PrestudentModel->escape($studiensemester_kurzbz) . " - ORDER BY GREATEST(pss.datum, '0001-01-01') DESC - LIMIT 1 - ) AS statusofsemester" + "public.get_rolle_prestudent(public.tbl_prestudent.prestudent_id, " + . $this->PrestudentModel->escape($studiensemester_kurzbz) + . ") AS statusofsemester" ); $this->PrestudentModel->addJoin('public.tbl_student s', 'prestudent_id', 'LEFT'); diff --git a/application/controllers/api/frontend/v1/stv/Students.php b/application/controllers/api/frontend/v1/stv/Students.php index 9dbea65f2..acacca052 100644 --- a/application/controllers/api/frontend/v1/stv/Students.php +++ b/application/controllers/api/frontend/v1/stv/Students.php @@ -801,14 +801,9 @@ class Students extends FHCAPI_Controller //add status per semester $this->PrestudentModel->addSelect( - "( - SELECT status_kurzbz - FROM public.tbl_prestudentstatus pss - WHERE pss.prestudent_id = public.tbl_prestudent.prestudent_id - AND pss.studiensemester_kurzbz = " . $this->PrestudentModel->escape($studiensemester_kurzbz) . " - ORDER BY GREATEST(pss.datum, '0001-01-01') DESC - LIMIT 1 - ) AS statusofsemester" + "public.get_rolle_prestudent(public.tbl_prestudent.prestudent_id, " + . $this->PrestudentModel->escape($studiensemester_kurzbz) + . ") AS statusofsemester" ); $this->addSelectPrioRel(); @@ -897,14 +892,9 @@ class Students extends FHCAPI_Controller //add status per semester $this->PrestudentModel->addSelect( - "( - SELECT status_kurzbz - FROM public.tbl_prestudentstatus pss - WHERE pss.prestudent_id = public.tbl_prestudent.prestudent_id - AND pss.studiensemester_kurzbz = " . $this->PrestudentModel->escape($studiensemester_kurzbz) . " - ORDER BY GREATEST(pss.datum, '0001-01-01') DESC - LIMIT 1 - ) AS statusofsemester" + "public.get_rolle_prestudent(public.tbl_prestudent.prestudent_id, " + . $this->PrestudentModel->escape($studiensemester_kurzbz) + . ") AS statusofsemester" ); $this->PrestudentModel->addSelect('UPPER(stg.typ || stg.kurzbz) AS studiengang'); From e016deb04276b598f4319774feb549debcd70eb4 Mon Sep 17 00:00:00 2001 From: Harald Bamberger Date: Thu, 12 Feb 2026 13:14:23 +0100 Subject: [PATCH 16/51] add more space between download and delete button --- public/js/components/Form/Upload/Dms/Item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/components/Form/Upload/Dms/Item.js b/public/js/components/Form/Upload/Dms/Item.js index 45c60419e..cc8db7827 100644 --- a/public/js/components/Form/Upload/Dms/Item.js +++ b/public/js/components/Form/Upload/Dms/Item.js @@ -27,7 +27,7 @@ export default {
  • {{ modelValue.name }} - + - +
  • From 043b1bcf114047eb39ece39ad801a3fbe330b4d7 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Thu, 12 Feb 2026 17:38:00 +0100 Subject: [PATCH 21/51] extracted email split method from stv/kontakt component to helperfile; adjusted that method to take subject param & make phrasen/alert call via parameter reference; --- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 19 +++---- .../Details/Kontaktieren.js | 49 ++----------------- public/js/helpers/EmailHelpers.js | 45 +++++++++++++++++ 3 files changed, 56 insertions(+), 57 deletions(-) create mode 100644 public/js/helpers/EmailHelpers.js diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index c0c8d7ba8..a7da9da5a 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -7,6 +7,7 @@ import ApiAbgabe from '../../../api/factory/abgabe.js' import ApiStudiensemester from '../../../api/factory/studiensemester.js'; import AbgabeterminStatusLegende from "./StatusLegende.js"; import FhcOverlay from "../../Overlay/FhcOverlay.js"; +import { splitMailsHelper } from "../../../helpers/EmailHelpers.js" // spoofed date testing // const todayISO = '2025-08-08' @@ -226,18 +227,17 @@ export const AbgabetoolAssistenz = { ]}; }, methods: { - sammelMailStudent() { + sammelMailStudent(param) { + const emails = this.selectedData .map(row => `${row.student_uid}@${this.domain}`) .join(','); - + const uniqueRecipients = [...new Set(emails)]; const subject = this.$p.t('abgabetool/c4sammelmailStudentBetreff', [this.selectedStudiengangOption?.bezeichnung]); - - const href = `mailto:${emails}?subject=${subject}`; - - window.location.href = href + splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p) }, - sammelMailBetreuer() { + sammelMailBetreuer(param) { + const recipientList = []; this.selectedData.forEach(row => { if (row.betreuer_mail) recipientList.push(row.betreuer_mail); @@ -246,11 +246,8 @@ export const AbgabetoolAssistenz = { // actually not necessary for email clients but looks better for assistenz if we avoid duplicates here const uniqueRecipients = [...new Set(recipientList)]; - const subject = this.$p.t('abgabetool/c4sammelmailBetreuerBetreff', [this.selectedStudiengangOption?.bezeichnung]); - const href = `mailto:${uniqueRecipients.join(',')}?subject=${encodeURIComponent(subject)}`; - - window.location.href = href; + splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p) }, selectHandler(e, cell) { const row = cell.getRow(); diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Kontaktieren.js b/public/js/components/Stv/Studentenverwaltung/Details/Kontaktieren.js index bd7554a47..43995b918 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details/Kontaktieren.js +++ b/public/js/components/Stv/Studentenverwaltung/Details/Kontaktieren.js @@ -1,3 +1,4 @@ +import { splitMailsHelper } from "../../../../helpers/EmailHelpers.js" export default { name: "Kontaktieren", computed: { @@ -22,60 +23,16 @@ export default { }, methods: { - async splitMails(mails, event) { - let splititem = ","; - let maillist = mails.join(splititem); - let mailto = ""; - - if (maillist.length > 2024) - { - if (await this.$fhcAlert.confirm({message: this.$p.t('stv', 'zuvieleEMails') }) === false) - return; - } - - let firstrun = true; - let useBcc = event?.ctrlKey || event?.metaKey; - while (maillist.length > 0) - { - if (maillist.length > 2024) - { - let splitposition = maillist.lastIndexOf(splititem, 1900); - mailto = maillist.substring(0, splitposition); - maillist = maillist.substring(splitposition + 1); - } - else - { - mailto = maillist; - maillist = ""; - } - - let mailLink = useBcc ? `mailto:?bcc=${mailto}` : `mailto:${mailto}`; - - if (firstrun) - { - window.location.href = mailLink; - firstrun = false; - } - else - { - if (await this.$fhcAlert.confirm({message: this.$p.t('stv', 'weitereEMail')}) === true) - { - window.location.href = mailLink; - } - } - - } - }, internMail(event) { if (this.internMails.length) { - this.splitMails(this.internMails, event); + splitMailsHelper(this.privateMails, event, null, this.$fhcAlert, this.$p) } }, privateMail(event) { if (this.privateMails.length) { - this.splitMails(this.privateMails, event); + splitMailsHelper(this.privateMails, event, null, this.$fhcAlert, this.$p) } } }, diff --git a/public/js/helpers/EmailHelpers.js b/public/js/helpers/EmailHelpers.js new file mode 100644 index 000000000..87daa828a --- /dev/null +++ b/public/js/helpers/EmailHelpers.js @@ -0,0 +1,45 @@ +export async function splitMailsHelper(mails, event, subject, alertPluginRef, phrasenPluginRef) { + let splititem = ","; + let maillist = mails.join(splititem); + let mailto = ""; + // take subject line length + '?subject=' length into account + const subjectlength = subject && typeof subject === 'string' ? subject.length + 9 : 0 + if (maillist.length > 2024) + { + if (await alertPluginRef.confirm({message: phrasenPluginRef.t('stv', 'zuvieleEMails') }) === false) + return; + } + + let firstrun = true; + let useBcc = event?.ctrlKey || event?.metaKey; + while (maillist.length > 0) + { + if (maillist.length + subjectlength > 2024) + { + let splitposition = maillist.lastIndexOf(splititem, 1900); + mailto = maillist.substring(0, splitposition); + maillist = maillist.substring(splitposition + 1); + } + else + { + mailto = maillist; + maillist = ""; + } + + let mailLink = useBcc ? `mailto:?bcc=${mailto}` : `mailto:${mailto}`; + if(subject && typeof subject === 'string') mailLink += `?subject=${subject}` + if (firstrun) + { + window.location.href = mailLink; + firstrun = false; + } + else + { + if (await alertPluginRef.confirm({message: phrasenPluginRef.t('stv', 'weitereEMail')}) === true) + { + window.location.href = mailLink; + } + } + + } +} \ No newline at end of file From d9d15c1ed3d5b61bd06e296485885f4d97c24b32 Mon Sep 17 00:00:00 2001 From: ma0048 Date: Fri, 13 Feb 2026 11:10:16 +0100 Subject: [PATCH 22/51] neue tag farben --- public/css/tags.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/css/tags.css b/public/css/tags.css index 9e0d7ee4b..e92f415b2 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -51,6 +51,14 @@ background-color: #6d4c41; } +.tag_dark_grey { + background-color: #595959; +} + +.tag_light_grey { + background-color: #9a9a9a; +} + .tag_blau { background-color: #508498; } From 632866c8c420b9adab3305e73fe6f64410b32932 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Fri, 13 Feb 2026 13:45:12 +0100 Subject: [PATCH 23/51] reset newTermin object when switching projektarbeit so they are assigned to the correct student --- .../Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 18 ++++++++++++++++++ .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 1 - 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index 42952d0df..b303a831e 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -643,6 +643,24 @@ export const AbgabeMitarbeiterDetail = { 'projektarbeit'(newVal) { // set invertedFixtermin field for UI/UX purposes -> avoid double negation in text + // reset newTermin object + const typ = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === 'zwischen') + this.newTermin = { + 'paabgabe_id': -1, + 'projektarbeit_id': newVal.projektarbeit_id, + 'fixtermin': false, + 'invertedFixtermin': true, + 'kurzbz': '', + 'datum': new Date().toISOString().split('T')[0], + 'note': this.allowedNotenOptions.find(opt => opt.note == 9), + 'beurteilungsnotiz': '', + 'upload_allowed': typ.upload_allowed_default, + 'paabgabetyp_kurzbz': '', + 'bezeichnung': typ, + 'abgabedatum': null, + 'insertvon': this.viewData?.uid ?? '' + } + newVal?.abgabetermine?.forEach(termin => termin.invertedFixtermin = !termin.fixtermin) // default select german if projektarbeit sprache was null diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 67e1d09af..ff414ee10 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -333,7 +333,6 @@ export const AbgabetoolMitarbeiter = { pa.student = `${pa.vorname} ${pa.nachname}` this.selectedProjektarbeit = pa - this.$refs.modalContainerAbgabeDetail.show() }).finally(()=>{this.loading = false}) From 60294dd8f2591031fe5b90b67a9e43a89d977ab0 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Mon, 16 Feb 2026 03:22:39 +0100 Subject: [PATCH 24/51] paBenotet evaluation fix --- public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index ff414ee10..f33333ea3 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -304,7 +304,7 @@ export const AbgabetoolMitarbeiter = { pa.isCurrent = res.data[1] let paIsBenotet = false - if(pa.note !== undefined && pa !== null) { + if(pa.note !== undefined && pa.note !== null) { // check if the note is not defined as a non final projektarbeit note const opt = this.notenOptionsNonFinal.find(opt => opt.note) // if thats the case allow further work From 3831f3c1d781ae1d9477e215d52a0006bba7b014 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Mon, 16 Feb 2026 03:41:30 +0100 Subject: [PATCH 25/51] consistent use of :optionDisabled="getOptionDisabled" for paabgabetyp dropdowns in assistenz view --- .../components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 1 + .../js/components/Cis/Abgabetool/AbgabetoolAssistenz.js | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index b303a831e..971783746 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -729,6 +729,7 @@ export const AbgabeMitarbeiterDetail = { v-model="newTermin.bezeichnung" :options="getAllowedAbgabeTypeOptions" :optionLabel="getOptionLabelAbgabetyp" + :optionDisabled="getOptionDisabled" scrollHeight="300px">
    diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index a7da9da5a..34ddd3fc2 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -966,7 +966,10 @@ export const AbgabetoolAssistenz = { // this.loadProjektarbeiten() this.calcMaxTableHeight() - } + }, + getOptionDisabled(option) { + return !option.aktiv + }, }, computed: { emailItems() { @@ -1176,7 +1179,8 @@ export const AbgabetoolAssistenz = { :style="{'width': '100%'}" v-model="serienTermin.bezeichnung" :options="abgabeTypeOptions" - :optionLabel="getOptionLabelAbgabetyp"> + :optionLabel="getOptionLabelAbgabetyp" + :optionDisabled="getOptionDisabled"> From 5415180b2ce2b77d15918cefe436c1aa08f0c77d Mon Sep 17 00:00:00 2001 From: Harald Bamberger Date: Mon, 16 Feb 2026 14:18:59 +0100 Subject: [PATCH 26/51] fetch count and paginated data in one query --- application/models/system/Message_model.php | 105 ++++++++++---------- 1 file changed, 51 insertions(+), 54 deletions(-) diff --git a/application/models/system/Message_model.php b/application/models/system/Message_model.php index e0a185f9b..741c96ade 100644 --- a/application/models/system/Message_model.php +++ b/application/models/system/Message_model.php @@ -242,74 +242,71 @@ class Message_model extends DB_Model */ public function getMessagesForTable($person_id, $offset, $limit) { - $sql_base = " - SELECT + $sql = <<execQuery($sql, $parametersArray); - - if (isError($count)) - return $count; - - $count = ceil(current(getData($count))->count/$limit); - $sql = " - SELECT * FROM ( - " . $sql_base . " - ) a - ORDER BY insertamum DESC - LIMIT ? - OFFSET ? - "; + (SELECT COALESCE(titelpre,'') || ' ' || COALESCE(vorname,'') || ' ' || COALESCE(nachname,'') || ' ' || COALESCE(titelpost,'') FROM public.tbl_person WHERE person_id = fm.sender_id) as sender, + (SELECT COALESCE(titelpre,'') || ' ' || COALESCE(vorname,'') || ' ' || COALESCE(nachname,'') || ' ' || COALESCE(titelpost,'') FROM public.tbl_person WHERE person_id = fm.recipient_id) as recipient, + fm.sender_id, + fm.recipient_id, + ms.status, + ms.insertamum as statusdatum + from + filtered_messages fm + join + public.tbl_msg_message m on fm.message_id = m.message_id + join + lastmsgstatus ms on fm.message_id = ms.message_id and fm.recipient_id = ms.person_id + order by + m.insertamum DESC + limit ? + offset ?; +EOSQL; $parametersArray = array($person_id, $person_id, $limit, $offset); + $count = 0; $data = $this->execQuery($sql, $parametersArray); if (isError($data)) return $data; $data = getData($data); + if($data) + { + $count = ceil($data[0]->total_msgs / $limit); + } return success(['data' => $data, 'count' => $count]); } From 962cbf4e783958bf75822dbe988aba3097166f5f Mon Sep 17 00:00:00 2001 From: Harald Bamberger Date: Mon, 16 Feb 2026 15:16:49 +0100 Subject: [PATCH 27/51] join person table for sender and recipient instead of using subselect --- application/models/system/Message_model.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/application/models/system/Message_model.php b/application/models/system/Message_model.php index 741c96ade..33e3d9649 100644 --- a/application/models/system/Message_model.php +++ b/application/models/system/Message_model.php @@ -276,8 +276,8 @@ class Message_model extends DB_Model m.body AS body, m.insertamum AS insertamum, m.relationmessage_id AS relationmessage_id, - (SELECT COALESCE(titelpre,'') || ' ' || COALESCE(vorname,'') || ' ' || COALESCE(nachname,'') || ' ' || COALESCE(titelpost,'') FROM public.tbl_person WHERE person_id = fm.sender_id) as sender, - (SELECT COALESCE(titelpre,'') || ' ' || COALESCE(vorname,'') || ' ' || COALESCE(nachname,'') || ' ' || COALESCE(titelpost,'') FROM public.tbl_person WHERE person_id = fm.recipient_id) as recipient, + (COALESCE(ps.titelpre,'') || ' ' || COALESCE(ps.vorname,'') || ' ' || COALESCE(ps.nachname,'') || ' ' || COALESCE(ps.titelpost,'')) as sender, + (COALESCE(pr.titelpre,'') || ' ' || COALESCE(pr.vorname,'') || ' ' || COALESCE(pr.nachname,'') || ' ' || COALESCE(pr.titelpost,'')) as recipient, fm.sender_id, fm.recipient_id, ms.status, @@ -288,6 +288,10 @@ class Message_model extends DB_Model public.tbl_msg_message m on fm.message_id = m.message_id join lastmsgstatus ms on fm.message_id = ms.message_id and fm.recipient_id = ms.person_id + left join + public.tbl_person ps on ps.person_id = fm.sender_id + left join + public.tbl_person pr on pr.person_id = fm.recipient_id order by m.insertamum DESC limit ? From 0496eb7cc947c874281dad9b28df1db758734b70 Mon Sep 17 00:00:00 2001 From: Harald Bamberger Date: Mon, 16 Feb 2026 15:56:40 +0100 Subject: [PATCH 28/51] use union instead of or to avoid parallel seq scan --- application/models/system/Message_model.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/application/models/system/Message_model.php b/application/models/system/Message_model.php index 33e3d9649..3a5579cc7 100644 --- a/application/models/system/Message_model.php +++ b/application/models/system/Message_model.php @@ -251,9 +251,23 @@ class Message_model extends DB_Model join public.tbl_msg_recipient mr on mr.message_id = m.message_id where - m.person_id = ? or mr.person_id = ? + m.person_id = ? group by m.message_id, m.person_id, mr.person_id + + union + + select + m.message_id, m.person_id as sender_id, mr.person_id as recipient_id + from + public.tbl_msg_message m + join + public.tbl_msg_recipient mr on mr.message_id = m.message_id + where + mr.person_id = ? + group by + m.message_id, m.person_id, mr.person_id + ), lastmsgstatus as ( select ms.* From e12b7e1ed55baf978d9911301e2561dc2769f1e1 Mon Sep 17 00:00:00 2001 From: Harald Bamberger Date: Tue, 17 Feb 2026 08:06:30 +0100 Subject: [PATCH 29/51] add indexes for person_id to table msg_message and msg_recipient, ensure tabulator data request is made before requests of create msg components --- application/models/system/Message_model.php | 2 +- .../Messages/Details/TableMessages.js | 1 + public/js/components/Messages/Messages.js | 10 +++++-- system/dbupdate_3.4.php | 1 + .../71645_studvw_messagetab_ladezeit.php | 28 +++++++++++++++++++ 5 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 system/dbupdate_3.4/71645_studvw_messagetab_ladezeit.php diff --git a/application/models/system/Message_model.php b/application/models/system/Message_model.php index 3a5579cc7..19129b606 100644 --- a/application/models/system/Message_model.php +++ b/application/models/system/Message_model.php @@ -255,7 +255,7 @@ class Message_model extends DB_Model group by m.message_id, m.person_id, mr.person_id - union + union all select m.message_id, m.person_id as sender_id, mr.person_id as recipient_id diff --git a/public/js/components/Messages/Details/TableMessages.js b/public/js/components/Messages/Details/TableMessages.js index a55ddec63..6a4cf5ca0 100644 --- a/public/js/components/Messages/Details/TableMessages.js +++ b/public/js/components/Messages/Details/TableMessages.js @@ -243,6 +243,7 @@ export default { title: this.$p.t('global', 'aktionen') }); */ + this.$emit('tabulator_tablebuilt'); } }, { diff --git a/public/js/components/Messages/Messages.js b/public/js/components/Messages/Messages.js index 1f9afcb9e..5e247ddb5 100644 --- a/public/js/components/Messages/Messages.js +++ b/public/js/components/Messages/Messages.js @@ -56,6 +56,7 @@ export default { }, data() { return { + tablebuilt: false, isVisibleDiv: false, messageId: null } @@ -139,8 +140,10 @@ export default { }, resetMessageId(){ this.messageId = null; + }, + tableBuilt: function() { + this.tablebuilt = true; } - }, template: `
    @@ -155,6 +158,7 @@ export default { -
    +
    diff --git a/system/dbupdate_3.4.php b/system/dbupdate_3.4.php index 793930243..4ddb38203 100644 --- a/system/dbupdate_3.4.php +++ b/system/dbupdate_3.4.php @@ -91,6 +91,7 @@ require_once('dbupdate_3.4/69065_Projektarbeiten_Firmen_verwalten.php'); require_once('dbupdate_3.4/68744_StV_settings.php'); require_once('dbupdate_3.4/62889_reihungstest_ueberwachung_mit_constructor.php'); require_once('dbupdate_3.4/71399_dashboard_update_widget_paths.php'); +require_once('dbupdate_3.4/71645_studvw_messagetab_ladezeit.php'); // *** Pruefung und hinzufuegen der neuen Attribute und Tabellen echo '

    Pruefe Tabellen und Attribute!

    '; diff --git a/system/dbupdate_3.4/71645_studvw_messagetab_ladezeit.php b/system/dbupdate_3.4/71645_studvw_messagetab_ladezeit.php new file mode 100644 index 000000000..4ad88fba9 --- /dev/null +++ b/system/dbupdate_3.4/71645_studvw_messagetab_ladezeit.php @@ -0,0 +1,28 @@ +db_query("SELECT * FROM pg_class WHERE relname='idx_tbl_msg_message_person_id'")) +{ + if ($db->db_num_rows($result) == 0) + { + $qry = "CREATE INDEX idx_tbl_msg_message_person_id ON public.tbl_msg_message USING btree (person_id)"; + + if (! $db->db_query($qry)) + echo 'idx_tbl_msg_message_person_id: ' . $db->db_last_error() . '
    '; + else + echo 'Index idx_tbl_msg_message_person_id angelegt
    '; + } +} + +if ($result = $db->db_query("SELECT * FROM pg_class WHERE relname='idx_tbl_msg_recipient_person_id'")) +{ + if ($db->db_num_rows($result) == 0) + { + $qry = "CREATE INDEX idx_tbl_msg_recipient_person_id ON public.tbl_msg_recipient USING btree (person_id)"; + + if (! $db->db_query($qry)) + echo 'idx_tbl_msg_recipient_person_id: ' . $db->db_last_error() . '
    '; + else + echo 'Index idx_tbl_msg_recipient_person_id angelegt
    '; + } +} From 3d1aef617f109e048ceb53f0f6993ad8f1b0fce1 Mon Sep 17 00:00:00 2001 From: Harald Bamberger Date: Tue, 17 Feb 2026 08:13:07 +0100 Subject: [PATCH 30/51] add phrase error.opproject_does_not_exists in category kvp --- system/phrasesupdate.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 5dc9bc1c0..deb856663 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -19087,6 +19087,27 @@ array( ) ) ), + array( + 'app' => 'core', + 'category' => 'kvp', + 'phrase' => 'error.opproject_does_not_exists', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => "Es ist kein Openproject Projekt verknüpft.", + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => "No Openproject project is linked.", + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + //******************* KVP end array( 'app' => 'international', 'category' => 'international', From 4825c75b5d9e00c2504252bf34cf618811d9e497 Mon Sep 17 00:00:00 2001 From: Harald Bamberger Date: Tue, 17 Feb 2026 12:11:37 +0100 Subject: [PATCH 31/51] revert changes made in commit b1a1cdf23550fc42337a944d4661f72d582b809d --- public/js/api/factory/studiengang.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/public/js/api/factory/studiengang.js b/public/js/api/factory/studiengang.js index 12322cb3a..6d5ae15aa 100644 --- a/public/js/api/factory/studiengang.js +++ b/public/js/api/factory/studiengang.js @@ -16,10 +16,17 @@ */ export default { - getAllStudiensemesterAndAktOrNext() { + studiengangInformation() { return { method: 'get', - url: '/api/frontend/v1/Studiensemester/getStudiengangInfo' + url: '/api/frontend/v1/Studgang/getStudiengangInfo' }; }, + getStudiengangByKz(studiengang_kz) { + return { + method: 'get', + url: '/api/frontend/v1/organisation/StudiengangEP/getStudiengangByKz', + params: { studiengang_kz } + }; + } }; \ No newline at end of file From c58674d1333baf2bf981356924e054f85c424568 Mon Sep 17 00:00:00 2001 From: Alexei Karpenko Date: Tue, 17 Feb 2026 15:15:30 +0100 Subject: [PATCH 32/51] Projektarbeiten cancelVertrag permission check bugfix (added array_column to get oes) --- application/controllers/api/frontend/v1/stv/Vertrag.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/application/controllers/api/frontend/v1/stv/Vertrag.php b/application/controllers/api/frontend/v1/stv/Vertrag.php index f94fe795e..c2b0f713c 100644 --- a/application/controllers/api/frontend/v1/stv/Vertrag.php +++ b/application/controllers/api/frontend/v1/stv/Vertrag.php @@ -76,9 +76,7 @@ class Vertrag extends FHCAPI_Controller if (isError($allOe)) $this->terminateWithError(getError($allOe), self::ERROR_TYPE_GENERAL); - $allOe = hasData($allOe) ? getData($allOe) : []; - - $this->addMeta('oe', $allOe); + $allOe = hasData($allOe) ? array_column(getData($allOe), 'oe_kurzbz') : []; // * then check if the user has permissions to cancel the corresponding lv-organisational units if (!$this->permissionlib->isBerechtigtMultipleOe('admin', $allOe, 'suid') && From ee7254a9642ca405502be1f19ad52b6f63a42078 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Tue, 17 Feb 2026 16:22:26 +0100 Subject: [PATCH 33/51] assistenz preserve table state (selection, scroll) when adding serientermin; update isPastDate() function to luxon timezone safe logic; --- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 53 ++++++++++++++++--- .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 4 +- .../Cis/Abgabetool/AbgabetoolStudent.js | 4 +- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 34ddd3fc2..e09171c25 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -212,7 +212,8 @@ export const AbgabetoolAssistenz = { abgabeTableEventHandlers: [ { event: "rowSelectionChanged", - handler: async(data) => { + handler: async(data) => + { this.selectedData.filter(sd => !data.includes(sd)).forEach(fsd => { if(fsd.checkbox) fsd.checkbox.checked = false }) @@ -220,7 +221,7 @@ export const AbgabetoolAssistenz = { data.forEach(d => { if(d.checkbox) d.checkbox.checked = true }) - + this.selectedData = data } } @@ -612,6 +613,9 @@ export const AbgabetoolAssistenz = { }, addSeries() { const pids = this.selectedData?.map(projekt => projekt.projektarbeit_id) + + const preserveSelected = [...this.selectedData] + this.saving = true this.serienTermin.fixtermin = !this.serienTermin.invertedFixtermin this.$api.call(ApiAbgabe.postSerientermin( @@ -644,14 +648,27 @@ export const AbgabetoolAssistenz = { }) // reset selection to empty - this.$refs.abgabeTable.tabulator.deselectRow() - - const mappedData = this.mapProjekteToTableData(this.projektarbeiten) + // this.$refs.abgabeTable.tabulator.deselectRow() + const table = this.$refs.abgabeTable.tabulator; + const scrollX = table.rowManager.element.scrollLeft; + const scrollY = table.rowManager.element.scrollTop; + + const mappedData = this.mapProjekteToTableData(this.projektarbeiten) + + table.setData(mappedData) + table.redraw(true) + + + requestAnimationFrame(() => { + table.rowManager.element.scrollLeft = scrollX; + table.rowManager.element.scrollTop = scrollY; + }); + + - this.$refs.abgabeTable.tabulator.setData(mappedData) - this.$refs.abgabeTable.tabulator.redraw(true) }).finally(()=>{ this.saving = false + this.selectedData = preserveSelected }) this.$refs.modalContainerAddSeries.hide() @@ -705,7 +722,9 @@ export const AbgabetoolAssistenz = { return str }, isPastDate(date) { - return new Date(date) < new Date(Date.now()) + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); + const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); + return nowInBerlin > deadline; }, setDetailComponent(details){ @@ -1034,6 +1053,24 @@ export const AbgabetoolAssistenz = { if(this.notenOptionFilter !== null && this.selectedStudiengangOption !== null) { this.loadProjektarbeiten() } + }, + selectedData(newVal) { + const table = this.$refs.abgabeTable?.tabulator + if(!table) return + + const allRows = table.getRows(); + + newVal.forEach(selected => { + const row = allRows.find(r => { + const data = r.getData() + if (data.projektarbeit_id == selected.projektarbeit_id) return r + }) + + row.select() + const cb = row.getElement().children[0]?.children[0]?.children[0] + if(cb) cb.checked = true + }) + } }, created() { diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index f33333ea3..1b8eff3e2 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -294,7 +294,9 @@ export const AbgabetoolMitarbeiter = { return str }, isPastDate(date) { - return new Date(date) < new Date(Date.now()) + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); + const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); + return nowInBerlin > deadline; }, setDetailComponent(details){ this.loading=true diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js index ff68b680f..d03ef3ffc 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js @@ -155,7 +155,9 @@ export const AbgabetoolStudent = { return qgate1positiv && qgate2positiv }, isPastDate(date) { - return new Date(date) < new Date(Date.now()) + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); + const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); + return nowInBerlin > deadline; }, setDetailComponent(details){ this.loading = true From 1d8c4b7159bfb7206895ce5e22019e97dfa4a078 Mon Sep 17 00:00:00 2001 From: ma0048 Date: Tue, 17 Feb 2026 16:49:09 +0100 Subject: [PATCH 34/51] bug behoben, login wieder nur mit zugangscode moeglich --- cis/testtool/login.php | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/cis/testtool/login.php b/cis/testtool/login.php index 182506ac3..cfc1ba63b 100644 --- a/cis/testtool/login.php +++ b/cis/testtool/login.php @@ -340,13 +340,26 @@ else } } -if ((isset($_SESSION['prestudent_id']) && !isset($_SESSION['pruefling_id']) && - !isset($_SESSION['confirmation_needed']) && !isset($_SESSION['confirmed_code'])) || - (isset($_SESSION['confirmation_needed']) && $_SESSION['confirmation_needed'] === true && - isset($_SESSION['confirmed_code']) && $_SESSION['confirmed_code'] === true && - isset($_SESSION['externe_ueberwachung']) && $_SESSION['externe_ueberwachung'] === true && - isset($_SESSION['externe_ueberwachung_verified']) && $_SESSION['externe_ueberwachung_verified'] === true && - isset($_SESSION['prestudent_id']) && !isset($_SESSION['pruefling_id']))) +if ( + ( + isset($_SESSION['prestudent_id']) && !isset($_SESSION['pruefling_id']) && + !isset($_SESSION['confirmation_needed']) && !isset($_SESSION['confirmed_code']) && + !isset($_SESSION['externe_ueberwachung']) && !isset($_SESSION['externe_ueberwachung_verified']) + ) + || + ( + isset($_SESSION['confirmation_needed']) && $_SESSION['confirmation_needed'] === true && + isset($_SESSION['confirmed_code']) && $_SESSION['confirmed_code'] === true && + isset($_SESSION['prestudent_id']) && !isset($_SESSION['pruefling_id']) + ) + || + ( + isset($_SESSION['externe_ueberwachung']) && $_SESSION['externe_ueberwachung'] === true && + isset($_SESSION['externe_ueberwachung_verified']) && $_SESSION['externe_ueberwachung_verified'] === true && + isset($_SESSION['prestudent_id']) && !isset($_SESSION['pruefling_id']) + ) + +) { $pruefling = new pruefling(); From a6daa7bf0ca7444d1336ac6138a336d49cd6fa03 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Tue, 17 Feb 2026 17:32:11 +0100 Subject: [PATCH 35/51] all abgabetool datepickers use date format via format="dd.MM.yyyy" instead of :format="formatDate" to enable text-input + autoapply; backend deadline datetime check for endupload; --- .../controllers/api/frontend/v1/Abgabe.php | 29 +++++++++++++++++++ .../Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 10 +++++-- .../Cis/Abgabetool/AbgabeStudentDetail.js | 3 +- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 3 +- .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 3 +- system/phrasesupdate.php | 20 +++++++++++++ 6 files changed, 62 insertions(+), 6 deletions(-) diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index b37c64713..23e11c202 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -373,6 +373,8 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } + $this->checkPaabgabeDeadline($paabgabe_id); + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); @@ -444,6 +446,33 @@ class Abgabe extends FHCAPI_Controller } } + + // validate paabgabe deadline against servertime just in case a student spoofs their local clock and thus + // unlocks the upload ui + private function checkPaabgabeDeadline($paabgabe_id) { + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + + $result = $this->PaabgabeModel->load($paabgabe_id); + $paabgabeArr = $this->getDataOrTerminateWithError($result, 'general'); + + if (count($paabgabeArr) > 0) { + $paabgabe = $paabgabeArr[0]; + } else { + $this->terminateWithError($this->p->t('abgabetool', 'c4projektabgabeNichtGefunden'), 'general'); + } + + $tz = new DateTimeZone('Europe/Berlin'); + $now = new DateTimeImmutable('now', $tz); + $deadline = DateTimeImmutable::createFromFormat( + 'Y-m-d H:i:s', + $paabgabe->datum . ' 23:59:59', + $tz + ); + + if($now >= $deadline) { + $this->terminateWithError($this->p->t('abgabetool', 'c4deadlineExceeded')); + } + } /** * tabulator tabledata fetch for abgabetool/mitarbeiter diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index 971783746..ad740e978 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -715,7 +715,8 @@ export const AbgabeMitarbeiterDetail = { v-model="newTermin.datum" :clearable="false" :enable-time-picker="false" - :format="formatDate" + locale="de" + format="dd.MM.yyyy" :text-input="true" auto-apply> @@ -864,7 +865,8 @@ export const AbgabeMitarbeiterDetail = { :clearable="false" :disabled="!termin.allowedToSave" :enable-time-picker="false" - :format="formatDate" + locale="de" + format="dd.MM.yyyy" :text-input="true" auto-apply> @@ -931,7 +933,9 @@ export const AbgabeMitarbeiterDetail = { v-model="termin.abgabedatum" :clearable="false" :disabled="true" - :format="formatDate"> + locale="de" + format="dd.MM.yyyy" + >
    diff --git a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js index 55120e223..9c14c2948 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js @@ -423,7 +423,8 @@ export const AbgabeStudentDetail = { :clearable="false" :disabled="true" :enable-time-picker="false" - :format="formatDate" + locale="de" + format="dd.MM.yyyy" :text-input="true" auto-apply> diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index e09171c25..db2eebaa8 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -1189,8 +1189,9 @@ export const AbgabetoolAssistenz = { style="width: 95%;" v-model="serienTermin.datum" :clearable="false" + locale="de" + format="dd.MM.yyyy" :enable-time-picker="false" - :format="formatDate" :text-input="true" auto-apply> diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 1b8eff3e2..8ee12bf79 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -525,7 +525,8 @@ export const AbgabetoolMitarbeiter = { v-model="serienTermin.datum" :clearable="false" :enable-time-picker="false" - :format="formatDate" + locale="de" + format="dd.MM.yyyy" :text-input="true" auto-apply> diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 45e977987..8860e2cf6 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -46373,6 +46373,26 @@ array( ) ) ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'c4deadlineExceeded', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Nicht rechtzeitig abgegeben!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Deadline exceeded!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), // ABGABETOOL PHRASEN END array( 'app' => 'core', From 90c845899f7645861c6daa4e933654643b4a853f Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Wed, 18 Feb 2026 11:15:59 +0100 Subject: [PATCH 36/51] =?UTF-8?q?explicitely=20set=20deadline=20to=20end?= =?UTF-8?q?=20of=20day=20to=20achieve=20the=20desired=20"valid=20until=202?= =?UTF-8?q?3:59"=20logic,=20instead=20of=20just=20moving=20the=20deadline?= =?UTF-8?q?=20by=20one=20day;=20endupload=20deadline=20is=20now=20optional?= =?UTF-8?q?=20by=20defining=20it=20as=20a=20"nachreichen=20m=C3=B6glich"?= =?UTF-8?q?=20aka=20non=20fixtermin;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/api/frontend/v1/Abgabe.php | 18 ++++++++++++++++++ .../Cis/Abgabetool/AbgabetoolStudent.js | 6 +++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index 23e11c202..f0744fb99 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -461,6 +461,9 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithError($this->p->t('abgabetool', 'c4projektabgabeNichtGefunden'), 'general'); } + // in that case any submission date is fine + if($paabgabe->fixtermin === false) return; + $tz = new DateTimeZone('Europe/Berlin'); $now = new DateTimeImmutable('now', $tz); $deadline = DateTimeImmutable::createFromFormat( @@ -502,6 +505,15 @@ class Abgabe extends FHCAPI_Controller $projektarbeiten = $this->ProjektarbeitModel->getMitarbeiterProjektarbeiten(getAuthUID(), $showAllBool); + $mapFunc = function($projektarbeit) { + return $projektarbeit->projektarbeit_id; + }; + $projektarbeiten_ids = array_map($mapFunc, $projektarbeiten->retval); + + $ret = $this->ProjektarbeitModel->getProjektarbeitenAbgabetermine($projektarbeiten_ids); + $projektabgaben = $this->getDataOrTerminateWithError($ret, 'general'); + + forEach($projektarbeiten->retval as $pa) { $result = $this->ProjektarbeitModel->getProjektbetreuerAnrede($pa->betreuer_person_id); @@ -518,6 +530,12 @@ class Abgabe extends FHCAPI_Controller Events::trigger('projektbeurteilung_formular_link', $pa->betreuerart_kurzbz, APP_ROOT, $pa->projektarbeit_id, $pa->student_uid, $returnFunc); $pa->beurteilungLinkNew = $newLink; $pa->beurteilungLinkOld = $oldLink; + + $filterFunc = function($projektabgabe) use ($pa) { + return $projektabgabe->projektarbeit_id == $pa->projektarbeit_id; + }; + + $pa->abgabetermine = array_values(array_filter($projektabgaben, $filterFunc)); } diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js index d03ef3ffc..fd88cbe02 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js @@ -155,7 +155,7 @@ export const AbgabetoolStudent = { return qgate1positiv && qgate2positiv }, isPastDate(date) { - const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }).endOf('day'); const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); return nowInBerlin > deadline; }, @@ -175,8 +175,8 @@ export const AbgabetoolStudent = { // old assumed production logic when qgates are required // termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesStrict(pa.abgabetermine) - // new larifari we want qgates but they are optional fhtw mode - termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesOptional(pa.abgabetermine) + const inTime = termin.fixtermin ? !this.isPastDate(termin.datum) : true + termin.allowedToUpload = inTime && this.checkQualityGatesOptional(pa.abgabetermine) // development purposes From 328affa35caa1ad174dcf05b78e416a846f9ca8c Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Wed, 18 Feb 2026 11:53:24 +0100 Subject: [PATCH 37/51] actually set deadline calculation to IANA timezone 'Europe/Vienna', so the code still works once Berlin moves to another timezone away from Austria. You never know. --- public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js | 6 +++--- public/js/components/Cis/Abgabetool/AbgabetoolStudent.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index db2eebaa8..e4609d050 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -722,9 +722,9 @@ export const AbgabetoolAssistenz = { return str }, isPastDate(date) { - const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); - const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); - return nowInBerlin > deadline; + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day'); + const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna'); + return nowInVienna > deadline; }, setDetailComponent(details){ diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js index fd88cbe02..4baf5316f 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js @@ -155,9 +155,9 @@ export const AbgabetoolStudent = { return qgate1positiv && qgate2positiv }, isPastDate(date) { - const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }).endOf('day'); - const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); - return nowInBerlin > deadline; + 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 From 6f28696556881cf7eb9cc2c56683f6f6ba8c747e Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Wed, 18 Feb 2026 13:00:19 +0100 Subject: [PATCH 38/51] getDateStyleClass evaluation also with precise luxon calculation on all pages; qgate12 status col, next/prev termin col on betreuer page; table persistence on mitarbeiter page; same rowheight on betreuer table as in assistenz to achieve similar UX; --- .../Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 51 +-- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 34 +- .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 304 +++++++++++++++++- .../Cis/Abgabetool/AbgabetoolStudent.js | 29 +- 4 files changed, 320 insertions(+), 98 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index ad740e978..f86fa44d9 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -270,48 +270,13 @@ export const AbgabeMitarbeiterDetail = { 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)) }, - convertDateToIsoString(date) { - // 1. Check if it is a Date object AND if the date value is valid (not 'Invalid Date') - if (param instanceof Date && !isNaN(param.getTime())) { - const year = param.getFullYear(); - // getMonth() is 0-indexed, so we add 1. - const month = param.getMonth() + 1; - const day = param.getDate(); - - // Helper to pad single-digit numbers with a leading zero - const pad = (num) => String(num).padStart(2, '0'); - - // Return the formatted string: YYYY-MM-DD - return `${year}-${pad(month)}-${pad(day)}`; - } - - // If it's not a valid Date, return the original parameter - return param; - }, - dateDiffInDays(datumParam){ - let datum = datumParam - if(datumParam instanceof Date && !isNaN(datum.getTime())) - { - const year = datumParam.getFullYear(); - const month = datumParam.getMonth() + 1; // getMonth() is 0-indexed - const day = datumParam.getDate(); - const pad = (num) => String(num).padStart(2, '0'); - datum = `${year}-${pad(month)}-${pad(day)}` - } - - const dateToday = luxon.DateTime.now().startOf('day'); - const dateDatum = luxon.DateTime.fromISO(datum).startOf('day'); - const duration = dateDatum.diff(dateToday, 'days'); - - return duration.values.days; - }, getDateStyleClass(termin) { - const datum = new Date(termin.datum) - const abgabedatum = new Date(termin.abgabedatum) - - termin.diffindays = this.dateDiffInDays(termin.datum) - - const isLate = termin.abgabedatum && abgabedatum > datum; + const zone = 'Europe/Vienna'; + const today = luxon.DateTime.now().setZone(zone); + 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) { @@ -396,6 +361,7 @@ export const AbgabeMitarbeiterDetail = { } }, formatDate(dateParam) { + // unsafe for datepickers, dont use there const date = new Date(dateParam) // handle missing leading 0 const padZero = (num) => String(num).padStart(2, '0'); @@ -476,7 +442,6 @@ export const AbgabeMitarbeiterDetail = { termin.kurzbz = '' } } - }, computed: { getAllowedToCreateNewTermin() { @@ -626,7 +591,6 @@ export const AbgabeMitarbeiterDetail = { return '' }, getProjektarbeitStudent(){ - if(this.projektarbeit?.student) return this.$capitalize(this.$p.t('person/student')) + ': ' + this.projektarbeit.student return '' @@ -671,7 +635,6 @@ export const AbgabeMitarbeiterDetail = { this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? '' this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? '' this.form.seitenanzahl = newVal.seitenanzahl ?? 1 - }, }, created() { diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index e4609d050..8b0fe7ddb 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -9,15 +9,6 @@ import AbgabeterminStatusLegende from "./StatusLegende.js"; import FhcOverlay from "../../Overlay/FhcOverlay.js"; import { splitMailsHelper } from "../../../helpers/EmailHelpers.js" -// spoofed date testing -// const todayISO = '2025-08-08' -// const today = new Date(todayISO) -// const now = luxon.DateTime.fromISO(todayISO) - -// prod code -const today = new Date() -const now = luxon.DateTime.now() - export const AbgabetoolAssistenz = { name: "AbgabetoolAssistenz", components: { @@ -386,6 +377,8 @@ export const AbgabetoolAssistenz = { }, checkAbgabetermineProjektarbeit(projekt) { + const now = luxon.DateTime.now() + // calculate Abgabetermin time diff to now and assign last and next to projekt projekt.abgabetermine.forEach(termin => { @@ -393,7 +386,7 @@ export const AbgabetoolAssistenz = { // while already looping through each termin, calculate datestyle beforehand termin.dateStyle = this.getDateStyleClass(termin) - const date = luxon.DateTime.fromISO(termin.datum) + const date = luxon.DateTime.fromISO(termin.datum).endOf('day') termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past if (termin.diffMs < 0) { @@ -770,22 +763,13 @@ export const AbgabetoolAssistenz = { this.$refs.modalContainerAbgabeDetail.show() }, - dateDiffInDays(datum){ - const dateToday = luxon.DateTime.now().startOf('day'); - - const dateDatum = luxon.DateTime.fromISO(datum).startOf('day'); - - const duration = dateDatum.diff(dateToday, 'days'); - - return duration.values.days; - }, getDateStyleClass(termin) { - const datum = new Date(termin.datum) - const abgabedatum = new Date(termin.abgabedatum) - - termin.diffindays = this.dateDiffInDays(termin.datum) - - const isLate = termin.abgabedatum && abgabedatum > datum; + const zone = 'Europe/Vienna'; + const today = luxon.DateTime.now().setZone(zone); + 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) { diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 8ee12bf79..2ad29d20c 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -79,7 +79,7 @@ export const AbgabetoolMitarbeiter = { placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')), selectable: true, selectableCheck: this.selectionCheck, - rowHeight: 80, + rowHeight: 40, columns: [ { formatter: function (cell, formatterParams, onRendered) { @@ -144,9 +144,14 @@ export const AbgabetoolMitarbeiter = { {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerart'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1} + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerart'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), headerFilter: true, field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), headerFilter: true, field: 'nextTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))), headerFilter: true, field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))), headerFilter: true, field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false} ], persistence: false, + persistenceID: 'abgabeTableBetreuer2026-02-18' }, abgabeTableEventHandlers: [{ event: "tableBuilt", @@ -182,6 +187,290 @@ export const AbgabetoolMitarbeiter = { ]}; }, methods: { + loadState() { + return JSON.parse(localStorage.getItem(this.abgabeTableOptions.persistenceID) || "null"); + }, + saveState(table) { + // avoid storing state after first restore part happened + if(!this.stateRestored) return + const rawLayout = table.getColumnLayout(); + const state = { + columns: rawLayout.map(col => ({ + field: col.field, + visible: col.visible, + width: col.width, + })), + sort: table.getSorters().map(s => ({ + field: s.field, + dir: s.dir, + })), + filters: table.getFilters(), + headerFilters: table.getHeaderFilters() + }; + + localStorage.setItem(this.abgabeTableOptions.persistenceID, JSON.stringify(state)); + }, + handleTableBuilt() { + const table = this.$refs.abgabeTable.tabulator + + this.tableBuiltResolve() + + table.on("columnMoved", () => { + this.saveState(table); + }); + + table.on("columnResized", () => { + this.saveState(table); + }); + + table.on("columnVisibilityChanged", () => { + this.saveState(table); + }); + + table.on("filterChanged", () => { + this.saveState(table); + }); + + table.on("headerFilterChanged", () => { + this.saveState(table); + }); + + table.on("dataSorted", () => { + this.saveState(table); + }); + + table.on("columnSorted", () => { + this.saveState(table); + }); + + table.on("sortersChanged", () => { + this.saveState(table); + }); + + const saved = this.loadState(); + + table.on("renderComplete", () => { + if(!this.stateRestored) { + + if (saved?.columns && !this.colLayoutRestored) { + const layout = saved.columns.map(col => ({ + field: col.field, + width: col.width, + visible: col.visible, + // add more if needed, but keep it simple + })); + + table.setColumnLayout(layout); + + this.colLayoutRestored = true; + } + + if (saved?.filters && !this.filtersRestored) { + this.filtersRestored = true // instantly avoid retriggers + table.setFilter(saved.filters); + } + if (saved?.headerFilters && !this.headerFiltersRestored) { + this.headerFiltersRestored = true // instantly avoid retriggers + for (let hf of saved.headerFilters) { + table.setHeaderFilterValue(hf.field, hf.value); + } + } + + if (saved?.sort?.length && !this.sortRestored) { + this.sortRestored = true; + + setTimeout(() => { + const sortList = saved.sort.map(s => { + const col = table.columnManager.findColumn(s.field); + if (!col) { + return null; + } + return { column: col, dir: s.dir }; + }).filter(Boolean); + + table.setSort(sortList); + }, 100); + } + this.stateRestored = true + + } + + }); + }, + checkQualityGateStatus(projekt) { + // TODO: might refine the representation of these states and maybe refactor code a little + const qgate1Termine = [] + const qgate2Termine = [] + + projekt.qgate1Status = this.$p.t('abgabetool/c4keinTerminVorhanden')// 'Kein Termin vorhanden' + projekt.qgate1StatusRank = 0 + projekt.qgate2Status = this.$p.t('abgabetool/c4keinTerminVorhanden') + projekt.qgate2StatusRank = 0 + + projekt.abgabetermine.forEach(termin => { + if(termin.paabgabetyp_kurzbz == 'qualgate1') qgate1Termine.push(termin) + if(termin.paabgabetyp_kurzbz == 'qualgate2') qgate2Termine.push(termin) + }) + + // calculate qgateStatusRank and display the highest order status rank of all quality gate termine until one + // counts as passed, which is just a positive note no matter if anything has been uploaded + + // reuse luxon calculated diffMs (termin.datum in relation to today) from previous datestyle check + qgate1Termine.forEach(qgate => { + if(qgate.note != null && projekt.qgate1StatusRank <= 5) { + const noteOpt = this.notenOptions.find(opt => opt.note == qgate.note) + if(noteOpt.positiv) { + projekt.qgate1Status = this.$p.t('abgabetool/c4positivBenotet') + projekt.qgate1StatusRank = 5 + } else { + projekt.qgate1Status = this.$p.t('abgabetool/c4negativBenotet') + projekt.qgate1StatusRank = 4 + } + } else if (qgate.note == null && projekt.qgate1StatusRank <= 3) { + projekt.qgate1Status = this.$p.t('abgabetool/c4notYetGraded') + projekt.qgate1StatusRank = 3 + } else if(qgate.upload_allowed == true && qgate.abgabedatum == null && projekt.qgate1StatusRank <= 2) { + projekt.qgate1Status = this.$p.t('abgabetool/c4notSubmitted') + projekt.qgate1StatusRank = 2 + } else if (qgate.upload_allowed == false && qgate.diffMs <= 0 && projekt.qgate1StatusRank <= 1) { + projekt.qgate1Status = this.$p.t('abgabetool/c4notHappenedYet') + projekt.qgate1StatusRank = 1 + } + }) + + qgate2Termine.forEach(qgate => { + if(qgate.note != null && projekt.qgate1StatusRank <= 5) { + const noteOpt = this.notenOptions.find(opt => opt.note == qgate.note) + if(noteOpt.positiv) { + projekt.qgate2Status = this.$p.t('abgabetool/c4positivBenotet') + projekt.qgate2StatusRank = 5 + } else { + projekt.qgate2Status = this.$p.t('abgabetool/c4negativBenotet') + projekt.qgate2StatusRank = 4 + } + } else if (qgate.note == null && projekt.qgate2StatusRank <= 3) { + projekt.qgate2Status = this.$p.t('abgabetool/c4notYetGraded') + projekt.qgate2StatusRank = 3 + } else if(qgate.upload_allowed == true && qgate.abgabedatum == null && projekt.qgate2StatusRank <= 2) { + projekt.qgate2Status = this.$p.t('abgabetool/c4notSubmitted') + projekt.qgate2StatusRank = 2 + } else if (qgate.upload_allowed == false && qgate.diffMs <= 0 && projekt.qgate2StatusRank <= 1) { + projekt.qgate2Status = this.$p.t('abgabetool/c4notHappenedYet') + projekt.qgate2StatusRank = 1 + } + }) + }, + checkAbgabetermineProjektarbeit(projekt) { + const now = luxon.DateTime.now() + // calculate Abgabetermin time diff to now and assign last and next to projekt + projekt.abgabetermine.forEach(termin => { + + // while already looping through each termin, calculate datestyle beforehand + termin.dateStyle = this.getDateStyleClass(termin) + + const date = luxon.DateTime.fromISO(termin.datum).endOf('day') + termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past + + if (termin.diffMs < 0) { + if (!projekt.prevTermin || + termin.diffMs > projekt.prevTermin.diffMs // larger (less negative) = closer to now + ) { + projekt.prevTermin = termin; + } + } else if (termin.diffMs > 0) { + if (!projekt.nextTermin || + termin.diffMs < projekt.nextTermin.diffMs // smaller positive = closer to now + ) { + projekt.nextTermin = termin; + } + } + }) + + // seperate check for quality gates + this.checkQualityGateStatus(projekt) + }, + getDateStyleClass(termin) { + const zone = 'Europe/Vienna'; + const today = luxon.DateTime.now().setZone(zone); + 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) { + if (termin.note.positiv) return 'bestanden'; + return 'nichtbestanden'; + } + + // ACTION REQUIRED FOR GRADE + if (termin.bezeichnung?.benotbar && datum < today) { + return 'beurteilungerforderlich'; + } + + // SUBMISSION STATUS + if (termin.upload_allowed) { + if (termin.abgabedatum) { + return isLate ? 'verspaetet' : 'abgegeben'; + } + + // no submission yet + if (datum < today) return 'verpasst'; + if (termin.diffindays <= 12) return 'abzugeben'; + return 'standard'; + } + + // GENERIC STATUS + return datum < today ? 'verpasst' : 'standard'; + }, + abgabterminFormatter(cell) { + const val = cell.getValue() + + if(val) { + let icon = '' + switch(val.dateStyle) { + case 'verspaetet': + icon = '' + break + case 'verpasst': + icon = '' + break + case 'abzugeben': + icon = '' + break + case 'standard': + icon = '' + break + case 'abgegeben': + icon = '' + break + case 'beurteilungerfolderlich': + icon = '' + break + case 'bestanden': + icon = '' + break + case 'nichtbestanden': + icon = '' + break + } + + const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung + + return '
    ' + + '
    ' + + icon + + '
    ' + + '
    ' + + '

    '+bezeichnung+' - '+ this.formatDate(val.datum)+'

    ' + + '
    '+ + '
    ' + + } else { + return '' + } + + }, selectHandler(e, cell) { const row = cell.getRow(); @@ -294,9 +583,9 @@ export const AbgabetoolMitarbeiter = { return str }, isPastDate(date) { - const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); - const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); - return nowInBerlin > deadline; + 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 @@ -381,11 +670,13 @@ export const AbgabetoolMitarbeiter = { return (projekt.typ + projekt.kurzbz)?.toUpperCase() }, setupData(data){ + + this.projektarbeiten = data[0] this.domain = data[1] this.tableData = data[0]?.retval?.map(projekt => { - + this.checkAbgabetermineProjektarbeit(projekt) projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter' return { @@ -601,6 +892,7 @@ export const AbgabetoolMitarbeiter = { @click:new=openAddSeriesModal :tabulator-options="abgabeTableOptions" :tabulator-events="abgabeTableEventHandlers" + @tableBuilt="handleTableBuilt" tableOnly :sideMenu="false" :useSelectionSpan="false" diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js index 4baf5316f..a0df7a81d 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js @@ -48,30 +48,13 @@ export const AbgabetoolStudent = { }; }, methods: { - dateDiffInDays(datumParam) { - let datum = datumParam - if(datumParam instanceof Date && !isNaN(datum.getTime())) - { - const year = datumParam.getFullYear(); - const month = datumParam.getMonth() + 1; // getMonth() is 0-indexed - const day = datumParam.getDate(); - const pad = (num) => String(num).padStart(2, '0'); - datum = `${year}-${pad(month)}-${pad(day)}` - } - - const dateToday = luxon.DateTime.now().startOf('day'); - const dateDatum = luxon.DateTime.fromISO(datum).startOf('day'); - const duration = dateDatum.diff(dateToday, 'days'); - - return duration.values.days; - }, getDateStyleClass(termin) { - const datum = new Date(termin.datum) - const abgabedatum = new Date(termin.abgabedatum) - - termin.diffindays = this.dateDiffInDays(termin.datum) - - const isLate = termin.abgabedatum && abgabedatum > datum; + const zone = 'Europe/Vienna'; + const today = luxon.DateTime.now().setZone(zone); + 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) { From 4724008c2df9231341e6ee3d64fcd9e060e5e0ac Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Wed, 18 Feb 2026 14:32:57 +0100 Subject: [PATCH 39/51] betreuer page update table after adding serientermin qgate1/2 status prev/next; preserve scrollX/Y in betreuer/assistenz page --- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 18 +++++++++--------- .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 8b0fe7ddb..fb6a8cd64 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -643,21 +643,21 @@ export const AbgabetoolAssistenz = { // reset selection to empty // this.$refs.abgabeTable.tabulator.deselectRow() const table = this.$refs.abgabeTable.tabulator; - const scrollX = table.rowManager.element.scrollLeft; - const scrollY = table.rowManager.element.scrollTop; + const scrollX = table.rowManager.scrollLeft; + const scrollY = table.rowManager.scrollTop; const mappedData = this.mapProjekteToTableData(this.projektarbeiten) table.setData(mappedData) table.redraw(true) - - requestAnimationFrame(() => { - table.rowManager.element.scrollLeft = scrollX; - table.rowManager.element.scrollTop = scrollY; - }); - - + Vue.nextTick(()=> { + const table = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder') + if(table) { + table.scrollLeft = scrollX; + table.scrollTop = scrollY; + } + }) }).finally(()=>{ this.saving = false diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 2ad29d20c..2453f33bf 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -563,6 +563,24 @@ export const AbgabetoolMitarbeiter = { )).then(res => { if (res.meta.status === "success" && res.data) { this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/serienTerminGespeichert')) + + const oldScrollLeft = this.$refs.abgabeTable?.tabulator.rowManager.scrollLeft + const oldScrollTop = this.$refs.abgabeTable?.tabulator.rowManager.scrollTop + this.loading = true + this.loadProjektarbeiten(this.showAll, () => { + this.$refs.abgabeTable?.tabulator.redraw(true) + this.$refs.abgabeTable?.tabulator.setSort([]); + this.loading = false + + Vue.nextTick(()=> { + const table = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder') + if(table) { + table.scrollLeft = oldScrollLeft; + table.scrollTop = oldScrollTop; + } + }) + + }) } else { this.$fhcAlert.alertError(this.$p.t('abgabetool/errorSerienterminSpeichern')) } From 7169cb68a2bbbf9d284ee99fabc366d7214ad954 Mon Sep 17 00:00:00 2001 From: Harald Bamberger Date: Thu, 19 Feb 2026 09:20:10 +0100 Subject: [PATCH 40/51] fix bug when sending multi messages introduced by loading time optimisation --- public/js/components/Messages/Messages.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/components/Messages/Messages.js b/public/js/components/Messages/Messages.js index 5e247ddb5..e1fb69dc3 100644 --- a/public/js/components/Messages/Messages.js +++ b/public/js/components/Messages/Messages.js @@ -158,7 +158,7 @@ export default {
    Date: Thu, 19 Feb 2026 17:33:41 +0100 Subject: [PATCH 41/51] avoid loading paabgaben a 2nd time for mitarbeiter; extracted getDateStyleClass from components; --- .../controllers/api/frontend/v1/Abgabe.php | 8 ++ .../Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 67 ++++------ .../Cis/Abgabetool/AbgabeStudentDetail.js | 2 +- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 48 +------ .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 118 ++++++++---------- .../Cis/Abgabetool/AbgabetoolStudent.js | 44 +------ .../Cis/Abgabetool/getDateStyleClass.js | 37 ++++++ 7 files changed, 126 insertions(+), 198 deletions(-) create mode 100644 public/js/components/Cis/Abgabetool/getDateStyleClass.js diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index f0744fb99..a6390e97d 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -531,6 +531,14 @@ class Abgabe extends FHCAPI_Controller $pa->beurteilungLinkNew = $newLink; $pa->beurteilungLinkOld = $oldLink; + // has previously been retrieved via getStudentProjektabgaben but is fetched in advance to avoid having to reload abgaben + $projektarbeitIsCurrent = false; + $returnFunc = function ($result) use (&$projektarbeitIsCurrent) { + $projektarbeitIsCurrent = $result; + }; + Events::trigger('projektarbeit_is_current', $pa->projektarbeit_id, $returnFunc); + $pa->isCurrent = $projektarbeitIsCurrent; + $filterFunc = function($projektabgabe) use ($pa) { return $projektabgabe->projektarbeit_id == $pa->projektarbeit_id; }; diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index f86fa44d9..b760f567d 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -1,8 +1,8 @@ import BsModal from '../../Bootstrap/Modal.js'; import VueDatePicker from '../../vueDatepicker.js.php'; import ApiAbgabe from '../../../api/factory/abgabe.js' +import { getDateStyleClass } from "./getDateStyleClass.js"; -const today = new Date() export const AbgabeMitarbeiterDetail = { name: "AbgabeMitarbeiterDetail", components: { @@ -125,10 +125,12 @@ export const AbgabeMitarbeiterDetail = { // only insert new abgabe if we actually created a new one, not when saving/editing existing if(!existingTerminRes){ + newTerminRes.dateStyle = getDateStyleClass(newTerminRes, this.notenOptions) this.projektarbeit.abgabetermine.push(newTerminRes) } else { const noteOptExisting = this.allowedNotenOptions.find(opt => opt.note == existingTerminRes.note) existingTerminRes.note = noteOptExisting + termin.dateStyle = getDateStyleClass(termin, this.notenOptions) } this.projektarbeit.abgabetermine.sort((a, b) =>new Date(a.datum) - new Date(b.datum)) @@ -270,40 +272,6 @@ export const AbgabeMitarbeiterDetail = { 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)) }, - getDateStyleClass(termin) { - const zone = 'Europe/Vienna'; - const today = luxon.DateTime.now().setZone(zone); - 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) { - if (termin.note.positiv) return 'bestanden'; - return 'nichtbestanden'; - } - - // ACTION REQUIRED FOR GRADE - if (termin.bezeichnung?.benotbar && datum < today) { - return 'beurteilungerforderlich'; - } - - // SUBMISSION STATUS - if (termin.upload_allowed) { - if (termin.abgabedatum) { - return isLate ? 'verspaetet' : 'abgegeben'; - } - - // no submission yet - if (datum < today) return 'verpasst'; - if (termin.diffindays <= 12) return 'abzugeben'; - return 'standard'; - } - - // GENERIC STATUS - return datum < today ? 'verpasst' : 'standard'; - }, openBeurteilungLink(link) { window.open(link, '_blank') }, @@ -769,20 +737,29 @@ export const AbgabeMitarbeiterDetail = {
    -