Compare commits

...
Author SHA1 Message Date
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 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 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 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 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 9e2cc68531 fixed prop type syntax 2026-10-07 17:09:22 +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
adisposkofh 4549c92d50 myLV lehreinheiten table formatting 2026-10-06 13:48:23 +02:00
adisposkofh a5aa4c8466 added myLv lehrenheiten table 2026-10-05 15:14:04 +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
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
36 changed files with 2555 additions and 161 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 = {};
+1 -1
View File
@@ -60,7 +60,7 @@ export default {
axisMainCollapsible: Boolean,
snapToGrid: Boolean,
overwrittenEvents: {
type: Array | null,
type: [Array, null],
default: null,
},
shouldMatchParentHeight: {
@@ -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>`
};
@@ -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
);