Compare commits

...
163 Commits
Author SHA1 Message Date
Harald Bamberger e818bc5e4d Merge branch 'cis40_2026-09_ma_rc' into demo-cis40 2026-10-12 14:26:11 +02:00
Harald Bamberger 3e06740758 add permissions cis/eigene_zeitsperren and cis/andere_zeitsperren and use them for zeitsperren and zeitsperrenma applications and api endpoints 2026-10-12 14:25:31 +02:00
Harald Bamberger dc4939ee78 Merge branch 'cis40_2026-09_ma_rc' into demo-cis40 2026-10-12 11:38:22 +02:00
Harald Bamberger bca2884aa2 CisMenuApp fetch Authinfo, Cis search use isMitarbeiter flag to conditinally render Zeitsperren Button in search result 2026-10-12 11:37:45 +02:00
Harald Bamberger e2c2339f89 Merge branch 'cis40_2026-09_ma_rc' into demo-cis40 2026-10-12 10:33:36 +02:00
Harald Bamberger 63d0bfc69b Merge branch 'cis40_2026-09_ma_rc_bugfix-84008-lvplan-with-lvid-shows-reservations' into cis40_2026-09_ma_rc 2026-10-12 10:30:29 +02:00
Harald Bamberger ab05f32fde Merge branch 'cis40_2026-09_ma_rc' into cis40_2026-09_ma_rc_bugfix-84008-lvplan-with-lvid-shows-reservations 2026-10-12 10:21:03 +02:00
Harald Bamberger 68482a429e Merge branch 'cis40_2026-09_ma_rc' into demo-cis40 2026-10-12 09:38:22 +02:00
Harald Bamberger 62e704a6cf add berechtigung dashboard/widget_studstatus_ma, adapt dbupdate for widget studstatus to use new permission and use dashboard_kurzbz and widget_kurzbz to link widget to CIS dashboard 2026-10-12 09:37:37 +02:00
Harald Bamberger 2d5446bfc7 Merge branch 'feature-83700/RaumsucheStandort' into cis40_2026-09_ma_rc 2026-10-12 08:12:01 +02:00
Harald Bamberger 6f4482cff4 Merge branch 'cis40_2026-09_ma_rc' into demo-cis40 2026-10-09 13:17:02 +02:00
Harald Bamberger 96a373682a Merge branch 'master' into cis40_2026-09_ma_rc 2026-10-09 13:16:44 +02:00
adisposkofh cdd36f61b1 in lv plan not fetching reservations if lv id query param is present 2026-10-09 12:10:32 +02:00
adisposkofh ebb0042881 Merge branch 'cis40_2026-05_ma_rc_83533-mylv-rework' into cis40_2026-09_ma_rc 2026-10-09 11:22:47 +02:00
adisposkofh 4e7a3b22fe displaying wahlname (if not null) instead of vorname in betreuungen table 2026-10-09 10:04:50 +02:00
Johann Hoffmann 221d810f3f raumsuche basic filter by reservieren & aktiv = true; added erweiterte filter and columns for m², stockwerk and gebäudeteil; added the standort category "Ohne Standort" for virtual ones; 2026-10-08 18:01:39 +02:00
adisposkofh 45647f0cec showing average grades in mylv table (already implemented in cards view) 2026-10-07 11:39:22 +02:00
adisposkofh 50a518ea7a additional mylv localizations 2026-10-07 10:53:46 +02:00
adisposkofh 2b44b421dc minor phrases fix 2026-10-07 10:16:26 +02:00
adisposkofh 70a93cc093 minor phrases fix 2026-10-07 10:15:57 +02:00
Andreas Österreicher 0c4d102850 Merge branch 'bug-83698/StudVW_Projektarbeit_Kontaktdaten' 2026-10-07 09:11:42 +02:00
Andreas Österreicher 2e15f2c5ab Edit Dialog von Kontaktdaten bei Projektbetreuer die Mitarbeiter sind wird nicht angezeigt 2026-10-07 09:10:51 +02:00
adisposkofh 4768fbda15 saving last selected myLv mode; hiding modes for students 2026-10-06 17:13:42 +02:00
adisposkofh d8959d9776 added groups to myLv lehreinheiten table 2026-10-06 17:00:28 +02:00
Harald Bamberger 9e2a818eab Merge branch 'cis40_2026-09_ma_rc' into demo-cis40 2026-10-06 15:35:02 +02:00
Harald Bamberger 3214ae3101 Merge branch 'cis40_2026-09_ma_rc' into demo-cis40 2026-10-06 14:20:21 +02:00
adisposkofh 4549c92d50 myLV lehreinheiten table formatting 2026-10-06 13:48:23 +02:00
Harald Bamberger 5313693651 Merge branch 'cis40_2026-09_ma_rc' into demo-cis40 2026-10-05 18:32:04 +02:00
adisposkofh a5aa4c8466 added myLv lehrenheiten table 2026-10-05 15:14:04 +02:00
Harald Bamberger 8db60c67f8 Merge branch 'cis40_2026-09_ma_rc' into demo-cis40 2026-10-05 13:11:41 +02:00
adisposkofh 8b58c82385 myLv supervisor table formatting 2026-10-05 12:58:38 +02:00
Manuela Thamer 3034615455 fix pagelock of modals
prevent editing contact data of employees
2026-10-01 17:27:54 +02:00
Johann Hoffmann 9b137452d9 standort filter in raumsuche; fix the ort/standort queries so _DUMMY rooms actually get filtered out as intended 2026-10-01 17:21:55 +02:00
Harald Bamberger 65c931ea2d Merge branch 'cis40_2026-09_ma_rc' into demo-cis40 2026-10-01 12:44:52 +02:00
Harald Bamberger 762fd1cadd update demo datenstand 2026-09-30 16:20:15 +02:00
Harald Bamberger 9feb442a07 Merge branch 'cis40_2026-09_ma_rc' into demo-cis40 2026-09-30 16:14:03 +02:00
adisposkofh af978e1312 implemented myLv supervisor view 2026-09-30 13:23:03 +02:00
adisposkofh 22500a96c1 Merge branch 'cis40_2026-09_ma_rc' into cis40_2026-05_ma_rc_83533-mylv-rework 2026-09-29 17:13:45 +02:00
adisposkofh 03af6bfda5 added different views (lvs, lehrenheiten, supervisor) to mylv page 2026-09-29 17:12:52 +02:00
Harald Bamberger 0ce4acc71b Merge branch 'master' into demo-cis40 2026-08-21 14:10:33 +02:00
Harald Bamberger 33038f409a Merge branch 'cis40_2026-05_ma_rc_routerlink_ma0080' into demo-cis40 2026-08-21 11:44:17 +02:00
Harald Bamberger f8cea9036c Merge branch 'cis40_2026-05_ma_rc_routerlink_ma0080' into demo-cis40 2026-08-21 08:45:25 +02:00
Harald Bamberger 759aad047b Merge branch 'cis40_2026-05_ma_rc_routerlink' into demo-cis40 2026-07-06 15:33:02 +02:00
Harald Bamberger fd0262f708 Merge branch 'cis40_2026-05_ma_rc_routerlink' into demo-cis40 2026-07-06 10:53:15 +02:00
Harald Bamberger 16689f4d6e Merge branch 'cis40_2026-05_ma_rc_routerlink' into demo-cis40 2026-07-03 18:11:59 +02:00
Harald Bamberger 0bd75716f2 Merge branch 'cis40_2026-05_ma_rc_routerlink' into demo-cis40 2026-07-03 16:51:32 +02:00
Harald Bamberger 9782fe5459 Merge branch 'cis40_2026-05_ma_rc_routerlink' into demo-cis40 2026-07-03 14:36:49 +02:00
Harald Bamberger e110e8fa57 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-07-01 13:04:44 +02:00
Harald Bamberger 76b9c6781d Merge branch 'studvw_2026_05_rc_tags' into demo-cis40 2026-06-26 15:17:42 +02:00
Harald Bamberger 856706bc1c Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-06-26 15:17:27 +02:00
Harald Bamberger d54747c4df Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-06-24 15:52:23 +02:00
Harald Bamberger da3b329772 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-06-24 15:42:16 +02:00
Harald Bamberger 41d40102f3 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-06-22 15:26:48 +02:00
Harald Bamberger 1496045c40 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-06-18 13:46:21 +02:00
Harald Bamberger e2dadcd208 Merge branch 'studvw_2026_05_rc_tags' into demo-cis40 2026-06-18 13:23:49 +02:00
Harald Bamberger 070ac1e878 override permission for all methods not only the overwritten ones 2026-06-15 14:37:43 +02:00
Harald Bamberger 63ff8dd4c4 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-29 13:40:02 +02:00
Harald Bamberger 366edb7e90 Merge branch 'studvw_2026_05_rc_tags' into demo-cis40 2026-05-26 11:01:57 +02:00
Harald Bamberger 9430135592 Merge branch 'studvw_2026_05_rc_tags' into demo-cis40 2026-05-26 09:40:11 +02:00
Harald Bamberger 199b62d13f Merge branch 'studvw_2026_05_rc' into demo-cis40 2026-05-26 09:07:04 +02:00
Harald Bamberger b0fc5a3618 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-22 10:37:30 +02:00
Harald Bamberger 6e599908cd Merge branch 'studvw_2026_05_rc_tags' into demo-cis40 2026-05-19 17:30:14 +02:00
Harald Bamberger b1be5a4ba4 Merge branch 'studvw_2026_05_rc_tags' into demo-cis40 2026-05-19 17:12:55 +02:00
Harald Bamberger 71f68da90b Merge branch 'studvw_2026_05_rc_tags' into demo-cis40 2026-05-19 16:52:36 +02:00
Harald Bamberger c11fd03362 Merge branch 'studvw_2026_05_rc_tags' into demo-cis40 2026-05-19 16:42:43 +02:00
Harald Bamberger ec1422a136 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-19 16:18:06 +02:00
Harald Bamberger 89e98056e6 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-19 16:12:31 +02:00
Harald Bamberger 5cf14aa886 Merge branch 'studvw_2026_05_rc_tags' into demo-cis40 2026-05-19 10:28:32 +02:00
Harald Bamberger 9ee6a0a01a Merge branch 'studvw_2026_05_rc_tags' into demo-cis40 2026-05-19 09:51:42 +02:00
Harald Bamberger 6d26ec2ab7 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-18 16:51:33 +02:00
Harald Bamberger 0a18fcf732 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-13 21:11:36 +02:00
Harald Bamberger e08b731cb5 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-12 11:05:28 +02:00
Harald Bamberger d1a4cfd5df Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-11 13:55:07 +02:00
Harald Bamberger 37b70861d1 update date of use db data 2026-05-07 15:34:53 +02:00
Harald Bamberger 150d54eeda Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-07 14:02:19 +02:00
Harald Bamberger 68b26a1091 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-06 18:36:48 +02:00
Harald Bamberger 5c371a20d2 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-06 18:32:35 +02:00
Harald Bamberger d42157c5f8 Merge branch 'cis40_2026-05_ma_rc' into demo-cis40 2026-05-06 18:19:53 +02:00
Harald Bamberger 502851856b Merge branch 'studvw_2026_05_rc' into demo-cis40 2026-05-06 17:57:36 +02:00
Harald Bamberger 4b767d4a57 Merge branch 'vv_und_studvw_2026_02_rc4_ma0080' into demo-cis40 2026-03-10 08:42:20 +01:00
Harald Bamberger 5d461a72f6 Merge branch 'vv_und_studvw_2026_02_rc4_ma0080' into demo-cis40 2026-03-09 12:11:14 +01:00
Harald Bamberger 942b4512fe Merge branch 'vv_und_studvw_2026_02_rc4_ma0080' into demo-cis40 2026-03-04 18:57:47 +01:00
Harald Bamberger 3778b27574 Datenstand 12.02.2026 2026-03-04 18:31:13 +01:00
Harald Bamberger 15b340db56 Merge branch 'vv_und_studvw_2026_02_rc4_ma0080' into demo-cis40 2026-03-04 17:58:14 +01:00
Andreas Österreicher 05838cc477 Merge branch 'master' into demo-cis40 2026-01-21 16:35:41 +01:00
Andreas Österreicher 96e02c9911 Merge branch 'epic-56039/LV-Evaluierung' into demo-cis40 2026-01-20 10:55:13 +01:00
Andreas Österreicher 248fceb4b5 Merge branch 'epic-56039/LV-Evaluierung' into demo-cis40 2026-01-20 10:34:10 +01:00
Harald Bamberger 92697d9468 Merge branch 'epic-56039/LV-Evaluierung' into demo-cis40 2026-01-12 16:30:15 +01:00
Harald Bamberger d1ec9c92eb Merge branch 'master' into demo-cis40 2026-01-12 16:10:13 +01:00
Harald Bamberger 454cf5ea64 Merge branch 'studvw_2025-12_rc3' into demo-cis40 2025-12-10 15:36:30 +01:00
Harald Bamberger 4563533e67 Merge branch 'feature-69570/StudVw_Filter_broken' into demo-cis40 2025-12-04 08:43:40 +01:00
Harald Bamberger 640e719eda Merge branch 'feature-69438/FHC4_Studierendenverwaltung/FeedbackPunkte' into demo-cis40 2025-12-02 09:46:25 +01:00
Harald Bamberger aece2b1d90 Merge branch 'feature-69065/Projektarbeiten_Firmen_verwalten' into demo-cis40 2025-11-28 11:16:24 +01:00
Harald Bamberger cb13655ecd Merge branch 'master' into demo-cis40 2025-11-24 17:14:14 +01:00
Harald Bamberger 9b3b6cec8f Merge branch 'feature-69388/Pruefung_API_insert_Propleme' into demo-cis40 2025-11-24 16:27:06 +01:00
Harald Bamberger 3ca1a1f1c7 Merge branch 'feature-69388/Pruefung_API_insert_Propleme' into demo-cis40 2025-11-24 09:48:38 +01:00
Harald Bamberger a7210208d6 Merge branch 'studvw_2025-11_rc2' into demo-cis40 2025-11-24 09:42:45 +01:00
Harald Bamberger eaad23ff69 Merge branch 'feature-69388/Pruefung_API_insert_Propleme' into demo-cis40 2025-11-21 12:30:37 +01:00
Harald Bamberger c7ddf0a2a9 Merge branch 'studvw_2025-11_rc2' into demo-cis40 2025-11-21 10:56:32 +01:00
Harald Bamberger b129ef873b Merge branch 'master' into demo-cis40 2025-11-18 16:46:11 +01:00
Harald Bamberger d927426d70 Merge branch 'master' into demo-cis40 2025-11-14 11:40:48 +01:00
Harald Bamberger 9ced137ded Merge branch 'studvw_2025-11_rc' into demo-cis40 2025-11-11 13:18:13 +01:00
Harald Bamberger a89da50e0a add missing comma 2025-11-10 13:08:36 +01:00
Harald Bamberger 9256046b6c Merge branch 'feature-67518/Studierendenverwaltung_Karteireiter_anzeigen_verstecken_wenn_Interessent_gewaehlt' into demo-cis40 2025-11-10 13:05:32 +01:00
Harald Bamberger fd95dd8023 Merge branch 'feature-63443/Studierendenverwaltung_Tab_Abschlusspruefung_Finetuning' into demo-cis40 2025-11-10 10:58:41 +01:00
Harald Bamberger cda2b84939 Merge branch 'feature-68738/FHC4_Studierendenverwaltung_Studentlist_Export' into demo-cis40 2025-11-10 09:28:36 +01:00
Harald Bamberger d893fe63e7 Merge branch 'master' into demo-cis40 2025-11-05 17:15:02 +01:00
Harald Bamberger 5aedf85982 Merge branch 'master' into demo-cis40 2025-11-05 16:52:01 +01:00
Harald Bamberger 55ab79c004 Merge branch 'master' into demo-cis40 2025-11-05 16:32:54 +01:00
Harald Bamberger 7c7a72600d Merge branch 'master' into demo-cis40 2025-11-05 16:18:00 +01:00
Harald Bamberger 01357654c0 Merge branch 'feature-67490/studstatus_suche_abort_controller_haengt' into demo-cis40 2025-11-05 11:03:40 +01:00
Harald Bamberger cd5900481b Merge branch 'feature-68745/Menue_zur_Verlinkung_von_Apps' into demo-cis40 2025-11-04 13:12:56 +01:00
Harald Bamberger ddef97d49f Merge branch 'master' into demo-cis40 2025-11-03 15:35:39 +01:00
Harald Bamberger 47b0449f22 Merge branch 'feature-68745/Menue_zur_Verlinkung_von_Apps' into demo-cis40 2025-10-29 07:41:17 +01:00
Harald Bamberger 0193460678 Merge branch 'master' into demo-cis40 2025-10-16 15:41:06 +02:00
Harald Bamberger e02b3e78d2 Merge branch 'master' into demo-cis40 2025-10-14 10:41:07 +02:00
Harald Bamberger c91e0a6b74 Merge branch 'feature-68390/Studierendenverwaltung-Details_Header_und_Tabsnav_nicht_mitscrollen' into demo-cis40 2025-10-01 18:47:36 +02:00
Harald Bamberger c6852a9514 Merge branch 'feature-63445/Studierendenverwaltung_Filter' into demo-cis40 2025-10-01 18:12:07 +02:00
Harald Bamberger 04b5240615 Merge branch 'master' into demo-cis40 2025-10-01 17:46:28 +02:00
Harald Bamberger 483547cd9c Merge branch 'feature-63394/StV_Favoriten_64_Zeichen_Variablenbeschraenkung' into demo-cis40 2025-09-30 16:37:34 +02:00
Harald Bamberger 345413cd1f change display date of database dump 2025-09-30 14:15:16 +02:00
Harald Bamberger 046a60b89c Merge branch 'master' into demo-cis40 2025-09-30 13:26:17 +02:00
Harald Bamberger da725e3ab1 Merge branch 'feature-63444/stv_mehrfachaktion_mail_an_private_oder_interne_adresse' into demo-cis40 2025-09-29 17:40:26 +02:00
Harald Bamberger edd34941cc Merge branch 'feature-61232/Studierendenverwaltung_Karteireiter_Projektarbeit_portieren' into demo-cis40 2025-09-29 16:57:10 +02:00
Harald Bamberger c65686ef42 Merge branch 'feature-63373/FHC4_Studierendenverwaltung_Dokumente_erstellen' into demo-cis40 2025-09-29 13:53:30 +02:00
Harald Bamberger c7729f85df Merge branch 'master' into demo-cis40 2025-09-29 08:09:33 +02:00
Harald Bamberger 53b187c9b6 Merge branch 'feature-63435/Studierendenverwaltung_Studierende_Verbandsgruppen_und_Spezialgruppen_zuordnen_Multiaktion' into demo-cis40 2025-09-18 07:23:43 +02:00
Harald Bamberger 7bc18e41b1 Merge branch 'feature-63374/Studierendenverwaltung_International_Baum_Filter' into demo-cis40 2025-09-18 07:22:24 +02:00
Harald Bamberger 8bc17eded5 Merge branch 'master' into demo-cis40 2025-09-18 07:17:28 +02:00
Harald Bamberger 9adeb1c63a Merge branch 'master' into demo-cis40 2025-09-17 14:39:30 +02:00
Harald Bamberger 091ed0dacd Merge branch 'master' into demo-cis40 2025-09-17 14:16:05 +02:00
Harald Bamberger cf0a47cab7 Merge branch 'master' into demo-cis40 2025-08-19 17:36:09 +02:00
Harald Bamberger bb7b24346a Merge branch 'master' into demo-cis40 2025-08-19 17:20:43 +02:00
Harald Bamberger a68e2e798b Merge branch 'master' into demo-cis40 2025-08-19 16:08:13 +02:00
Harald Bamberger bd49f3c420 Merge branch 'master' into demo-cis40 2025-08-19 14:00:40 +02:00
Harald Bamberger 3c1d67267d Merge branch 'master' into demo-cis40 2025-08-19 13:53:28 +02:00
Harald Bamberger 61e1933a5b activate button new Student for demo-cis4 2025-08-19 13:33:39 +02:00
Harald Bamberger 25c443098c Merge branch 'master' into demo-cis40 2025-08-19 13:23:37 +02:00
Harald Bamberger 352c6e1d68 Merge branch 'master' into demo-cis40 2025-08-14 18:16:36 +02:00
Harald Bamberger 0e2444d7ce Merge branch 'feature-63411/Notizen_Anzahl_im_Tab_Header_anzeigen' into demo-cis40 2025-08-14 16:46:22 +02:00
Harald Bamberger c4390819a2 Merge branch 'feature-63411/Notizen_Anzahl_im_Tab_Header_anzeigen' into demo-cis40 2025-08-14 16:36:43 +02:00
Harald Bamberger 51104268d8 Merge branch 'feature-63411/Notizen_Anzahl_im_Tab_Header_anzeigen' into demo-cis40 2025-08-14 15:34:25 +02:00
Harald Bamberger cb5d43ab21 Merge branch 'master' into demo-cis40 2025-08-14 10:31:23 +02:00
Harald Bamberger 08de5cd377 Merge branch 'master' into demo-cis40 2025-08-13 18:48:08 +02:00
Harald Bamberger 274fa63658 Merge branch 'feature-62779/xul_abloese_haustechnik_schluesselverwaltung' into demo-cis40 2025-08-13 07:34:45 +02:00
Harald Bamberger 81b2ed6551 Merge branch 'merge_FHC4_55354_55991_55992_60874_60876_60875_61229_61230_61231' into demo-cis40 2025-08-13 07:05:21 +02:00
Harald Bamberger d9c6c3947b Merge branch 'feature-60973/komponente_fuer_lehrfaecherverteilung' into demo-cis40 2025-08-13 07:04:51 +02:00
Harald Bamberger 2d6dbbdb16 alter position and styling of demo info 2025-08-12 09:30:12 +02:00
Harald Bamberger b60faa1cab styling demo info 2025-08-12 09:24:09 +02:00
Harald Bamberger fd19953864 add demo info 2025-08-12 09:20:39 +02:00
Harald Bamberger 927bafd93a Merge branch 'master' into demo-cis40 2025-08-12 09:06:31 +02:00
Harald Bamberger d93b21f0af Merge branch 'master' into demo-cis40 2025-08-11 11:28:38 +02:00
Harald Bamberger 6a8f6fa05a Merge branch 'master' into demo-cis40 2025-08-06 12:17:25 +02:00
Harald Bamberger 7f997c9411 Merge branch 'rc1_FHC4_C4' into demo-cis40 2025-08-05 18:36:34 +02:00
Harald Bamberger 2e3f98deb0 Merge branch 'rc1_FHC4_C4' into demo-cis40 2025-08-05 16:34:44 +02:00
Harald Bamberger a5feb656e0 Merge branch 'rc1_FHC4_C4' into demo-cis40 2025-08-05 16:19:02 +02:00
Harald Bamberger 9329955328 Merge branch 'rc1_FHC4_C4' into demo-cis40 2025-08-05 13:29:03 +02:00
Harald Bamberger 5b99dd2685 Merge branch 'rc1_FHC4_C4' into demo-cis40 2025-08-05 12:21:03 +02:00
Harald Bamberger 19288fc8f5 Merge branch 'rc1_FHC4_C4' into demo-cis40 2025-08-05 12:00:03 +02:00
Andreas Österreicher 9f97de5952 Merge branch 'rc1_FHC4_C4' into demo-cis40 2025-08-05 09:42:35 +02:00
Andreas Österreicher c476f7339c Merge branch 'rc1_FHC4_C4' into demo-cis40 2025-08-05 09:31:18 +02:00
Harald Bamberger 3dc1cf520a Merge branch 'rc1_FHC4_C4' into demo-cis40 2025-08-05 07:22:01 +02:00
Harald Bamberger 616c335a3b Merge branch 'rc1_FHC4_C4' into demo-cis40 2025-08-05 07:10:49 +02:00
36 changed files with 2561 additions and 162 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ class Zeitsperren extends Auth_Controller
public function __construct()
{
parent::__construct([
'index' => ['basis/cis:r'],
'index' => ['cis/eigene_zeitsperren:r'],
]);
// Load Libraries
@@ -13,7 +13,7 @@ class Zeitsperrenma extends Auth_Controller
public function __construct()
{
parent::__construct([
'index' => ['basis/cis:r']
'index' => ['cis/andere_zeitsperren:r']
]);
}
@@ -7,14 +7,14 @@ class MaZeitsperren extends FHCAPI_Controller
public function __construct()
{
parent::__construct([
'getAllActiveZeitsperren' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'getAllZeitsperrenFixeMa' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'getAllZeitsperrenLector' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'getAllZeitsperrenOes' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'getZeitsperrenAss' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'getAllActiveZeitsperren' => ['cis/andere_zeitsperren:r', 'admin:r', 'assistenz:r'],
'getAllZeitsperrenFixeMa' => ['cis/andere_zeitsperren:r', 'admin:r', 'assistenz:r'],
'getAllZeitsperrenLector' => ['cis/andere_zeitsperren:r', 'admin:r', 'assistenz:r'],
'getAllZeitsperrenOes' => ['cis/andere_zeitsperren:r', 'admin:r', 'assistenz:r'],
'getZeitsperrenAss' => ['cis/andere_zeitsperren:r', 'admin:r', 'assistenz:r'],
'getStgLectors' => self::PERM_LOGGED,
'loadZeitsperrenLectorStg' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'loadZeitsperrenMa' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'loadZeitsperrenLectorStg' => ['cis/andere_zeitsperren:r', 'admin:r', 'assistenz:r'],
'loadZeitsperrenMa' => ['cis/andere_zeitsperren:r', 'admin:r', 'assistenz:r'],
'getDetailsMa' => self::PERM_LOGGED
]);
+123 -6
View File
@@ -25,7 +25,19 @@ if (! defined('BASEPATH')) exit('No direct script access allowed');
*/
class Ort extends FHCAPI_Controller
{
// advanced filters of getRooms: tbl_ort column => filter type, the same list as in Raumsuche.js.
// text: contains, case-insensitive; select: equals; range: <column>_min, <column>_max; flag: true; exists: not null
const ADVANCED_FILTERS = [
'ort_kurzbz' => 'text',
'bezeichnung' => 'text',
'planbezeichnung' => 'text',
'stockwerk' => 'range',
'gebteil' => 'select',
'm2' => 'range',
'lehre' => 'flag',
'content_id' => 'exists'
];
/**
* Object initialization
*/
@@ -37,7 +49,9 @@ class Ort extends FHCAPI_Controller
'getOrtKurzbzContent' => self::PERM_LOGGED,
'getRooms' => self::PERM_LOGGED,
'getTypes' => self::PERM_LOGGED,
'getRoomInfo' => self::PERM_LOGGED
'getRoomInfo' => self::PERM_LOGGED,
'getStandorte' => self::PERM_LOGGED,
'getAdvancedFilterOptions' => self::PERM_LOGGED
]);
$this->load->model('ressource/Ort_model', 'OrtModel');
@@ -57,6 +71,17 @@ class Ort extends FHCAPI_Controller
$this->form_validation->set_rules('datum','Datum','required');
$this->form_validation->set_rules('von','Uhrzeit Von','required|regex_match[/^[0-9]{2}:[0-9]{2}$/]');
$this->form_validation->set_rules('bis','Uhrzeit Bis','required|regex_match[/^[0-9]{2}:[0-9]{2}$/]');
$this->form_validation->set_rules('standort_id','Standort','regex_match[/^([0-9]+|none)$/]');
foreach (self::ADVANCED_FILTERS as $column => $type) {
if ($type == 'range') {
$this->form_validation->set_rules($column.'_min', $column, 'numeric');
$this->form_validation->set_rules($column.'_max', $column, 'numeric');
} elseif ($type == 'flag' || $type == 'exists') {
$this->form_validation->set_rules($column, $column, 'in_list[true]');
} else {
$this->form_validation->set_rules($column, $column, 'max_length[255]');
}
}
if($this->form_validation->run() == FALSE) {
$this->terminateWithValidationErrors($this->form_validation->error_array());
}
@@ -66,6 +91,7 @@ class Ort extends FHCAPI_Controller
$bis = $this->input->get('bis', TRUE);
$typ = $this->input->get('typ', TRUE);
$personenanzahl = $this->input->get('personenanzahl', TRUE);
$standort_id = $this->input->get('standort_id', TRUE);
$this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
@@ -78,21 +104,33 @@ class Ort extends FHCAPI_Controller
$this->load->model('ressource/Ort_model', 'OrtModel');
$params = array();
$qry = "SELECT DISTINCT tbl_ort.*
$qry = "SELECT DISTINCT tbl_ort.*, tbl_standort.bezeichnung AS standort
FROM public.tbl_ort JOIN public.tbl_ortraumtyp USING(ort_kurzbz)
WHERE aktiv AND lehre AND ort_kurzbz NOT LIKE '\\\\_%'";
LEFT JOIN public.tbl_standort USING(standort_id)
WHERE aktiv AND reservieren";
if($typ) {
$params[] = $typ;
$qry.= "AND raumtyp_kurzbz = ?";
$qry.= " AND raumtyp_kurzbz = ?";
}
if($standort_id === 'none') { // rooms without a Standort
$qry.= " AND standort_id IS NULL";
} elseif($standort_id) {
$params[] = $standort_id;
$qry.= " AND standort_id = ?";
} else {
$qry.= " AND standort_id IS NOT NULL";
}
$qry.= $this->advancedFilterSql($params);
if(!$isMitarbeiter) { // students are only allowed to get a subset defined by config
$qry.= ' AND raumtyp_kurzbz IN ?';
$params[] = $this->config->item('roomtypes_student');
$this->addMeta('config', $this->config->item('roomtypes_student'));
}
$qry.= "AND (max_person>= ? OR max_person is null)";
$qry.= " AND (max_person>= ? OR max_person is null)";
$params[] = $personenanzahl;
$qry.=" AND ort_kurzbz NOT IN
@@ -130,6 +168,41 @@ class Ort extends FHCAPI_Controller
$this->terminateWithSuccess(getData($result));
}
/**
* Standorte of the rooms that pass the base filter of getRooms
*/
public function getStandorte()
{
$qry = "SELECT DISTINCT standort_id, tbl_standort.bezeichnung
FROM public.tbl_ort JOIN public.tbl_standort USING(standort_id)
WHERE aktiv AND reservieren
ORDER BY tbl_standort.bezeichnung";
$result = $this->OrtModel->execReadOnlyQuery($qry);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
/**
* Values of the select filters: column => distinct values of the rooms that pass the base filter of getRooms
*/
public function getAdvancedFilterOptions()
{
$options = [];
foreach (self::ADVANCED_FILTERS as $column => $type) {
if ($type != 'select')
continue;
$result = $this->OrtModel->execReadOnlyQuery("SELECT DISTINCT ".$column." AS value
FROM public.tbl_ort
WHERE aktiv AND reservieren AND ".$column." <> ''
ORDER BY ".$column);
$options[$column] = array_column($this->getDataOrTerminateWithError($result), 'value');
}
$this->terminateWithSuccess($options);
}
/**
* Gets a JSON body via HTTP POST and provides the parameters
*/
@@ -188,5 +261,49 @@ class Ort extends FHCAPI_Controller
$this->terminateWithSuccess($room);
}
//------------------------------------------------------------------------------------------------------------------
// Private methods
/**
* SQL conditions of the advanced filters in the GET parameters, adds their values to $params
*/
private function advancedFilterSql(&$params)
{
// no xss_clean: the values are bound, and it could change a select value so that it no longer equals the column
$sql = '';
foreach (self::ADVANCED_FILTERS as $column => $type) {
if ($type == 'range') {
$min = $this->input->get($column.'_min');
$max = $this->input->get($column.'_max');
if ($min !== null && $min !== '') {
$sql.= " AND tbl_ort.".$column." >= ?::numeric";
$params[] = $min;
}
if ($max !== null && $max !== '') {
$sql.= " AND tbl_ort.".$column." <= ?::numeric";
$params[] = $max;
}
continue;
}
$value = $this->input->get($column);
if ($value === null || $value === '')
continue;
if ($type == 'flag') {
$sql.= " AND tbl_ort.".$column;
} elseif ($type == 'exists') {
$sql.= " AND tbl_ort.".$column." IS NOT NULL";
} elseif ($type == 'select') {
$sql.= " AND tbl_ort.".$column." = ?";
$params[] = $value;
} else {
$sql.= " AND strpos(lower(tbl_ort.".$column."), lower(?)) > 0";
$params[] = $value;
}
}
return $sql;
}
}
@@ -7,14 +7,14 @@ class Zeitsperren extends FHCAPI_Controller
public function __construct()
{
parent::__construct([
'getZeitsperrenUser' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'getZeitsperrenUser' => ['cis/eigene_zeitsperren:r', 'admin:r', 'assistenz:r'],
'getTypenZeitsperren' => self::PERM_LOGGED,
'getTypenErreichbarkeit' => self::PERM_LOGGED,
'getStunden' => self::PERM_LOGGED,
'loadZeitsperre' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'add' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'update' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'delete' => ['basis/mitarbeiter:r', 'admin:r', 'assistenz:r'],
'loadZeitsperre' => ['cis/eigene_zeitsperren:r', 'admin:r', 'assistenz:r'],
'add' => ['cis/eigene_zeitsperren:rw', 'admin:rw', 'assistenz:rw'],
'update' => ['cis/eigene_zeitsperren:rw', 'admin:rw', 'assistenz:rw'],
'delete' => ['cis/eigene_zeitsperren:rw', 'admin:rw', 'assistenz:rw'],
]);
// Load Libraries
@@ -0,0 +1,81 @@
<?php
/**
* Copyright (C) 2026 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Betreuungen extends FHCAPI_Controller
{
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getBetreuungen' => self::PERM_LOGGED,
]);
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
public function getBetreuungen()
{
$semester = $this->input->get("semester");
if (!$semester) $this->terminateWithError("Missing parameters!");
$personId = getAuthPersonId();
$betreuungenQuery = "SELECT
tbl_lehrveranstaltung.bezeichnung,
tbl_projektarbeit.titel,
(SELECT nachname || ' ' || (CASE WHEN wahlname IS NOT NULL THEN wahlname ELSE vorname END)
FROM public.tbl_benutzer
JOIN public.tbl_person USING(person_id)
WHERE uid=student_uid)
as name,
(SELECT uid
FROM public.tbl_benutzer
WHERE uid=student_uid)
as uid,
tbl_lehrveranstaltung.studiengang_kz,
tbl_lehrveranstaltung.semester,
tbl_studiengang.kurzbzlang as studiengang,
tbl_studiengang.email,
tbl_betreuerart.beschreibung AS beutreuerart_beschreibung,
tbl_projektbetreuer.stunden
FROM
lehre.tbl_lehrveranstaltung, lehre.tbl_projektarbeit, lehre.tbl_projektbetreuer, public.tbl_studiengang, lehre.tbl_betreuerart
WHERE
tbl_lehrveranstaltung.lehrveranstaltung_id=tbl_projektarbeit.lehrveranstaltung_id AND
tbl_projektarbeit.studiensemester_kurzbz = '$semester' AND
tbl_projektarbeit.projektarbeit_id = tbl_projektbetreuer.projektarbeit_id AND
tbl_lehrveranstaltung.studiengang_kz = tbl_studiengang.studiengang_kz AND
tbl_projektbetreuer.betreuerart_kurzbz = tbl_betreuerart.betreuerart_kurzbz AND
tbl_projektbetreuer.person_id = '$personId'";
$this->addMeta("query", $betreuungenQuery);
$this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
$result = $this->LehrveranstaltungModel->execReadOnlyQuery($betreuungenQuery);
$result = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($result);
}
}
@@ -19,7 +19,8 @@ class Lehreinheit extends FHCAPI_Controller
'getLehrfach' => ['admin:r', 'assistenz:r'],
'getSprache' => ['admin:r', 'assistenz:r'],
'getRaumtyp' => ['admin:r', 'assistenz:r'],
'getLehrform' => ['admin:r', 'assistenz:r']
'getLehrform' => ['admin:r', 'assistenz:r'],
'getLehreinheitenBySemester' => self::PERM_LOGGED,
]);
$this->_ci = &get_instance();
@@ -438,6 +439,27 @@ class Lehreinheit extends FHCAPI_Controller
$this->terminateWithSuccess($this->p->t('global', 'gespeichert'));
}
public function getLehreinheitenBySemester()
{
$semester = $this->input->get('semester');
$lehreinheiten = $this->LehreinheitModel->getLehrenheitenBySemester(getAuthUID(), $semester);
$lehreinheiten = $this->getDataOrTerminateWithError($lehreinheiten);
foreach ($lehreinheiten as $lehreinheit) {
$groups = $this->LehreinheitModel->getGroupsByLehreinheit($lehreinheit->lehreinheit_id);
$groups = $this->getDataOrTerminateWithError($groups);
$stgKurzbz = $lehreinheit->stg_kurzbz;
$lehreinheit->groups = array_map(
function ($group) use ($stgKurzbz) {
return $group->gruppe_kurzbz ?? $stgKurzbz . "-" . $group->semester . $group->verband . $group->gruppe;
},
$groups
);
}
$this->terminateWithSuccess($lehreinheiten);
}
private function checkPermission($lehreinheit_id)
{
@@ -574,6 +574,19 @@ class Kontakt extends FHCAPI_Controller
$person_id = $this->input->post('person_id');
$standort_id = $this->input->post('standort_id');
//check person_id: allow editing only of persons who are no mitarbeiter
if($person_id)
{
$this->load->model('person/Benutzer_model', 'BenutzerModel');
$this->BenutzerModel->addJoin('public.tbl_mitarbeiter ma', 'ON (ma.mitarbeiter_uid = public.tbl_benutzer.uid)');
$result = $this->BenutzerModel->loadWhere([
'person_id' => $person_id
]);
if(hasData($result))
return $this->terminateWithError($this->p->t('ui', 'editingMitarbeiterPVonly'), self::ERROR_TYPE_GENERAL);
}
$result = $this->KontaktModel->update(
[
'kontakt_id' => $kontakt_id
@@ -862,4 +862,47 @@ EOSQL;
return $this->execQuery($query, [$lva_id, $sem_kurzbz, $ma_uid]);
}
public function getLehrenheitenBySemester($uid, $semester)
{
$query = "SELECT
tbl_lehreinheit.lehreinheit_id,
lehrfach.kurzbz as lehrfach,
tbl_lehreinheit.lehrform_kurzbz as lehrform,
tbl_lehrveranstaltung.bezeichnung as lv_bezeichnung,
(SELECT kurzbz FROM public.tbl_mitarbeiter
WHERE mitarbeiter_uid=tbl_lehreinheitmitarbeiter.mitarbeiter_uid) as lektor,
UPPER(tbl_studiengang.typ::varchar(1) || tbl_studiengang.kurzbz) as stg_kurzbz,
tbl_lehrveranstaltung.semester as lv_semester,
tbl_lehreinheit.raumtyp,
tbl_lehreinheit.raumtypalternativ,
tbl_lehreinheit.stundenblockung,
tbl_lehreinheit.wochenrythmus,
tbl_lehreinheitmitarbeiter.semesterstunden as semesterstunden,
tbl_lehreinheit.start_kw,
lvangebot.anmeldefenster_start,
lvangebot.anmeldefenster_ende,
tbl_lehrveranstaltung.lehrveranstaltung_id
FROM
lehre.tbl_lehreinheit JOIN lehre.tbl_lehreinheitmitarbeiter USING(lehreinheit_id)
JOIN lehre.tbl_lehrveranstaltung USING(lehrveranstaltung_id)
JOIN public.tbl_studiengang USING(studiengang_kz)
JOIN lehre.tbl_lehrveranstaltung as lehrfach ON(tbl_lehreinheit.lehrfach_id=lehrfach.lehrveranstaltung_id)
LEFT JOIN lehre.tbl_lvangebot as lvangebot ON(tbl_lehrveranstaltung.lehrveranstaltung_id=lvangebot.lehrveranstaltung_id)
WHERE
tbl_lehreinheit.studiensemester_kurzbz = '$semester'
AND mitarbeiter_uid = '$uid'
ORDER BY stg_kurzbz,lv_semester,lv_bezeichnung";
return $this->execReadOnlyQuery($query);
}
public function getGroupsByLehreinheit($lehreinheitId)
{
$query = "SELECT *
FROM lehre.tbl_lehreinheitgruppe
WHERE lehreinheit_id='$lehreinheitId'";
return $this->execReadOnlyQuery($query);
}
}
@@ -22,6 +22,7 @@ $includesArray = array(
'public/css/components/FilterComponent.css',
'public/css/components/Profil.css',
'public/css/components/FormUnderline.css',
'public/css/components/inViewTooltip.css',
'public/css/components/abgabetool/abgabe.css',
'public/css/Cis4/Cms.css',
'public/css/Cis4/Studium.css',
+81
View File
@@ -0,0 +1,81 @@
/* the panel of js/directives/inViewTooltip.js. place() sets its place, max width and max height
inside the viewport, so neither a scrolling container nor a modal body clips it */
.fhc-in-view-tooltip {
--fhc-in-view-tooltip-progress: var(--fhc-blue-primary, #0d6efd);
position: fixed;
z-index: 1080;
display: flex;
flex-direction: column;
width: max-content;
border: 1px solid var(--bs-border-color, #dee2e6);
border-radius: 0.375rem;
background-color: var(--bs-body-bg, #fff);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
color: var(--bs-body-color, #212529);
font-size: 0.875rem;
font-weight: normal;
line-height: 1.5;
text-align: left;
}
/* the dark primary reaches 2:1 on the dark panel, the highlight 3.9:1 */
html.dark .fhc-in-view-tooltip {
--fhc-in-view-tooltip-progress: var(--fhc-blue-primary-highlight, #6ea8fe);
}
/* the body scrolls, so the progress border stays on the panel edge */
.fhc-in-view-tooltip-body {
min-height: 0;
overflow-y: auto;
padding: 0.75rem 1rem;
white-space: pre-line; /* the text keeps its line breaks */
}
/* the hover timer of a sticky panel: after a pause the border fills up clockwise, then the panel sticks.
The 2 px stroke covers the 1 px border and 1 px inside it */
.fhc-in-view-tooltip-progress {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
pointer-events: none;
}
.fhc-in-view-tooltip-progress rect {
fill: none;
stroke: var(--fhc-in-view-tooltip-progress);
stroke-width: 2;
stroke-dasharray: var(--fhc-in-view-tooltip-perimeter);
stroke-dashoffset: var(--fhc-in-view-tooltip-perimeter);
}
.fhc-in-view-tooltip--holding .fhc-in-view-tooltip-progress rect {
animation: fhc-in-view-tooltip-hold var(--fhc-in-view-tooltip-hold-fill) ease-in var(--fhc-in-view-tooltip-hold-delay) forwards;
}
/* a click during the hover timer fills the rest of the border quickly */
.fhc-in-view-tooltip--pinned .fhc-in-view-tooltip-progress rect {
stroke-dashoffset: 0;
transition: stroke-dashoffset 0.15s linear;
}
@keyframes fhc-in-view-tooltip-hold {
to {
stroke-dashoffset: 0;
}
}
/* the ? icon of components/InViewHelp.js takes the color of its context, e.g. white in the blue
modal header. A class like text-muted overrides it */
.fhc-in-view-help-btn,
.fhc-in-view-help-btn:hover,
.fhc-in-view-help-btn:focus-visible,
.btn.fhc-in-view-help-btn:active {
color: inherit;
}
.fhc-in-view-help-btn {
line-height: 1;
text-decoration: none;
}
+28
View File
@@ -0,0 +1,28 @@
/**
* Copyright (C) 2026 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getBetreuungen(semester) {
return {
method: 'get',
url: '/api/frontend/v1/lv/Betreuungen/getBetreuungen',
params: {
semester
},
};
},
};
+28
View File
@@ -0,0 +1,28 @@
/**
* Copyright (C) 2026 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getLehreinheitenBySemester(semester) {
return {
method: 'get',
url: '/api/frontend/v1/lv/Lehreinheit/getLehreinheitenBySemester',
params: {
semester
},
};
},
};
+18 -4
View File
@@ -23,11 +23,11 @@ export default {
params: { ort_kurzbz: ort_kurbz },
};
},
getRooms(datum, von, bis, typ, personenanzahl = 0) {
getRooms(datum, von, bis, typ, personenanzahl = 0, standort_id = null, advancedFilters = {}) {
return {
method: "get",
url: "/api/frontend/v1/Ort/getRooms",
params: { datum, von, bis, typ, personenanzahl },
method: 'get',
url: '/api/frontend/v1/Ort/getRooms',
params: { datum, von, bis, typ, personenanzahl, standort_id, ...advancedFilters }
};
},
getRoomTypes() {
@@ -46,4 +46,18 @@ export default {
},
};
},
getStandorte() {
return {
method: 'get',
url: '/api/frontend/v1/Ort/getStandorte',
params: { }
};
},
getAdvancedFilterOptions() {
return {
method: 'get',
url: '/api/frontend/v1/Ort/getAdvancedFilterOptions',
params: { }
};
}
};
+11 -1
View File
@@ -2,6 +2,8 @@ import CisMenu from "../../components/Cis/Menu.js";
import PluginsPhrasen from '../../plugins/Phrasen.js';
import Theme from "../../plugins/Theme.js";
import ApiAuthinfo from "../../api/factory/authinfo.js";
const app = Vue.createApp({
name: 'CisMenuApp',
components: {
@@ -9,11 +11,15 @@ const app = Vue.createApp({
},
data: () => ({
windowWidth: 0,
isStudent: null,
isMitarbeiter: null,
}),
provide() {
return {
isNarrow: Vue.computed(() => this.windowWidth < 992),
isMobile: Vue.computed(() => this.windowWidth < 767),
isStudent: Vue.computed(() => this.isStudent),
isMitarbeiter: Vue.computed(() => this.isMitarbeiter)
}
},
methods: {
@@ -21,8 +27,12 @@ const app = Vue.createApp({
this.windowWidth = window.innerWidth;
},
},
created() {
async created() {
this.windowWidth = window.innerWidth;
await this.$api.call(ApiAuthinfo.getAuthInfo()).then((res) => {
this.isMitarbeiter = res.data.isMitarbeiter;
this.isStudent = res.data.isStudent;
});
},
mounted() {
window.addEventListener("resize", this.handleWindowResize);
+7
View File
@@ -103,6 +103,13 @@ export default {
keyboard: this.keyboard
});
},
//to avoid pagelocks if more than one instance
beforeUnmount() {
if (this.modal) {
this.modal.dispose();
this.modal = null;
}
},
popup(body, options, title, footer) {
const BsModal = this,
slots = {};
@@ -83,10 +83,26 @@ export default {
});
},
getPromiseFunc(start, end) {
return [
this.$api.call(ApiLvPlan.eventsLv(this.propsViewData.lv_id, start.toISODate(), end.toISODate())),
this.$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
let functions = [
this.$api.call(
ApiLvPlan.eventsLv(
this.propsViewData.lv_id,
start.toISODate(),
end.toISODate(),
),
),
];
if (!this.$props.propsViewData.lv_id?.length) {
functions.push(
this.$api.call(
ApiLvPlan.getLvPlanReservierungen(
start.toISODate(),
end.toISODate(),
),
),
);
}
return functions;
}
},
created() {
+5 -1
View File
@@ -65,6 +65,10 @@ export default {
label: Vue.computed(() => this.$p.t("profil/zeitsperren")),
icon: "fas fa-calendar-days",
type: "link",
isMitarbeiter: Vue.computed(() => this.isMitarbeiter),
renderif: function() {
return this.isMitarbeiter;
},
action: function (data) {
const uid = JSON.parse(data.data).uid;
const link =
@@ -163,7 +167,7 @@ export default {
openMenuHierarchy: [],
};
},
inject: ["isNarrow", "isMobile"],
inject: ["isNarrow", "isMobile", "isMitarbeiter", "isStudent"],
provide(){
return{
setActiveEntry: this.setActiveEntry,
@@ -1,5 +1,5 @@
import Phrasen from "../../../../../mixins/Phrasen.js";
import ApiLehre from "../../../../../api/factory/lehre.js";
import Phrasen from "../../../mixins/Phrasen.js";
import ApiLehre from "../../../api/factory/lehre.js";
export default {
mixins: [
@@ -0,0 +1,226 @@
import { CoreFilterCmpt } from "../../filter/Filter.js";
import ApiLehreinheit from "../../../api/factory/lehreinheit.js";
export default {
name: "Lehreinheiten",
components: {
CoreFilterCmpt,
},
props: {
semester: null | String,
},
data() {
return {
lehreinheiten: [],
isFetchingLehreinheiten: false,
lehreinheitTableOptions: {
layout: "fitColumns",
columns: [
{
titlePhrase: "lehreinheiten/inputGrades",
title: "placeholder",
field: "inputGrades",
widthGrow: 2,
formatter: () => {
return '<i class="fa-solid fa-up-right-from-square"></i>';
},
hozAlign: "center",
cellClick: (event, cell) => {
window.open(
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/Cis/Compat/legacy/cis/private/lehre/benotungstool/lvgesamtnoteverwalten.php?lvid=" +
cell._cell.row.data.lvId +
"&stsem=" +
this.$props.semester,
"_blank",
);
},
},
{
title: "ID",
field: "lehreinheitId",
widthGrow: 2,
},
{
titlePhrase: "lehreinheiten/subject",
title: "placeholder",
field: "subject",
widthGrow: 2,
},
{
titlePhrase: "lehreinheiten/lehrform",
title: "placeholder",
field: "lehrform",
widthGrow: 2,
},
{
titlePhrase: "lehreinheiten/title",
title: "placeholder",
field: "title",
widthGrow: 3,
},
{
titlePhrase: "lehreinheiten/lector",
title: "placeholder",
field: "lector",
widthGrow: 2,
},
{
titlePhrase: "lehreinheiten/dp",
title: "placeholder",
field: "stg",
widthGrow: 2,
},
{
titlePhrase: "lehreinheiten/sem",
title: "placeholder",
field: "semester",
widthGrow: 1,
},
{
titlePhrase: "lehreinheiten/groups",
title: "placeholder",
field: "groups",
widthGrow: 2,
formatter: "textarea",
},
{
titlePhrase: "lehreinheiten/room",
title: "placeholder",
field: "room",
widthGrow: 3,
},
{
titlePhrase: "lehreinheiten/alt",
title: "placeholder",
field: "alternativeRoom",
widthGrow: 3,
},
{
titlePhrase: "lehreinheiten/block",
title: "placeholder",
field: "block",
widthGrow: 1,
},
{
titlePhrase: "lehreinheiten/wr",
title: "placeholder",
field: "weeklyRhythm",
widthGrow: 1,
},
{
titlePhrase: "lehreinheiten/hrs",
title: "placeholder",
field: "hours",
widthGrow: 1,
bottomCalc: "sum",
bottomCalcParams: { precision: 2 },
},
{
titlePhrase: "lehreinheiten/cw",
title: "placeholder",
field: "cw",
widthGrow: 1,
},
{
titlePhrase: "lehreinheiten/registrationWindowStart",
title: "placeholder",
field: "registrationWindowStart",
widthGrow: 2,
},
{
titlePhrase: "lehreinheiten/registrationWindowEnd",
title: "placeholder",
field: "registrationWindowEnd",
widthGrow: 2,
},
],
locale: true,
persistence: false,
persistenceID: "supervisorTable",
},
};
},
watch: {
semester() {
this.getLehreinheiten();
},
},
methods: {
async getLehreinheiten() {
if (!this.$props.semester?.length) return;
this.lehreinheiten = [];
this.isFetchingLehreinheiten = true;
const lehreinheitenResponse = await this.$api.call(
ApiLehreinheit.getLehreinheitenBySemester(this.$props.semester),
);
if (lehreinheitenResponse.meta.status === "success") {
this.lehreinheiten = lehreinheitenResponse.data.map(
(lehreinheitData) => {
return {
lehreinheitId: lehreinheitData.lehreinheit_id,
lvId: lehreinheitData.lehrveranstaltung_id,
subject: lehreinheitData.lehrfach,
lehrform: lehreinheitData.lehrform,
title: lehreinheitData.lv_bezeichnung,
lector: lehreinheitData.lektor,
stg: lehreinheitData.stg_kurzbz,
semester: lehreinheitData.lv_semester,
groups: lehreinheitData.groups?.join("\n"),
room: lehreinheitData.raumtyp,
alternativeRoom: lehreinheitData.raumtypalternativ,
block: lehreinheitData.stundenblockung,
weeklyRhythm: lehreinheitData.wochenrythmus,
hours: lehreinheitData.semesterstunden,
cw: lehreinheitData.start_kw,
registrationWindowStart:
lehreinheitData.anmeldefenster_start,
registrationWindowEnd:
lehreinheitData.anmeldefenster_ende,
};
},
);
}
this.setTableData();
this.isFetchingLehreinheiten = false;
},
handleTableBuilt() {
this.setTableData();
},
setTableData() {
if (this.$refs.lehreinheitenTable && this.lehreinheiten?.length) {
this.$refs.lehreinheitenTable.tabulator.setData(
this.lehreinheiten,
);
}
},
},
mounted() {
this.getLehreinheiten();
},
template: /*html*/ `
<div>
<div v-if="isFetchingLehreinheiten" class="d-flex flex-row justify-content-center pt-3">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<div v-else-if="lehreinheiten?.length">
<h5>{{ lehreinheiten.length + " " + $p.t("mylv/lehreinheiten") }}</h5>
<core-filter-cmpt
@tableBuilt="handleTableBuilt()"
ref="lehreinheitenTable"
tableOnly
:tabulator-options="lehreinheitTableOptions"
:sideMenu="false"
/>
</div>
<div v-else class="d-flex flex-row justify-content-center pt-3">
<h4>No teaching units found!</h4>
</div>
</div>
`,
};
+61
View File
@@ -0,0 +1,61 @@
import MylvSemesterCards from "./Semester.js";
import MylvTable from "./Table.js";
import MylvSemesterStudiengangAverageGrade from "./AverageGrade.js";
export default {
name: "Lvs",
components: {
MylvSemesterCards,
MylvTable,
MylvSemesterStudiengangAverageGrade,
},
inject: ['isStudent'],
props: ["current"],
data() {
return {
mode: localStorage.getItem('myLvaDefaultMode') ?? 'cards'
};
},
methods: {
selectMode(mode) {
localStorage.setItem('myLvaDefaultMode', mode)
this.mode = mode
},
},
template: /*html*/ `
<div class="d-flex flex-column gap-3">
<div class="d-flex flex-row justify-content-center gap-2">
<button
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'cards'}"
@click="selectMode('cards')"
>
<i class="fa fa-grip"></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'table'}"
@click="selectMode('table')"
>
<i class="fa fa-table"></i>
</button>
</div>
<mylv-semester-cards v-if="mode == 'cards'" v-bind="$props.current">
<template #averageGrade v-if="isStudent && $props.current.semester">
<mylv-semester-studiengang-average-grade
:semesterInfo="$props.current.semester"
/>
</template>
</mylv-semester-cards>
<mylv-table v-else-if="mode == 'table'" v-bind="$props.current">
<template #averageGrade v-if="isStudent && $props.current.semester">
<mylv-semester-studiengang-average-grade
:semesterInfo="$props.current.semester"
/>
</template>
</mylv-table>
</div>
`,
}
+51 -48
View File
@@ -1,15 +1,15 @@
import MylvSemesterCards from "./Semester.js";
import MylvTable from "./Table.js";
import ApiAddons from "../../../api/factory/addons.js"
import Lvs from "./Lvs.js";
import Lehreinheiten from "./Lehreinheiten.js";
import Supervisor from "./Supervisor.js";
// TODO(chris): phrase: global/studiensemester_auswaehlen
// TODO(chris): phrase: next & prev +aria-label
import ApiAddons from "../../../api/factory/addons.js"
export default {
name: 'MyLv',
components: {
MylvSemesterCards,
MylvTable
Lvs,
Lehreinheiten,
Supervisor,
},
data: () => {
return {
@@ -17,7 +17,8 @@ export default {
studiensemester: null,
lvs: {},
currentSemester: null,
mode: localStorage.getItem('myLvaDefaultMode') ?? 'cards'
modes: ["lvs", "lehreinheiten", "supervision", "all"],
selectedMode: "lvs"
};
},
provide() {
@@ -91,13 +92,22 @@ export default {
},
currentIsLast() {
return this.studiensemester[this.studiensemester.length-1].studiensemester_kurzbz == this.currentSemester;
}
},
},
watch: {
isMitarbeiter() {
if (this.isMitarbeiter) {
const lastSelectedMode = window.localStorage.getItem("cis4-mylv-lastSelectedMode");
if (this.modes.includes(lastSelectedMode)) {
this.selectedMode = lastSelectedMode;
}
}
},
selectedMode(mode) {
window.localStorage.setItem("cis4-mylv-lastSelectedMode", mode);
},
},
methods: {
clickMode(evt, mode) {
localStorage.setItem('myLvaDefaultMode', mode)
this.mode = mode
},
prevSem() {
this.$refs.studiensemester.selectedIndex--;
this.$refs.studiensemester.dispatchEvent(new Event('change', { bubbles: true }));
@@ -126,7 +136,7 @@ export default {
next();
},
template: `
template: /*html*/ `
<div>
<h2>{{$p.t('lehre/myLV')}}</h2>
<hr>
@@ -135,48 +145,41 @@ export default {
<div class="col-auto d-none">
<label class="col-form-label">{{$p.t('lehre/studiensemester')}}</label>
</div>
<div class="col-auto">
<div class="input-group">
<button :aria-label="$p.t('lehre','previousStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','previousStudSemester')}" class="btn btn-outline-secondary" type="button" :disabled="currentIsFirst" @click="prevSem">
<i class="fa fa-caret-left" aria-hidden="true"></i>
</button>
<select ref="studiensemester" v-model="currentSemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="updateRouter($event.target.value)">
<option v-for="semester in studiensemester" :key="semester.studiensemester_kurzbz">{{semester.studiensemester_kurzbz}}</option>
<div class="d-flex flex-column flex-md-row justify-content-center gap-2">
<div>
<div class="input-group">
<button :aria-label="$p.t('lehre','previousStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','previousStudSemester')}" class="btn btn-outline-secondary" type="button" :disabled="currentIsFirst" @click="prevSem">
<i class="fa fa-caret-left" aria-hidden="true"></i>
</button>
<select ref="studiensemester" v-model="currentSemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="updateRouter($event.target.value)">
<option v-for="semester in studiensemester" :key="semester.studiensemester_kurzbz">{{semester.studiensemester_kurzbz}}</option>
</select>
<button class="btn btn-outline-secondary" :aria-label="$p.t('lehre','nextStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','nextStudSemester')}" type="button" :disabled="currentIsLast" @click="nextSem">
<i class="fa fa-caret-right" aria-hidden="true"></i>
</button>
</div>
</div>
<div v-if="isMitarbeiter">
<select v-model="selectedMode" class="form-select" :aria-label="'mode selector placeholder'">
<option v-for="mode in modes" :key="mode" :value="mode"> {{ $p.t("mylv/" + mode) }} </option>
</select>
<button class="btn btn-outline-secondary" :aria-label="$p.t('lehre','nextStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','nextStudSemester')}" type="button" :disabled="currentIsLast" @click="nextSem">
<i class="fa fa-caret-right" aria-hidden="true"></i>
</button>
</div>
</div>
<div class=" col-auto my-lva-modes">
<div class="d-flex gap-1 justify-content-end" role="group">
<button
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'cards'}"
@click="clickMode($event, 'cards')"
>
<i class="fa fa-grip"></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'table'}"
@click="clickMode($event, 'table')"
>
<i class="fa fa-table"></i>
</button>
</div>
<div class="pt-4">
<lvs v-if="selectedMode === 'lvs' || selectedMode === 'all'" :current="current" />
<hr v-if="selectedMode === 'all'">
<lehreinheiten v-if="selectedMode === 'lehreinheiten' || selectedMode === 'all'" :semester="currentSemester" />
<hr v-if="selectedMode === 'all'">
<supervisor v-if="selectedMode === 'supervision' || selectedMode === 'all'" :semester="currentSemester" />
</div>
</div>
<div class="alert alert-danger" role="alert" v-else>
<div v-else class="alert alert-danger" role="alert">
{{$p.t('lehre/noLvFound')}}
</div>
<mylv-semester-cards v-if="mode == 'cards'" v-bind="current"/>
<mylv-table v-else-if="mode == 'table'" v-bind="current"/>
</div>
<div class="mylv text-center" v-else>
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</div>`
};
</div>
`,
}
+12 -2
View File
@@ -37,9 +37,19 @@ export default {
return this.lvs.filter(lv => lv.studiengang_kz == studiengang.studiengang_kz && lv.semester == studiengang.semester);
}
},
template: `
template: /*html*/ `
<div class="mylv-semester" v-if="ready">
<mylv-semester-studiengang v-for="studiengang in studiengaenge" :key="studiengang.studiengang_kz" v-bind="studiengang" :lvs="lvsForStudiengang(studiengang)" :semesterInfo="$props.semester" />
<mylv-semester-studiengang
v-for="studiengang in studiengaenge"
:key="studiengang.studiengang_kz"
v-bind="studiengang"
:lvs="lvsForStudiengang(studiengang)"
:semesterInfo="$props.semester"
>
<template #averageGrade>
<slot name="averageGrade"></slot>
</template>
</mylv-semester-studiengang>
</div>
<div class="mylv-semester text-center" v-else>
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
@@ -1,5 +1,4 @@
import MylvSemesterStudiengangLv from "./Studiengang/Lv.js";
import MylvSemesterStudiengangAverageGrade from "./Studiengang/AverageGrade.js";
import Phrasen from "../../../../mixins/Phrasen.js";
import ApiAuthinfo from '../../../../api/factory/authinfo.js';
@@ -8,7 +7,6 @@ export default {
name: 'Studiengang',
components: {
MylvSemesterStudiengangLv,
MylvSemesterStudiengangAverageGrade
},
mixins: [
Phrasen
@@ -21,11 +19,6 @@ export default {
lvs: Array,
sg_bezeichnung_eng: String
},
data() {
return {
isAverageGradeDisplayed: false,
}
},
computed: {
lehrveranstaltungen() {
return [... new Map(
@@ -41,16 +34,8 @@ export default {
note(lv) {
return lv.benotung ? lv.znote || lv.lvnote || null : null;
},
async checkIfAverageGradeIsDisplayed() {
const authInfoResponse = await this.$api.call(ApiAuthinfo.getAuthInfo());
const authInfo = authInfoResponse.data;
this.isAverageGradeDisplayed = !!authInfo.isStudent;
},
},
created() {
this.checkIfAverageGradeIsDisplayed();
},
template: `
template: /*html*/ `
<div class="card mb-3">
<div class="card-body">
<h4 class="card-title mb-3">{{$p.user_language.value === 'English' ? sg_bezeichnung_eng : bezeichnung}} - {{kuerzel}}
@@ -61,7 +46,7 @@ export default {
<mylv-semester-studiengang-lv v-bind="lv" class="text-center h-100"></mylv-semester-studiengang-lv>
</div>
</div>
<mylv-semester-studiengang-average-grade v-if="isAverageGradeDisplayed" :semesterInfo="$props.semesterInfo" />
<slot name="averageGrade"></slot>
</div>
</div>`
};
+147
View File
@@ -0,0 +1,147 @@
import { CoreFilterCmpt } from "../../../components/filter/Filter.js";
import ApiBetreuungen from "../../../api/factory/betreuungen.js";
export default {
name: "Supervisor",
components: {
CoreFilterCmpt,
},
props: {
semester: null | String,
},
data() {
return {
students: [],
isFetchingStudents: false,
supervisorTableOptions: {
layout: "fitColumns",
columns: [
{
titlePhrase: "betreuungen/studiengang",
title: "placeholder",
field: "studiengang",
widthGrow: 1,
formatter: "link",
formatterParams: {
target: "_blank",
url: (cell) => {
return "mailto:" + cell._cell.row.data.email;
},
},
},
{
titlePhrase: "betreuungen/semester",
title: "placeholder",
field: "semester",
widthGrow: 1,
},
{
titlePhrase: "betreuungen/stunden",
title: "placeholder",
field: "stunden",
widthGrow: 1,
bottomCalc: "sum",
bottomCalcParams: { precision: 2 },
},
{
titlePhrase: "betreuungen/lvBezeichnung",
title: "placeholder",
field: "bezeichnung",
widthGrow: 3,
},
{
titlePhrase: "betreuungen/student",
title: "placeholder",
field: "name",
widthGrow: 3,
formatter: "link",
formatterParams: {
target: "_blank",
url: (cell) => {
return this.$router.resolve({
name: "ProfilView",
params: { uid: cell._cell.row.data.uid },
}).href;
},
},
},
{
titlePhrase: "betreuungen/betreuungsart",
title: "placeholder",
field: "beutreuerart_beschreibung",
widthGrow: 3,
},
{
titlePhrase: "betreuungen/titelProjektarbeit",
title: "placeholder",
field: "titel",
widthGrow: 6,
},
],
locale: true,
persistence: false,
persistenceID: "supervisorTable",
},
};
},
watch: {
semester() {
this.getStudents();
},
},
methods: {
async getStudents() {
if (!this.$props.semester?.length) return;
this.students = [];
this.isFetchingStudents = true;
const studentsResponse = await this.$api.call(
ApiBetreuungen.getBetreuungen(this.$props.semester),
);
if (studentsResponse.meta.status === "success") {
this.students = studentsResponse.data.map((studentData) => {
studentData.stunden = parseFloat(
studentData.stunden,
).toFixed(2);
return { ...studentData };
});
this.setTableData();
}
this.isFetchingStudents = false;
},
handleTableBuilt() {
this.setTableData();
},
setTableData() {
if (this.$refs.supervisorTable && this.students?.length) {
this.$refs.supervisorTable.tabulator.setData(this.students);
}
},
},
mounted() {
this.getStudents();
},
template: /*html*/ `
<div>
<div v-if="isFetchingStudents" class="d-flex flex-row justify-content-center pt-3">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<div v-else-if="students?.length">
<h5>{{ students.length + " " + $p.t("mylv/supervised_students") }}</h5>
<core-filter-cmpt
@tableBuilt="handleTableBuilt()"
ref="supervisorTable"
tableOnly
:tabulator-options="supervisorTableOptions"
:sideMenu="false"
/>
</div>
<div v-else class="d-flex flex-row justify-content-center pt-3">
<h4>{{ $p.t("mylv/no_supervised") }}</h4>
</div>
</div>
`,
};
+27 -23
View File
@@ -22,21 +22,22 @@ export default {
layout: 'fitDataStretch',
placeholder: this.$p.t('global/noDataAvailable'),
columns: [
{title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/studiengang'))), field: 'sg_bezeichnung', widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('global/bezeichnung'))), field: 'bezeichnung', widthGrow: 2},
{title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/orgform'))), field: 'orgform_kurzbz', widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/kurzbz'))), field: 'studiengang_kuerzel', widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/semesterstunden'))), field: 'semesterstunden',
{titlePhrase: 'lehre/studiengang', title: 'placeholder', field: 'sg_bezeichnung', widthGrow: 1},
{titlePhrase: 'global/bezeichnung', title: 'placeholder', field: 'bezeichnung', widthGrow: 2},
{titlePhrase: 'lehre/orgform', title: 'placeholder', field: 'orgform_kurzbz', widthGrow: 1},
{titlePhrase: 'lehre/kurzbz', title: 'placeholder', field: 'studiengang_kuerzel', widthGrow: 1},
{titlePhrase: 'lehre/semesterstunden', title: 'placeholder', field: 'semesterstunden',
bottomCalc: this.semesterstundenCalc, widthGrow: 1, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('global/actions'))), headerSort: false,
field: 'menu', formatter: this.actionFormatter, widthGrow: 1, tooltip: this.spoofingFunc}
],
persistence: false,
persistenceID: "mylv_2026_04_17"
persistenceID: "mylv_2026_04_17",
locale: true,
},
mylvTableEventHandlers: [
]
],
}
},
computed: {
@@ -324,22 +325,25 @@ export default {
deep: true
}
},
template: `
<div class="mylv-semester-table" v-if="ready">
<core-filter-cmpt
v-if="phrasenResolved"
@uuidDefined="handleUuidDefined"
:title="''"
ref="mylvTable"
:tabulator-options="mylvTableOptions"
:tabulator-events="mylvTableEventHandlers"
@tableBuilt="handleTableBuilt"
tableOnly
:sideMenu="false"
/>
</div>
<div v-if="tabulatorUuid === null" class="text-center d-flex justify-content-center align-items-center h-100" >
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
template: /*html*/ `
<div class="d-flex flex-column gap-3">
<div class="mylv-semester-table" v-if="ready">
<core-filter-cmpt
v-if="phrasenResolved"
@uuidDefined="handleUuidDefined"
:title="''"
ref="mylvTable"
:tabulator-options="mylvTableOptions"
:tabulator-events="mylvTableEventHandlers"
@tableBuilt="handleTableBuilt"
tableOnly
:sideMenu="false"
/>
</div>
<div v-if="tabulatorUuid === null" class="text-center d-flex justify-content-center align-items-center h-100" >
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<slot name="averageGrade"></slot>
</div>
`
};
+163 -28
View File
@@ -2,12 +2,39 @@ import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
import FormInput from "../../Form/Input.js";
import VueDatePicker from '../../vueDatepicker.js.php';
import ApiOrt from '../../../api/factory/ort.js'
import InViewHelp from '../../InViewHelp.js';
// advanced filters: tbl_ort column, filter type and label, the same list as Ort::ADVANCED_FILTERS
const ADVANCED_FILTERS = [
{ column: 'lehre', type: 'flag', label: 'rauminfo/nurLehrraeume', checked: true },
{ column: 'content_id', type: 'exists', label: 'rauminfo/nurMitInfoseite' },
{ column: 'ort_kurzbz', type: 'text', label: 'rauminfo/raum_kurzbz' },
{ column: 'bezeichnung', type: 'text', label: 'global/bezeichnung' },
{ column: 'planbezeichnung', type: 'text', label: 'rauminfo/raumnummer' },
{ column: 'stockwerk', type: 'range', label: 'rauminfo/stockwerk' },
{ column: 'gebteil', type: 'select', label: 'rauminfo/gebaeudeteil' },
{ column: 'm2', type: 'range', label: 'rauminfo/flaeche' }
];
// the GET parameters of the advanced filters with their default values
const ADVANCED_DEFAULTS = {};
for (const { column, type, checked } of ADVANCED_FILTERS) {
if (type === 'range') {
ADVANCED_DEFAULTS[column + '_min'] = '';
ADVANCED_DEFAULTS[column + '_max'] = '';
} else {
ADVANCED_DEFAULTS[column] = type === 'flag' || type === 'exists' ? !!checked : '';
}
}
export const Raumsuche = {
name: "Raumsuche",
components: {
VueDatePicker,
CoreFilterCmpt,
FormInput,
InViewHelp,
InputNumber: primevue.inputnumber,
},
inject: ["isMobile"],
data() {
@@ -18,13 +45,17 @@ export const Raumsuche = {
tableBuiltResolve: null,
tableBuiltPromise: null,
roomtypes: null,
defaultType: {
raumtyp_kurzbz: '',
beschreibung: Vue.computed(() => this.$p.t('global/alle'))
},
anzahl: 1,
anzahlInput: '1',
selectedType: null,
standorte: null,
standort_id: null,
roomsRequest: 0,
loading: false,
advancedFilterFields: ADVANCED_FILTERS,
advancedFilters: { ...ADVANCED_DEFAULTS },
advancedFilterOptions: {},
showAdvancedFilters: false,
datum: new Date(),
von: Vue.ref({
hours: new Date().getHours(),
@@ -57,7 +88,12 @@ export const Raumsuche = {
{title: Vue.computed(() => this.$p.t('rauminfo/raum_kurzbz')), field: 'ort_kurzbz', widthGrow: 1},
{title: Vue.computed(() => this.$p.t('global/bezeichnung')), field: 'bezeichnung', widthGrow: 2},
{title: Vue.computed(() => this.$p.t('rauminfo/raumnummer')), field: 'nummer', widthGrow: 1},
{title: Vue.computed(() => this.$p.t('rauminfo/standort')), field: 'standort', widthGrow: 1},
{title: Vue.computed(() => this.$p.t('rauminfo/gebaeudeteil')), field: 'gebteil', widthGrow: 1, visible: false},
{title: Vue.computed(() => this.$p.t('rauminfo/stockwerk')), field: 'stockwerk', sorter: 'number', widthGrow: 1, visible: false},
{title: Vue.computed(() => this.$p.t('rauminfo/personcap')), field: 'personen', widthGrow: 1},
{title: Vue.computed(() => this.$p.t('rauminfo/flaeche')), field: 'm2', sorter: 'number', widthGrow: 1, visible: false},
{title: Vue.computed(() => this.$p.t('rauminfo/lehrraum')), field: 'lehre', formatter: 'tickCross', hozAlign: 'center', widthGrow: 1, visible: false},
{title: Vue.computed(() => this.$p.t('rauminfo/rauminfo')),
field: 'linkInfo', formatter: this.linkFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('rauminfo/roomReservations')),
@@ -76,8 +112,21 @@ export const Raumsuche = {
computed: {
isDarkMode(){
return this.$theme.theme_name.value == 'dark';
},
// shown on the toggle, the panel can be closed while its filters are active
activeAdvancedFilters() {
return Object.keys(ADVANCED_DEFAULTS).filter(key => this.advancedFilters[key] !== ADVANCED_DEFAULTS[key]).length
}
},
watch: {
datum: 'loadRooms',
von: 'loadRooms',
bis: 'loadRooms',
selectedType: 'loadRooms',
standort_id: 'loadRooms',
anzahl: 'loadRooms',
advancedFilters: { handler: 'loadRooms', deep: true }
},
methods: {
tableResolve(resolve) {
this.tableBuiltResolve = resolve
@@ -117,7 +166,12 @@ export const Raumsuche = {
ort_kurzbz: room.ort_kurzbz,
bezeichnung: room.bezeichnung.replace('&amp;', '&'),
nummer: room.planbezeichnung,
standort: room.standort,
gebteil: room.gebteil,
stockwerk: room.stockwerk,
personen: room.max_person,
m2: room.m2,
lehre: room.lehre,
linkInfo: room.content_id ? this.roomInfoLink(room) : null,
linkRes: this.roomPlanLink(room)
@@ -133,15 +187,34 @@ export const Raumsuche = {
res?.data?.forEach(type => {
type.beschreibung = type.beschreibung?.replace('&amp;', '&')
})
this.selectedType = this.defaultType
this.roomtypes = res?.data ?? []
})
},
loadRooms() {
this.$api.call(ApiOrt.getRooms(this.datum.toISOString(), this.getTimeString(this.von), this.getTimeString(this.bis), this.selectedType?.raumtyp_kurzbz ?? '', this.anzahl))
loadStandorte() {
this.$api.call(ApiOrt.getStandorte())
.then(res => {
if(res?.data?.retval) this.setupData(res.data.retval)
})
this.standorte = res?.data ?? []
})
},
loadAdvancedFilterOptions() {
this.$api.call(ApiOrt.getAdvancedFilterOptions())
.then(res => {
this.advancedFilterOptions = res?.data ?? {}
})
},
loadRooms() {
// only the latest request may fill the table, older answers can arrive later
const request = ++this.roomsRequest
this.loading = true
// empty and unchecked filters stay out of the request
const advancedFilters = Object.fromEntries(Object.entries(this.advancedFilters).filter(([, value]) => value !== '' && value !== false))
this.$api.call(ApiOrt.getRooms(this.datum.toISOString(), this.getTimeString(this.von), this.getTimeString(this.bis), this.selectedType?.raumtyp_kurzbz ?? '', this.anzahl, this.standort_id, advancedFilters))
.then(res => {
if(request === this.roomsRequest && res?.data?.retval) this.setupData(res.data.retval)
})
.finally(() => {
if(request === this.roomsRequest) this.loading = false
})
},
handleUuidDefined(uuid) {
this.tabulatorUuid = uuid
@@ -160,6 +233,9 @@ export const Raumsuche = {
this.normalizeAnzahl()
this.loadRooms()
},
resetAdvancedFilters() {
this.advancedFilters = { ...ADVANCED_DEFAULTS }
},
setRoute(val) {
// TODO: router push
},
@@ -180,6 +256,8 @@ export const Raumsuche = {
await this.tableBuiltPromise
this.loadRoomTypes()
this.loadStandorte()
this.loadAdvancedFilterOptions()
this.loadRooms()
const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : ''
@@ -195,14 +273,17 @@ export const Raumsuche = {
}
},
created() {
this.phrasenPromise = this.$p.loadCategory(['rauminfo', 'global'])
this.phrasenPromise = this.$p.loadCategory(['rauminfo', 'global', 'ui', 'filter'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
},
mounted() {
this.setupMounted()
},
template: `
<h1 class="h3">{{$p.t('rauminfo/roomSearch')}}</h1>
<div class="d-flex align-items-center gap-2 mb-2">
<h1 class="h3 mb-0">{{$p.t('rauminfo/roomSearch')}}</h1>
<in-view-help button-class="fs-5 text-body-secondary" :text="$p.t('rauminfo/tooltipRaumsuchev2')"></in-view-help>
</div>
<hr>
<div class="row">
<div :class="{'pb-1': isMobile}" class="col-12 col-lg-2">
@@ -248,16 +329,24 @@ export const Raumsuche = {
</VueDatePicker>
</div>
<div :class="{'pb-1': isMobile}" class="col-12 col-lg-3">
<div :class="{'pb-1': isMobile}" class="col-12 col-lg-2">
<select ref="raumtyp" id="raumtypSelect" v-model="selectedType" class="form-select"
:aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setRoute($event.target.value)">
<option :key="defaultType" selected :value="defaultType">{{defaultType.beschreibung}}</option>
:aria-label="$p.t('rauminfo/raumtyp')" @change="setRoute($event.target.value)">
<option :value="null">{{ $p.t('global/alle') }}</option>
<option v-for="typ in roomtypes" :key="typ" :value="typ">{{typ.beschreibung}}</option>
</select>
</div>
<div :class="{'pb-1': isMobile}" class="col-12 col-lg-2">
<select id="standortSelect" v-model="standort_id" class="form-select" :aria-label="$p.t('rauminfo/standort')">
<option :value="null">{{ $p.t('rauminfo/alleStandorte') }}</option>
<option value="none">{{ $p.t('rauminfo/ohneStandort') }}</option>
<option v-for="standort in standorte" :key="standort.standort_id" :value="standort.standort_id">{{ standort.bezeichnung }}</option>
</select>
</div>
<div :class="{'pb-2': isMobile}" class="col-12 col-lg-3">
<div :class="{'pb-2': isMobile}" class="col-12 col-lg">
<div class="input-group">
<span class="input-group-text">{{ $p.t('rauminfo/minCapacity') }}:</span>
<FormInput
@@ -275,22 +364,68 @@ export const Raumsuche = {
/>
</div>
</div>
<div class="col-12 col-lg-2">
<button class="btn btn-primary border-0" @click="search">{{ $p.t('rauminfo/roomSearch') }} <i class="fa fa-magnifying-glass"></i></button>
</div>
</div>
<core-filter-cmpt
v-if="phrasenResolved"
@uuidDefined="handleUuidDefined"
:title="''"
ref="raumsucheTable"
:tabulator-options="raumsucheTableOptions"
:tabulator-events="raumsucheTableEventHandlers"
tableOnly
:sideMenu="false"
/>
<div class="d-flex justify-content-end my-1">
<button type="button" class="btn btn-link btn-sm p-0 text-decoration-none text-body-secondary"
:aria-expanded="showAdvancedFilters" aria-controls="raumsucheAdvancedFilters"
@click="showAdvancedFilters = !showAdvancedFilters">
<i class="fa-solid fa-sliders me-1"></i>{{ $p.t('rauminfo/erweiterteFilter') }}
<span v-if="activeAdvancedFilters" class="badge rounded-pill bg-primary ms-1">{{ activeAdvancedFilters }}</span>
<i class="fa-solid ms-1" :class="showAdvancedFilters ? 'fa-chevron-up' : 'fa-chevron-down'"></i>
</button>
</div>
<div v-show="showAdvancedFilters" id="raumsucheAdvancedFilters" class="row g-2 mb-3">
<div v-for="filter in advancedFilterFields" :key="filter.column" class="col-12 col-md-6 col-lg-3"
:class="{'align-self-end': filter.type === 'flag' || filter.type === 'exists'}">
<div v-if="filter.type === 'flag' || filter.type === 'exists'" class="form-check">
<input :id="'raumsucheFilter-' + filter.column" v-model="advancedFilters[filter.column]" type="checkbox" class="form-check-input">
<label :for="'raumsucheFilter-' + filter.column" class="form-check-label">{{ $p.t(filter.label) }}</label>
</div>
<template v-else>
<label :for="'raumsucheFilter-' + filter.column" class="form-label small mb-1">{{ $p.t(filter.label) }}</label>
<input v-if="filter.type === 'text'" :id="'raumsucheFilter-' + filter.column"
v-model.lazy.trim="advancedFilters[filter.column]" type="text" class="form-control form-control-sm">
<select v-else-if="filter.type === 'select'" :id="'raumsucheFilter-' + filter.column"
v-model="advancedFilters[filter.column]" class="form-select form-select-sm">
<option value="">{{ $p.t('ui/alle') }}</option>
<option v-for="option in advancedFilterOptions[filter.column]" :key="option" :value="option">{{ option }}</option>
</select>
<div v-else class="input-group input-group-sm">
<input :id="'raumsucheFilter-' + filter.column" v-model.lazy="advancedFilters[filter.column + '_min']"
type="number" step="any" class="form-control"
:placeholder="$p.t('ui/von')" :aria-label="$p.t(filter.label) + ' ' + $p.t('ui/von')">
<input v-model.lazy="advancedFilters[filter.column + '_max']"
type="number" step="any" class="form-control"
:placeholder="$p.t('global/bis')" :aria-label="$p.t(filter.label) + ' ' + $p.t('global/bis')">
</div>
</template>
</div>
<div class="col-12 text-end">
<button type="button" class="btn btn-link btn-sm p-0" :disabled="!activeAdvancedFilters" @click="resetAdvancedFilters">
{{ $p.t('filter/filterDelete') }}
</button>
</div>
</div>
<div class="position-relative">
<div :class="{'opacity-50': loading}">
<core-filter-cmpt
v-if="phrasenResolved"
@uuidDefined="handleUuidDefined"
:title="''"
ref="raumsucheTable"
:tabulator-options="raumsucheTableOptions"
:tabulator-events="raumsucheTableEventHandlers"
tableOnly
:sideMenu="false"
/>
</div>
<div v-if="loading" class="position-absolute top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</div>
`,
};
+34
View File
@@ -0,0 +1,34 @@
import InViewTooltip from '../directives/inViewTooltip.js';
/**
* ? icon with a help text in the in-view tooltip. The panel stays inside the viewport, also at the
* screen edge, in a modal and on a phone: a plain tooltip ran out of the screen there.
*
* Help texts are long, so the panel is sticky by default: it sticks after a hover of 1.4 s or a click,
* a tap toggles it on touch. sticky: false makes it a plain hover tooltip.
*
* The view needs public/css/components/inViewTooltip.css.
*
* Usage: <in-view-help :text="$p.t('category/phrase')" button-class="fs-5 text-body-secondary"></in-view-help>
*/
export default {
name: 'InViewHelp',
directives: {
tooltip: InViewTooltip
},
props: {
text: { type: String, default: '' },
sticky: { type: Boolean, default: true },
buttonClass: { type: [String, Array, Object], default: '' }
},
template: /*html*/ `
<button
type="button"
class="btn btn-link p-0 fhc-in-view-help-btn"
:class="buttonClass"
:aria-label="$p.t('ui/hilfe')"
v-tooltip="{ value: text, sticky }"
>
<i class="fa fa-circle-question" aria-hidden="true"></i>
</button>`
};
@@ -515,7 +515,7 @@ export default {
},
template: /* html */`
<div class="stv" :class="{ 'sidebar-collapsed': sidebarCollapsed }">
<header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow">
<header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow" style="background-color: green !important;">
<div class="col-md-4 col-lg-3 col-xl-2 d-flex align-items-center">
<button
class="btn btn-outline-light border-0 m-1 collapsed"
@@ -528,7 +528,12 @@ export default {
>
<span class="svg-icon svg-icon-apps"></span>
</button>
<a class="navbar-brand me-0" :href="stvRoot">StudVw: {{studiensemesterKurzbz}} {{studiengangKuerzel}}</a>
<a class="navbar-brand me-0" :href="stvRoot">
<span style="font-size: 10px; line-height: 1; display: block; width: 100%; text-wrap: wrap; text-align: left;">
DEMO Datenstand 29.09.2026
</span>
StudVw: {{studiensemesterKurzbz}} {{studiengangKuerzel}}
</a>
<button
class="btn btn-outline-light border-0 d-none d-md-inline-flex m-1 ms-auto"
type="button"
@@ -263,7 +263,7 @@ export default {
this.$refs[modalRef].hide();
},
reload() {
this.$refs.table.reloadTable();
this.$refs.table?.reloadTable();
},
searchFirma(event) {
if (this.abortController.firmen) {
@@ -30,6 +30,9 @@ export default {
computed: {
betreuerFormOpened() {
return this.newMode || this.editMode;
},
isMitarbeiter(){
return this.autocompleteSelectedBetreuer?.name?.includes('Mitarbeiter') ?? false;
}
},
props: {
@@ -430,6 +433,11 @@ export default {
},
actionKontaktdatenBearbeiten() {
if (!this.autocompleteSelectedBetreuer) return;
if (this.isMitarbeiter)
{
this.$fhcAlert.alertError(this.$p.t('ui', 'editingMitarbeiterPVonly'));
return;
}
this.$refs.kontaktdatenModal.show();
},
// stuff to do after new person has been saved
@@ -494,7 +502,12 @@ export default {
<button class="btn btn-outline-secondary" @click="actionNewPerson">{{ $p.t('projektarbeit', 'neuePersonAnlegen') }}</button>
</div>
<div class="col-6">
<button class="btn btn-outline-secondary float-end" @click="actionKontaktdatenBearbeiten">{{ $p.t('projektarbeit', 'kontaktdatenBearbeiten') }}</button>
<button
class="btn btn-outline-secondary float-end"
@click="actionKontaktdatenBearbeiten"
>
{{ $p.t('projektarbeit', 'kontaktdatenBearbeiten') }}
</button>
</div>
</div>
@@ -605,7 +618,7 @@ export default {
</contact>
</div>
</div>
</bs-modal>
`
}
+352
View File
@@ -0,0 +1,352 @@
/**
* v-tooltip with a panel that stays in view, also at the screen edge, in a modal and on a phone.
*
* - The panel is fixed and stays inside the viewport. It opens below the anchor, above it when
* it fits only there or that side has more space. It grows away from the nearer viewport side.
* - On an anchor wider than the panel, e.g. a long table cell, it opens at the mouse, not at the anchor edge.
* - One panel at a time, appended to the body. A modal or a scrolling container cannot clip it.
* - A mouse hover or a keyboard focus opens it. Escape, a click outside or a scroll closes it.
*
* Sticky, for several sentences or a list: a short label is read before the hover pause ends, a long text is not.
* - After HOLD_DELAY_MS + HOLD_FILL_MS of hovering the panel sticks, the border fills up in the end.
* A click on the anchor makes it stick at once. Touch has no hover: a tap toggles the sticky panel.
* - A sticky panel stays until a click outside, Escape, a scroll or an opening bootstrap modal.
*
* The view needs public/css/components/inViewTooltip.css.
*
* Usage examples:
*
* 1) Directive, registered for the app or the component:
* app.directive('tooltip', InViewTooltip) or directives: { tooltip: InViewTooltip }
* <button v-tooltip="label"> a short label, hover only
* <span v-tooltip.sticky="longText"> several sentences or a list
* v-tooltip="{ value: text, disabled: bool, sticky: bool }", the modifier .top prefers the place above
*
* 2) Without a directive, e.g. a tabulator cell. The content is a text, a DOM node or a function:
* { event: 'cellMouseEnter', handler: (e, cell) => hoverTooltip(cell.getElement(), () => buildNode(cell), { delay: 300, sticky: true, closeOnClick: true }) },
* { event: 'cellMouseLeave', handler: (e, cell) => hideTooltip(cell.getElement()) }
*
* 3) A ? icon with a help text: components/InViewHelp.js
*/
// hover time until a sticky panel sticks: a pause without motion filters out a pointer that only passes by,
// then a short fill shows the pin. A long fill would draw the eye away from the text
const HOLD_DELAY_MS = 1000
const HOLD_FILL_MS = 400
const HIDE_DELAY_MS = 150 // the pointer can cross the gap from the anchor into the panel
const WATCH_MS = 250 // an anchor can leave the page without an unmount, e.g. in a hidden keep-alive tab
const PANEL_MAX_WIDTH = 512 // px
const PANEL_MARGIN = 16 // px between the panel and the viewport edge
const PANEL_GAP = 6 // px between the anchor and the panel
const PANEL_RADIUS = 5 // px, the inner radius of the panel border. The progress border follows it
const PANEL_POINTER_INSET = 16 // px, the horizontal padding of the panel: the text starts below the mouse
const clamp = (value, min, max) => Math.min(Math.max(value, min), max)
let active = null // the open panel, only one at a time
let pending = null // a delayed show
let panelCount = 0
// the last pointer. Its type tells a tap from a mouse also for the mouse events of tabulator, a tap fires
// emulated mouse events too: pointerover fires before them. Its place puts the panel next to the mouse
const pointer = { type: 'mouse', x: null, y: null }
const trackPointer = event => {
pointer.type = event.pointerType
pointer.x = event.clientX
pointer.y = event.clientY
}
for (const name of ['pointerover', 'pointermove', 'pointerdown'])
document.addEventListener(name, trackPointer, { capture: true, passive: true })
/**
* Opens the panel for an anchor element.
*
* content: a text, a DOM node or a function that returns one of them.
* options: prefer ('bottom' | 'top'), delay (ms), sticky (the hover timer pins the panel), pinned (sticks at once),
* closeOnClick (a click on the anchor closes the panel, e.g. a table cell with a click action)
*/
export function showTooltip(anchor, content, options = {}) {
cancelPending()
if (!options.delay) return open(anchor, content, options)
pending = { anchor, timer: setTimeout(() => { pending = null; open(anchor, content, options) }, options.delay) }
}
// for mouse events without a pointer type, e.g. the cell events of tabulator
export function hoverTooltip(anchor, content, options) {
if (pointer.type === 'mouse') showTooltip(anchor, content, options)
}
// closes the panel of the anchor after HIDE_DELAY_MS, a sticky panel stays
export function hideTooltip(anchor) {
if (pending?.anchor === anchor) cancelPending()
if (active?.anchor !== anchor || active.pinned) return
clearTimeout(active.hideTimer)
active.hideTimer = setTimeout(close, HIDE_DELAY_MS)
}
// closes the open panel, with an anchor only the panel of that anchor
export function closeTooltip(anchor) {
if (!anchor || pending?.anchor === anchor) cancelPending()
if (!anchor || active?.anchor === anchor) close()
}
function isTooltipPinned(anchor) {
return active?.anchor === anchor && active.pinned
}
// new content for an open panel, an empty text closes it
function updateTooltip(anchor, content) {
if (active?.anchor !== anchor) return
if (!content) return close()
render(active.panel, content)
place()
}
function open(anchor, content, { prefer = 'bottom', sticky = false, pinned = false, closeOnClick = false }) {
if (active?.anchor === anchor) {
clearTimeout(active.hideTimer)
if (pinned) pin()
return
}
close()
sticky = sticky || pinned
const panel = document.createElement('div')
panel.id = 'fhc-in-view-tooltip-' + ++panelCount
panel.className = 'fhc-in-view-tooltip'
panel.setAttribute('role', 'tooltip')
panel.innerHTML = '<div class="fhc-in-view-tooltip-body"></div>'
if (sticky) {
panel.style.setProperty('--fhc-in-view-tooltip-hold-delay', HOLD_DELAY_MS + 'ms')
panel.style.setProperty('--fhc-in-view-tooltip-hold-fill', HOLD_FILL_MS + 'ms')
panel.insertAdjacentHTML('beforeend', '<svg class="fhc-in-view-tooltip-progress" aria-hidden="true">'
+ '<rect width="100%" height="100%" rx="' + PANEL_RADIUS + '"></rect></svg>')
}
render(panel, content)
document.body.appendChild(panel)
// the pointer at the opening, while it is over the anchor. A keyboard focus has none
const rect = anchor.getBoundingClientRect()
const overAnchor = pointer.x !== null && pointer.x >= rect.left && pointer.x <= rect.right
&& pointer.y >= rect.top && pointer.y <= rect.bottom
active = {
anchor,
panel,
prefer,
pointerX: overAnchor ? pointer.x : null,
sticky,
closeOnClick,
pinned: false,
describedBy: anchor.getAttribute('aria-describedby'),
holdTimer: null,
hideTimer: null,
watchTimer: setInterval(() => {
if (!anchor.isConnected || !anchor.getClientRects().length) close()
}, WATCH_MS)
}
place()
anchor.setAttribute('aria-describedby', panel.id)
if (pinned) {
pin()
} else if (sticky) {
panel.classList.add('fhc-in-view-tooltip--holding')
active.holdTimer = setTimeout(pin, HOLD_DELAY_MS + HOLD_FILL_MS)
}
panel.addEventListener('pointerenter', onPanelEnter)
panel.addEventListener('pointerleave', onPanelLeave)
panel.addEventListener('click', onPanelClick)
document.addEventListener('pointerdown', onPointerDown, true)
document.addEventListener('keydown', onKeyDown, true)
// an anchor often opens a modal on click, the sticky panel must not cover it
document.addEventListener('show.bs.modal', close)
window.addEventListener('scroll', onScroll, true)
window.addEventListener('resize', place)
}
function pin() {
if (!active || active.pinned || !active.sticky) return
clearTimeout(active.holdTimer)
clearTimeout(active.hideTimer)
active.pinned = true
active.panel.classList.remove('fhc-in-view-tooltip--holding')
active.panel.classList.add('fhc-in-view-tooltip--pinned')
}
function close() {
if (!active) return
const { anchor, panel, describedBy } = active
clearTimeout(active.holdTimer)
clearTimeout(active.hideTimer)
clearInterval(active.watchTimer)
active = null
panel.remove()
if (describedBy) anchor.setAttribute('aria-describedby', describedBy)
else anchor.removeAttribute('aria-describedby')
document.removeEventListener('pointerdown', onPointerDown, true)
document.removeEventListener('keydown', onKeyDown, true)
document.removeEventListener('show.bs.modal', close)
window.removeEventListener('scroll', onScroll, true)
window.removeEventListener('resize', place)
}
function cancelPending() {
if (!pending) return
clearTimeout(pending.timer)
pending = null
}
function render(panel, content) {
if (typeof content === 'function') content = content()
const body = panel.querySelector('.fhc-in-view-tooltip-body')
if (content instanceof Node) body.replaceChildren(content)
else body.textContent = content
}
function place() {
if (!active) return
const { anchor, panel, prefer } = active
const rect = anchor.getBoundingClientRect()
const viewWidth = document.documentElement.clientWidth
const viewHeight = document.documentElement.clientHeight
// measure the natural size first
panel.style.maxWidth = Math.min(PANEL_MAX_WIDTH, viewWidth - 2 * PANEL_MARGIN) + 'px'
panel.style.maxHeight = ''
panel.style.left = '0px'
panel.style.top = '0px'
panel.style.bottom = ''
const width = panel.offsetWidth
const height = panel.offsetHeight
const space = {
bottom: viewHeight - rect.bottom - PANEL_GAP - PANEL_MARGIN,
top: rect.top - PANEL_GAP - PANEL_MARGIN
}
const other = prefer === 'top' ? 'bottom' : 'top'
let side = prefer
if (space[prefer] < height) side = space[other] >= height || space[other] > space[prefer] ? other : prefer
// a panel at least as wide as the anchor starts at the anchor edge, grows away from the nearer viewport side
// and spans the whole anchor. On a wider anchor that edge can be far from the mouse: the text starts below
// the pointer, the clamp moves the panel to the left only at the right viewport edge
const { pointerX } = active
let left
if (pointerX === null || width >= rect.width) left = rect.left + rect.width / 2 < viewWidth / 2 ? rect.left : rect.right - width
else left = pointerX - PANEL_POINTER_INSET
panel.style.left = clamp(left, PANEL_MARGIN, viewWidth - PANEL_MARGIN - width) + 'px'
panel.style.maxHeight = space[side] + 'px'
if (side === 'bottom') {
panel.style.top = rect.bottom + PANEL_GAP + 'px'
} else {
panel.style.top = ''
panel.style.bottom = viewHeight - rect.top + PANEL_GAP + 'px'
}
if (!active.sticky) return
// the dash of the progress border is as long as the rounded rect along the inner edge of the 1 px border.
// Rounded up, else a gap stays at its start: clientWidth drops the fraction of the width
const box = panel.getBoundingClientRect()
const perimeter = 2 * (box.width + box.height - 4) - (8 - 2 * Math.PI) * PANEL_RADIUS
panel.style.setProperty('--fhc-in-view-tooltip-perimeter', Math.ceil(perimeter) + 1)
}
function onPanelEnter() {
cancelPending()
clearTimeout(active.hideTimer)
}
function onPanelLeave(event) {
if (event.pointerType === 'mouse') hideTooltip(active.anchor)
}
// a click into the panel closes it, a text selection keeps it
function onPanelClick() {
if (!String(window.getSelection())) close()
}
function onPointerDown(event) {
const { anchor, panel, closeOnClick } = active
if (panel.contains(event.target)) return
if (anchor.contains(event.target) && !closeOnClick) return
cancelPending()
close()
}
// Escape closes only the panel, not a modal around the anchor
function onKeyDown(event) {
if (event.key !== 'Escape') return
event.stopPropagation()
close()
}
// the panel is fixed, a scroll moves the anchor away from it. A scroll inside the panel keeps it
function onScroll(event) {
if (!active.panel.contains(event.target)) close()
}
// reads the binding: "text" or { value, disabled, sticky }, the modifiers .sticky and .top
function read(state, binding) {
const value = binding.value
const isObject = value && typeof value === 'object'
state.text = (isObject ? (value.disabled ? '' : value.value) : value) ?? ''
state.sticky = !!(binding.modifiers.sticky || isObject && value.sticky)
state.prefer = binding.modifiers.top ? 'top' : 'bottom'
}
export default {
mounted(el, binding) {
const state = el._fhcInViewTooltip = { pointerType: 'mouse' }
read(state, binding)
const show = options => {
if (state.text) showTooltip(el, state.text, { prefer: state.prefer, sticky: state.sticky, ...options })
}
state.listeners = {
pointerenter: event => { if (event.pointerType === 'mouse') show() },
pointerleave: event => { if (event.pointerType === 'mouse') hideTooltip(el) },
pointerdown: event => { state.pointerType = event.pointerType },
// a mouse click or a key pins a sticky panel at once, a tap toggles it
click: () => {
if (!state.sticky) return
if (state.pointerType !== 'mouse' && isTooltipPinned(el)) closeTooltip(el)
else show({ pinned: true })
state.pointerType = 'mouse' // a key click has no pointerdown
},
focusin: event => { if (event.target.matches(':focus-visible')) show() },
focusout: () => hideTooltip(el)
}
// the click captures: it pins the panel before the click action of the anchor runs, so a modal
// that this action opens closes the panel again. The pointer events must not capture the children
for (const [name, listener] of Object.entries(state.listeners)) el.addEventListener(name, listener, name === 'click')
},
updated(el, binding) {
const state = el._fhcInViewTooltip
const text = state.text
read(state, binding)
if (state.text !== text) updateTooltip(el, state.text)
},
beforeUnmount(el) {
const state = el._fhcInViewTooltip
closeTooltip(el)
for (const [name, listener] of Object.entries(state.listeners)) el.removeEventListener(name, listener, name === 'click')
delete el._fhcInViewTooltip
}
}
+1
View File
@@ -105,6 +105,7 @@ require_once('dbupdate_3.4/78292_gehaltsanpassungtyp.php');
require_once('dbupdate_3.4/54225_reihungstest_ablauf_fuer_quereinsteiger.php');
require_once('dbupdate_3.4/77757_widgets_berechtigungen.php');
require_once('dbupdate_3.4/77048_cis4_mitarbeiter_studstatus_widget.php');
require_once('dbupdate_3.4/83538_cis4_employees.php');
// *** Pruefung und hinzufuegen der neuen Attribute und Tabellen
echo '<H2>Pruefe Tabellen und Attribute!</H2>';
@@ -1,4 +1,22 @@
<?php
// Add permission: dashboard/widget_studstatus_ma
if($result = @$db->db_query("SELECT 1 FROM system.tbl_berechtigung WHERE berechtigung_kurzbz = 'dashboard/widget_studstatus_ma';"))
{
if($db->db_num_rows($result) == 0)
{
$qry = "INSERT INTO system.tbl_berechtigung(berechtigung_kurzbz, beschreibung) VALUES('dashboard/widget_studstatus_ma', 'Dashboard Widget Studstatus f. Mitarbeiter');";
if(!$db->db_query($qry))
{
echo '<strong>system.tbl_berechtigung '.$db->db_last_error().'</strong><br>';
}
else
{
echo 'system.tbl_berechtigung: Added permission "dashboard/widget_studstatus_ma"<br>';
}
}
}
// neues widget mit widget_kurzbz = 'studstatus' als Zeile hinzufügen
if($result = @$db->db_query("SELECT 1 FROM dashboard.tbl_widget WHERE widget_kurzbz= 'studstatus';"))
{
@@ -6,14 +24,13 @@ if($result = @$db->db_query("SELECT 1 FROM dashboard.tbl_widget WHERE widget_kur
{
$qry = "
INSERT INTO dashboard.tbl_widget (
widget_id,
widget_kurzbz,
beschreibung,
arguments,
setup
setup,
berechtigung_kurzbz
)
VALUES (
9,
'studstatus',
'Widget Todos Studierendenstatus',
'{
@@ -33,7 +50,8 @@ if($result = @$db->db_query("SELECT 1 FROM dashboard.tbl_widget WHERE widget_kur
\"min\": 1
},
\"hideFooter\": true
}'::jsonb
}'::jsonb,
'dashboard/widget_studstatus_ma'
);";
if(!$db->db_query($qry))
@@ -44,7 +62,7 @@ if($result = @$db->db_query("SELECT 1 FROM dashboard.tbl_widget WHERE widget_kur
}
// neuer eintrag in dashboard.tbl_dashboard_widget
if($result = @$db->db_query("SELECT 1 FROM dashboard.tbl_dashboard_widget WHERE dashboard_id= 1 AND widget_id = 9;"))
if($result = @$db->db_query("SELECT 1 FROM dashboard.tbl_dashboard_widget join dashboard.tbl_dashboard USING(dashboard_id) join dashboard.tbl_widget USING(widget_id) WHERE dashboard_kurzbz= 'CIS' AND widget_kurzbz = 'studstatus';"))
{
if($db->db_num_rows($result) == 0)
{
@@ -52,12 +70,33 @@ if($result = @$db->db_query("SELECT 1 FROM dashboard.tbl_dashboard_widget WHERE
INSERT INTO
dashboard.tbl_dashboard_widget (dashboard_id, widget_id)
VALUES
(1, 9);
(
(SELECT dashboard_id FROM dashboard.tbl_dashboard WHERE dashboard_kurzbz = 'CIS'),
(SELECT widget_id FROM dashboard.tbl_widget WHERE widget_kurzbz = 'studstatus')
);
";
if(!$db->db_query($qry))
echo '<strong>dashboard.tbl_dashboard_widget: '.$db->db_last_error().'</strong><br>';
else
echo '<br>dashboard.tbl_dashboard_widget: Widget studstatus hinzugefuegt!<br>';
echo '<br>dashboard.tbl_dashboard_widget: Widget "studstatus" zum Dashboard "CIS" hinzugefuegt!<br>';
}
}
// Add permission: dashboard/widget_studstatus_ma if widget was created without
if($result = @$db->db_query("SELECT 1 FROM dashboard.tbl_widget WHERE berechtigung_kurzbz = 'dashboard/widget_studstatus_ma' and widget_kurzbz = 'studstatus';"))
{
if($db->db_num_rows($result) == 0)
{
$qry = "UPDATE dashboard.tbl_widget SET berechtigung_kurzbz = 'dashboard/widget_studstatus_ma' WHERE widget_kurzbz = 'studstatus';";
if(!$db->db_query($qry))
{
echo '<strong>dashboard.tbl_widget '.$db->db_last_error().'</strong><br>';
}
else
{
echo 'dashboard.tbl_widget: Added permission "dashboard/widget_studstatus_ma" to widget "studstatus"<br>';
}
}
}
@@ -0,0 +1,35 @@
<?php
// Add permission: cis/eigene_zeitsperren
if($result = @$db->db_query("SELECT 1 FROM system.tbl_berechtigung WHERE berechtigung_kurzbz = 'cis/eigene_zeitsperren';"))
{
if($db->db_num_rows($result) == 0)
{
$qry = "INSERT INTO system.tbl_berechtigung(berechtigung_kurzbz, beschreibung) VALUES('cis/eigene_zeitsperren', 'CI/VueJs Applikation Eigene Zeitsperren');";
if(!$db->db_query($qry))
{
echo '<strong>system.tbl_berechtigung '.$db->db_last_error().'</strong><br>';
}
else
{
echo 'system.tbl_berechtigung: Added permission "cis/eigene_zeitsperren"<br>';
}
}
}
// Add permission: cis/andere_zeitsperren
if($result = @$db->db_query("SELECT 1 FROM system.tbl_berechtigung WHERE berechtigung_kurzbz = 'cis/andere_zeitsperren';"))
{
if($db->db_num_rows($result) == 0)
{
$qry = "INSERT INTO system.tbl_berechtigung(berechtigung_kurzbz, beschreibung) VALUES('cis/andere_zeitsperren', 'CI/VueJs Applikation Zeitsperren anderer MA anzeigen');";
if(!$db->db_query($qry))
{
echo '<strong>system.tbl_berechtigung '.$db->db_last_error().'</strong><br>';
}
else
{
echo 'system.tbl_berechtigung: Added permission "cis/andere_zeitsperren"<br>';
}
}
}
+884
View File
@@ -21669,6 +21669,264 @@ array(
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'standort',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Standort",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "Site",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'alleStandorte',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Alle Standorte",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "All Sites",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'ohneStandort',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Ohne Standort",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "No Site",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'tooltipRaumsuchev2',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Die Raumsuche zeigt freie Räume für einen Zeitraum. Ein Raum ist frei, wenn im Stundenplan keine Lehrveranstaltung steht und niemand den Raum reserviert hat. Beim Öffnen zeigt die Liste die Räume, die ab jetzt für eine Stunde frei sind.
So finden Sie einen freien Raum:
1. Wählen Sie Datum und Uhrzeit.
2. Optional: Wählen Sie einen Raumtyp, einen Standort und eine Mindestpersonenkapazität.
Die Liste lädt neu, sobald Sie einen Filter ändern.
In der Liste:
• Raum Informationen: Das Symbol öffnet die Infoseite des Raums. Ein Strich bedeutet: Der Raum hat keine Infoseite.
• Raum Reservierungen: Das Symbol öffnet den Kalender des Raums. Der Kalender zeigt alle Lehrveranstaltungen und Reservierungen.",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "The room search shows free rooms for a time period. A room is free when the timetable shows no course in the room and nobody reserved the room. When you open the page, the list shows the rooms that are free from now for one hour.
How to find a free room:
1. Select the date and the time.
2. Optional: select a room type, a site and a minimum person capacity.
The list loads again when you change a filter.
In the list:
• Room Information: the icon opens the information page of the room. A dash means that the room has no information page.
• Room Reservations: the icon opens the calendar of the room. The calendar shows all courses and reservations.",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'erweiterteFilter',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Erweiterte Filter",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "Advanced filters",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'nurLehrraeume',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Nur Lehrräume",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "Only teaching rooms",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'nurMitInfoseite',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Nur Räume mit Infoseite",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "Only rooms with an information page",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'lehrraum',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Lehrraum",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "Teaching room",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'stockwerk',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Stockwerk",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "Floor",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'gebaeudeteil',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Gebäudeteil",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "Building section",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'flaeche',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Fläche in m²",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "Area in m²",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'rauminfo',
'phrase' => 'raumtyp',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Raumtyp",
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => "Room Type",
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehre',
@@ -63774,6 +64032,26 @@ I have been informed that I am under no obligation to consent to the transmissio
)
),
// ### Infocenter Onboarding END
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'editingMitarbeiterPVonly',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Kontaktdaten von Mitarbeiter*innen dürfen nur in der Personalverwaltung editiert werden.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Employee contact details may only be edited in the personnel administration system.',
'description' => '',
'insertvon' => 'system'
)
)
),
//**************************** CIS Projektabgabeuebersicht start
array(
'app' => 'core',
@@ -67193,6 +67471,612 @@ I have been informed that I am under no obligation to consent to the transmissio
)
)
),
// BETREUUNGEN PHRASES START
array(
'app' => 'core',
'category' => 'betreuungen',
'phrase' => 'studiengang',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Stg',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'DP',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'betreuungen',
'phrase' => 'semester',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Sem',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Sem',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'betreuungen',
'phrase' => 'stunden',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Std',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Hrs',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'betreuungen',
'phrase' => 'lvBezeichnung',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'LV Bezeichnung',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Course title',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'betreuungen',
'phrase' => 'student',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Student/in',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Student',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'betreuungen',
'phrase' => 'betreuungsart',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Betreuungsart',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Supervision type',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'betreuungen',
'phrase' => 'titelProjektarbeit',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Titel der Projektarbeit',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Project title',
'description' => '',
'insertvon' => 'system'
)
)
),
// BETREUUNGEN PHRASES END
// LEHREINHEITEN PHRASES END
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'inputGrades',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Noten eintragen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Input grades',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'subject',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Lehrfach',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Subject',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'lehrform',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Lehrform',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Course Type',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'title',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'LV Bezeichnung',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Course Title',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'lector',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Lektor/in',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Lector',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'dp',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Stg',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'DP',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'sem',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Sem',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Sem',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'groups',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Gruppen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Groups',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'room',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Raumtyp',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Room',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'alt',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Alternativ',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Alternative',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'block',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Block',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Block',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'wr',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'WR',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'WR',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'hrs',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Std',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Hrs',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'cw',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'KW',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'CW',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'registrationWindowStart',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Anm. von',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Registration Start',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'lehreinheiten',
'phrase' => 'registrationWindowEnd',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Anm. bis',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Registration End',
'description' => '',
'insertvon' => 'system'
)
)
),
// LEHREINHEITEN PHRASES END
// MYLV PHRASES START
array(
'app' => 'core',
'category' => 'mylv',
'phrase' => 'lvs',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'LVs',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Course programs',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'mylv',
'phrase' => 'lehreinheiten',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Lehreinheiten',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Teaching units',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'mylv',
'phrase' => 'supervision',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Betreuungen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Supervision',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'mylv',
'phrase' => 'all',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Alles zeigen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Show all',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'mylv',
'phrase' => 'supervised_students',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Betreute Studierende',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Supervised students',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'mylv',
'phrase' => 'no_supervised',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Keine Betreute Studierende gefunden!',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'No supervised students found!',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'mylv',
'phrase' => 'no_lehreinheiten',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Keine Lehreinheiten gefunden!',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'No teaching units found!',
'description' => '',
'insertvon' => 'system'
)
)
),
// MYLV PHRASES END
);