Merge branch 'feature-68298/Tempus_Grundstruktur' into test-83711/Tempus_Automatisierte_Tests

This commit is contained in:
Ivymaster
2026-10-15 10:56:39 +02:00
36 changed files with 1973 additions and 987 deletions
@@ -173,6 +173,13 @@ class Kalender extends FHCAPI_Controller
$start_date = $this->_ci->input->post('start_date', TRUE);
$end_date = $this->_ci->input->post('end_date', TRUE);
$isCollisionCheckEnabled = filter_var(
$this->_ci->input->post('collisionCheck', TRUE) ?? false,
FILTER_VALIDATE_BOOLEAN
);
$maxDailyEventLimit = $this->_ci->input->post('maxDailyEventLimit', TRUE) ?? false;
$filter = $this->_checkFilter(self::ALLOWED_PLAN_FILTER);
$stundenplan_data = $this->_ci->kalenderlib->getPlanForPlanner(
@@ -180,7 +187,9 @@ class Kalender extends FHCAPI_Controller
$end_date,
isset($filter->ort) ? $filter->ort : null,
isset($filter->uid) ? $filter->uid : null,
isset($filter->stg) ? $filter->stg : null
isset($filter->stg) ? $filter->stg : null,
$isCollisionCheckEnabled,
$maxDailyEventLimit
);
$this->terminateWithSuccess($stundenplan_data);
@@ -198,9 +207,17 @@ class Kalender extends FHCAPI_Controller
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$isCollisionCheckEnabled = filter_var(
$this->_ci->input->get('collisionCheck', TRUE) ?? false,
FILTER_VALIDATE_BOOLEAN
);
$maxDailyEventLimit = $this->_ci->input->get('maxDailyEventLimit', TRUE) ?? false;
$stundenplan_data = $this->_ci->kalenderlib->getPlanForStudent(
$start_date,
$end_date
$end_date,
$isCollisionCheckEnabled,
$maxDailyEventLimit
);
$this->terminateWithSuccess($stundenplan_data);
@@ -218,9 +235,17 @@ class Kalender extends FHCAPI_Controller
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$isCollisionCheckEnabled = filter_var(
$this->_ci->input->get('collisionCheck', TRUE) ?? false,
FILTER_VALIDATE_BOOLEAN
);
$maxDailyEventLimit = $this->_ci->input->get('maxDailyEventLimit', TRUE) ?? false;
$stundenplan_data = $this->_ci->kalenderlib->getPlanForLecturer(
$start_date,
$end_date
$end_date,
$isCollisionCheckEnabled,
$maxDailyEventLimit
);
$this->terminateWithSuccess($stundenplan_data);
@@ -12,10 +12,11 @@ class Reservierung extends FHCAPI_Controller
{
parent::__construct([
'addReservierung' => 'lehre/lvplan:rw',
'getRollen' => 'lehre/lvplan:rw',
'getInformation' => 'lehre/lvplan:rw',
'getLektor' => 'lehre/lvplan:rw',
'searchGroup' => 'lehre/lvplan:rw',
'getRollen' => 'lehre/lvplan:r',
'getInformation' => 'lehre/lvplan:r',
'getLektor' => 'lehre/lvplan:r',
'searchGroup' => 'lehre/lvplan:r',
'updateTeilnehmer' => 'lehre/reservierung:rw',
]);
$this->_ci =& get_instance();
@@ -72,12 +73,17 @@ class Reservierung extends FHCAPI_Controller
$rollen = $this->_ci->KalenderEventRolleModel->load();
$this->_ci->KalenderEventRolleModel->addOrder('sort');
$this->_ci->KalenderEventRolleModel->addSelect('rolle_kurzbz, array_to_json(bezeichnung_mehrsprachig::varchar[])->>'. $language. ' as bezeichnung');
$gruppen_rollen = $this->_ci->KalenderEventRolleModel->loadWhere(array('rolle_kurzbz !=' => 'organisator'));
$this->_ci->StudiensemesterModel->addOrder('start', 'DESC');
$studiensemester = $this->_ci->StudiensemesterModel->load();
$return_array['studiengaenge'] = hasData($studiengaenge) ? getData($studiengaenge) : [];
$return_array['berechtigt'] = true;
$return_array['rollen'] = hasData($rollen) ? getData($rollen) : [];
$return_array['gruppen_rollen'] = hasData($gruppen_rollen) ? getData($gruppen_rollen) : [];
$return_array['studiensemester'] = hasData($studiensemester) ? getData($studiensemester) : [];
@@ -236,4 +242,45 @@ class Reservierung extends FHCAPI_Controller
$this->terminateWithSuccess(hasData($result) ? getData($result) : array());
}
public function updateTeilnehmer()
{
$this->_ci->form_validation->set_rules('kalender_id', 'kalender_id', 'required|numeric', [
'required' => $this->p->t('ui', 'error_fieldRequired'),
]);
if ($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
if (!$this->_ci->permissionlib->isBerechtigt('lehre/reservierung'))
$this->terminateWithError($this->p->t('ui', 'keineBerechtigung'), self::ERROR_TYPE_GENERAL);
$kalender_id = $this->_ci->input->post('kalender_id', TRUE);
$teilnehmer = $this->_ci->input->post('teilnehmer', TRUE);
$gruppen = $this->_ci->input->post('gruppen', TRUE);
$teilnehmer = is_array($teilnehmer) ? $teilnehmer : [];
$gruppen = is_array($gruppen) ? $gruppen : [];
$hat_organisator = false;
foreach ($teilnehmer as $teil)
{
if (!empty($teil['uid']) && isset($teil['rolle']) && $teil['rolle'] === 'organisator')
{
$hat_organisator = true;
break;
}
}
if (!$hat_organisator)
$teilnehmer[] = array('uid' => getAuthUID(), 'rolle' => 'organisator');
$result = $this->_ci->kalenderlib->updateReservierungTeilnehmer($kalender_id, $teilnehmer, $gruppen);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
}
+107 -9
View File
@@ -104,7 +104,7 @@ class KalenderLib
$data = $this->_loadFilteredBasePlan($kalender_entry->von, $kalender_entry->bis);
return $this->_mapEvents($data);
}
public function getPlanForPlanner($start_date, $end_date, $ort = null, $uids = null, $studiengaenge = null)
public function getPlanForPlanner($start_date, $end_date, $ort = null, $uids = null, $studiengaenge = null, $collisionCheck = true, $maxDailyEventLimit = null)
{
$this->_buildBasePlanQuery();
@@ -240,11 +240,10 @@ class KalenderLib
$data = $this->_loadFilteredBasePlan($start_date, $end_date);
return $this->_mapEvents($data);
return $this->_mapEvents($data, $collisionCheck, $maxDailyEventLimit);
}
public function getPlanForStudent($start_date, $end_date)
public function getPlanForStudent($start_date, $end_date, $collisionCheck = true, $maxDailyEventLimit = null)
{
$this->_buildBasePlanQuery();
@@ -252,11 +251,11 @@ class KalenderLib
$data = $this->_loadFilteredBasePlan($start_date, $end_date);
return $this->_mapEvents($data);
return $this->_mapEvents($data, $collisionCheck, $maxDailyEventLimit);
}
public function getPlanForLecturer($start_date, $end_date)
public function getPlanForLecturer($start_date, $end_date, $collisionCheck = true, $maxDailyEventLimit = null)
{
$this->_buildBasePlanQuery();
@@ -272,7 +271,7 @@ class KalenderLib
)", NULL, FALSE);
$data = $this->_loadFilteredBasePlan($start_date, $end_date);
return $this->_mapEvents($data);
return $this->_mapEvents($data, $collisionCheck, $maxDailyEventLimit);
}
public function getZeitsperren($start_date, $end_date, $emp)
{
@@ -1394,6 +1393,72 @@ class KalenderLib
return $dbModel->execReadOnlyQuery($history_sql, $params);
}
public function updateReservierungTeilnehmer($kalender_id, $teilnehmer, $specialFinalGroups)
{
$entryResult = $this->_loadKalenderEntry($kalender_id);
if (isError($entryResult))
return $entryResult;
$kalender_entry = getData($entryResult);
if ($kalender_entry->typ !== 'reservierung')
{
return error([
'message' => $this->_ci->phraseslib->t('ui', 'entry_not_editable'),
'errorCode' => 'entry_not_editable'
]);
}
$this->_ci->KalenderModel->db->trans_start();
$deleteresult = $this->_deleteReservierungTeilnehmerEntry($kalender_entry);
if (isError($deleteresult))
{
$this->_ci->KalenderModel->db->trans_rollback();
return $deleteresult;
}
foreach ($teilnehmer as $teil)
{
$teilnehmerresult = $this->_addTeilnehmerToEvent($kalender_id, $teil['uid'], $teil['rolle']);
if (isError($teilnehmerresult))
{
$this->_ci->KalenderModel->db->trans_rollback();
return $teilnehmerresult;
}
}
foreach ($specialFinalGroups as $group)
{
$specialgroupresult = $this->_addFinalGroupToEvent($kalender_id, $group['gid'], !($group['lehrverband'] === 'false'), $group['gruppe_kurzbz'], $group['studiensemester_kurzbz'], $group['rolle']);
if (isError($specialgroupresult))
{
$this->_ci->KalenderModel->db->trans_rollback();
return $specialgroupresult;
}
}
$entryResult = $this->_loadKalenderEntry($kalender_id);
if (isError($entryResult))
{
$this->_ci->KalenderModel->db->trans_rollback();
return $entryResult;
}
$kalender_entry = getData($entryResult);
$errors = $this->_ci->collisionchecker->run($kalender_entry);
if (!empty($errors))
{
$this->_ci->KalenderModel->db->trans_rollback();
return error($errors);
}
$this->_ci->KalenderModel->db->trans_complete();
return success($kalender_id);
}
private function _buildFilteredKalenderCte($start_date, $end_date)
{
$end_date = date('Y-m-d', strtotime($end_date . ' +1 day'));
@@ -1423,6 +1488,7 @@ class KalenderLib
tbl_kalender.bis,
tbl_kalender_ort.location,
tbl_kalender_ort.ort_kurzbz,
tbl_ort.content_id as ort_content_id,
tbl_lehreinheit.lehreinheit_id,
tbl_lehreinheit.anmerkung as le_anmerkung,
tbl_lehreinheit.lehrveranstaltung_id,
@@ -1444,6 +1510,10 @@ class KalenderLib
COALESCE(verbandgruppe.verband::varchar, \'\') ||
COALESCE(verbandgruppe.gruppe, \'\'),
\'\') as verband_grp,
teilnehmer.studiensemester_kurzbz AS teilnehmer_studiensemester_kurzbz,
teilnehmer.rolle_kurzbz AS teilnehmer_rolle_kurzbz,
teilnehmergruppe.gid AS teilnehmerg_grp_gid,
verbandgruppe.gid AS verband_grp_gid,
tbl_kalender_event.beschreibung,
tbl_kalender_event.titel,
teilmitarbeiter.kurzbz as teilnehmer_kurzbz,
@@ -1528,6 +1598,7 @@ class KalenderLib
AND TRIM(COALESCE(tbl_lehreinheitgruppe.verband::text, \'\')) = TRIM(le_lehrverband.verband::text)
AND TRIM(COALESCE(tbl_lehreinheitgruppe.gruppe::text, \'\')) = TRIM(le_lehrverband.gruppe::text)', 'LEFT');
$this->_ci->KalenderModel->addJoin('public.tbl_studiengang le_studiengang', 'le_lehrverband.studiengang_kz = le_studiengang.studiengang_kz', 'LEFT');
$this->_ci->KalenderModel->addJoin('public.tbl_ort', 'tbl_ort.ort_kurzbz = tbl_kalender_ort.ort_kurzbz', 'LEFT');
// Include tags in the query
$this->_ci->load->config('tempus');
@@ -1606,7 +1677,7 @@ class KalenderLib
return $this->_ci->KalenderModel->execReadOnlyQuery($query);
}
private function _mapEvents($data, $collisionCheck = true)
private function _mapEvents($data, $collisionCheck = true, $maxDailyEventLimit = null)
{
$stundenplan_data = [];
@@ -1637,6 +1708,7 @@ class KalenderLib
'tooltip' => 'tip',
'status_kurzbz' => $row->status_kurzbz,
'ort_kurzbz' => [],
'ort_infos' => [],
'location' => null,
'lehrfach' => isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '',
'farbe' => isset($row->farbe) ? $row->farbe : '',
@@ -1684,7 +1756,10 @@ class KalenderLib
];
}
if (isset($row->ort_kurzbz) && $row->ort_kurzbz !== '' && !in_array($row->ort_kurzbz, $events[$id]->ort_kurzbz))
{
$events[$id]->ort_kurzbz[] = $row->ort_kurzbz;
$events[$id]->ort_infos[] = ['ort_kurzbz' => $row->ort_kurzbz, 'ort_content_id' => $row->ort_content_id];
}
if (isset($row->location) && $row->location !== '' && is_null($events[$id]->location))
$events[$id]->location = $row->location;
@@ -1712,6 +1787,10 @@ class KalenderLib
{
$events[$id]->teilnehmer_gruppe[] = [
'gruppe_kurzbz' => $row->verband_grp,
'gid' => $row->verband_grp_gid,
'lehrverband' => 'true',
'studiensemester_kurzbz' => $row->teilnehmer_studiensemester_kurzbz,
'rolle' => $row->teilnehmer_rolle_kurzbz,
];
}
}
@@ -1722,6 +1801,10 @@ class KalenderLib
{
$events[$id]->teilnehmer_gruppe[] = [
'gruppe_kurzbz' => $row->teilnehmerg_grp,
'gid' => $row->teilnehmerg_grp_gid,
'lehrverband' => 'false',
'studiensemester_kurzbz' => $row->teilnehmer_studiensemester_kurzbz,
'rolle' => $row->teilnehmer_rolle_kurzbz,
];
}
}
@@ -1774,6 +1857,21 @@ class KalenderLib
usort($event->ort_kurzbz, 'strnatcasecmp');
}
if (!is_null($maxDailyEventLimit) && is_numeric($maxDailyEventLimit) && $maxDailyEventLimit > 0)
{
$eventsPerDay = [];
$events = array_filter($events, function ($event) use (&$eventsPerDay, $maxDailyEventLimit) {
$date = $event->datum;
$eventsPerDay[$date] = $eventsPerDay[$date] ?? 0;
if ($eventsPerDay[$date] >= $maxDailyEventLimit)
return false;
$eventsPerDay[$date]++;
return true;
});
}
return array_values($events);
}
@@ -1891,7 +1989,7 @@ class KalenderLib
'kalender_id' => $kalender_id,
'studiengang_kz' => $gruppe->studiengang_kz,
'studiensemester_kurzbz' => $studiensemester_kurzbz,
'semester' => isEmptyString($gruppe->semester) ? null : $gruppe->semester,
'semester' => ($gruppe->semester === null || $gruppe->semester === '') ? null : $gruppe->semester,
'verband' => isEmptyString($gruppe->verband) ? null : $gruppe->verband,
'gruppe' => isEmptyString($gruppe->gruppe) ? null : $gruppe->gruppe,
'rolle_kurzbz' => $rolle
@@ -53,7 +53,7 @@ class LectureCollisionCheck implements ICollisionCheck
$kollisionsfreie_user = unserialize(KOLLISIONSFREIE_USER);
$grouped = [];
$this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, current_lehreinheit_ma.mitarbeiter_uid');
$this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, current_lehreinheit_ma.mitarbeiter_uid, other_kalender.von, other_kalender.kalender_id as other_kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit current_kalender_le', 'current_kalender_le.kalender_id = tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit current_lehreinheit', 'current_lehreinheit.lehreinheit_id = current_kalender_le.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter current_lehreinheit_ma', 'current_lehreinheit_ma.lehreinheit_id = current_lehreinheit.lehreinheit_id');
@@ -87,11 +87,13 @@ class LectureCollisionCheck implements ICollisionCheck
{
$grouped[$row->kalender_id][] = [
'message' => $this->_ci->phraseslib->t('ui', 'ma_le_kollision') . ': ' . $row->mitarbeiter_uid,
'kalender_id' => $row->other_kalender_id,
'datum' => $row->von,
];
}
}
$this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, current_lehreinheit_ma.mitarbeiter_uid');
$this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, current_lehreinheit_ma.mitarbeiter_uid, other_kalender.von, other_kalender.kalender_id as other_kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit current_kalender_le', 'current_kalender_le.kalender_id = tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit current_lehreinheit', 'current_lehreinheit.lehreinheit_id = current_kalender_le.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter current_lehreinheit_ma', 'current_lehreinheit_ma.lehreinheit_id = current_lehreinheit.lehreinheit_id');
@@ -124,11 +126,13 @@ class LectureCollisionCheck implements ICollisionCheck
{
$grouped[$row->kalender_id][] = [
'message' => $this->_ci->phraseslib->t('ui', 'reservierung_kollision') . ': ' . $row->mitarbeiter_uid,
'kalender_id' => $row->other_kalender_id,
'datum' => $row->von,
];
}
}
$this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, z.mitarbeiter_uid');
$this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, z.mitarbeiter_uid, tbl_kalender.von');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit current_kalender_le', 'current_kalender_le.kalender_id = tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit current_lehreinheit', 'current_lehreinheit.lehreinheit_id = current_kalender_le.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter current_lehreinheit_ma', 'current_lehreinheit_ma.lehreinheit_id = current_lehreinheit.lehreinheit_id');
@@ -54,7 +54,7 @@ class RoomCollisionCheck implements ICollisionCheck
{
if (empty($kalender_ids)) return [];
$this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, current_ort.ort_kurzbz');
$this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, current_ort.ort_kurzbz, other_kalender.von, other_kalender.kalender_id as other_kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort current_ort', 'current_ort.kalender_id = tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort other_ort', 'other_ort.ort_kurzbz = current_ort.ort_kurzbz');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender other_kalender', 'other_kalender.kalender_id = other_ort.kalender_id');
@@ -83,7 +83,9 @@ class RoomCollisionCheck implements ICollisionCheck
foreach (getData($result) as $row)
{
$grouped[$row->kalender_id][] = [
'message' => $this->_ci->phraseslib->t('ui', 'raum_kollision') . ': ' . $row->ort_kurzbz
'message' => $this->_ci->phraseslib->t('ui', 'raum_kollision') . ': ' . $row->ort_kurzbz,
'kalender_id' => $row->other_kalender_id,
'datum' => $row->von,
];
}
@@ -199,7 +199,7 @@ class StudentCollisionCheck implements ICollisionCheck
$placeholders = implode(',', array_fill(0, count($kalender_ids), '?'));
$sql = "
SELECT DISTINCT current_kalender.kalender_id, current_benutzergruppe.uid
SELECT DISTINCT current_kalender.kalender_id, current_benutzergruppe.uid, other_kalender.von, other_kalender.kalender_id as other_kalender_id
FROM lehre.tbl_kalender current_kalender
JOIN lehre.tbl_kalender_lehreinheit current_kalender_le ON current_kalender_le.kalender_id = current_kalender.kalender_id
JOIN lehre.tbl_lehreinheit current_lehreinheit ON current_lehreinheit.lehreinheit_id = current_kalender_le.lehreinheit_id
@@ -236,7 +236,7 @@ class StudentCollisionCheck implements ICollisionCheck
UNION ALL
SELECT DISTINCT current_kalender.kalender_id, current_studentlehrverband.student_uid AS uid
SELECT DISTINCT current_kalender.kalender_id, current_studentlehrverband.student_uid AS uid, other_kalender.von, other_kalender.kalender_id as other_kalender_id
FROM lehre.tbl_kalender current_kalender
JOIN lehre.tbl_kalender_lehreinheit current_kalender_le ON current_kalender_le.kalender_id = current_kalender.kalender_id
JOIN lehre.tbl_lehreinheit current_lehreinheit ON current_lehreinheit.lehreinheit_id = current_kalender_le.lehreinheit_id
@@ -279,7 +279,9 @@ class StudentCollisionCheck implements ICollisionCheck
foreach (getData($result) as $row)
{
$grouped[$row->kalender_id][] = [
'message' => $this->_ci->phraseslib->t('ui', 'student_kollision') . ': ' . $row->uid
'message' => $this->_ci->phraseslib->t('ui', 'student_kollision') . ': ' . $row->uid,
'kalender_id' => $row->other_kalender_id,
'datum' => $row->von,
];
}
@@ -319,7 +319,8 @@ class VerbandCollisionCheck implements ICollisionCheck
UPPER(stg.typ::text || stg.kurzbz::text) || '-' || other_lehreinheitguppe.semester ||
COALESCE(other_lehreinheitguppe.verband::text, '') ||
COALESCE(other_lehreinheitguppe.gruppe::text, '')
) AS gruppenname
) AS gruppenname,
other_kalender.von, other_kalender.kalender_id as other_kalender_id
FROM lehre.tbl_kalender current_kalender
JOIN (
@@ -412,7 +413,11 @@ class VerbandCollisionCheck implements ICollisionCheck
$grouped = [];
foreach (getData($result) as $row)
{
$grouped[$row->kalender_id][] = ['message' => $this->_ci->phraseslib->t('ui', 'verband_kollision') . ': ' . $row->gruppenname];
$grouped[$row->kalender_id][] = [
'message' => $this->_ci->phraseslib->t('ui', 'verband_kollision') . ': ' . $row->gruppenname,
'kalender_id' => $row->other_kalender_id,
'datum' => $row->von,
];
}
return $grouped;
+14
View File
@@ -96,3 +96,17 @@
.fhc-roominformation .calendar-event-default .event-place {
display: none;
}
.tag-row-cell {
width: 100%;
max-width: 0;
}
.tag-row-wrapper,
.tag-row-wrapper > * {
display: flex;
flex-wrap: wrap !important;
max-width: 100%;
white-space: normal !important;
overflow: visible !important;
}
+97 -29
View File
@@ -6,19 +6,19 @@
@import './components/Notiz.css';
html {
font-size: .875em;
height: 100%;
font-size: 0.875em;
height: 100%;
}
body {
/*display: flex;*/
height: 100%;
height: 100%;
}
.heightfull {
height: 95%;
height: 95%;
}
.navbar-dark .navbar-brand:focus {
box-shadow: 0 0 0 .25rem rgba(13,110,253,.25);
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
z-index: 3;
}
#main {
@@ -28,13 +28,13 @@ body {
height: 100%;
}
.searchbar {
margin-right: 0!important;
margin-right: 0 !important;
}
.searchbar > .input-group {
margin-right: 0!important;
margin-right: 0 !important;
}
.searchbar > .input-group > * {
border-radius: 0!important;
border-radius: 0 !important;
}
#sidebarMenu {
@@ -55,7 +55,7 @@ body {
@media (min-width: 768px) {
#sidebarMenu {
visibility: visible!important;
visibility: visible !important;
transform: none;
position: inherit;
z-index: 1;
@@ -64,27 +64,24 @@ body {
.toast.toast-success {
color: #0f5132;
background-color: #d1e7dd!important;
border-color: #badbcc!important;
background-color: #d1e7dd !important;
border-color: #badbcc !important;
}
.toast.toast-danger {
color: #842029;
background-color: #f8d7da!important;
border-color: #f5c2c7!important;
background-color: #f8d7da !important;
border-color: #f5c2c7 !important;
}
.has-filter .fa-filter {
color: var(--bs-success);
}
#parkingslot {
border: 1px dashed;
min-height: 5vh;
max-height: 15vh;
color: #AAAAAA;
color: #aaaaaa;
text-align: center;
font-size: large;
margin-top: 1.25rem;
@@ -96,7 +93,7 @@ body {
}
.course-picker {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
display: flex;
flex-direction: column;
flex: 1 1 0;
@@ -132,11 +129,11 @@ body {
}
.eckerltest {
box-shadow: 3px 3px 3px #ccc;
box-shadow: 3px 3px 3px #ccc;
}
.verband-selection {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
margin-bottom: 0.5rem;
max-height: 450px;
overflow-y: auto;
@@ -145,7 +142,7 @@ body {
}
.lecture-selection {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
margin-bottom: 0.5rem;
max-height: 20vh;
overflow-y: auto;
@@ -154,7 +151,7 @@ body {
}
.room-selection {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
margin-bottom: 0.5rem;
padding: 0.5rem 0 0.5rem 0.5rem;
}
@@ -169,10 +166,8 @@ body {
transform: scale(1.1);
}
.bg-lecturer-wish {
opacity: .15;
opacity: 0.15;
}
.bg-lecturer-block {
background: rgba(255, 0, 0, 0.15);
@@ -195,18 +190,21 @@ body {
.wish-w--2 {
background-color: #FF2200;
background-color: #ff2200;
}
.wish-w--1 {
background-color: #FF9922;
background-color: #ff9922;
}
.wish-w-1 {
background-color: #CCFFCC;
background-color: #ccffcc;
}
.wish-w-2 {
background-color: #48FA66;
background-color: #48fa66;
}
.fhc-calendar-base-grid-line-event {
position: relative !important;
}
.room-option {
display: grid;
@@ -215,6 +213,76 @@ body {
width: 100%;
}
.updated-event {
position: relative !important;
z-index: 10;
animation: modernFocus 1s ease-out;
border-radius: 0.25rem;
}
.updated-event-long {
position: relative !important;
z-index: 10;
animation: modernFocus 1.8s ease-out;
border-radius: 0.25rem;
}
@keyframes modernFocus {
0% {
background-color: #cfd4d8;
box-shadow:
0 0 0 0 rgba(120, 120, 120, 0.8),
0 0 0 0 rgba(255, 255, 255, 0.6);
}
30% {
background-color: #eef1f3;
box-shadow:
0 0 0 8px rgba(120, 120, 120, 0.25),
0 0 30px 8px rgba(255, 255, 255, 0.7);
}
60% {
background-color: #f7f8f9;
box-shadow:
0 0 0 14px rgba(120, 120, 120, 0.15),
0 0 45px 12px rgba(255, 255, 255, 0.5);
}
100% {
background-color: inherit;
box-shadow:
0 0 0 24px rgba(120, 120, 120, 0),
0 0 60px 20px rgba(255, 255, 255, 0);
}
}
.spinner-overlay {
position: absolute;
inset: 0; /* top:0 right:0 bottom:0 left:0 */
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
background: rgba(255, 255, 255, 1);
opacity: 0.4;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #ddd;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.timepicker-row {
display: flex;
align-items: center;
+23 -7
View File
@@ -1,11 +1,17 @@
export default {
getPlan(filter, start_date, end_date)
getPlan(filter, start_date, end_date, collisionCheck = true, maxDailyEventLimit = null)
{
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/getPlan',
params: { ...filter, start_date, end_date }
params: {
...filter,
start_date,
end_date,
collisionCheck,
...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit })
}
};
},
getRaumvorschlagSlots(lehreinheit_id, start_date, end_date)
@@ -16,20 +22,30 @@ export default {
params: { lehreinheit_id, start_date, end_date }
};
},
getPlanLecturer(start_date, end_date)
getPlanLecturer(start_date, end_date, collisionCheck = true, maxDailyEventLimit = null)
{
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getPlanLecturer',
params: { start_date, end_date }
params: {
start_date,
end_date,
collisionCheck,
...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit })
}
};
},
getPlanStudent(start_date, end_date)
getPlanStudent(start_date, end_date, collisionCheck = true, maxDailyEventLimit = null)
{
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getPlanStudent',
params: { start_date, end_date }
params: {
start_date,
end_date,
collisionCheck,
...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit })
}
};
},
@@ -96,7 +112,7 @@ export default {
params: { kalender_id}
};
},
addKalenderEvent(lehreinheit_id, ort_kurzbz, start_date, end_date, first_run) {
addKalenderEvent(lehreinheit_id, ort_kurzbz, start_date, end_date, first_run = true) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/addKalenderEvent',
@@ -55,5 +55,12 @@ export default {
params: { titel, beschreibung, orte, start_date, end_date, teilnehmer, specialFinalGroups, specialGroups, groups}
};
},
updateTeilnehmer(kalender_id, teilnehmer, gruppen) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Reservierung/updateTeilnehmer',
params: { kalender_id, teilnehmer, gruppen }
};
},
};
+6 -1
View File
@@ -123,6 +123,10 @@ export default {
draggableEvents: [Boolean, Array, Function],
dropableEvents: [Boolean, Array, Function],
resizableEvents: [Boolean, Array, Function],
eventReloadKey: {
type: Number,
default: 0,
},
onDragover: Function,
onDrop: Function,
onResize: Function
@@ -338,6 +342,7 @@ export default {
</base-header>
<component
:is="modes ? modes[cMode] : null || 'div'"
:key="eventReloadKey"
ref="mode"
v-model:current-date="cDate"
:range-end="pickedRangeEnd"
@@ -360,4 +365,4 @@ export default {
</bs-modal>
</div>
`
}
}
+2 -2
View File
@@ -558,7 +558,7 @@ export default {
>
<grid-line-event
v-for="(event, i) in events"
:key="i"
:key="event.orig?.eindeutige_kalender_gruppen_id ?? event.orig?.kalender_id ?? i"
:event="event"
>
<template v-slot="slot">
@@ -649,4 +649,4 @@ export default {
</div>
</div>
`
}
}
+21 -20
View File
@@ -1,5 +1,5 @@
import LineEvent from "./Line/Event.js";
import LineBackground from "./Line/Background.js";
import LineEvent from './Line/Event.js';
import LineBackground from './Line/Background.js';
/**
* TODO(chris):
@@ -7,19 +7,19 @@ import LineBackground from "./Line/Background.js";
*/
export default {
name: "GridLine",
name: 'GridLine',
components: {
LineEvent,
LineBackground,
},
inject: {
axisRow: "axisRow",
axisRow: 'axisRow',
shouldCompactEvents: {
from: "shouldCompactEvents",
from: 'shouldCompactEvents',
default: undefined,
},
compactibleEventTypes: {
from: "compactibleEventTypes",
from: 'compactibleEventTypes',
default: undefined,
},
},
@@ -50,19 +50,20 @@ export default {
let formattedEvents = this.events.map((event) => {
event.rows = [1, -1];
if (event.startsHere) {
event.rows[0] =
"t_" + event.start.diff(this.date).toMillis();
event.rows[0] = 't_' + event.start.diff(this.date).toMillis();
}
if (event.endsHere) {
event.rows[1] = "t_" + event.end.diff(this.date).toMillis();
event.rows[1] = 't_' + event.end.diff(this.date).toMillis();
}
return event;
});
if (this.shouldCompactEvents && this.compactibleEventTypes?.length) {
formattedEvents =
this.compactEvents(formattedEvents, this.compactibleEventTypes);
formattedEvents = this.compactEvents(
formattedEvents,
this.compactibleEventTypes,
);
}
return formattedEvents;
@@ -71,12 +72,9 @@ export default {
methods: {
compactEvents(events, compactibleEventTypes) {
let formattedEvents = events
.filter(
(event) =>
!compactibleEventTypes.includes(event.type),
)
.filter((event) => !compactibleEventTypes.includes(event.type))
.map((event) => {
event.display = "default";
event.display = 'default';
return event;
});
let eventsToBeCompacted = events.filter((event) =>
@@ -110,17 +108,17 @@ export default {
compactedEvents.forEach((compactedEvent) => {
if (compactedEvent.events.length < 4) {
formattedEvents.push({
display: "compacted",
display: 'compacted',
...compactedEvent,
});
} else {
formattedEvents.push({
display: "compacted",
display: 'compacted',
events: compactedEvent.events.slice(0, 3),
rows: compactedEvent.rows,
});
formattedEvents.push({
display: "compactedExtra",
display: 'compactedExtra',
events: compactedEvent.events.slice(3),
rows: compactedEvent.rows,
});
@@ -142,7 +140,10 @@ export default {
:end="end"
:background="bg"
></line-background>
<template v-for="(event, i) in formattedEvents" :key="i">
<template
v-for="(event, i) in formattedEvents"
:key="event.orig?.eindeutige_kalender_gruppen_id ?? event.orig?.kalender_id ?? i"
>
<line-event
v-if="!event.display || event.display === 'default'"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
@@ -3,72 +3,74 @@ import drop from '../../../../../directives/drop.js';
import CalClick from '../../../../../directives/Calendar/Click.js';
export default {
name: "GridLineEvent",
name: 'GridLineEvent',
directives: {
draggable,
drop,
CalClick
CalClick,
},
emits: [
'resize-start'
],
emits: ['resize-start'],
data() {
return {
contextMenu: {
show: false,
x: 0,
y: 0
}
y: 0,
},
};
},
inject: {
draggableEvents: "draggableEvents",
draggableEvents: 'draggableEvents',
resizableEvents: {
from: "resizableEvents",
default: () => () => false
from: 'resizableEvents',
default: () => () => false,
},
mode: "mode",
mode: 'mode',
contextMenuActions: {
from: "contextMenuActions",
default: () => ({})
from: 'contextMenuActions',
default: () => ({}),
},
onDrop: {
from: "onDrop",
default: () => null
from: 'onDrop',
default: () => null,
},
onDropEvent: {
from: "onDropEvent",
default: () => () => {}
}
from: 'onDropEvent',
default: () => () => {},
},
},
props: {
event: {
type: Object,
required: true,
validator(value) {
return (value.start && value.end && value.orig);
}
}
return value.start && value.end && value.orig;
},
},
},
computed: {
isHeaderOrFooter() {
return ['header', 'footer'].includes(this.event.orig);
},
draggable() {
return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode);
return (
!this.isHeaderOrFooter &&
this.draggableEvents(this.event.orig, this.mode)
);
},
resizable() {
return !this.isHeaderOrFooter && this.resizableEvents(this.event.orig, this.mode);
return (
!this.isHeaderOrFooter &&
this.resizableEvents(this.event.orig, this.mode)
);
},
classes() {
const classes = [];
if (this.isHeaderOrFooter) {
classes.push('event-' + this.event.orig);
} else {
if (this.event.startsHere)
classes.push('event-begin');
if (this.event.endsHere)
classes.push('event-end');
if (this.event.startsHere) classes.push('event-begin');
if (this.event.endsHere) classes.push('event-end');
}
classes.push(`calender_id-${this.event.orig.kalender_id}`);
@@ -85,9 +87,14 @@ export default {
activeContextActions() {
if (this.isHeaderOrFooter) return [];
const type = this.event.orig?.type ?? 'lehreinheit';
const actions = this.contextMenuActions[type] ?? this.contextMenuActions['default'] ?? [];
return actions.filter(action => !action.visible || action.visible(this.event.orig));
}
const actions =
this.contextMenuActions[type] ??
this.contextMenuActions['default'] ??
[];
return actions.filter(
(action) => !action.visible || action.visible(this.event.orig),
);
},
},
methods: {
onResizeStart(edge, evt) {
@@ -95,7 +102,7 @@ export default {
edge,
evt,
el: this.$refs.eventEl,
event: this.event
event: this.event,
});
},
onRightClick(evt) {
@@ -116,27 +123,24 @@ export default {
evt.dataTransfer.setData('fhc-grab-offset-x', evt.clientX - rect.left);
},
onDropOnCard(evt, items) {
if (this.isHeaderOrFooter || !this.onDrop)
return;
if (this.isHeaderOrFooter || !this.onDrop) return;
const list = Array.isArray(items) ? items : [items];
const obj = list[0];
if (!obj)
return;
if (!obj) return;
if ((evt.ctrlKey || evt.metaKey) && obj.type === 'lehreinheit')
{
if ((evt.ctrlKey || evt.metaKey) && obj.type === 'lehreinheit') {
return this.onDrop({
item: [obj],
ctrlKey: true,
targetKalenderId: this.event.orig?.kalender_id ?? null
targetKalenderId: this.event.orig?.kalender_id ?? null,
});
}
return this.onDropEvent(evt, items, this.event.start.startOf('day'));
},
},
template:`
template: `
<div
class="fhc-calendar-base-grid-line-event event"
:class="classes"
@@ -195,5 +199,5 @@ export default {
</ul>
</teleport>
</div>
`
}
`,
};
+3 -12
View File
@@ -27,18 +27,9 @@ export default {
},
computed: {
range() {
let first = this.focusDate.startOf('month').startOf('week', { useLocaleWeeks: true });
let last = first.plus({ days: 41 }).endOf('day'); // NOTE(chris): 6 weeks minus 1 day
if (this.rangeOffset != 0) {
const nextFocusDate = this.focusDate.plus({ months: this.rangeOffset});
const nextRangeStart = nextFocusDate.startOf('month').startOf('week', { useLocaleWeeks: true });
if (this.rangeOffset < 0) {
first = nextRangeStart;
} else {
last = nextRangeStart.plus({ days: 41 }).endOf('day');
}
}
const targetDate = this.focusDate.plus({ months: this.rangeOffset });
const first = targetDate.startOf('month').startOf('week', { useLocaleWeeks: true });
const last = first.plus({ days: 41 }).endOf('day'); // NOTE(chris): 6 weeks minus 1 day
return luxon.Interval.fromDateTimes(first, last);
}
@@ -315,8 +315,8 @@ export default {
if (e.target.closest('a, input'))
return;
let rowIndex = row.getData().row_index;
let eventData = this.events.find(ev => ev.id === rowIndex)?.orig ?? row.getData();
let rowData = row.getData();
let eventData = this.events.find(ev => ev.orig?.eindeutige_kalender_gruppen_id === rowData.eindeutige_kalender_gruppen_id)?.orig ?? rowData;
let event = new CustomEvent('cal-click', {
cancelable: true,
+4 -11
View File
@@ -27,16 +27,9 @@ export default {
},
computed: {
range() {
let first = this.focusDate.startOf('week', { useLocaleWeeks: true });
let last = this.focusDate.endOf('week', { useLocaleWeeks: true });
if (this.rangeOffset != 0) {
if (this.rangeOffset < 0) {
first = first.plus({ weeks: this.rangeOffset });
} else {
last = last.plus({ weeks: this.rangeOffset });
}
}
const targetDate = this.focusDate.plus({ weeks: this.rangeOffset });
const first = targetDate.startOf('week', { useLocaleWeeks: true });
const last = targetDate.endOf('week', { useLocaleWeeks: true });
return luxon.Interval.fromDateTimes(first, last);
}
@@ -99,7 +92,7 @@ export default {
this.$emit('update:range', this.range);
this.$refs.view.$refs.grid.enableAutoScroll();
},
template: `
template: /* html */`
<div
class="fhc-calendar-mode-week flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
+112 -73
View File
@@ -1,6 +1,6 @@
import FhcCalendar from "./Base.js";
import FhcCalendar from './Base.js';
import { useEventLoader } from '../../composables/EventLoader.js';
import { useEventLoader } from '../../composables/Tempus/TempusEventLoader.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
@@ -8,11 +8,10 @@ import ModeTable from './Mode/Table.js';
import ApiKalender from '../../api/factory/tempus/kalender.js';
import draggable from '../../directives/draggable.js';
export default {
name: "CalendarTempus",
name: 'CalendarTempus',
components: {
FhcCalendar
FhcCalendar,
},
inject: {
renderers: {from: 'renderers'},
@@ -23,9 +22,9 @@ export default {
appConfig: {
from: 'appConfig',
default: {
visible_status: 'all'
}
}
visible_status: 'all',
},
},
},
directives: {
draggable,
@@ -39,35 +38,39 @@ export default {
props: {
timezone: {
type: String,
required: true
required: true,
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local()
default: luxon.DateTime.local(),
},
mode: {
type: String,
default: 'Week'
default: 'Week',
},
getPromiseFunc: {
type: Function,
required: true
required: true,
},
cacheMultiplier: {
type: Number,
default: 1
},
parkedEvents: {
type: Object,
default: () => new Set()
default: () => new Set(),
},
visibleLecturers: {
type: Array,
default: null
default: null,
},
extraBackgrounds: {
type: Array,
default: () => []
default: () => [],
},
visibleStatus: {
type: Array,
default: () => ['all']
default: () => ['all'],
},
showEvents: {
type: Boolean,
@@ -83,11 +86,14 @@ export default {
"resize",
"event-hover",
"event-unhover",
"open-reservierung"
"open-reservierung",
"events-reloaded",
],
data() {
return {
eventReloadKey: 0,
refreshEventsAfterReload: false,
modes: {
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth),
@@ -96,11 +102,11 @@ export default {
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound'))
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')),
},
week: {
collapseEmptyDays: false
}
collapseEmptyDays: false,
},
},
currentMode: this.mode,
teachingunits: null,
@@ -108,57 +114,67 @@ export default {
showRaster: true,
};
},
watch: {
events() {
if (this.refreshEventsAfterReload) {
this.eventReloadKey += 1;
this.refreshEventsAfterReload = false;
}
this.$emit('events-reloaded');
}
},
computed: {
backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone);
let past = [];
if (this.mode == 'Month')
{
past = [{
class: 'background-past',
end: now.startOf('day')
}];
}
else
{
past = [{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}];
if (this.mode == 'Month') {
past = [
{
class: 'background-past',
end: now.startOf('day'),
},
];
} else {
past = [
{
class: 'background-past',
end: now,
label: now
.startOf('minute')
.toISOTime({ suppressSeconds: true, includeOffset: false }),
},
];
}
return [
...past,
...(this.extraBackgrounds || [])
];
return [...past, ...(this.extraBackgrounds || [])];
},
visibleEvents()
{
visibleEvents() {
let list = this.events;
if (Array.isArray(this.visibleLecturers))
{
if (Array.isArray(this.visibleLecturers)) {
const visibleLectures = new Set(this.visibleLecturers);
list = list.filter(event => {
if (!event.lektor?.length)
return true;
return event.lektor.some(lektor => visibleLectures.has(lektor.mitarbeiter_uid));
list = list.filter((event) => {
if (!event.lektor?.length) return true;
return event.lektor.some((lektor) =>
visibleLectures.has(lektor.mitarbeiter_uid),
);
});
}
if (!this.visibleStatus.length || this.visibleStatus.includes('all'))
return list;
return list.filter(event => this.visibleStatus.includes(event.status_kurzbz));
return list.filter((event) =>
this.visibleStatus.includes(event.status_kurzbz),
);
},
},
methods: {
eventStyle(event) {
if (!event.farbe)
return undefined;
if (!event.farbe) return undefined;
return '--event-bg:#' + event.farbe;
},
updateRange(rangeInterval) {
@@ -177,11 +193,15 @@ export default {
ondrop(payload){
this.$emit('drop', payload);
},
onresize(payload){
onresize(payload) {
this.$emit('resize', payload);
},
resetEventLoader() {
this.reset();
resetEventLoader(arePreviousEventsCleared = true, isLoaderEventVisualReset = true) {
this.reset(arePreviousEventsCleared, isLoaderEventVisualReset);
},
reloadEvents() {
this.refreshEventsAfterReload = true;
this.resetEventLoader(false);
},
navigatePrev() {
this.$refs.calendar.clickPrev();
@@ -189,13 +209,35 @@ export default {
navigateNext() {
this.$refs.calendar.clickNext();
},
clearOutCalendarEventEmphasis() {
this.$refs.calendar.$el
.querySelectorAll(
'.fhc-calendar-base-grid .fhc-calendar-base-grid-line-event',
)
.forEach((el) => {
const spinner = el.querySelector('.spinner-overlay');
if (spinner) {
spinner.remove();
}
el.classList.remove(
'updating-event',
'updated-event',
'updated-event-long',
);
});
},
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc);
const { events, lv, reset } = useEventLoader(
rangeInterval,
props.getPromiseFunc,
() => props.cacheMultiplier
);
Vue.watch(lv, newValue => {
Vue.watch(lv, (newValue) => {
context.emit('update:lv', newValue);
});
@@ -203,31 +245,27 @@ export default {
rangeInterval,
events,
lv,
reset
reset,
};
},
created() {
this.$api
.call(ApiKalender.getStunden())
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
start: el.beginn,
end: el.ende
}));
});
this.$api.call(ApiKalender.getStunden()).then((res) => {
return (this.teachingunits = res.data.map((el) => ({
id: el.stunde,
start: el.beginn,
end: el.ende,
})));
});
this.$api
.call(ApiKalender.getCalendarHours())
.then(res => {
this.hoursplan = {
start: res.data.start,
end: res.data.end
};
});
this.$api.call(ApiKalender.getCalendarHours()).then((res) => {
this.hoursplan = {
start: res.data.start,
end: res.data.end,
};
});
},
template: /* html */`
template: /* html */ `
<fhc-calendar
ref="calendar"
class="fhc-calendar-lvplan"
@@ -238,6 +276,7 @@ export default {
:timezone="timezone"
:locale="$p.user_locale.value"
:events="visibleEvents || []"
:event-reload-key="eventReloadKey"
:backgrounds="backgrounds"
:time-grid="showRaster ? teachingunits : null"
:hours-plan="hoursplan"
@@ -0,0 +1,59 @@
export default {
name: 'EventKollisionRow',
props: {
event: {
type: Object,
required: true,
}
},
inject: {
modalActions: {
from: 'modalActions',
default: () => ({})
},
},
computed: {
collisions() {
return Array.isArray(this.event.collisions) ? this.event.collisions : [];
},
},
methods: {
isJumpable(collision) {
return !!collision.datum;
},
jumpTo(collision) {
if (!this.isJumpable(collision))
return;
const modalEl = this.$el?.closest?.('.modal');
if (modalEl)
bootstrap.Modal.getInstance(modalEl)?.hide();
this.modalActions?.goToEvent({
kalender_id: collision.kalender_id ?? null,
datum: collision.datum,
});
}
},
template: /*html*/`
<tr v-if="collisions.length">
<th>
<i class="fa-solid fa-triangle-exclamation text-danger"></i>
{{ $p.t('ui','collision') }}:
</th>
<td>
<div v-for="(collision, index) in collisions" :key="collision.kalender_id ?? index" class="d-block">
<a
v-if="isJumpable(collision)"
href="#"
@click.prevent="jumpTo(collision)"
>
{{ collision.message }}
</a>
<template v-else>
{{ collision.message }}
</template>
</div>
</td>
</tr>`
};
@@ -0,0 +1,79 @@
import RaumauswahlModal from '../RaumauswahlModal.js';
export default {
name: 'RaumRow',
components: {
RaumauswahlModal
},
props: {
event: {
type: Object,
required: true,
}
},
inject: {
reload: {
from: 'reload',
default: () => () => {}
},
modalActions: {
from: 'modalActions',
default: () => ({})
},
},
computed: {
orte() {
const ort = this.event.ort_kurzbz;
if (Array.isArray(ort))
return ort;
return ort ? [ort] : [];
},
ortLinks()
{
let base_url = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
let ortLinks = {};
this.event.ort_infos.forEach(ort => {
if (ort.ort_content_id)
ortLinks[ort.ort_kurzbz] = `${base_url}/CisVue/Cms/content/${ort.ort_content_id}`;
})
return ortLinks;
},
},
methods: {
openRaumvorschlag() {
this.$refs.raumModal.show(this.event);
},
addToRoomFilter(ort) {
this.modalActions?.addToFilter({ ort_kurzbz: ort }, 'ort');
}
},
template: /*html*/`
<tr>
<th>
{{ $p.t('global','raum') + ':' }}
<i
class="fa-solid fa-refresh"
role="button"
:title="$p.t('global','raum')"
@click="openRaumvorschlag"
></i>
</th>
<td>
<div v-for="ort in orte" :key="ort" class="d-block">
<a v-if="ortLinks[ort]" target='_blank' :aria-label="$p.t('lvplan','rauminformationenAnzeigen')" :title="$p.t('lvplan','rauminformationenAnzeigen')" :href="ortLinks[ort]"><i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true"></i></a>
<i
class="fa fa-filter"
role="button"
@click="addToRoomFilter(ort)"
></i>
{{ ort }}
</div>
<div>
{{event.location}}
</div>
<raumauswahl-modal ref="raumModal" @saved="reload()" />
</td>
</tr>`
};
+105
View File
@@ -0,0 +1,105 @@
import { parseJsonField } from '../../../helpers/Tempus/modalHelper.js';
import ApiTempusTag from "../../../api/factory/tempus/tag.js";
import CoreTag from '../../../components/Tag/Tag.js';
import { idTagFormatter } from "../../Tag/tagFormatter.js";
export default {
name: 'TagRow',
components: {
CoreTag
},
props: {
event: {
type: Object,
required: true,
}
},
data() {
return {
tagEndpoint: ApiTempusTag
};
},
computed: {
tagValues() {
return this.event.eindeutige_kalender_gruppen_id ? [this.event.eindeutige_kalender_gruppen_id] : [];
},
tags() {
return parseJsonField(this.event.tags);
},
},
watch: {
tags: {
handler() {
this.$nextTick(() => this.renderTags());
},
deep: true
}
},
methods: {
async fetchAssignedTagsByCalender(calendarGroupId) {
let result = await this.$api.call(
ApiTempusTag.getTagsByCalendar(calendarGroupId),
);
if (result.meta.status === "success")
return result.data.filter(tag => !!tag);
this.$fhcAlert.alertError(
this.$p.t("ui", "failed_assigned_tags_fetch_error_message"),
);
return [];
},
async onTagsChanged() {
let id = this.event.eindeutige_kalender_gruppen_id;
if (!id) return;
this.event.tags = await this.fetchAssignedTagsByCalender(id);
},
renderTags() {
let el = this.$refs.tagWrapper;
if (!el) return;
const tagData = this.tags.map(t => ({
...t,
notiz_id: t.id,
bezeichnung: t.beschreibung,
}));
el.replaceChildren(
idTagFormatter(
this.event.eindeutige_kalender_gruppen_id,
tagData,
this.$refs.tagComponent,
'eindeutige_kalender_gruppen_id'
) ?? ''
);
},
},
mounted() {
this.renderTags();
},
template: /*html*/`
<tr>
<th>
<div class="d-flex align-items-center">
{{ $p.t('ui','tags') }}:
<div class="fw-normal">
<core-tag
v-if="tagValues.length"
ref="tagComponent"
:endpoint="tagEndpoint"
:values="tagValues"
zuordnung_typ="eindeutige_kalender_gruppen_id"
show-hover
@added="onTagsChanged"
@deleted="onTagsChanged"
@updated="onTagsChanged"
></core-tag>
</div>
</div>
</th>
<td class="tag-row-cell">
<div ref="tagWrapper" class="tag-row-wrapper"></div>
</td>
</tr>`
};
@@ -0,0 +1,219 @@
import FormInput from '../../Form/Input.js';
import ApiReservierung from '../../../api/factory/tempus/reservierung.js';
export default {
name: 'TeilnehmerRow',
components: {
FormInput
},
props: {
event: {
type: Object,
default: null,
}
},
data() {
return {
teilnehmer: [],
gruppen: [],
rollen: [],
gruppen_rollen: [],
studiensemester: [],
filteredUsers: [],
filteredGroups: [],
userAbortController: null,
groupAbortController: null,
};
},
created() {
this.$api.call(ApiReservierung.getInformation())
.then(result => {
this.rollen = result.data.rollen ?? [];
this.gruppen_rollen = result.data.gruppen_rollen ?? [];
this.studiensemester = result.data.studiensemester ?? [];
});
if (this.event)
this.fillFromEvent();
else
this.reset();
},
methods: {
reset() {
this.teilnehmer = [{ uid: null, label: '', rolle: null }];
this.gruppen = [{ gid: null, label: '', gruppe_kurzbz: null, lehrverband: null, studiensemester_kurzbz: null, rolle: null }];
},
fillFromEvent() {
let organisierende = this.event.lektor.map(lektor => ({
uid: lektor.mitarbeiter_uid,
label: lektor.vorname + ' ' + lektor.nachname,
rolle: 'organisator'
}));
let teilnehmende = this.event.teilnehmer_person.map(person => ({
uid: person.uid,
label: person.vorname + ' ' + person.nachname,
rolle: 'teilnehmer'
}));
this.teilnehmer = organisierende.concat(teilnehmende);
this.gruppen = this.event.teilnehmer_gruppe.map(gruppe => ({ ...gruppe, label: gruppe.gruppe_kurzbz }));
},
save() {
let teilnehmer = this.teilnehmer
.filter(t => t.uid && t.rolle)
.map(t => ({ uid: t.uid, rolle: t.rolle }));
let gruppen = this.gruppen
.filter(g => g.gid && g.rolle && g.studiensemester_kurzbz)
.map(g => ({ gid: g.gid, lehrverband: g.lehrverband, gruppe_kurzbz: g.gruppe_kurzbz, rolle: g.rolle, studiensemester_kurzbz: g.studiensemester_kurzbz }));
return this.$api.call(ApiReservierung.updateTeilnehmer(this.event.kalender_id, teilnehmer, gruppen));
},
searchUser(event) {
const query = event.query.trim();
if (query.length < 2) {
this.filteredUsers = [];
return;
}
this.userAbortController?.abort();
this.userAbortController = new AbortController();
this.$api.call(ApiReservierung.searchTeilnehmer(query), { signal: this.userAbortController.signal })
.then(result => {
this.filteredUsers = result.data.map(u => ({
label: `${u.nachname} ${u.vorname} (${u.uid})`,
uid: u.uid
}));
})
.catch(this.$fhcAlert.handleSystemError);
},
searchGroup(event) {
const query = event.query.trim();
if (query.length < 2) {
this.filteredGroups = [];
return;
}
this.groupAbortController?.abort();
this.groupAbortController = new AbortController();
this.$api.call(ApiReservierung.searchGroup(query), { signal: this.groupAbortController.signal })
.then(result => {
this.filteredGroups = result.data.map(gruppe => ({
label: gruppe.bezeichnung
? `${gruppe.gruppe_kurzbz.trim()} (${gruppe.bezeichnung})`
: gruppe.gruppe_kurzbz.trim(),
gid: gruppe.gid,
gruppe_kurzbz: gruppe.gruppe_kurzbz.trim(),
lehrverband: gruppe.lehrverband,
}));
})
.catch(this.$fhcAlert.handleSystemError);
},
selectUser(event, nehmer) {
nehmer.uid = event.value.uid;
nehmer.label = event.value.label;
},
selectGroup(event, group) {
group.gid = event.value.gid;
group.label = event.value.label;
group.gruppe_kurzbz = event.value.gruppe_kurzbz;
group.lehrverband = event.value.lehrverband;
},
addTeilnehmer() {
this.teilnehmer.push({ uid: null, label: '', rolle: null });
},
removeTeilnehmer(index) {
this.teilnehmer.splice(index, 1);
},
addGruppe() {
this.gruppen.push({ gid: null, label: '', gruppe_kurzbz: null, lehrverband: null, studiensemester_kurzbz: null, rolle: null });
},
removeGruppe(index) {
this.gruppen.splice(index, 1);
},
},
template: /*html*/`
<div>
<div class="mb-3">
<div v-for="(nehmer, i) in teilnehmer" :key="i" class="d-flex gap-2 mb-2 align-items-end">
<form-input
type="autocomplete"
:label="$p.t('ui', 'teilnehmende')"
:suggestions="filteredUsers"
v-model="nehmer.label"
field="label"
container-class="flex-grow-1"
:name="'user_' + i"
@complete="searchUser"
@item-select="selectUser($event, nehmer)"
></form-input>
<form-input
type="select"
:label="$p.t('lehre', 'status_rolle')"
v-model="nehmer.rolle"
:name="'rolle_' + i"
>
<option v-for="rolle in rollen" :value="rolle.rolle_kurzbz" :key="rolle.rolle_kurzbz">
{{ rolle.bezeichnung }}
</option>
</form-input>
<button type="button" class="btn btn-outline-danger" @click="removeTeilnehmer(i)">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" @click="addTeilnehmer">
<i class="fa-solid fa-plus me-1"></i>{{ $capitalize($p.t('global', 'hinzufuegen')) }}
</button>
</div>
<div>
<div v-for="(group, i) in gruppen" :key="i" class="d-flex gap-3 mb-3 align-items-end">
<form-input
type="autocomplete"
:label="$p.t('lehre', 'gruppe')"
:suggestions="filteredGroups"
v-model="group.label"
field="label"
container-class="flex-grow-1"
:name="'group_' + i"
@complete="searchGroup"
@item-select="selectGroup($event, group)"
></form-input>
<form-input
type="select"
:label="$p.t('lehre', 'studiensemester')"
v-model="group.studiensemester_kurzbz"
:name="'studiensemester_' + i"
>
<option v-for="sem in studiensemester" :value="sem.studiensemester_kurzbz" :key="sem.studiensemester_kurzbz">
{{ sem.studiensemester_kurzbz }}
</option>
</form-input>
<form-input
type="select"
:label="$p.t('lehre', 'status_rolle')"
v-model="group.rolle"
:name="'gruppe_rolle_' + i"
>
<option v-for="rolle in gruppen_rollen" :value="rolle.rolle_kurzbz" :key="rolle.rolle_kurzbz">
{{ rolle.bezeichnung }}
</option>
</form-input>
<button type="button" class="btn btn-outline-danger" @click="removeGruppe(i)">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" @click="addGruppe">
<i class="fa-solid fa-plus me-1"></i>{{ $p.t('lehre', 'gruppe') }} {{ $p.t('global', 'hinzufuegen') }}
</button>
</div>
</div>`
};
@@ -0,0 +1,156 @@
import FormInput from "../../Form/Input.js";
import { normalizeTime, normalizeDate } from "../../../helpers/Tempus/modalHelper.js";
export default {
name: 'ZeitRow',
components: {
FormInput
},
props: {
event: {
type: Object,
required: true,
}
},
inject: {
showRaster: {
from: 'showRaster',
default: false
},
teachingunits: {
from: 'teachingunits',
default: () => []
},
modalActions: {
from: 'modalActions',
default: () => ({})
},
},
data() {
const beginn = normalizeTime(this.event.beginn);
const ende = normalizeTime(this.event.ende);
return {
editDatum: normalizeDate(this.event.datum),
editBeginn: beginn,
editEnde: ende,
editStundeVon: this.teachingunits.find(unit => normalizeTime(unit.start) === beginn)?.id ?? null,
editStundeBis: this.teachingunits.find(unit => normalizeTime(unit.end) === ende)?.id ?? null,
};
},
computed: {
teachingunitsBis() {
if (this.editStundeVon === null)
return this.teachingunits;
return this.teachingunits.filter(unit => unit.id >= this.editStundeVon);
}
},
watch: {
editStundeVon(newVal) {
if (this.editStundeBis === null || this.editStundeBis < newVal)
this.editStundeBis = newVal;
}
},
methods: {
getTimeRange() {
if (!this.editDatum) {
this.$refs.editDatumInput.setFeedback(
false,
this.$p.t('ui', 'error_fieldRequired', { field: this.$p.t('global', 'datum') })
);
return null;
}
if (this.showRaster && this.editStundeVon !== null && this.editStundeBis !== null) {
const vonUnit = this.teachingunits.find(unit => unit.id === this.editStundeVon);
const bisUnit = this.teachingunits.find(unit => unit.id === this.editStundeBis);
return {
start_time: this.editDatum + ' ' + vonUnit.start,
end_time: this.editDatum + ' ' + bisUnit.end,
};
}
if (!this.editBeginn || !this.editEnde || this.editBeginn > this.editEnde)
return null;
return {
start_time: this.editDatum + ' ' + this.editBeginn,
end_time: this.editDatum + ' ' + this.editEnde,
};
},
save() {
const range = this.getTimeRange();
if (!range)
return;
this.modalActions.saveEventTime({
kalender_id: this.event.kalender_id,
...range
});
}
},
template: /*html*/`
<tr>
<th>{{ $p.t('global','datum') + ':' }}</th>
<td>
<form-input
ref="editDatumInput"
type="DatePicker"
v-model="editDatum"
model-type="yyyy-MM-dd"
name="editDatum"
:enable-time-picker="false"
text-input
auto-apply
format="dd.MM.yyyy"
class="form-control form-control-sm d-inline-block w-auto"
></form-input>
</td>
</tr>
<tr>
<th>{{ $p.t('ui','zeitraum') + ':' }}</th>
<td>
<template v-if="showRaster">
<select v-model="editStundeVon" class="form-select form-select-sm w-auto d-inline-block">
<option v-for="unit in teachingunits" :key="unit.id" :value="unit.id">
{{ unit.id }}. Stunde ({{ unit.start }}–{{ unit.end }})
</option>
</select>
–
<select v-model="editStundeBis" class="form-select form-select-sm w-auto d-inline-block">
<option v-for="unit in teachingunitsBis" :key="unit.id" :value="unit.id">
{{ unit.id }}. Stunde ({{ unit.start }}–{{ unit.end }})
</option>
</select>
</template>
<template v-else>
<div class="timepicker-row">
<form-input
type="DatePicker"
v-model="editBeginn"
time-picker
model-type="HH:mm"
name="editBeginn"
text-input
auto-apply
format="HH:mm"
class="form-control form-control-sm"
/>
<span>–</span>
<form-input
type="DatePicker"
v-model="editEnde"
time-picker
model-type="HH:mm"
name="editEnde"
text-input
auto-apply
format="HH:mm"
class="form-control form-control-sm"
/>
</div>
</template>
</td>
</tr>`
};
@@ -86,7 +86,8 @@ export default {
this.pendingEvent.lehreinheit_id,
this.selectedRooms.map((room) => room.ort_kurzbz),
this.pendingEvent.start_time,
this.pendingEvent.end_time
this.pendingEvent.end_time,
false
)
).then((result) => result.data)
@@ -1,15 +1,20 @@
import ApiKalender from '../../../../api/factory/tempus/kalender.js';
import FormInput from "../../../Form/Input.js";
import RaumauswahlModal from '../../RaumauswahlModal.js';
import CoreTag from '../../../../components/Tag/Tag.js';
import ApiTempusTag from "../../../../api/factory/tempus/tag.js";
import { idTagFormatter } from "../../../Tag/tagFormatter.js";
import { parseJsonField, buildLektorenLinks } from '../../../../helpers/Tempus/modalHelper.js';
import ZeitRow from '../../Modal/ZeitRow.js'
import RaumRow from '../../Modal/RaumRow.js'
import KollisionRow from '../../Modal/KollisionRow.js'
import TagRow from '../../Modal/TagRow.js'
export default {
components:{
FormInput,
RaumauswahlModal,
CoreTag
ZeitRow,
RaumRow,
KollisionRow,
TagRow
},
props:{
event: {
@@ -18,17 +23,6 @@ export default {
}
},
inject: {
showRaster: {
from: 'showRaster', default: false
},
teachingunits : {
from: 'teachingunits',
default: () => []
},
hoursPlan : {
from: 'hoursPlan',
default: null
},
reload: {
from: 'reload',
default: () => {}
@@ -40,42 +34,21 @@ export default {
data() {
return {
lvMenu: [],
editDatum: this.normalizeDate(this.event.datum),
editBeginn: this.normalizeTime(this.event.beginn),
editEnde: this.normalizeTime(this.event.ende),
editStundeVon: this.teachingunits.find(unit => this.normalizeTime(unit.start) === this.normalizeTime(this.event.beginn))?.id ?? null,
editStundeBis: this.teachingunits.find(unit => this.normalizeTime(unit.end) === this.normalizeTime(this.event.ende))?.id ?? null,
anzahl: '',
anzahlSchwund: '',
mailto: '',
zeitwunschMap: {},
tagEndpoint: ApiTempusTag,
};
},
watch: {
editStundeVon(newVal) {
if (this.editStundeBis !== null && this.editStundeBis < newVal)
this.editStundeBis = newVal;
},
tags: {
handler() {
this.renderTags();
},
deep: true
}
},
computed: {
tagValues() {
return this.event.eindeutige_kalender_gruppen_id ? [this.event.eindeutige_kalender_gruppen_id] : [];
lektorenLinks() {
return buildLektorenLinks(this.event.lektor);
},
lektorenLinks: function () {
if (!this.event || !Array.isArray(this.event.lektor) || !this.event.lektor.length) return "a";
let lektorenLinks = {};
this.event.lektor.forEach((lektor) => {
lektorenLinks[lektor.kurzbz] = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/Cis/Profil/View/${lektor.mitarbeiter_uid}`;
})
return lektorenLinks;
resourcesValue() {
const resources = parseJsonField(this.event.resources, null);
return resources ? resources.map(r => r.beschreibung).join(', ') : null;
},
lvLinks()
{
@@ -103,31 +76,6 @@ export default {
})
return leLinks;
},
tags() {
if (typeof this.event.tags === 'string') {
try {
return JSON.parse(this.event.tags);
} catch (e) {
console.error('Failed to parse tags:', e);
return [];
}
}
return this.event.tags || [];
},
resourcesValue() {
let resources = this.event.resources;
if (typeof this.event.resources === 'string') {
try {
resources = JSON.parse(this.event.resources);
} catch (e) {
console.error('Failed to parse resources:', e);
return null;
}
}
return resources.map(resource => resource.beschreibung).join(', ');
},
},
methods: {
formatDate(d){
@@ -135,110 +83,12 @@ export default {
return '';
return luxon.DateTime.fromSQL(d).toFormat('dd.MM.yyyy HH:mm');
},
normalizeTime(value) {
if (!value)
return null;
if (value instanceof Date)
return luxon.DateTime.fromJSDate(value).toFormat('HH:mm');
return luxon.DateTime.fromFormat(value, 'HH:mm:ss').isValid
? luxon.DateTime.fromFormat(value, 'HH:mm:ss').toFormat('HH:mm')
: value;
},
normalizeDate(value) {
if (!value)
return null;
if (value instanceof Date)
return luxon.DateTime.fromJSDate(value).toFormat('yyyy-MM-dd');
return value.slice(0, 10);
},
onSave()
{
if (!this.editDatum)
{
this.$refs.editDatumInput.setFeedback(false, this.$p.t('ui','error_fieldRequired', { field: this.$p.t('global','datum') }));
return;
}
if (this.editBeginn > this.editEnde)
{
return;
}
let start_time;
let end_time;
if (this.showRaster && this.editStundeVon !== null && this.editStundeBis !== null)
{
let vonUnit = this.teachingunits.find(unit => unit.id === this.editStundeVon);
let bisUnit = this.teachingunits.find(unit => unit.id === this.editStundeBis);
start_time = this.editDatum + ' ' + vonUnit.start;
end_time = this.editDatum + ' ' + bisUnit.end;
}
else
{
start_time = this.editDatum + ' ' + this.editBeginn;
end_time = this.editDatum + ' ' + this.editEnde;
}
this.modalActions.saveEventTime({kalender_id: this.event.kalender_id, start_time, end_time});
},
async fetchAssignedTagsByCalender(calendarGroupId) {
let result = await this.$api.call(
ApiTempusTag.getTagsByCalendar(calendarGroupId),
);
if (result.meta.status === "success")
return result.data.filter(tag => !!tag);
this.$fhcAlert.alertError(
this.$p.t("ui", "failed_assigned_tags_fetch_error_message"),
);
return [];
},
async onTagsChanged() {
let id = this.event.eindeutige_kalender_gruppen_id;
if (!id) return;
this.event.tags = await this.fetchAssignedTagsByCalender(id);
},
editTag(tag) {
this.$refs.tagComponent?.editTag(tag.id);
},
openRaumvorschlag()
{
this.$refs.raumModal.show(this.event);
},
addLecturerFilter(lektor) {
this.modalActions.addToFilter({
uid: lektor.mitarbeiter_uid,
name: `${lektor.vorname} ${lektor.nachname}`,
}, 'mitarbeiter');
},
addToRoomFilter(room) {
this.modalActions.addToFilter({
ort_kurzbz: room,
}, 'ort');
},
renderTags()
{
let el = this.$refs.tagWrapper;
if (!el) return;
const tagData = this.tags.map(t => ({
...t,
notiz_id: t.id,
bezeichnung: t.beschreibung,
}));
el.replaceChildren(
idTagFormatter(
this.event.eindeutige_kalender_gruppen_id,
tagData,
this.$refs.tagComponent,
'eindeutige_kalender_gruppen_id'
) ?? ''
);
}
},
created() {
if (this.event.type == 'lehreinheit') {
@@ -253,10 +103,6 @@ export default {
})
}
},
mounted()
{
this.renderTags();
},
template: /*html*/`
<div>
<h5>
@@ -264,98 +110,9 @@ export default {
</h5>
<table class="table table-hover mb-4">
<tbody>
<tr v-if="this.event.collisions.length > 0">
<th><i class="fa-solid fa-triangle-exclamation text-danger"></i> {{ $p.t('ui','collision') }}:</th>
<td>
<div v-for="collision in this.event.collisions" class="d-block">
{{collision.message}}
</div>
</td>
</tr>
<tr>
<th>{{$p.t('global','datum')+':'}}</th>
<td>
<form-input
ref="editDatumInput"
type="DatePicker"
v-model="editDatum"
model-type="yyyy-MM-dd"
name="editDatum"
:enable-time-picker="false"
text-input
auto-apply
format="dd.MM.yyyy"
class="form-control form-control-sm d-inline-block w-auto"
>
</form-input>
</td>
</tr>
<tr>
<th>{{$p.t('ui','zeitraum')+':'}}</th>
<td>
<template v-if="showRaster">
<select v-model="editStundeVon" class="form-select form-select-sm w-auto d-inline-block">
<option v-for="unit in teachingunits" :key="unit.id" :value="unit.id">
{{ unit.id }}. Stunde ({{ unit.start }}–{{ unit.end }})
</option>
</select>
–
<select v-model="editStundeBis" class="form-select form-select-sm w-auto d-inline-block">
<option
v-for="unit in teachingunits.filter(tunit => editStundeVon === null || tunit.id >= editStundeVon)"
:key="unit.id"
:value="unit.id"
>
{{ unit.id }}. Stunde ({{ unit.start }}–{{ unit.end }})
</option>
</select>
</template>
<template v-else>
<div class="timepicker-row">
<form-input
type="DatePicker"
v-model="editBeginn"
time-picker
model-type="HH:mm"
name="editBeginn"
text-input
auto-apply
format="HH:mm"
class="form-control form-control-sm"
/>
<span>–</span>
<form-input
type="DatePicker"
v-model="editEnde"
time-picker
model-type="HH:mm"
name="editEnde"
text-input
auto-apply
format="HH:mm"
class="form-control form-control-sm"
/>
</div>
</template>
</td>
</tr>
<tr>
<th>{{$p.t('global','raum')}}:
<i
class="fa-solid fa-refresh"
@click="openRaumvorschlag"
></i>
</th>
<td>
<div v-for="ort in event.ort_kurzbz" class="d-block">
<i class="fa fa-filter" @click="addToRoomFilter(ort)"></i> {{ort}}
</div>
</td>
</tr>
<kollision-row :event="event" />
<zeit-row ref="zeit" :event="event" />
<raum-row :event="event" />
<tr>
<th>{{$p.t('global','anzahl')}} {{$p.t('global','students')}}:
</th>
@@ -371,8 +128,8 @@ export default {
}}</th>
<td>
<div v-for="lehrveranstaltung in event.lehrveranstaltung_infos" class="d-block">
<a v-if="lvLinks.lvverwaltung[lehrveranstaltung.lehrveranstaltung_id]" target='_blank' :aria-label="$p.t('lehre','lehrveranstaltung')" :title="$p.t('lehre','lehrveranstaltung')" :href="lvLinks.lvverwaltung[lehrveranstaltung.lehrveranstaltung_id]"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C" aria-hidden="true"></i></a>
<a v-if="lvLinks.vilesci[lehrveranstaltung.lehrveranstaltung_id]" target='_blank' :aria-label="$p.t('lehre','lvInfoBearbeiten')" :title="$p.t('lehre','lehrveranstaltung')" :href="lvLinks.vilesci[lehrveranstaltung.lehrveranstaltung_id]"><i class="fa fa-pen-to-square me-1" style="color:#00649C" aria-hidden="true"></i></a>
<a v-if="lvLinks.lvverwaltung[lehrveranstaltung.lehrveranstaltung_id]" target='_blank' :aria-label="$p.t('lehre','lehrveranstaltung')" :title="$p.t('lehre','lehrveranstaltung')" :href="lvLinks.lvverwaltung[lehrveranstaltung.lehrveranstaltung_id]"><i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true"></i></a>
<a v-if="lvLinks.vilesci[lehrveranstaltung.lehrveranstaltung_id]" target='_blank' :aria-label="$p.t('lehre','lvInfoBearbeiten')" :title="$p.t('lehre','lehrveranstaltung')" :href="lvLinks.vilesci[lehrveranstaltung.lehrveranstaltung_id]"><i class="fa fa-pen-to-square me-1" aria-hidden="true"></i></a>
<span v-tooltip="lehrveranstaltung.oe_bezeichnung">{{'('+ lehrveranstaltung.lehrform_kurzbz + ') ' + lehrveranstaltung.lehrfach_bezeichnung }} </span>
</div>
</td>
@@ -385,7 +142,7 @@ export default {
}}</th>
<td>
<div v-for="lehreinheit in event.lehreinheit_infos" class="d-block">
<a v-if="leLinks[lehreinheit.lehreinheit_id]" target='_blank' :aria-label="$p.t('lehre','lehreinheit')" :title="$p.t('lehre','lehreinheit')" :href="leLinks[lehreinheit.lehreinheit_id]"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C" aria-hidden="true"></i></a>
<a v-if="leLinks[lehreinheit.lehreinheit_id]" target='_blank' :aria-label="$p.t('lehre','lehreinheit')" :title="$p.t('lehre','lehreinheit')" :href="leLinks[lehreinheit.lehreinheit_id]"><i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true"></i></a>
<span v-tooltip="lehreinheit.anmerkung">{{lehreinheit.lehrfach_bezeichnung}}</span>
</div>
</td>
@@ -399,7 +156,7 @@ export default {
<td>
<div id="lektorenContainer">
<div v-for="lektor in event.lektor" class="d-block">
<a v-if="lektorenLinks[lektor.kurzbz]" target='_blank' :aria-label="$p.t('lehre','lektor')" :title="$p.t('lehre','lektor')" :href="lektorenLinks[lektor.kurzbz]"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C" aria-hidden="true"></i></a>
<a v-if="lektorenLinks[lektor.kurzbz]" target='_blank' :aria-label="$p.t('lehre','lektor')" :title="$p.t('lehre','lektor')" :href="lektorenLinks[lektor.kurzbz]"><i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true"></i></a>
<i class="fa fa-filter" @click="addLecturerFilter(lektor)"></i>
{{lektor.vorname}} {{lektor.nachname}} ({{lektor.kurzbz}})
<small v-if="zeitwunschMap[lektor.mitarbeiter_uid]" class="text-muted">
@@ -415,29 +172,7 @@ export default {
{{ resourcesValue }}
</td>
</tr>
<tr>
<th>
<div class="d-flex align-items-center flex-wrap">
{{$p.t('ui','tags')}}:
<div class="fw-normal">
<core-tag
v-if="tagValues.length"
ref="tagComponent"
:endpoint="tagEndpoint"
:values="tagValues"
zuordnung_typ="eindeutige_kalender_gruppen_id"
show-hover
@added="onTagsChanged"
@deleted="onTagsChanged"
@updated="onTagsChanged"
></core-tag>
</div>
</div>
</th>
<td>
<div ref="tagWrapper"></div>
</td>
</tr>
<tag-row :event="event" />
<tr>
<th>{{$p.t('lehre','mail')}}: </th>
<td>
@@ -449,6 +184,6 @@ export default {
</tbody>
</table>
<raumauswahl-modal ref="raumModal" @saved="reload()"/>
<button type="button" class="btn btn-primary " @click.stop="onSave">{{$p.t('ui', 'speichern')}}</button>
<button type="button" class="btn btn-primary" @click.stop="$refs.zeit.save()">{{$p.t('ui', 'speichern')}}</button>
</div>`,
}
@@ -58,13 +58,13 @@ export default {
if (Array.isArray(this.event.teilnehmer_person) && this.event.teilnehmer_person.length > 0) {
if (this.event.teilnehmer_person.length > 3) {
tooltipArray.push([
this.$p.t('lehre/teilnehmer'),
this.$p.t('ui/teilnehmende'),
this.event.teilnehmer_person.slice(0, 3).map(person => `${person.vorname} ${person.nachname}`).join("\n")
+ "\n" + this.$p.t('lehre/weitereTeilnehmer', [this.event.teilnehmer_person.length - 3])
+ "\n" + this.$p.t('ui/weitereTeilnehmende', [this.event.teilnehmer_person.length - 3])
].join(": "));
} else {
tooltipArray.push([
this.$p.t('lehre/teilnehmer'),
this.$p.t('ui/teilnehmende'),
this.event.teilnehmer_person.map(person => `${person.vorname} ${person.nachname}`).join("\n")
].join(": "));
}
@@ -1,6 +1,8 @@
import { numberPadding, formatDate } from "../../../../helpers/DateHelpers.js"
import ApiTempusTag from "../../../../api/factory/tempus/tag.js";
import CoreTag from '../../../../components/Tag/Tag.js';
import ZeitRow from '../../Modal/ZeitRow.js'
import RaumRow from '../../Modal/RaumRow.js'
import KollisionRow from '../../Modal/KollisionRow.js'
import TagRow from '../../Modal/TagRow.js'
import TeilnehmerRow from '../../Modal/TeilnehmerRow.js'
export default {
props:{
@@ -10,90 +12,17 @@ export default {
},
},
components: {
CoreTag
},
data()
{
return {
tagEndpoint: ApiTempusTag
}
},
computed: {
tagValues() {
return this.event.eindeutige_kalender_gruppen_id ? [this.event.eindeutige_kalender_gruppen_id] : [];
},
lektorenLinks: function () {
if (!this.event || !Array.isArray(this.event.lektor) || !this.event.lektor.length) return "a";
let lektorenLinks = {};
this.event.lektor.forEach((lektor) => {
lektorenLinks[lektor.kurzbz] = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/Cis/Profil/View/${lektor.mitarbeiter_uid}`;
})
return lektorenLinks;
},
getOrtContentLink: function () {
if (!this.event || !this.event.ort_content_id) return "a";
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/CisVue/Cms/content/${this.event.ort_content_id}`
},
start_time: function () {
if (!this.event.beginn)
return 'N/A';
if (!(this.event.beginn instanceof Date)) {
return this.event.beginn;
}
return numberPadding(this.event.beginn.getHours()) + ":" + numberPadding(this.event.beginn.getMinutes());
},
end_time: function () {
if (!this.event.ende)
return 'N/A';
if (!(this.event.ende instanceof Date)) {
return this.event.ende;
}
return numberPadding(this.event.ende.getHours()) + ":" + numberPadding(this.event.ende.getMinutes());
},
ortString() {
return Array.isArray(this.event.ort_kurzbz) ? this.event.ort_kurzbz.join(', ') : this.event.ort_kurzbz;
},
tags() {
if (typeof this.event.tags === 'string') {
try {
return JSON.parse(this.event.tags);
} catch (e) {
console.error('Failed to parse tags:', e);
return [];
}
}
return this.event.tags || [];
},
ZeitRow,
RaumRow,
KollisionRow,
TagRow,
TeilnehmerRow
},
methods: {
methodFormatDate: function (d) {
return formatDate(d);
},
async fetchAssignedTagsByCalender(calendarGroupId) {
let result = await this.$api.call(
ApiTempusTag.getTagsByCalendar(calendarGroupId),
);
if (result.meta.status === "success")
return result.data.filter(tag => !!tag);
this.$fhcAlert.alertError(
this.$p.t("ui", "failed_assigned_tags_fetch_error_message"),
);
return [];
},
async onTagsChanged() {
let id = this.event.eindeutige_kalender_gruppen_id;
if (!id) return;
this.event.tags = await this.fetchAssignedTagsByCalender(id);
},
editTag(tag) {
this.$refs.tagComponent?.editTag(tag.id);
},
async saveAll() {
await this.$refs.teilnehmerRow.save();
this.$refs.zeit.save();
}
},
template: `
<div>
@@ -108,91 +37,13 @@ export default {
<table class="table table-hover mb-4">
<tbody>
<tr>
<th>{{
$p.t('global','datum')?
$p.t('global','datum')+':'
:''
}}</th>
<td>{{methodFormatDate(event.datum)}}</td>
</tr>
<tr>
<th>{{
$p.t('ui','zeitraum')?
$p.t('ui','zeitraum')+':'
:''
}}</th>
<td>{{start_time + ' - ' + end_time}}</td>
</tr>
<tr>
<th>{{
$p.t('global','raum')?
$p.t('global','raum')+':'
:''
}}</th>
<td>
<a v-if="event.ort_content_id" :aria-label="$p.t('global','raum')" :title="$p.t('global','raum')" :href="getOrtContentLink"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C" aria-hidden="true"></i></a>
{{ortString}}
</td>
</tr>
<tr>
<th>{{
$p.t('ui','organisierende')?
$p.t('ui','organisierende')+':'
:''
}}</th>
<td>
<div v-for="lektor in event.lektor" class="d-block">
<a v-if="lektorenLinks[lektor.kurzbz]" :aria-label="$p.t('lehre','lektor')" :tooltip="$p.t('lehre','lektor')" :href="lektorenLinks[lektor.kurzbz]"><i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true" style="color:#00649C"></i></a>
{{lektor.kurzbz}}
</div>
</td>
</tr>
<tr>
<th>{{$p.t('ui','teilnehmende')+':'}}</th>
<td>
<div v-for="teilnehmer in event.teilnehmer_person" class="d-block">
{{teilnehmer.kurzbz}}
</div>
<div v-for="gruppe in event.teilnehmer_gruppe" class="d-block">
{{gruppe.gruppe_kurzbz}}
</div>
</td>
</tr>
<tr>
<th>
<div class="d-flex align-items-center flex-wrap">
{{$p.t('ui','tags')}}:
<div class="fw-normal">
<core-tag
v-if="tagValues.length"
ref="tagComponent"
:endpoint="tagEndpoint"
:values="tagValues"
zuordnung_typ="eindeutige_kalender_gruppen_id"
show-hover
@added="onTagsChanged"
@deleted="onTagsChanged"
@updated="onTagsChanged"
></core-tag>
</div>
</div>
</th>
<td>
<span
v-for="tag in tags"
:key="tag.id"
:class="[tag.style, { tag_done: tag.done }]"
@click="editTag(tag)"
class="tag"
>{{ tag.beschreibung }}</span>
</td>
</tr>
<kollision-row :event="event" />
<zeit-row ref="zeit" :event="event" />
<raum-row :event="event" />
<tag-row :event="event" />
</tbody>
</table>
<teilnehmer-row ref="teilnehmerRow" :event="event" class="mb-3" />
<button type="button" class="btn btn-primary" @click.stop="saveAll">{{$p.t('ui', 'speichern')}}</button>
</div>`,
}
+18 -272
View File
@@ -1,14 +1,12 @@
import BsModal from '../Bootstrap/Modal.js';
import FormInput from '../Form/Input.js';
import ApiReservierung from '../../api/factory/tempus/reservierung.js';
import ReservierungForm from './ReservierungForm.js';
export default {
name: 'ReservierungModal',
components: {
BsModal,
FormInput,
Multiselect: primevue.multiselect,
ReservierungForm
},
props: {
rooms: {
@@ -17,123 +15,10 @@ export default {
}
},
emits: ['saved'],
data() {
return {
titel: '',
beschreibung: '',
start: null,
end: null,
selected_rooms: [],
raeume_array: [],
studiensemester_array: [],
teilnehmer: [],
specialFinalGroups: [],
rollen_array: [],
studiengaenge: [],
show_all_fields: false,
filteredUsers: [],
filteredGroups: [],
abortController: null,
location: null
};
},
created()
{
this.$api.call(ApiReservierung.getInformation())
.then(result => result.data)
.then(result => {
if (result.berechtigt)
{
this.studiengaenge = result.studiengaenge
}
this.show_all_fields = result.berechtigt;
this.raeume_array = result.raeume;
this.rollen_array = result.rollen;
this.studiensemester_array = result.studiensemester;
})
.catch(this.$fhcAlert.handleSystemError);
},
methods: {
async searchGroup(event)
show(start = null, end = null)
{
const query = event.query.trim();
if (query.length < 2)
return [];
if (this.abortController)
this.abortController.abort();
this.abortController = new AbortController();
this.$api.call(ApiReservierung.searchGroup(query), { signal: this.abortController.signal })
.then(result => {
this.filteredGroups =result.data.map(gruppe => ({
label: gruppe.bezeichnung
? `${gruppe.gruppe_kurzbz.trim()} (${gruppe.bezeichnung})`
: gruppe.gruppe_kurzbz.trim(),
gid: gruppe.gid,
gruppe_kurzbz: gruppe.gruppe_kurzbz.trim(),
lehrverband: gruppe.lehrverband,
}));
})
.catch(this.$fhcAlert.handleSystemError);
},
searchUser(event)
{
const query = event.query.trim();
if (!query || query.length < 2)
{
this.filteredUsers = [];
return;
}
if (this.abortController)
this.abortController.abort();
this.abortController = new AbortController();
this.$api.call(ApiReservierung.searchTeilnehmer(query), { signal: this.abortController.signal })
.then(result => {
this.filteredUsers = result.data.map(u => ({
label: `${u.nachname} ${u.vorname} (${u.uid})`,
uid: u.uid
}));
})
.catch(this.$fhcAlert.handleSystemError);
},
selectUser(event, index)
{
this.teilnehmer[index].uid = event.value.uid;
this.teilnehmer[index].label = event.value.label;
},
selectFinalGroup(event, index)
{
this.specialFinalGroups[index].gid = event.value.gid;
this.specialFinalGroups[index].gruppe_kurzbz = event.value.gruppe_kurzbz;
this.specialFinalGroups[index].lehrverband = event.value.lehrverband;
},
show(start, end)
{
this.titel = '';
this.beschreibung = '';
this.start = start;
this.end = end;
let prop_orte = (this.rooms ?? []).map(r => r.ort_kurzbz);
this.selected_rooms = this.raeume_array.filter(raum => prop_orte.includes(raum.ort_kurzbz));
this.teilnehmer = [{ uid: null, rolle: null }];
this.specialFinalGroups = [{ gid: null, studiensemester_kurzbz: null, lehrverband: null, gruppe_kurzbz: null, rolle: null }];
this.$refs.form.reset(start, end, this.rooms);
this.$refs.modal.show();
},
hide()
@@ -142,20 +27,23 @@ export default {
},
save()
{
let data = this.$refs.form.getData();
let orte = {
ort_kurzbz: this.selected_rooms.map(r => r.ort_kurzbz),
location: this.location
if (!data.start || !data.end)
{
this.$fhcAlert.alertError(this.$p.t('ui', 'error_fieldRequired', { field: this.$p.t('ui', 'zeitraum') }));
return;
}
this.$api.call(
ApiReservierung.addReservierung(
this.titel,
this.beschreibung,
orte,
luxon.DateTime.fromFormat(this.start, 'yyyy-MM-dd HH:mm').toISO(),
luxon.DateTime.fromFormat(this.end, 'yyyy-MM-dd HH:mm').toISO(),
this.teilnehmer.filter(t => t.uid && t.rolle).map(nehmer => ({ uid: nehmer.uid, rolle: nehmer.rolle })),
this.specialFinalGroups.filter(group => group.gid && group.rolle && group.studiensemester_kurzbz).map(group => ({ gid: group.gid, lehrverband: group.lehrverband, gruppe_kurzbz: group.gruppe_kurzbz, rolle: group.rolle, studiensemester_kurzbz: group.studiensemester_kurzbz })),
data.titel,
data.beschreibung,
data.orte,
luxon.DateTime.fromFormat(data.start, 'yyyy-MM-dd HH:mm').toISO(),
luxon.DateTime.fromFormat(data.end, 'yyyy-MM-dd HH:mm').toISO(),
data.teilnehmer,
data.gruppen,
)
).then(() => {
this.$refs.modal.hide();
@@ -163,153 +51,11 @@ export default {
});
},
},
// language=HTML
template: `
<bs-modal ref="modal" class="bootstrap-prompt" dialogClass="modal-xl" data-cy="reservationModal">
<template #title>Neue Reservierung</template>
<template #default>
<div class="row g-3">
<form-input
label="Titel"
type="text"
container-class="col-12"
name="titel"
v-model="titel"
></form-input>
<form-input
label="Beschreibung"
type="textarea"
container-class="col-12"
name="beschreibung"
v-model="beschreibung"
></form-input>
<div class="col-6">
<form-input
type="datepicker"
v-model="start"
name="star_date"
format="dd.MM.yyyy HH:mm"
auto-apply
:enable-time-picker="true"
preview-format="dd.MM.yyyy HH:mm"
model-type="yyyy-MM-dd HH:mm"
:label="$p.t('ui', 'von')"
/>
</div>
<div class="col-6">
<form-input
type="datepicker"
v-model="end"
name="end_time"
format="dd.MM.yyyy HH:mm"
auto-apply
:enable-time-picker="true"
preview-format="dd.MM.yyyy HH:mm"
model-type="yyyy-MM-dd HH:mm"
:label="$p.t('global', 'bis')"
/>
</div>
<div class="col-6">
<label class="form-label">{{ $p.t('global', 'raum') }}</label>
<Multiselect
v-model="selected_rooms"
option-label="ort_kurzbz"
:options="raeume_array"
placeholder="Raum"
display="chip"
:hide-selected="false"
class="w-100"
:selectionLimit="3"
>
<template #option="{ option }">
{{ option.ort_kurzbz }} {{ option.bezeichnung }}
</template>
</Multiselect>
</div>
<div class="col-6">
<form-input
:label="$p.t('global', 'raum')"
type="textarea"
name="location"
v-model="location"
></form-input>
</div>
<div class="col-12" v-if="show_all_fields">
<div v-for="(nehmer, i) in teilnehmer" :key="i" class="d-flex gap-2 mb-2 align-items-end">
<form-input
type="autocomplete"
:label="$p.t('ui', 'teilnehmende')"
:suggestions="filteredUsers"
v-model="nehmer.label"
field="label"
container-class="flex-grow-1"
:name="'user_' + i"
@complete="searchUser($event, i)"
@item-select="selectUser($event, i)"
></form-input>
<form-input
type="select"
:label="$p.t('lehre', 'status_rolle')"
v-model="nehmer.rolle"
:name="'rolle_' + i"
>
<option v-for="rolle in rollen_array" :value="rolle.rolle_kurzbz" :key="rolle.rolle_kurzbz">
{{ rolle.bezeichnung }}
</option>
</form-input>
<button type="button" class="btn btn-outline-danger" @click="teilnehmer.splice(i, 1)">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" @click="teilnehmer.push({ uid: null, label: '', rolle: null })">
<i class="fa-solid fa-plus me-1"></i>{{$capitalize( $p.t('global', 'hinzufuegen') )}}
</button>
</div>
<div class="col-12" v-if="show_all_fields">
<div v-for="(group, i) in specialFinalGroups" :key="i" class="d-flex gap-3 mb-3 align-items-end">
<form-input
type="autocomplete"
:label="$p.t('lehre', 'gruppe')"
:suggestions="filteredGroups"
v-model="group.label"
field="label"
container-class="flex-grow-1"
:name="'group_' + i"
@complete="searchGroup($event, i)"
@item-select="selectFinalGroup($event, i)"
></form-input>
<form-input
type="select"
:label="$p.t('lehre', 'studiensemester')"
v-model="group.studiensemester_kurzbz"
:name="'studiensemester_' + i"
>
<option v-for="studiensemester in studiensemester_array" :value="studiensemester.studiensemester_kurzbz" :key="studiensemester.studiensemester_kurzbz">
{{ studiensemester.studiensemester_kurzbz }}
</option>
</form-input>
<form-input
type="select"
:label="$p.t('lehre', 'status_rolle')"
v-model="group.rolle"
:name="'rolle_' + i"
>
<option v-for="rolle in rollen_array" :value="rolle.rolle_kurzbz" :key="rolle.rolle_kurzbz">
{{ rolle.bezeichnung }}
</option>
</form-input>
<button type="button" class="btn btn-outline-danger" @click="specialFinalGroups.splice(i, 1)">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" @click="specialFinalGroups.push({ gruppe_kurzbz: null, studiensemester_kurzbz: null, label: '', rolle: null })">
<i class="fa-solid fa-plus me-1"></i>{{$p.t('lehre', 'gruppe')}} {{$p.t('global', 'hinzufuegen')}}
</button>
</div>
</div>
<reservierung-form ref="form" />
</template>
<template #footer>
<button type="button" class="btn btn-primary" @click="save">Speichern</button>
@@ -0,0 +1,156 @@
import FormInput from '../Form/Input.js';
import ApiReservierung from '../../api/factory/tempus/reservierung.js';
import TeilnehmerRow from './Modal/TeilnehmerRow.js';
export default {
name: 'ReservierungForm',
components: {
FormInput,
Multiselect: primevue.multiselect,
TeilnehmerRow
},
data() {
return {
titel: '',
beschreibung: '',
datum: null,
beginn: null,
ende: null,
selected_rooms: [],
location: null,
raeume_array: [],
show_all_fields: false,
};
},
created() {
this.$api.call(ApiReservierung.getInformation())
.then(result => result.data)
.then(result => {
this.show_all_fields = result.berechtigt;
this.raeume_array = result.raeume;
})
.catch(this.$fhcAlert.handleSystemError);
},
methods: {
reset(start = null, end = null, rooms = [])
{
this.titel = '';
this.beschreibung = '';
this.datum = start ? start.slice(0, 10) : null;
this.beginn = start ? start.slice(11, 16) : null;
this.ende = end ? end.slice(11, 16) : null;
this.location = null;
let prop_orte = (rooms ?? []).map(r => r.ort_kurzbz);
this.selected_rooms = this.raeume_array.filter(raum => prop_orte.includes(raum.ort_kurzbz));
this.$refs.teilnehmerRow?.reset();
},
getData()
{
let teilnehmer = this.$refs.teilnehmerRow?.teilnehmer ?? [];
let gruppen = this.$refs.teilnehmerRow?.gruppen ?? [];
return {
titel: this.titel,
beschreibung: this.beschreibung,
start: this.datum && this.beginn ? this.datum + ' ' + this.beginn : null,
end: this.datum && this.ende ? this.datum + ' ' + this.ende : null,
orte: {
ort_kurzbz: this.selected_rooms.map(r => r.ort_kurzbz),
location: this.location
},
teilnehmer: teilnehmer
.filter(t => t.uid && t.rolle)
.map(t => ({ uid: t.uid, rolle: t.rolle })),
gruppen: gruppen
.filter(g => g.gid && g.rolle && g.studiensemester_kurzbz)
.map(g => ({ gid: g.gid, lehrverband: g.lehrverband, gruppe_kurzbz: g.gruppe_kurzbz, rolle: g.rolle, studiensemester_kurzbz: g.studiensemester_kurzbz })),
};
},
},
template: /*html*/`
<div class="row g-3">
<form-input
label="Titel"
type="text"
container-class="col-12"
name="titel"
v-model="titel"
></form-input>
<form-input
label="Beschreibung"
type="textarea"
container-class="col-12"
name="beschreibung"
v-model="beschreibung"
></form-input>
<div class="col-4">
<form-input
type="datepicker"
v-model="datum"
name="datum"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
:enable-time-picker="false"
text-input
auto-apply
:label="$p.t('global', 'datum')"
/>
</div>
<div class="col-4">
<form-input
type="datepicker"
v-model="beginn"
name="beginn"
time-picker
format="HH:mm"
model-type="HH:mm"
text-input
auto-apply
:label="$p.t('ui', 'von')"
/>
</div>
<div class="col-4">
<form-input
type="datepicker"
v-model="ende"
name="ende"
time-picker
format="HH:mm"
model-type="HH:mm"
text-input
auto-apply
:label="$p.t('global', 'bis')"
/>
</div>
<div class="col-6">
<label class="form-label">{{ $p.t('global', 'raum') }}</label>
<Multiselect
v-model="selected_rooms"
option-label="ort_kurzbz"
:options="raeume_array"
placeholder="Raum"
display="chip"
:hide-selected="false"
class="w-100"
:selectionLimit="3"
>
<template #option="{ option }">
{{ option.ort_kurzbz }} {{ option.bezeichnung }}
</template>
</Multiselect>
</div>
<div class="col-6">
<form-input
:label="$p.t('global', 'raum')"
type="textarea"
name="location"
v-model="location"
></form-input>
</div>
<div class="col-12" v-if="show_all_fields">
<teilnehmer-row ref="teilnehmerRow" />
</div>
</div>`
};
+216 -22
View File
@@ -98,6 +98,9 @@ export default {
modalActions: {
saveEventTime: (payload) => this.handleSaveEventTime(payload),
addToFilter: (filter, type) => this.addToFilter(filter, type),
goToEvent: ({datum, kalender_id}) => {
this.jumpToDate(datum);
}
},
reload: () => {
this.reload();
@@ -139,11 +142,13 @@ export default {
endTime: null,
},
studiengaenge_all: [],
maxDailyEventLimitForMonthView: 10,
raumvorschlagPreview: null,
raumvorschlagLoading: false,
showEvents: true,
reservierungPending: false,
bcc: null,
currentlyUpdatedEvent: null,
};
},
computed: {
@@ -176,7 +181,7 @@ export default {
if (!orig?.kalender_id) return;
await this.deleteEntryCall(orig);
this.$refs.calendar.resetEventLoader();
this.$refs.calendar.resetEventLoader(false);
this.$refs.sidebar.reloadCoursepicker();
this.rebuildRaumvorschlag();
},
@@ -187,7 +192,7 @@ export default {
await Promise.allSettled(
validList.map((orig) => this.deleteEntryCall(orig)),
);
this.$refs.calendar.resetEventLoader();
this.$refs.calendar.resetEventLoader(false);
this.$refs.sidebar.reloadCoursepicker();
},
async deleteEntryCall(orig) {
@@ -409,6 +414,22 @@ export default {
this.$refs.calendar.resetEventLoader();
if (this.lastRange) this.handleRange(this.lastRange);
},
jumpToDate(value) {
if (!value) return;
let zone = this.config.timezone;
let date = luxon.DateTime.fromSQL(value, { zone });
if (!date.isValid)
date = luxon.DateTime.fromISO(value, { zone });
if (!date.isValid)
return;
this.calendarDatesByMode = {
...this.calendarDatesByMode,
week: date.startOf('week').toISODate(),
};
this.currentMode = 'week';
},
jumpToKw(kw) {
const num = parseInt(kw);
if (!num) return;
@@ -461,6 +482,9 @@ export default {
return this.$api.call(ApiSearchbar.search(params));
},
getPromiseFunc(start, end) {
const isMonth = this.currentMode?.toLowerCase() === "month";
const collisionCheck = !isMonth;
const maxDailyEventLimit = isMonth ? this.maxDailyEventLimitForMonthView : null;
const hasRooms = this.rooms.length > 0;
const hasLektoren = this.lecturers.length > 0;
const hasStg = this.studiengaenge.length > 0;
@@ -480,25 +504,50 @@ export default {
if (hasLektoren)
filter.uid = this.lecturers.map((lecture) => lecture.uid);
if (this.previewRole === 'lektor')
return [
let response = null;
if (this.previewRole === 'lektor') {
response = [
this.$api.call(
ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()),
ApiKalender.getPlanLecturer(
start.toISODate(),
end.toISODate(),
collisionCheck,
maxDailyEventLimit,
),
),
];
if (this.previewRole === 'student')
return [
} else if (this.previewRole === 'student'){
response = [
this.$api.call(
ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()),
ApiKalender.getPlanStudent(
start.toISODate(),
end.toISODate(),
collisionCheck,
maxDailyEventLimit,
),
),
];
} else {
response = [
this.$api.call(
ApiKalender.getPlan(
filter,
start.toISODate(),
end.toISODate(),
collisionCheck,
maxDailyEventLimit,
),
),
];
}
return [
this.$api.call(
ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()),
),
];
if (response) {
response[0].then((result) => {
this.scrollToAndEmphasizeCurrentlyUpdatedCalendarEvent();
});
}
return response;
},
_parseDates(start, end) {
const startDT = luxon.DateTime.fromISO(start);
@@ -540,7 +589,19 @@ export default {
ApiKalender.updateKalenderEvent(obj.orig.kalender_id, updatedInfos),
)
.then(() => {
if (onSuccess) onSuccess();
if (onSuccess) {
onSuccess();
this.$refs.calendar.$refs.calendar.$refs.mode.$refs.view.$refs.grid.disableAutoScroll();
this.currentlyUpdatedEvent = obj.orig;
}
})
.catch((error) => {
this.currentlyUpdatedEvent = null;
this.removeVisualForUpdatingCalendarEvent(obj.orig.eindeutige_kalender_gruppen_id);
this.$nextTick(() => {
this.$refs.calendar.resetEventLoader(false, false);
});
throw error;
});
},
@@ -551,12 +612,24 @@ export default {
const { item, start, end } = payload;
const obj = item[0];
if (!obj?.orig?.kalender_id)
return alert('Kein gültiges Kalender-Event zum Resizen');
return;
const dates = this._parseDates(start, end);
if (!dates) return;
if (
luxon.DateTime.fromISO(obj.orig.isostart).toMillis() ===
dates.startDT.toMillis() &&
luxon.DateTime.fromISO(obj.orig.isoend).toMillis() ===
dates.endDT.toMillis()
)
return;
this.addVisualForUpdatingCalendarEvent(
obj.orig.eindeutige_kalender_gruppen_id
);
return this._updateKalenderEvent(
obj,
dates.startDT,
@@ -564,7 +637,7 @@ export default {
dates.start_time,
dates.end_time,
() => {
this.$refs.calendar.resetEventLoader();
this.$refs.calendar.resetEventLoader(false, false);
this.$refs.sidebar.reloadCoursepicker();
this.rebuildRaumvorschlag();
},
@@ -595,7 +668,7 @@ export default {
),
)
.then(() => {
this.$refs.calendar.resetEventLoader();
this.$refs.calendar.resetEventLoader(false, false);
this.$refs.sidebar.reloadCoursepicker();
this.rebuildRaumvorschlag();
this.bcc.postMessage('dropped');
@@ -605,8 +678,20 @@ export default {
const dates = this._parseDates(start, end);
if (!dates) return;
if (
luxon.DateTime.fromISO(obj.orig.isostart).toMillis() ===
dates.startDT.toMillis() &&
luxon.DateTime.fromISO(obj.orig.isoend).toMillis() ===
dates.endDT.toMillis()
)
return;
const { startDT, endDT, start_time, end_time } = dates;
this.addVisualForUpdatingCalendarEvent(
obj.orig.eindeutige_kalender_gruppen_id
);
if (obj.type === 'reservierung') {
this.reservierungPending = true;
this.$refs.reservierung.show(start_time, end_time);
@@ -652,7 +737,7 @@ export default {
return;
}
this.$refs.calendar.resetEventLoader();
this.$refs.calendar.resetEventLoader(false);
this.$refs.sidebar.reloadCoursepicker();
this.rebuildRaumvorschlag();
this.bcc.postMessage('dropped');
@@ -670,7 +755,7 @@ export default {
),
)
.then(() => {
this.$refs.calendar.resetEventLoader();
this.$refs.calendar.resetEventLoader(false);
this.$refs.sidebar.reloadCoursepicker();
this.bcc.postMessage('dropped');
this.rebuildRaumvorschlag();
@@ -695,7 +780,7 @@ export default {
type: obj.type,
id: obj.orig.kalender_id,
});
this.$refs.calendar.resetEventLoader();
this.$refs.calendar.resetEventLoader(false, false);
this.rebuildRaumvorschlag();
this.bcc.postMessage('dropped');
},
@@ -715,7 +800,7 @@ export default {
this.multiWeekModal.show = false;
},
onMultiWeekConfirmed() {
this.$refs.calendar.resetEventLoader();
this.$refs.calendar.resetEventLoader(false);
this.$refs.sidebar.reloadCoursepicker();
this.rebuildRaumvorschlag();
this.bcc.postMessage('dropped');
@@ -896,6 +981,114 @@ export default {
focusSearchbar() {
this.$refs.header?.focusSearchbar();
},
scrollToAndEmphasizeCurrentlyUpdatedCalendarEvent() {
if (!this.currentlyUpdatedEvent) {
this.$refs.calendar.clearOutCalendarEventEmphasis();
return;
}
document
.querySelectorAll(
'.fhc-calendar-base-grid .fhc-calendar-base-grid-line-event',
)
.forEach((el) => {
el.classList.remove(
'updating-event',
'updated-event',
'updated-event-long',
);
});
document
.querySelectorAll(
'.fhc-calendar-base-grid .fhc-calendar-base-grid-line-event:not([data-group-id="event-group-' + this.currentlyUpdatedEvent.eindeutige_kalender_gruppen_id +
'"])',
)
.forEach((el) => {
const spinner = el.querySelector('.spinner-overlay');
if (!spinner) return;
spinner.remove();
});
setTimeout(() => {
const eventEl = document.querySelector(
`[data-group-id="event-group-${this.currentlyUpdatedEvent.eindeutige_kalender_gruppen_id}"]`,
);
if (!eventEl) return;
this.removeVisualForUpdatingCalendarEvent(
this.currentlyUpdatedEvent.eindeutige_kalender_gruppen_id,
);
const calendar = document.querySelector('.fhc-calendar-base-grid');
const eventRect = eventEl.getBoundingClientRect();
const offset = 300;
const isInsideScrolledView =
eventEl.offsetLeft < calendar.scrollLeft + calendar.clientWidth &&
eventEl.offsetLeft + eventEl.offsetWidth > calendar.scrollLeft &&
eventEl.offsetTop < calendar.scrollTop + calendar.clientHeight &&
eventEl.offsetTop + eventEl.offsetHeight > calendar.scrollTop;
const rect = eventEl.getBoundingClientRect();
if (!isInsideScrolledView) {
eventEl.scrollIntoView({
behavior: 'smooth',
inline: 'center',
block: 'nearest',
});
}
let timeout = 0;
let emphasizeUpdateClassName = isInsideScrolledView
? 'updated-event'
: 'updated-event-long';
if (!isInsideScrolledView) timeout = 300;
setTimeout(() => {
eventEl.classList.add(emphasizeUpdateClassName);
}, timeout);
this.currentlyUpdatedEvent = null;
}, 100);
},
addVisualForUpdatingCalendarEvent(calendarGruppenId) {
if (!calendarGruppenId)
return;
const calendar = this.$refs.calendar?.$el;
const element = calendar?.querySelector(
`[data-group-id="event-group-${calendarGruppenId}"]`,
);
if (!element) return;
const outerDiv = document.createElement('div');
outerDiv.className = 'spinner-overlay';
const innerDiv = document.createElement('div');
innerDiv.className = 'spinner';
outerDiv.appendChild(innerDiv);
element.appendChild(outerDiv);
},
removeVisualForUpdatingCalendarEvent(calendarGruppenId) {
if (!calendarGruppenId)
return;
const calendar = this.$refs.calendar?.$el;
let element = calendar?.querySelector(
`[data-group-id="event-group-${calendarGruppenId}"]`,
);
if (!element) return;
const spinner = element.querySelector('.spinner-overlay');
if (spinner) {
spinner.remove();
}
},
},
watch: {
lecturers: {
@@ -1046,6 +1239,7 @@ export default {
:extra-backgrounds="extraBackgrounds"
@update:range="handleRange"
class="responsive-calendar"
:cache-multiplier="currentMode === 'week' ? 1 : 0"
/>
</template>
</horizontal-split>
-2
View File
@@ -1,5 +1,3 @@
// TODO(chris): load events that are longer than the interval without doubling it
export function useEventLoader(rangeInterval, getPromiseFunc) {
let loading_id = 0;
const events = Vue.ref([]);
@@ -0,0 +1,270 @@
export function useEventLoader(
rangeInterval,
getPromiseFunc,
cacheMultiplier = 1,
) {
let hasFirstLoadOccurred = false;
let allEvents = Vue.ref([]);
const lv = Vue.ref(null);
let newlyLoadedEvents = [];
let cachedEventsStartTimestamp = null;
let cachedEventsEndTimestamp = null;
let allowedCacheStartTimestamp = null;
let allowedCacheEndTimestamp = null;
let currentlyDisplayedDateRange;
const getCachePadding = () => {
const cacheSize = Math.max(Vue.toValue(cacheMultiplier), 0);
return Math.round(currentlyDisplayedDateRange.length() * cacheSize) + 1;
};
const reload = (isCacheEnabled = true, isLoaderEventVisualReset = true) => {
if (
currentlyDisplayedDateRange &&
currentlyDisplayedDateRange?.start?.ts ===
rangeInterval?.value?.start?.ts &&
currentlyDisplayedDateRange?.end?.ts === rangeInterval?.value?.end?.ts &&
isCacheEnabled
) {
return;
}
currentlyDisplayedDateRange = Vue.toValue(rangeInterval);
if (
!currentlyDisplayedDateRange?.start ||
!currentlyDisplayedDateRange?.end
)
return;
if (!(currentlyDisplayedDateRange instanceof luxon.Interval)) return;
if (
cachedEventsStartTimestamp === currentlyDisplayedDateRange.start.ts &&
cachedEventsEndTimestamp === currentlyDisplayedDateRange.end.ts &&
isCacheEnabled
) {
return;
}
const promises = requestEvents(
currentlyDisplayedDateRange.start.ts,
currentlyDisplayedDateRange.end.ts,
isCacheEnabled,
isLoaderEventVisualReset
);
ensureCacheRangeIsInAllowedRange(
allowedCacheStartTimestamp,
allowedCacheEndTimestamp,
);
if (promises.length === 0) {
allEvents.value = removeVisualForEventsLoading(
ensureEventsAreInValidCacheRange(allEvents.value),
);
return;
}
addVisualForEventsLoading();
Promise.allSettled(promises).then((results) => {
let newlyLoadedEvents = [];
let hasSuccessfullyLoadedEvents = false;
results.forEach((res) => {
if (
!(res.status === "fulfilled" && res.value.meta.status === "success")
)
return;
hasSuccessfullyLoadedEvents = true;
if (res.value.meta.lv) lv.value = res.value.meta.lv;
newlyLoadedEvents = res.value.data;
});
let tempAllEvents = Array.from(
new Map(
[...allEvents.value, ...newlyLoadedEvents].map((event) => [
event.eindeutige_kalender_gruppen_id,
event,
]),
).values(),
);
if (!isCacheEnabled && hasSuccessfullyLoadedEvents) {
const newlyLoadedEventIds = new Set(
newlyLoadedEvents.map(
(event) => event.eindeutige_kalender_gruppen_id,
),
);
tempAllEvents = tempAllEvents.filter((event) => {
const eventStart = getTimestampFromISODate(event.isostart);
const eventEnd = getTimestampFromISODate(event.isoend);
const overlapsDisplayedDateRange =
eventStart < currentlyDisplayedDateRange.end.ts &&
eventEnd > currentlyDisplayedDateRange.start.ts;
return (
!overlapsDisplayedDateRange ||
newlyLoadedEventIds.has(event.eindeutige_kalender_gruppen_id)
);
});
}
allEvents.value = removeVisualForEventsLoading(
ensureEventsAreInValidCacheRange(tempAllEvents),
);
hasFirstLoadOccurred = true;
});
};
Vue.watchEffect(reload);
const reset = (arePreviousEventsCleared = true, isLoaderEventVisualReset = true) => {
if (arePreviousEventsCleared) {
allEvents.value = [];
}
reload(false, isLoaderEventVisualReset);
};
const requestEvents = (startTimestamp, endTimestamp, isCacheEnabled = true, isLoaderEventVisualReset = false) => {
let result = [];
if (!startTimestamp || !endTimestamp) return result;
if (startTimestamp >= endTimestamp) return result;
let modifiedRequestStartTimestamp = startTimestamp;
let modifiedRequestEndTimestamp = endTimestamp;
if (cachedEventsStartTimestamp && cachedEventsEndTimestamp && isCacheEnabled) {
if (
(
(startTimestamp < cachedEventsStartTimestamp &&
endTimestamp > cachedEventsEndTimestamp) ||
(startTimestamp < cachedEventsStartTimestamp &&
endTimestamp < cachedEventsEndTimestamp && Math.abs(getLuxonDateFromMillis(endTimestamp).diff(getLuxonDateFromMillis(cachedEventsStartTimestamp), 'days')) > 1) ||
(startTimestamp > cachedEventsStartTimestamp &&
endTimestamp > cachedEventsEndTimestamp && Math.abs(getLuxonDateFromMillis(startTimestamp).diff(getLuxonDateFromMillis(cachedEventsEndTimestamp), 'days')) > 1)
)
) {
modifiedRequestStartTimestamp = startTimestamp;
modifiedRequestEndTimestamp = endTimestamp;
cachedEventsStartTimestamp = startTimestamp;
cachedEventsEndTimestamp = endTimestamp;
} else if (
startTimestamp < cachedEventsStartTimestamp &&
endTimestamp <= cachedEventsEndTimestamp
) {
modifiedRequestStartTimestamp = startTimestamp;
modifiedRequestEndTimestamp = cachedEventsStartTimestamp;
cachedEventsStartTimestamp = startTimestamp;
} else if (
startTimestamp >= cachedEventsStartTimestamp &&
endTimestamp > cachedEventsEndTimestamp
) {
modifiedRequestStartTimestamp = cachedEventsEndTimestamp;
modifiedRequestEndTimestamp = endTimestamp;
cachedEventsEndTimestamp = endTimestamp;
} else {
return result;
}
} else if (isCacheEnabled) {
cachedEventsStartTimestamp = modifiedRequestStartTimestamp;
cachedEventsEndTimestamp = modifiedRequestEndTimestamp;
}
const cachePadding = getCachePadding();
if (isLoaderEventVisualReset) {
hasFirstLoadOccurred = false;
}
allowedCacheStartTimestamp = startTimestamp - cachePadding;
allowedCacheEndTimestamp = endTimestamp + cachePadding;
return mergePromiseElements(
getPromiseFunc(
getLuxonDateFromMillis(modifiedRequestStartTimestamp),
getLuxonDateFromMillis(modifiedRequestEndTimestamp),
),
result,
);
};
const addVisualForEventsLoading = (startTimestamp, endTimestamp) => {
if (hasFirstLoadOccurred) return;
allEvents.value.push({
loading_id: 1,
type: "loading",
isostart: getISODateFromTimestamp(currentlyDisplayedDateRange.start.ts),
isoend: getISODateFromTimestamp(currentlyDisplayedDateRange.end.ts),
});
};
const removeVisualForEventsLoading = (events) => {
return events.filter((event) => event.type !== "loading");
};
const ensureEventsAreInValidCacheRange = (events) => {
events = events.filter((event) => {
const start = getTimestampFromISODate(event.isostart);
const end = getTimestampFromISODate(event.isoend);
return (
allowedCacheStartTimestamp <= start && allowedCacheEndTimestamp >= end
);
});
return events;
};
const ensureCacheRangeIsInAllowedRange = (
allowedCacheStartTimestamp,
allowedCacheEndTimestamp,
) => {
if (
cachedEventsStartTimestamp < allowedCacheEndTimestamp &&
cachedEventsEndTimestamp > allowedCacheStartTimestamp
) {
if (cachedEventsStartTimestamp < allowedCacheStartTimestamp) {
cachedEventsStartTimestamp = allowedCacheStartTimestamp;
}
if (cachedEventsEndTimestamp > allowedCacheEndTimestamp) {
cachedEventsEndTimestamp = allowedCacheEndTimestamp;
}
}
};
return { events: allEvents, lv, reset };
}
const mergePromiseElements = (n, o) => {
if (Array.isArray(n)) return o.concat(n);
return (o.push(n), o);
};
const getDateFromTimestamp = (timestamp) => {
return luxon.DateTime.fromMillis(timestamp).toFormat("yyyy-MM-dd");
};
const getISODateFromTimestamp = (timestamp) => {
return luxon.DateTime.fromMillis(timestamp).toISO();
};
const getTimestampFromISODate = (isoDate) => {
return luxon.DateTime.fromISO(isoDate).ts;
};
const getLuxonDateFromMillis = (millis) => {
return luxon.DateTime.fromMillis(millis);
};
+16 -6
View File
@@ -1,11 +1,17 @@
const clickListeners = [];
function saveAddClickListener(el, source, value) {
function removeClickListener(el) {
const index = clickListeners.findIndex(data => data.el == el);
if (index >= 0) {
el.removeEventListener('click', clickListeners[index].listener);
clickListeners.splice(index, 1);
}
if (index < 0)
return;
el.removeEventListener('click', clickListeners[index].listener);
clickListeners.splice(index, 1);
}
function saveAddClickListener(el, source, value) {
removeClickListener(el);
const listener = evt => {
evt.preventDefault();
evt.stopPropagation();
@@ -45,5 +51,9 @@ export default {
if (binding.arg != 'container') {
saveAddClickListener(el, binding.arg, binding.value);
}
},
beforeUnmount(el, binding) {
if (binding.arg != 'container')
removeClickListener(el);
}
}
}
+46
View File
@@ -0,0 +1,46 @@
export function normalizeTime(value) {
if (!value)
return null;
if (value instanceof Date)
return luxon.DateTime.fromJSDate(value).toFormat('HH:mm');
const dt = luxon.DateTime.fromFormat(value, 'HH:mm:ss');
return dt.isValid ? dt.toFormat('HH:mm') : value;
}
export function normalizeDate(value) {
if (!value)
return null;
if (value instanceof Date)
return luxon.DateTime.fromJSDate(value).toFormat('yyyy-MM-dd');
return value.slice(0, 10);
}
export function parseJsonField(value, fallback = []) {
if (typeof value !== 'string')
return value || fallback;
try {
return JSON.parse(value);
} catch (e) {
console.error('Failed to parse JSON field:', e);
return fallback;
}
}
export function profilLink(uid) {
return FHC_JS_DATA_STORAGE_OBJECT.app_root
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ `/Cis/Profil/View/${uid}`;
}
export function buildLektorenLinks(lektoren) {
if (!Array.isArray(lektoren))
return {};
const links = {};
lektoren.forEach(lektor => {
links[lektor.kurzbz] = profilLink(lektor.mitarbeiter_uid);
});
return links;
}
+40
View File
@@ -528,6 +528,26 @@ $phrases = array(
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'weitereTeilnehmende',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => '...und {0} weitere Teilnehmende',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => '...and {0} more participants',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'ui',
@@ -4243,6 +4263,26 @@ $phrases = array(
)
)
),
array(
'app' => 'pep',
'category' => 'ui',
'phrase' => 'already_present',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Bereits vorhanden',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Already added',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'pep',
'category' => 'ui',