Conflict resolved

This commit is contained in:
Ivymaster
2026-07-09 10:50:16 +02:00
27 changed files with 1634 additions and 332 deletions
@@ -32,7 +32,6 @@ class Coursepicker extends FHCAPI_Controller
//TODO Where weiter anpassen z.B. Fachbereich //TODO Where weiter anpassen z.B. Fachbereich
$this->_ci->LehreinheitModel->addSelect('tbl_lehreinheit.lehreinheit_id, $this->_ci->LehreinheitModel->addSelect('tbl_lehreinheit.lehreinheit_id,
tbl_lehreinheit.unr,
tbl_lehreinheit.lvnr, tbl_lehreinheit.lvnr,
tbl_lehreinheit.lehrfach_id, tbl_lehreinheit.lehrfach_id,
lehrfach.kurzbz AS lehrfach, lehrfach.kurzbz AS lehrfach,
@@ -103,7 +102,6 @@ class Coursepicker extends FHCAPI_Controller
$this->_ci->LehreinheitModel->addSelect(' $this->_ci->LehreinheitModel->addSelect('
tbl_lehreinheit.lehreinheit_id, tbl_lehreinheit.lehreinheit_id,
tbl_lehreinheit.unr,
tbl_lehreinheit.lvnr, tbl_lehreinheit.lvnr,
tbl_lehreinheit.lehrfach_id, tbl_lehreinheit.lehrfach_id,
lehrfach.kurzbz AS lehrfach, lehrfach.kurzbz AS lehrfach,
@@ -146,15 +144,30 @@ class Coursepicker extends FHCAPI_Controller
)); ));
$result = hasData($result) ? getData($result) : array(); $result = hasData($result) ? getData($result) : array();
$lehreinheit_ids = array_values(array_unique(array_column($result, 'lehreinheit_id')));
$offen_map = array();
if (!empty($lehreinheit_ids))
{
$offen_result = $this->_ci->LehreinheitModel->getOffeneStunden($lehreinheit_ids);
if (hasData($offen_result))
{
foreach (getData($offen_result) as $row)
{
$offen_map[$row->lehreinheit_id . '_' . $row->mitarbeiter_uid] = $row;
}
}
}
$grouped = array(); $grouped = array();
foreach ($result as $row) foreach ($result as $row)
{ {
$unr = $row->unr; $lehreinheit_id = $row->lehreinheit_id;
if (!isset($grouped[$unr])) if (!isset($grouped[$lehreinheit_id]))
{ {
$grouped[$unr] = (object)array( $grouped[$lehreinheit_id] = (object)array(
'unr' => $row->unr,
'lehrfach_id' => $row->lehrfach_id, 'lehrfach_id' => $row->lehrfach_id,
'lehrfach_bez' => $row->lehrfach_bez, 'lehrfach_bez' => $row->lehrfach_bez,
'lehrfach_farbe' => $row->lehrfach_farbe, 'lehrfach_farbe' => $row->lehrfach_farbe,
@@ -186,7 +199,7 @@ class Coursepicker extends FHCAPI_Controller
); );
} }
$group = $grouped[$unr]; $group = $grouped[$lehreinheit_id];
$group->lektoren[$row->lektor_uid] = (object)array( $group->lektoren[$row->lektor_uid] = (object)array(
'uid' => $row->lektor_uid, 'uid' => $row->lektor_uid,
@@ -202,9 +215,15 @@ class Coursepicker extends FHCAPI_Controller
$group->wochenrythmus[] = $row->wochenrythmus; $group->wochenrythmus[] = $row->wochenrythmus;
$group->planstunden[] = $row->planstunden; $group->planstunden[] = $row->planstunden;
$group->start_kw[] = $row->start_kw; $group->start_kw[] = $row->start_kw;
$group->verplant[] = isset($row->verplant) ? $row->verplant : 0;
$group->offenestunden[] = isset($row->offenestunden) ? $row->offenestunden : 0; $offen_key = $lehreinheit_id . '_' . $row->lektor_uid;
$group->verplant_gesamt += isset($row->verplant) ? $row->verplant : 0; $offen = isset($offen_map[$offen_key]) ? $offen_map[$offen_key] : null;
$verplant_wert = $offen ? $offen->verplant : 0;
$offenestunden_wert = $offen ? $offen->offenestunden : 0;
$group->verplant[] = $verplant_wert;
$group->offenestunden[] = $offenestunden_wert;
$group->verplant_gesamt += $verplant_wert;
$lvb = $row->studiengang . '-' . $row->semester; $lvb = $row->studiengang . '-' . $row->semester;
@@ -244,7 +263,7 @@ class Coursepicker extends FHCAPI_Controller
$values = array_values(array_unique($arr)); $values = array_values(array_unique($arr));
$formatted = implode(' ', $values); $formatted = implode(' ', $values);
if (count($formatted) > 1) if (count($values) > 1)
$formatted .= ' ?'; $formatted .= ' ?';
return $formatted; return $formatted;
@@ -30,6 +30,9 @@ class Kalender extends FHCAPI_Controller
'getZeitsperren' => self::PERM_LOGGED, 'getZeitsperren' => self::PERM_LOGGED,
'updateKalenderEvent' => 'lehre/lvplan:rw', 'updateKalenderEvent' => 'lehre/lvplan:rw',
'addKalenderEvent' => 'lehre/lvplan:rw', 'addKalenderEvent' => 'lehre/lvplan:rw',
'calculateMultiWeekPlan' => 'lehre/lvplan:rw',
'confirmMultiWeekPlan' => 'lehre/lvplan:rw',
'addToKalenderEvent' => 'lehre/lvplan:rw',
'addReservierung' => 'lehre/lvplan:rw', 'addReservierung' => 'lehre/lvplan:rw',
'sync' => 'lehre/lvplan:rw', 'sync' => 'lehre/lvplan:rw',
]); ]);
@@ -322,8 +325,6 @@ class Kalender extends FHCAPI_Controller
$ort_kurzbz = $this->_ci->input->post('ort_kurzbz', TRUE); $ort_kurzbz = $this->_ci->input->post('ort_kurzbz', TRUE);
$start_date = $this->_ci->input->post('start_date', TRUE); $start_date = $this->_ci->input->post('start_date', TRUE);
$end_date = $this->_ci->input->post('end_date', TRUE); $end_date = $this->_ci->input->post('end_date', TRUE);
$result = $this->_ci->kalenderlib->addKalenderEvent($start_date, $end_date, $lehreinheit_id, $ort_kurzbz); $result = $this->_ci->kalenderlib->addKalenderEvent($start_date, $end_date, $lehreinheit_id, $ort_kurzbz);
if (isError($result)) if (isError($result))
@@ -332,6 +333,69 @@ class Kalender extends FHCAPI_Controller
$this->terminateWithSuccess(getData($result)); $this->terminateWithSuccess(getData($result));
} }
public function calculateMultiWeekPlan()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required");
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$lehreinheit_id = $this->_ci->input->post('lehreinheit_id', TRUE);
$ort_kurzbz = $this->_ci->input->post('ort_kurzbz', TRUE);
$start_date = $this->_ci->input->post('start_date', TRUE);
$end_date = $this->_ci->input->post('end_date', TRUE);
$result = $this->_ci->kalenderlib->calculateMultiWeekPlan($start_date, $end_date, $lehreinheit_id, $ort_kurzbz);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function confirmMultiWeekPlan()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('plan[]',"plan","required");
$this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$plan = $this->_ci->input->post('plan', TRUE);
$lehreinheit_id = $this->_ci->input->post('lehreinheit_id', TRUE);
$result = $this->_ci->kalenderlib->confirmMultiWeekPlan($plan, $lehreinheit_id);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function addToKalenderEvent()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('target_kalender_id',"target_kalender_id","required");
$this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$target_kalender_id = $this->_ci->input->post('target_kalender_id', TRUE);
$lehreinheit_id = $this->_ci->input->post('lehreinheit_id', TRUE);
$result = $this->_ci->kalenderlib->addToKalenderEvent($target_kalender_id, $lehreinheit_id);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function addReservierung() public function addReservierung()
{ {
$this->_ci->form_validation->set_data($_POST); $this->_ci->form_validation->set_data($_POST);
@@ -8,7 +8,7 @@
if (! defined('BASEPATH')) exit('No direct script access allowed'); if (! defined('BASEPATH')) exit('No direct script access allowed');
class MigrateKalender extends Auth_Controller class MigrateKalender extends CLI_Controller
{ {
/** /**
@@ -35,8 +35,14 @@ class MigrateKalender extends Auth_Controller
/** /**
* Everything has a beginning * Everything has a beginning
*/ */
public function migrateStundenplan($von = null, $bis = null, $studiengang_kz = null) public function migrateStundenplan($von, $bis = null, $studiengang_kz = null)
{ {
if (is_null($von))
return error('Parameter "von" ist erforderlich');
if (is_null($bis))
$bis = '2100-12-31';
$db = new DB_Model(); $db = new DB_Model();
$stpldevsql = ' $stpldevsql = '
@@ -71,8 +77,8 @@ class MigrateKalender extends Auth_Controller
bk.block_nr, bk.block_nr,
MIN(bk.stunde) AS stunde_von, MIN(bk.stunde) AS stunde_von,
MAX(bk.stunde) AS stunde_bis, MAX(bk.stunde) AS stunde_bis,
MIN(sp.lehreinheit_id) AS lehreinheit_id, array_agg(DISTINCT sp.lehreinheit_id) AS lehreinheit_id,
MIN(sp.ort_kurzbz) AS ort_kurzbz, array_agg(DISTINCT sp.ort_kurzbz) AS ort_kurzbz,
array_agg(sp.stundenplandev_id ORDER BY bk.stunde) AS stundenplandev_ids, array_agg(sp.stundenplandev_id ORDER BY bk.stunde) AS stundenplandev_ids,
MIN(sp.insertamum) AS insertamum, MIN(sp.insertamum) AS insertamum,
(array_agg(sp.insertvon ORDER BY sp.insertamum ASC))[1] AS insertvon, (array_agg(sp.insertvon ORDER BY sp.insertamum ASC))[1] AS insertvon,
@@ -118,7 +124,7 @@ class MigrateKalender extends Auth_Controller
$ids = is_array($block->stundenplandev_ids) ? $block->stundenplandev_ids : explode(',', $block->stundenplandev_ids); $ids = is_array($block->stundenplandev_ids) ? $block->stundenplandev_ids : explode(',', $block->stundenplandev_ids);
/*$ids = array_map('intval', $ids);*/ /*$ids = array_map('intval', $ids);*/
$this->SyncModel->db->where('stundenplandev_id IN (' . implode(',', $ids) . ')'); $this->SyncModel->db->where_in('stundenplandev_id', $ids);
$sync_result = $this->SyncModel->load(); $sync_result = $this->SyncModel->load();
if (!hasData($sync_result)) if (!hasData($sync_result))
@@ -152,8 +158,14 @@ class MigrateKalender extends Auth_Controller
} }
} }
public function migrateReservierung($von = null, $bis = null, $ort_kurzbz = null) public function migrateReservierung($von, $bis = null, $ort_kurzbz = null)
{ {
if (is_null($von))
return error('Parameter "von" ist erforderlich');
if (is_null($bis))
$bis = '2100-12-31';
$db = new DB_Model(); $db = new DB_Model();
$qry = "WITH eindeutige_stunden AS ( $qry = "WITH eindeutige_stunden AS (
@@ -170,43 +182,42 @@ class MigrateKalender extends Auth_Controller
} }
$qry .= "), $qry .= "),
block_keys AS ( block_keys AS (
SELECT
titel, beschreibung, datum, stunde,
stunde - ROW_NUMBER() OVER (PARTITION BY titel, beschreibung, datum ORDER BY stunde) AS block_nr
FROM eindeutige_stunden
),
blocks AS (
SELECT SELECT
titel, beschreibung, datum, stunde, bk.titel,
stunde - ROW_NUMBER() OVER (PARTITION BY titel, beschreibung, datum ORDER BY stunde) AS block_nr bk.beschreibung,
FROM eindeutige_stunden bk.datum,
), bk.block_nr,
blocks AS ( MIN(bk.stunde) AS stunde_von,
SELECT MAX(bk.stunde) AS stunde_bis,
bk.titel, array_agg(DISTINCT r.reservierung_id::text) AS reservierung_ids,
bk.beschreibung, array_agg(DISTINCT r.uid) AS uids,
bk.datum, array_agg(DISTINCT r.gruppe_kurzbz) AS gruppen_kurzbz,
bk.block_nr, array_agg(DISTINCT ROW(r.semester, r.verband, r.gruppe, r.studiengang_kz)::text) AS svg_kombis,
MIN(bk.stunde) AS stunde_von, array_agg(DISTINCT r.ort_kurzbz) AS ort_kurzbz,
MAX(bk.stunde) AS stunde_bis, MIN(r.studiengang_kz) AS studiengang_kz,
array_agg(DISTINCT r.reservierung_id::text) AS reservierung_ids, MIN(r.reservierung_id) AS reservierung_id,
array_agg(DISTINCT r.uid) AS uids, MAX(r.insertamum) AS insertamum,
array_agg(DISTINCT r.gruppe_kurzbz) AS gruppen_kurzbz, (array_agg(r.insertvon ORDER BY r.insertamum ASC))[1] AS insertvon
array_agg(DISTINCT ROW(r.semester, r.verband, r.gruppe)::text) AS svg_kombis, FROM block_keys bk
array_agg(DISTINCT r.ort_kurzbz) AS orte_kurzbz, JOIN campus.tbl_reservierung r
MIN(r.studiengang_kz) AS studiengang_kz, ON r.titel = bk.titel AND r.beschreibung = bk.beschreibung AND r.datum = bk.datum AND r.stunde = bk.stunde
MIN(r.veranstaltung_id) AS veranstaltung_id, WHERE r.datum >= ? AND r.datum <= ?
MIN(r.reservierung_id) AS reservierung_id, GROUP BY bk.titel, bk.beschreibung, bk.datum, bk.block_nr
MAX(r.insertamum) AS insertamum, )
(array_agg(r.insertvon ORDER BY r.insertamum ASC))[1] AS insertvon
FROM block_keys bk
JOIN campus.tbl_reservierung r
ON r.titel = bk.titel AND r.beschreibung = bk.beschreibung AND r.datum = bk.datum AND r.stunde = bk.stunde
WHERE r.datum >= ? AND r.datum <= ?
GROUP BY bk.titel, bk.beschreibung, bk.datum, bk.block_nr
)
SELECT SELECT
b.*, b.*,
(b.datum + s_von.beginn) AS von, (b.datum + s_von.beginn) AS von,
(b.datum + s_bis.ende) AS bis (b.datum + s_bis.ende) AS bis
FROM blocks b FROM blocks b
JOIN lehre.tbl_stunde s_von ON s_von.stunde = b.stunde_von JOIN lehre.tbl_stunde s_von ON s_von.stunde = b.stunde_von
JOIN lehre.tbl_stunde s_bis ON s_bis.stunde = b.stunde_bis JOIN lehre.tbl_stunde s_bis ON s_bis.stunde = b.stunde_bis
ORDER BY b.reservierung_id DESC;"; ORDER BY b.reservierung_id DESC;";
array_push($params, $von, $bis); array_push($params, $von, $bis);
@@ -222,7 +233,7 @@ class MigrateKalender extends Auth_Controller
$ids = is_array($block->reservierung_ids) ? $block->reservierung_ids : explode(',', $block->reservierung_ids); $ids = is_array($block->reservierung_ids) ? $block->reservierung_ids : explode(',', $block->reservierung_ids);
$this->SyncReservierungModel->db->where('reservierung_id IN (' . implode(',', $ids) . ')'); $this->SyncReservierungModel->db->where_in('reservierung_id', $ids);
$sync_result = $this->SyncReservierungModel->load(); $sync_result = $this->SyncReservierungModel->load();
if (!hasData($sync_result)) if (!hasData($sync_result))
@@ -379,21 +390,30 @@ class MigrateKalender extends Auth_Controller
if ($typ === 'lehreinheit') if ($typ === 'lehreinheit')
{ {
$this->KalenderLehreinheitModel->insert( $lehreinheit_ids = is_array($block->lehreinheit_id) ? $block->lehreinheit_id : explode(',', $block->lehreinheit_id);
array ( foreach ($lehreinheit_ids as $lehreinheit_id)
'kalender_id' => $kalender_id, {
'lehreinheit_id'=> $block->lehreinheit_id $leResult = $this->KalenderLehreinheitModel->insert(
) array (
); 'kalender_id' => $kalender_id,
'lehreinheit_id'=> $lehreinheit_id
)
);
if (!isSuccess($leResult))
return null;
}
} }
else if ($typ === 'reservierung') else if ($typ === 'reservierung')
{ {
$this->KalenderEventModel->insert(array( $eventResult = $this->KalenderEventModel->insert(array(
'kalender_id' => $kalender_id, 'kalender_id' => $kalender_id,
'titel' => $block->titel, 'titel' => $block->titel,
'beschreibung' => $block->beschreibung 'beschreibung' => $block->beschreibung
)); ));
if (!isSuccess($eventResult))
return null;
if ($block->insertvon) if ($block->insertvon)
{ {
@@ -420,7 +440,10 @@ class MigrateKalender extends Auth_Controller
} }
$semester_range = $this->StudiensemesterModel->getByDateRange($block->von, $block->bis); $semester_range = $this->StudiensemesterModel->getByDateRange($block->von, $block->bis);
if (isError($semester_range)) return $semester_range;
if (isError($semester_range))
return null;
$studiensemester_kurzbz = getData($semester_range)[0]->studiensemester_kurzbz ?? null; $studiensemester_kurzbz = getData($semester_range)[0]->studiensemester_kurzbz ?? null;
$gruppen = is_array($block->gruppen_kurzbz) ? $block->gruppen_kurzbz : explode(',', $block->gruppen_kurzbz ?? ''); $gruppen = is_array($block->gruppen_kurzbz) ? $block->gruppen_kurzbz : explode(',', $block->gruppen_kurzbz ?? '');
@@ -458,18 +481,19 @@ class MigrateKalender extends Auth_Controller
foreach ($block->svg_kombis as $kombi_str) foreach ($block->svg_kombis as $kombi_str)
{ {
$kombi_str = trim($kombi_str, '()'); $kombi_str = trim($kombi_str, '()');
list($sem, $verb, $grp) = explode(',', $kombi_str); list($sem, $verb, $grp, $stg_kz) = explode(',', $kombi_str);
$sem = trim($sem) === '' ? null : trim($sem); $sem = trim($sem) === '' ? null : trim($sem);
$verb = trim($verb) === '' ? null : trim($verb); $verb = trim($verb) === '' ? null : trim($verb);
$grp = trim($grp) === '' ? null : trim($grp); $grp = trim($grp) === '' ? null : trim($grp);
$stg_kz = trim($stg_kz) === '' ? null : trim($stg_kz);
if (is_null($sem) && is_null($verb) && is_null($grp)) if (is_null($sem) && is_null($verb) && is_null($grp) && is_null($stg_kz))
continue; continue;
$this->KalenderEventTeilnehmerModel->insert(array( $this->KalenderEventTeilnehmerModel->insert(array(
'kalender_id' => $kalender_id, 'kalender_id' => $kalender_id,
'studiengang_kz' => $block->studiengang_kz, 'studiengang_kz' => $stg_kz,
'semester' => $sem, 'semester' => $sem,
'verband' => $verb, 'verband' => $verb,
'gruppe' => $grp, 'gruppe' => $grp,
@@ -479,6 +503,22 @@ class MigrateKalender extends Auth_Controller
} }
} }
$orte = is_array($block->ort_kurzbz) ? $block->ort_kurzbz : explode(',', $block->ort_kurzbz ?? '');
foreach ($orte as $ort)
{
$ort = trim($ort);
if (!empty($ort))
{
$this->KalenderOrtModel->insert(
array (
'kalender_id' => $kalender_id,
'ort_kurzbz' => $ort
)
);
}
}
return $kalender_id; return $kalender_id;
} }
@@ -523,6 +563,21 @@ class MigrateKalender extends Auth_Controller
'updatevon' => $block->updatevon 'updatevon' => $block->updatevon
) )
); );
$orte = is_array($block->ort_kurzbz) ? $block->ort_kurzbz : explode(',', $block->ort_kurzbz ?? '');
$orte = array_filter(array_map('trim', $orte));
$this->KalenderOrtModel->delete(array('kalender_id' => $kalender_id));
foreach ($orte as $ort)
{
$this->KalenderOrtModel->insert(
array (
'kalender_id' => $kalender_id,
'ort_kurzbz' => $ort
)
);
}
} }
private function _updateSync($ids, $kalender_id) private function _updateSync($ids, $kalender_id)
+388 -78
View File
@@ -21,9 +21,16 @@ class KalenderLib
$this->_ci->load->model('ressource/Ort_model', 'OrtModel'); $this->_ci->load->model('ressource/Ort_model', 'OrtModel');
$this->_ci->load->model('organisation/gruppe_model', 'GruppeModel'); $this->_ci->load->model('organisation/gruppe_model', 'GruppeModel');
$this->_ci->load->model('organisation/Lehrverband_model', 'LehrverbandModel'); $this->_ci->load->model('organisation/Lehrverband_model', 'LehrverbandModel');
$this->_ci->load->model('ressource/Stunde_model', 'StundeModel');
$this->_ci->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$this->_ci->load->model('system/Variable_model', 'VariableModel');
$this->_ci->load->model('organisation/Ferien_model', 'FerienModel');
$this->_ci->load->library('CollisionChecker'); $this->_ci->load->library('CollisionChecker');
$this->_ci->load->library('PhrasesLib', array('ui')); $this->_ci->load->library('PhrasesLib', array('ui'));
$this->_ci->load->library('VariableLib', array('uid' => getAuthUID()));
} }
@@ -62,9 +69,7 @@ class KalenderLib
teilmitarbeiter.kurzbz as teilnehmer_kurzbz, teilmitarbeiter.kurzbz as teilnehmer_kurzbz,
teilperson.vorname as teilnehmer_vorname, teilperson.vorname as teilnehmer_vorname,
teilperson.nachname as teilnehmer_nachname, teilperson.nachname as teilnehmer_nachname,
teilbenutzer.uid as teilnehmer_uid, teilbenutzer.uid as teilnehmer_uid,
CASE CASE
WHEN tbl_lehreinheitgruppe.gruppe_kurzbz IS NULL THEN WHEN tbl_lehreinheitgruppe.gruppe_kurzbz IS NULL THEN
COALESCE( COALESCE(
@@ -164,7 +169,7 @@ class KalenderLib
'isoend' => $bis->format('c'), 'isoend' => $bis->format('c'),
'tooltip' => 'tip', 'tooltip' => 'tip',
'status_kurzbz' => $row->status_kurzbz, 'status_kurzbz' => $row->status_kurzbz,
'ort_kurzbz' => isset($row->ort_kurzbz) ? $row->ort_kurzbz : '', 'ort_kurzbz' => [],
'lehrform' => isset($row->lehrform_kurzbz) ? $row->lehrform_kurzbz : '', 'lehrform' => isset($row->lehrform_kurzbz) ? $row->lehrform_kurzbz : '',
'lehrfach' => isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '', 'lehrfach' => isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '',
'lehrfach_bez' => isset($row->lehrfach_bezeichnung) ? $row->lehrfach_bezeichnung : '', 'lehrfach_bez' => isset($row->lehrfach_bezeichnung) ? $row->lehrfach_bezeichnung : '',
@@ -178,15 +183,22 @@ class KalenderLib
'gruppe' => [], 'gruppe' => [],
'titel' => isset($row->titel) ? $row->titel : '', 'titel' => isset($row->titel) ? $row->titel : '',
'beschreibung' => isset($row->beschreibung) ? $row->beschreibung : '', 'beschreibung' => isset($row->beschreibung) ? $row->beschreibung : '',
'topic' => (isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '').' '.(isset($row->lehrform_kurzbz) ? $row->lehrform_kurzbz : ''), 'topic' => [],
'has_assigned_resources' => isset($row->has_assigned_resources) ? $row->has_assigned_resources : false,
'collisions' => false, 'collisions' => false,
'has_assigned_resources' => isset($row->has_assigned_resources) ? $row->has_assigned_resources : false
]; ];
} }
if ($row->lehreinheit_id && !in_array($row->lehreinheit_id, $events[$id]->lehreinheit_id)) if ($row->lehreinheit_id && !in_array($row->lehreinheit_id, $events[$id]->lehreinheit_id))
$events[$id]->lehreinheit_id[] = $row->lehreinheit_id; $events[$id]->lehreinheit_id[] = $row->lehreinheit_id;
if (isset($row->ort_kurzbz) && $row->ort_kurzbz !== '' && !in_array($row->ort_kurzbz, $events[$id]->ort_kurzbz))
$events[$id]->ort_kurzbz[] = $row->ort_kurzbz;
$topic = trim((isset($row->lehrfach_kurzbz) ? $row->lehrfach_kurzbz : '').' '.(isset($row->lehrform_kurzbz) ? $row->lehrform_kurzbz : ''));
if ($topic !== '' && !in_array($topic, $events[$id]->topic))
$events[$id]->topic[] = $topic;
if ($row->mitarbeiter_uid) if ($row->mitarbeiter_uid)
{ {
if (!in_array($row->mitarbeiter_uid, array_column($events[$id]->lektor, 'mitarbeiter_uid'))) if (!in_array($row->mitarbeiter_uid, array_column($events[$id]->lektor, 'mitarbeiter_uid')))
@@ -341,7 +353,13 @@ class KalenderLib
$this->_ci->KalenderModel->db->where('tbl_kalender_ort.ort_kurzbz', $ort); $this->_ci->KalenderModel->db->where('tbl_kalender_ort.ort_kurzbz', $ort);
if (!is_null($uids)) if (!is_null($uids))
{
$this->_ci->KalenderModel->db->group_start();
$this->_ci->KalenderModel->db->where_in('tbl_lehreinheitmitarbeiter.mitarbeiter_uid', $uids); $this->_ci->KalenderModel->db->where_in('tbl_lehreinheitmitarbeiter.mitarbeiter_uid', $uids);
$this->_ci->KalenderModel->db->or_where_in('orginasator.uid', $uids);
$this->_ci->KalenderModel->db->or_where_in('teilnehmer.uid', $uids);
$this->_ci->KalenderModel->db->group_end();
}
if (!is_null($stg)) if (!is_null($stg))
$this->_ci->KalenderModel->db->where('tbl_lehrveranstaltung.studiengang_kz', $stg); $this->_ci->KalenderModel->db->where('tbl_lehrveranstaltung.studiengang_kz', $stg);
@@ -500,10 +518,57 @@ class KalenderLib
return $zeitwuensche_array; return $zeitwuensche_array;
} }
public function addKalenderEvent($start_date, $end_date, $lehreinheit_id, $ort_kurzbz)
{
$this->_ci->KalenderModel->db->trans_start();
public function addToKalenderEvent($target_kalender_id, $lehreinheit_id)
{
$check = $this->_ci->KalenderLehreinheitModel->loadWhere(array('kalender_id' => $target_kalender_id, 'lehreinheit_id' => $lehreinheit_id));
if (hasData($check))
{
return error([
'message' => $this->_ci->phraseslib->t('ui', 'already_present'),
'errorCode' => 'already_present'
]);
}
$kalenderlehreinheitresult = $this->_ci->KalenderLehreinheitModel->insert(
array(
'kalender_id' => $target_kalender_id,
'lehreinheit_id' => $lehreinheit_id
)
);
if (isError($kalenderlehreinheitresult))
return $kalenderlehreinheitresult;
return success($target_kalender_id);
}
private function _addWeeks($date_str, $weeks)
{
$d = new DateTime($date_str);
$d->modify('+' . $weeks . ' week');
return $d->format('Y-m-d');
}
//TODO (david) studiengang_kz übergeben
private function _isFerien($date_str)
{
$this->_ci->FerienModel->db->where('vondatum <=', $date_str);
$this->_ci->FerienModel->db->where('bisdatum >=', $date_str);
$this->_ci->FerienModel->db->where('studiengang_kz =', 0);
$ferien_result = $this->_ci->FerienModel->load();
if (isError($ferien_result))
return false;
if (hasData($ferien_result))
return true;
}
private function _insertKalenderEventRaw($start_date, $end_date, $lehreinheit_id, $ort_kurzbz)
{
$kalenderresult = $this->_ci->KalenderModel->insert( $kalenderresult = $this->_ci->KalenderModel->insert(
array ( array (
'von' => $start_date, 'von' => $start_date,
@@ -516,50 +581,244 @@ class KalenderLib
) )
); );
if (isSuccess($kalenderresult) && hasData($kalenderresult)) if (!isSuccess($kalenderresult) || !hasData($kalenderresult))
return ['kalender_id' => null, 'result' => $kalenderresult];
$kalender_id = getData($kalenderresult);
$kalenderlehreinheitresult = $this->_ci->KalenderLehreinheitModel->insert(
array (
'kalender_id' => $kalender_id,
'lehreinheit_id' => $lehreinheit_id
)
);
if (isSuccess($kalenderlehreinheitresult) && !is_null($ort_kurzbz))
{ {
$kalender_id = getData($kalenderresult); $ortresult = $this->_addKalenderOrt($kalender_id, $ort_kurzbz);
if (isError($ortresult))
$kalenderlehreinheitresult = $this->_ci->KalenderLehreinheitModel->insert( return ['kalender_id' => $kalender_id, 'result' => $ortresult];
array (
'kalender_id' => $kalender_id,
'lehreinheit_id' => $lehreinheit_id
)
);
if (isSuccess($kalenderlehreinheitresult) && !is_null($ort_kurzbz))
{
$ortresult = $this->_addKalenderOrt($kalender_id, $ort_kurzbz);
if (isError($ortresult))
{
$this->_ci->KalenderModel->db->trans_rollback();
return $ortresult;
}
}
$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 $kalenderlehreinheitresult;
} }
$entryResult = $this->_loadKalenderEntry($kalender_id);
if (isError($entryResult))
return ['kalender_id' => $kalender_id, 'result' => $entryResult];
$kalender_entry = getData($entryResult);
$errors = $this->_ci->collisionchecker->run($kalender_entry);
if (!empty($errors))
return ['kalender_id' => $kalender_id, 'result' => error($errors)];
return ['kalender_id' => $kalender_id, 'result' => $kalenderlehreinheitresult];
}
public function calculateMultiWeekPlan($start_date, $end_date, $lehreinheit_id, $ort_kurzbz)
{
$this->_ci->load->library('RaumvorschlagLib');
$le_result = $this->_ci->LehreinheitModel->load($lehreinheit_id);
if (!hasData($le_result))
return error('Lehreinheit nicht gefunden');
$le = getData($le_result)[0];
$block = $le->stundenblockung;
$wochenrythmus = $le->wochenrythmus;
$studiensemester_kurzbz = $le->studiensemester_kurzbz;
if ($block <= 0)
return error('Stundenblockung ist ungültig');
if ($wochenrythmus <= 0)
return error('Wochenrythmus ist ungültig');
$offen_result = $this->_ci->LehreinheitModel->getOffeneStunden([$lehreinheit_id]);
if (!hasData($offen_result))
return error('Offene Stunden nicht gefunden');
$rows = getData($offen_result);
$offenestunden_werte = array_values(array_unique(array_column($rows, 'offenestunden')));
if (count($offenestunden_werte) > 1)
return error('Offene Stunden sind nicht eindeutig');
$rest = $offenestunden_werte[0];
if ($rest <= 0)
return error('Es sind bereits alle Stunden verplant');
$studiensemester = $this->_ci->StudiensemesterModel->loadWhere(array('studiensemester_kurzbz' => $studiensemester_kurzbz));
if (!hasData($studiensemester))
return error('Studiensemester nicht gefunden');
$semesterende = getData($studiensemester)[0]->ende;
$start_time = (new DateTime($start_date))->format('H:i:s');
$start_stunde_result = $this->_ci->StundeModel->loadWhere(array('beginn' => $start_time));
if (!hasData($start_stunde_result))
return error('Startzeit entspricht keinem gültigen Raster');
$start_stunde_nr = getData($start_stunde_result)[0]->stunde;
$current_date = (new DateTime($start_date))->format('Y-m-d');
$ende = new DateTime($semesterende);
$plan = [];
$skipped_weeks = [];
$errors = [];
$original_ignore = $this->_ci->variablelib->getVar('ignore_kollision');
$this->_ci->VariableModel->setVariable(
getAuthUID(),
'ignore_kollision',
'false'
);
$this->_ci->KalenderModel->db->trans_start();
while ($rest > 0 && new DateTime($current_date) < $ende)
{
$max_skip = 52;
$skip_count = 0;
while ($this->_isFerien($current_date) && $skip_count < $max_skip)
{
$skipped_weeks[] = $current_date;
$current_date = $this->_addWeeks($current_date, $wochenrythmus);
$skip_count++;
}
if ($skip_count >= $max_skip)
{
$errors[] = ['message' => 'Keine ferienfreie Woche bis Semesterende gefunden.'];
break;
}
if (new DateTime($current_date) >= $ende)
break;
$current_block = ($rest < $block) ? $rest : $block;
$end_stunde_nr = $start_stunde_nr + $current_block - 1;
$end_stunde_result = $this->_ci->StundeModel->loadWhere(array('stunde' => $end_stunde_nr));
if (!hasData($end_stunde_result))
{
$errors[] = [
'datum' => $current_date,
'message' => 'Endzeit entspricht keinem gültigen Raster',
];
$current_date = $this->_addWeeks($current_date, $wochenrythmus);
continue;
}
$end_time = getData($end_stunde_result)[0]->ende;
$start_str = $current_date . ' ' . $start_time;
$end_str = $current_date . ' ' . $end_time;
$insert = $this->_insertKalenderEventRaw($start_str, $end_str, $lehreinheit_id, $ort_kurzbz);
$kalender_id = $insert['kalender_id'];
$insert_result = $insert['result'];
$collisions = isError($insert_result) ? getError($insert_result) : [];
$raum_vorschlaege = [];
if ($kalender_id)
{
$vorschlaege = $this->_ci->raumvorschlaglib->getVorschlaege($kalender_id);
$raum_vorschlaege = is_array($vorschlaege) ? $vorschlaege : [];
}
$plan[] = [
'datum' => $current_date,
'start' => $start_str,
'ende' => $end_str,
'block' => $current_block,
'ort_kurzbz' => $ort_kurzbz,
'collisions' => $collisions,
'raum_vorschlaege' => $raum_vorschlaege,
];
$rest -= $current_block;
$current_date = $this->_addWeeks($current_date, $wochenrythmus);
}
$this->_ci->KalenderModel->db->trans_rollback(); $this->_ci->KalenderModel->db->trans_rollback();
return $kalenderresult;
$this->_ci->VariableModel->setVariable(
getAuthUID(),
'ignore_kollision',
$original_ignore
);
return success([
'plan' => $plan,
'skipped_weeks' => $skipped_weeks,
'errors' => $errors,
'rest_offen' => max(0, $rest),
'lehreinheit_id' => $lehreinheit_id,
'ort_kurzbz' => $ort_kurzbz,
]);
}
public function confirmMultiWeekPlan($plan, $lehreinheit_id)
{
$created = [];
$errors = [];
$original_ignore = $this->_ci->variablelib->getVar('ignore_kollision');
$this->_ci->VariableModel->setVariable(
getAuthUID(),
'ignore_kollision',
'true'
);
foreach ($plan as $termin)
{
$ort_kurzbz = $termin['selected_ort_kurzbz'] ?? $termin['ort_kurzbz'];
$result = $this->addKalenderEvent($termin['start'], $termin['ende'], $lehreinheit_id, $ort_kurzbz);
if (isError($result))
{
$errors[] = [
'datum' => $termin['datum'],
'message' => getError($result),
];
}
else
{
$created[] = getData($result);
}
}
$this->_ci->VariableModel->setVariable(
getAuthUID(),
'ignore_kollision',
$original_ignore
);
return success([
'created' => $created,
'errors' => $errors,
]);
}
public function addKalenderEvent($start_date, $end_date, $lehreinheit_id, $ort_kurzbz)
{
$this->_ci->KalenderModel->db->trans_start();
$insert = $this->_insertKalenderEventRaw($start_date, $end_date, $lehreinheit_id, $ort_kurzbz);
$result = $insert['result'];
if (isError($result))
{
$this->_ci->KalenderModel->db->trans_rollback();
return $result;
}
$this->_ci->KalenderModel->db->trans_complete();
return $result;
} }
public function addReservierung($titel, $beschreibung, $ort_kurzbz, $start_date, $end_date, $teilnehmer, $specialFinalGroups, $specialGroups, $groups) public function addReservierung($titel, $beschreibung, $ort_kurzbz, $start_date, $end_date, $teilnehmer, $specialFinalGroups, $specialGroups, $groups)
@@ -853,8 +1112,8 @@ class KalenderLib
}, getData($calendarResources)); }, getData($calendarResources));
} }
$kalender_entry->ort_kurzbz = $ort_kurzbz ?? $kalender_entry->ort_kurzbz;
$kalender_entry->betriebsmittel_ids = $calendarResourcesIDs; $kalender_entry->betriebsmittel_ids = $calendarResourcesIDs;
$kalender_entry->ort_kurzbz = !is_null($ort_kurzbz) ? (array) $ort_kurzbz : $kalender_entry->ort_kurzbz;
$kalender_entry->von = $start_time ?? $kalender_entry->von; $kalender_entry->von = $start_time ?? $kalender_entry->von;
$kalender_entry->bis = $end_time ?? $kalender_entry->bis; $kalender_entry->bis = $end_time ?? $kalender_entry->bis;
@@ -862,7 +1121,7 @@ class KalenderLib
if (!empty($errors)) return error($errors); if (!empty($errors)) return error($errors);
if (!is_null($ort_kurzbz) && !isEmptyString($ort_kurzbz)) if (!is_null($ort_kurzbz) && !empty($ort_kurzbz))
{ {
$result = $this->updateOrt($kalender_id, $ort_kurzbz); $result = $this->updateOrt($kalender_id, $ort_kurzbz);
if (isError($result)) return $result; if (isError($result)) return $result;
@@ -914,8 +1173,9 @@ class KalenderLib
); );
} }
public function updateOrt($kalender_id, $ort_kurzbz) public function updateOrt($kalender_id, $ort_kurzbz_array)
{ {
$ort_kurzbz_array = (array)$ort_kurzbz_array;
$entryResult = $this->_loadKalenderEntry($kalender_id); $entryResult = $this->_loadKalenderEntry($kalender_id);
if (isError($entryResult)) return $entryResult; if (isError($entryResult)) return $entryResult;
@@ -932,21 +1192,29 @@ class KalenderLib
} }
if (in_array($kalender_entry->status_kurzbz, array('live', 'preview'))) if (in_array($kalender_entry->status_kurzbz, array('live', 'preview')))
return $this->_createHistoryEntry($kalender_entry, null, null, $ort_kurzbz); return $this->_createHistoryEntry($kalender_entry, null, null, $ort_kurzbz_array);
} }
$exist = $this->_ci->KalenderOrtModel->load(array('kalender_id' => $kalender_id)); $bestehende = $this->_ci->KalenderOrtModel->load(array('kalender_id' => $kalender_id));
if (hasData($exist)) $alte_orte = hasData($bestehende) ? array_column(getData($bestehende), 'ort_kurzbz') : [];
$zu_loeschen = array_diff($alte_orte, $ort_kurzbz_array);
$neu_anzulegen = array_diff($ort_kurzbz_array, $alte_orte);
if (!empty($zu_loeschen))
{ {
return $this->_ci->KalenderOrtModel->update( $this->_ci->KalenderOrtModel->db->where('kalender_id', $kalender_id);
array('kalender_id' => $kalender_id), $this->_ci->KalenderOrtModel->db->where_in('ort_kurzbz', $zu_loeschen);
array('ort_kurzbz' => $ort_kurzbz) $this->_ci->KalenderOrtModel->db->delete('lehre.tbl_kalender_ort');
);
} }
else
foreach ($neu_anzulegen as $ort_kurzbz)
{ {
return $this->_addKalenderOrt($kalender_id, $ort_kurzbz); $result = $this->_addKalenderOrt($kalender_id, $ort_kurzbz);
if (isError($result)) return $result;
} }
return success();
} }
public function updateStatus($kalender_id, $status_kurzbz) public function updateStatus($kalender_id, $status_kurzbz)
@@ -981,7 +1249,7 @@ class KalenderLib
return success(); return success();
} }
private function _createHistoryEntry($kalender_entry, $start_date = null, $end_date = null, $ort_kurzbz = null) private function _createHistoryEntry($kalender_entry, $start_date = null, $end_date = null, $ort_kurzbz_array = null)
{ {
$old_id = $kalender_entry->kalender_id; $old_id = $kalender_entry->kalender_id;
@@ -1003,24 +1271,39 @@ class KalenderLib
$new_kalender_id = getData($kalenderresult); $new_kalender_id = getData($kalenderresult);
$kalenderlehreinheitresult = $this->_ci->KalenderLehreinheitModel->insert( $lehreinheit_ids = (array) $kalender_entry->lehreinheit_id;
array(
'kalender_id' => $new_kalender_id,
'lehreinheit_id' => $kalender_entry->lehreinheit_id
)
);
if (!isSuccess($kalenderlehreinheitresult)) foreach ($lehreinheit_ids as $lehreinheit_id)
return $kalenderlehreinheitresult; {
$kalenderlehreinheitresult = $this->_ci->KalenderLehreinheitModel->insert(
array(
'kalender_id' => $new_kalender_id,
'lehreinheit_id' => $lehreinheit_id
)
);
$new_ort = $ort_kurzbz ? $ort_kurzbz : $kalender_entry->ort_kurzbz; if (!isSuccess($kalenderlehreinheitresult))
if (!is_null($new_ort) || !is_null($kalender_entry->location)) return $kalenderlehreinheitresult;
}
if (!empty($ort_kurzbz_array))
{
$new_orte = (array) $ort_kurzbz_array;
$new_locations = array_fill(0, count($new_orte), null);
}
else
{
$new_orte = (array) $kalender_entry->ort_kurzbz;
$new_locations = (array) $kalender_entry->location;
}
foreach ($new_orte as $index => $ort)
{ {
$ortresult = $this->_ci->KalenderOrtModel->insert( $ortresult = $this->_ci->KalenderOrtModel->insert(
array( array(
'kalender_id' => $new_kalender_id, 'kalender_id' => $new_kalender_id,
'ort_kurzbz' => $new_ort, 'ort_kurzbz' => $ort,
'location' => $kalender_entry->location 'location' => $new_locations[$index] ?? null
) )
); );
@@ -1033,9 +1316,31 @@ class KalenderLib
private function _loadKalenderEntry($kalender_id) private function _loadKalenderEntry($kalender_id)
{ {
$this->_ci->KalenderModel->addSelect('tbl_kalender.*, tbl_kalender_lehreinheit.lehreinheit_id, tbl_kalender_ort.ort_kurzbz, tbl_kalender_ort.location'); $this->_ci->KalenderModel->addSelect('
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit', 'tbl_kalender.kalender_id = tbl_kalender_lehreinheit.kalender_id', 'LEFT'); tbl_kalender.*,
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'tbl_kalender.kalender_id = tbl_kalender_ort.kalender_id', 'LEFT'); le.lehreinheit_id,
orte.ort_kurzbz,
orte.location
');
$this->_ci->KalenderModel->addJoin(
'(SELECT kalender_id,
array_agg(lehreinheit_id) AS lehreinheit_id
FROM lehre.tbl_kalender_lehreinheit
GROUP BY kalender_id) le',
'tbl_kalender.kalender_id = le.kalender_id',
'LEFT'
);
$this->_ci->KalenderModel->addJoin(
'(SELECT kalender_id,
array_agg(ort_kurzbz ORDER BY ort_kurzbz) AS ort_kurzbz,
array_agg(location ORDER BY ort_kurzbz) AS location
FROM lehre.tbl_kalender_ort
GROUP BY kalender_id) orte',
'tbl_kalender.kalender_id = orte.kalender_id',
'LEFT'
);
$result = $this->_ci->KalenderModel->load(array('tbl_kalender.kalender_id' => $kalender_id)); $result = $this->_ci->KalenderModel->load(array('tbl_kalender.kalender_id' => $kalender_id));
@@ -1043,7 +1348,12 @@ class KalenderLib
if (!hasData($result)) if (!hasData($result))
return error(['message' => 'Not found', 'errorCode' => 'not_found']); return error(['message' => 'Not found', 'errorCode' => 'not_found']);
return success(getData($result)[0]); $entry = getData($result)[0];
$entry->lehreinheit_id = $entry->lehreinheit_id ?? [];
$entry->ort_kurzbz = $entry->ort_kurzbz ?? [];
$entry->location = $entry->location ?? [];
return success($entry);
} }
+56 -23
View File
@@ -89,62 +89,95 @@ class RaumvorschlagLib
} }
private function _getOrtDetails($ort_kurzbz) private function _getOrtDetails($ort_kurzbz_array)
{ {
$ort_kurzbz_array = (array) $ort_kurzbz_array;
if (empty($ort_kurzbz_array)) return [];
$this->_ci->OrtModel->addSelect('ort_kurzbz, stockwerk, standort_id'); $this->_ci->OrtModel->addSelect('ort_kurzbz, stockwerk, standort_id');
$this->_ci->OrtModel->db->where('ort_kurzbz', $ort_kurzbz); $this->_ci->OrtModel->db->where_in('ort_kurzbz', $ort_kurzbz_array);
$result = $this->_ci->OrtModel->load(); $result = $this->_ci->OrtModel->load();
return hasData($result) ? getData($result)[0] : null;
return hasData($result) ? getData($result) : [];
} }
private function _rateLektor(&$rating, $raum, $lektor_davor_ort) private function _rateLektor(&$rating, $raum, $lektor_davor_orte)
{ {
if (!$lektor_davor_ort) return; if (empty($lektor_davor_orte)) return;
if ($lektor_davor_ort->ort_kurzbz === $raum->ort_kurzbz) foreach ($lektor_davor_orte as $ort)
{ {
$rating['score'] += 20; if ($ort->ort_kurzbz === $raum->ort_kurzbz)
$rating['details'][] = '+20 ' . $this->_ci->phraseslib->t('ui', 'lecturer_already_here'); {
return; $rating['score'] += 20;
$rating['details'][] = '+20 ' . $this->_ci->phraseslib->t('ui', 'lecturer_already_here');
return;
}
} }
if ($lektor_davor_ort->standort_id !== $raum->standort_id) $gleiches_gebaeude = array_filter($lektor_davor_orte, function($ort) use ($raum)
{
return $ort->standort_id === $raum->standort_id;
});
if (empty($gleiches_gebaeude))
{ {
$rating['score'] -= 20; $rating['score'] -= 20;
$rating['details'][] = '-20 '. $this->_ci->phraseslib->t('ui', 'lecturer_building_change'); $rating['details'][] = '-20 '. $this->_ci->phraseslib->t('ui', 'lecturer_building_change');
return;
} }
elseif ($lektor_davor_ort->stockwerk !== $raum->stockwerk)
$diffs = array_map(function($ort) use ($raum)
{
return abs($ort->stockwerk - $raum->stockwerk);
}, $gleiches_gebaeude);
$diff = min($diffs);
if ($diff > 0)
{ {
$diff = abs($lektor_davor_ort->stockwerk - $raum->stockwerk);
$rating['score'] -= $diff * 5; $rating['score'] -= $diff * 5;
$rating['details'][] = '-' . ($diff * 5) . ' ' . $this->_ci->phraseslib->t('ui', 'lecturer_floor_change'); $rating['details'][] = '-' . ($diff * 5) . ' ' . $this->_ci->phraseslib->t('ui', 'lecturer_floor_change');
} }
} }
private function _rateGruppen(&$rating, $raum, $gruppen_davor_ort) private function _rateGruppen(&$rating, $raum, $gruppen_davor_orte)
{ {
if (!$gruppen_davor_ort) return; if (empty($gruppen_davor_orte)) return;
if ($gruppen_davor_ort->ort_kurzbz === $raum->ort_kurzbz) foreach ($gruppen_davor_orte as $ort)
{ {
$rating['score'] += 10; if ($ort->ort_kurzbz === $raum->ort_kurzbz)
$rating['details'][] = '+10 ' . $this->_ci->phraseslib->t('ui', 'student_already_here'); {
return; $rating['score'] += 10;
$rating['details'][] = '+10 ' . $this->_ci->phraseslib->t('ui', 'student_already_here');
return;
}
} }
if ($gruppen_davor_ort->standort_id !== $raum->standort_id) $gleiches_gebaeude = array_filter($gruppen_davor_orte, function($ort) use ($raum)
{
return $ort->standort_id === $raum->standort_id;
});
if (empty($gleiches_gebaeude))
{ {
$rating['score'] -= 20; $rating['score'] -= 20;
$rating['details'][] = '-20 '. $this->_ci->phraseslib->t('ui', 'student_building_change'); $rating['details'][] = '-20 '. $this->_ci->phraseslib->t('ui', 'student_building_change');
return;
} }
elseif ($gruppen_davor_ort->stockwerk !== $raum->stockwerk)
$diffs = array_map(function($ort) use ($raum)
{
return abs($ort->stockwerk - $raum->stockwerk);
}, $gleiches_gebaeude);
$diff = min($diffs);
if ($diff > 0)
{ {
$diff = abs($gruppen_davor_ort->stockwerk - $raum->stockwerk);
$rating['score'] -= $diff * 5; $rating['score'] -= $diff * 5;
$rating['details'][] = '-' . ($diff * 5) . ' '. $this->_ci->phraseslib->t('ui', 'student_floor_change'); $rating['details'][] = '-' . ($diff * 5) . ' '. $this->_ci->phraseslib->t('ui', 'student_floor_change');
} }
} }
private function _getEventDavor($events, $von, $uids, $type) private function _getEventDavor($events, $von, $uids, $type)
@@ -20,7 +20,7 @@ class RoomCollisionCheck implements ICollisionCheck
public function check($data) public function check($data)
{ {
if (!isset($data->ort_kurzbz, $data->von, $data->bis, $data->kalender_id)) return []; if (!isset($data->ort_kurzbz, $data->von, $data->bis, $data->kalender_id) || isEmptyArray($data->ort_kurzbz)) return [];
if ($this->_ci->variablelib->getVar('ignore_kollision') === 'true') return []; if ($this->_ci->variablelib->getVar('ignore_kollision') === 'true') return [];
@@ -31,10 +31,10 @@ class RoomCollisionCheck implements ICollisionCheck
$this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)', null, false); $this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)', null, false);
$this->_ci->KalenderModel->db->where_not_in('tbl_kalender.status_kurzbz', ['archived', 'deleted', 'to_delete']); $this->_ci->KalenderModel->db->where_not_in('tbl_kalender.status_kurzbz', ['archived', 'deleted', 'to_delete']);
$this->_ci->KalenderModel->db->where_in('ort_kurzbz', $data->ort_kurzbz);
$result = $this->_ci->KalenderModel->loadWhere(array( $result = $this->_ci->KalenderModel->loadWhere(array(
'von <' => $data->bis, 'von <' => $data->bis,
'bis >' => $data->von, 'bis >' => $data->von,
'ort_kurzbz' => $data->ort_kurzbz,
)); ));
if (isError($result)) return []; if (isError($result)) return [];
@@ -67,6 +67,7 @@ class RoomCollisionCheck implements ICollisionCheck
$result = $this->_ci->KalenderModel->load(); $result = $this->_ci->KalenderModel->load();
if (isError($result) || !hasData($result)) return []; if (isError($result) || !hasData($result)) return [];
$grouped = []; $grouped = [];
@@ -395,6 +395,77 @@ EOSQL;
return $this->execReadOnlyQuery($qry, $params); return $this->execReadOnlyQuery($qry, $params);
} }
public function getOffeneStunden($lehreinheit_ids)
{
if (!is_array($lehreinheit_ids))
$lehreinheit_ids = [$lehreinheit_ids];
if (empty($lehreinheit_ids))
return success([]);
$db = new DB_Model();
$placeholders = implode(',', array_fill(0, count($lehreinheit_ids), '?'));
$qry = "WITH eventdauer AS (
SELECT
tbl_kalender_lehreinheit.lehreinheit_id,
tbl_kalender.von,
tbl_kalender.bis,
tbl_kalender.kalender_id
FROM lehre.tbl_lehreinheit
JOIN lehre.tbl_kalender_lehreinheit USING(lehreinheit_id)
JOIN lehre.tbl_kalender USING(kalender_id)
WHERE tbl_lehreinheit.lehreinheit_id IN ($placeholders)
GROUP BY tbl_kalender_lehreinheit.lehreinheit_id, tbl_kalender.von, tbl_kalender.bis, tbl_kalender.kalender_id
),
raster AS (
SELECT
eventdauer.lehreinheit_id,
eventdauer.kalender_id,
eventdauer.von,
eventdauer.bis,
vonstunde.stunde AS vonstunde_nr,
bisstunde.stunde AS bisstunde_nr
FROM eventdauer
LEFT JOIN lehre.tbl_stunde vonstunde ON eventdauer.von::time = vonstunde.beginn
LEFT JOIN lehre.tbl_stunde bisstunde ON eventdauer.bis::time = bisstunde.ende
),
unterrichtsdauer AS (
SELECT
raster.lehreinheit_id,
raster.kalender_id,
sum(EXTRACT(EPOCH FROM (LEAST(stunde.ende, raster.bis::time) - GREATEST(stunde.beginn, raster.von::time))) / 60) AS minuten
FROM raster JOIN lehre.tbl_stunde stunde ON stunde.beginn < raster.bis::time AND stunde.ende > raster.von::time
WHERE raster.vonstunde_nr IS NULL OR raster.bisstunde_nr IS NULL
GROUP BY raster.lehreinheit_id, raster.kalender_id
),
verplantestunden AS (
SELECT raster.lehreinheit_id, raster.kalender_id,
CASE
WHEN raster.vonstunde_nr IS NOT NULL AND raster.bisstunde_nr IS NOT NULL
THEN (raster.bisstunde_nr - raster.vonstunde_nr + 1)
ELSE coalesce(unterrichtsdauer.minuten, 0) / 45
END AS verplantestunde
FROM raster LEFT JOIN unterrichtsdauer ON unterrichtsdauer.lehreinheit_id = raster.lehreinheit_id AND unterrichtsdauer.kalender_id = raster.kalender_id
),
verplant_gesamt AS (
SELECT lehreinheit_id, SUM(verplantestunde) AS verplant
FROM verplantestunden
GROUP BY lehreinheit_id
)
SELECT
lema.lehreinheit_id,
lema.mitarbeiter_uid,
lema.planstunden,
ROUND(coalesce(verplant_gesamt.verplant, 0), 2) AS verplant,
ROUND((lema.planstunden - coalesce(verplant_gesamt.verplant, 0)), 2) AS offenestunden
FROM lehre.tbl_lehreinheitmitarbeiter lema
LEFT JOIN verplant_gesamt ON verplant_gesamt.lehreinheit_id = lema.lehreinheit_id
WHERE lema.lehreinheit_id IN ($placeholders)";
return $db->execReadOnlyQuery($qry, array_merge($lehreinheit_ids, $lehreinheit_ids));
}
private function getLVTmp($stg_kz = null) private function getLVTmp($stg_kz = null)
{ {
$qry = "SELECT DISTINCT ON(lehrveranstaltung_id) *, $qry = "SELECT DISTINCT ON(lehrveranstaltung_id) *,
+22
View File
@@ -88,6 +88,28 @@ export default {
params: { lehreinheit_id, ort_kurzbz, start_date, end_date} params: { lehreinheit_id, ort_kurzbz, start_date, end_date}
}; };
}, },
calculateMultiWeekPlan(lehreinheit_id, ort_kurzbz, start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/calculateMultiWeekPlan',
params: { lehreinheit_id, ort_kurzbz, start_date, end_date }
};
},
confirmMultiWeekPlan(plan, lehreinheit_id ) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/confirmMultiWeekPlan',
params: { plan, lehreinheit_id }
};
},
addToKalenderEvent(target_kalender_id, lehreinheit_id ) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/addToKalenderEvent',
params: { target_kalender_id, lehreinheit_id }
};
},
getRaumvorschlag(kalender_id) { getRaumvorschlag(kalender_id) {
return { return {
method: 'get', method: 'get',
+4 -1
View File
@@ -199,7 +199,10 @@ export default {
// choose default mode // choose default mode
let mode = this.mode; let mode = this.mode;
if (mode) if (mode)
mode = mode.toLowerCase(); {
const lower = mode.toLowerCase();
mode = Object.keys(this.modes).find(key => key.toLowerCase() === lower) || null;
}
if (!mode || !this.modes[mode]) if (!mode || !this.modes[mode])
mode = Object.keys(this.modes).find(Boolean); // start with first entry as active mode mode = Object.keys(this.modes).find(Boolean); // start with first entry as active mode
return mode || ''; return mode || '';
+4 -2
View File
@@ -398,7 +398,8 @@ export default {
this.onDrop?.({ this.onDrop?.({
item: [obj], item: [obj],
start: dropStart.toISO(), start: dropStart.toISO(),
end: dropEnd.toISO() end: dropEnd.toISO(),
ctrlKey: !!(evt?.ctrlKey || evt?.metaKey)
}); });
}, },
@@ -437,7 +438,8 @@ export default {
this.onDrop?.({ this.onDrop?.({
item: [obj], item: [obj],
start: dropStart.toISO(), start: dropStart.toISO(),
end: dropEnd.toISO() end: dropEnd.toISO(),
ctrlKey: !!(evt?.ctrlKey || evt?.metaKey)
}); });
}, },
handleResizeStart({ edge, evt, el, event }) handleResizeStart({ edge, evt, el, event })
@@ -1,10 +1,12 @@
import draggable from '../../../../../directives/draggable.js'; import draggable from '../../../../../directives/draggable.js';
import drop from '../../../../../directives/drop.js';
import CalClick from '../../../../../directives/Calendar/Click.js'; import CalClick from '../../../../../directives/Calendar/Click.js';
export default { export default {
name: "GridLineEvent", name: "GridLineEvent",
directives: { directives: {
draggable, draggable,
drop,
CalClick CalClick
}, },
emits: [ emits: [
@@ -29,6 +31,10 @@ export default {
contextMenuActions: { contextMenuActions: {
from: "contextMenuActions", from: "contextMenuActions",
default: () => ({}) default: () => ({})
},
onDrop: {
from: "onDrop",
default: () => null
} }
}, },
props: { props: {
@@ -60,6 +66,8 @@ export default {
if (this.event.endsHere) if (this.event.endsHere)
classes.push('event-end'); classes.push('event-end');
} }
classes.push(`calender_id-${this.event.orig.kalender_id}`);
return classes; return classes;
}, },
dragKalenderCollection() { dragKalenderCollection() {
@@ -102,18 +110,45 @@ export default {
evt.dataTransfer.setData('fhc-grab-offset-y', evt.clientY - rect.top); evt.dataTransfer.setData('fhc-grab-offset-y', evt.clientY - rect.top);
evt.dataTransfer.setData('fhc-grab-offset-x', evt.clientX - rect.left); evt.dataTransfer.setData('fhc-grab-offset-x', evt.clientX - rect.left);
}, },
onDropOnCard(evt, items) {
if (this.isHeaderOrFooter || !this.onDrop)
return;
const list = Array.isArray(items) ? items : [items];
const obj = list[0];
if (!obj)
return;
if ((evt.ctrlKey || evt.metaKey) && obj.type === 'lehreinheit')
{
return this.onDrop({
item: [obj],
ctrlKey: true,
targetKalenderId: this.event.orig?.kalender_id ?? null
});
}
return this.onDrop({
item: [obj],
start: this.event.start.toISO(),
end: this.event.end.toISO(),
ctrlKey: false,
targetKalenderId: null
});
},
}, },
template:` template:`
<div <div
class="fhc-calendar-base-grid-line-event event" class="fhc-calendar-base-grid-line-event event"
:class="classes" :class="classes"
style="z-index: 2" style="z-index: 11"
:draggable="draggable" :draggable="draggable"
:data-id="'event-' + event.orig.kalender_id" :data-id="'event-' + event.orig.kalender_id"
:data-group-id="'event-group-' + event.orig.eindeutige_gruppen_id" :data-group-id="'event-group-' + event.orig.eindeutige_gruppen_id"
ref="eventEl" ref="eventEl"
@dragstart="onDragStart" @dragstart="onDragStart"
v-draggable:move.noimage="draggable ? dragKalenderCollection : {}" v-draggable:move.noimage="draggable ? dragKalenderCollection : {}"
v-drop:move.lehreinheit.kalender.reservierung="onDropOnCard"
v-cal-click:event="isHeaderOrFooter ? event : event.orig" v-cal-click:event="isHeaderOrFooter ? event : event.orig"
@contextmenu.prevent="onRightClick" @contextmenu.prevent="onRightClick"
data-cy="calendar-event" data-cy="calendar-event"
@@ -13,6 +13,9 @@ export default {
dragKalenderCollection() { dragKalenderCollection() {
return this.event return this.event
}, },
topicString() {
return Array.isArray(this.event.orig.topic) ? this.event.orig.topic.join(', ') : this.event.orig.topic;
},
}, },
template: ` template: `
<div <div
@@ -22,7 +25,7 @@ export default {
data-cy="calendar-event" data-cy="calendar-event"
> >
<div class="title"> <div class="title">
{{ event.orig.topic || event.orig.titel || event.orig.lehrfach }} {{ topicString || event.orig.titel || event.orig.lehrfach }}
</div> </div>
<div> <div>
{{ event.orig.datum }} {{ event.orig.beginn }}{{ event.orig.ende }} {{ event.orig.datum }} {{ event.orig.beginn }}{{ event.orig.ende }}
@@ -8,7 +8,8 @@ export default {
name: "TableView", name: "TableView",
inject: { inject: {
events: "events", events: "events",
timezone: "timezone" timezone: "timezone",
tableActions: "tableActions",
}, },
components: { components: {
CoreFilterCmpt, CoreFilterCmpt,
@@ -91,6 +92,19 @@ export default {
{ {
openModal() { openModal() {
this.$refs.raumModal.show(); this.$refs.raumModal.show();
},
async deleteSelected() {
let selected = this.$refs.tableViewTable?.tabulator?.getSelectedData() ?? [];
if (!selected.length) return;
let isConfirmed = await this.$fhcAlert.confirmDelete();
if (!isConfirmed) return;
await this.tableActions?.deleteEntries(selected)
this.$refs.tableViewTable.tabulator.deselectRow();
} }
}, },
watch: { watch: {
@@ -98,7 +112,7 @@ export default {
this.$refs.tableViewTable?.tabulator?.setData(newData); this.$refs.tableViewTable?.tabulator?.setData(newData);
} }
}, },
mounted() { /* mounted() {
this.$api.call(ApiDetails.getRaumtyp()) this.$api.call(ApiDetails.getRaumtyp())
.then(result => { .then(result => {
@@ -106,7 +120,7 @@ export default {
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}, },*/
template: /* html */` template: /* html */`
<div class="fhc-calendar-mode-table-view h-100 overflow-auto"> <div class="fhc-calendar-mode-table-view h-100 overflow-auto">
<core-filter-cmpt <core-filter-cmpt
@@ -117,12 +131,13 @@ export default {
:download="true" :download="true"
> >
<template #actions> <template #actions>
<button @click="deleteSelected" class="btn btn-outline-danger btn-sm">Löschen</button>
<button class="btn btn-outline-secondary btn-sm">Verschieben</button> <button class="btn btn-outline-secondary btn-sm">Verschieben</button>
<button @click="openModal" class="btn btn-outline-secondary btn-sm">Raum wechsel</button> <button @click="openModal" class="btn btn-outline-secondary btn-sm">Raum wechsel</button>
</template> </template>
</core-filter-cmpt> </core-filter-cmpt>
<bs-modal ref="raumModal" class="bootstrap-prompt" dialogClass="modal-lg"> <!--<bs-modal ref="raumModal" class="bootstrap-prompt" dialogClass="modal-lg">
<template #title>Raum verschiebung</template> <template #title>Raum verschiebung</template>
<form-input <form-input
:label="$p.t('lehre', 'raumtyp')" :label="$p.t('lehre', 'raumtyp')"
@@ -141,7 +156,7 @@ export default {
<template #footer> <template #footer>
<button type="button" class="btn btn-primary">{{ $p.t('ui', 'speichern') }}</button> <button type="button" class="btn btn-primary">{{ $p.t('ui', 'speichern') }}</button>
</template> </template>
</bs-modal> </bs-modal>-->
</div> </div>
` `
} }
+69 -53
View File
@@ -9,57 +9,65 @@ import ApiKalender from "../../api/factory/tempus/kalender.js";
import draggable from "../../directives/draggable.js"; import draggable from "../../directives/draggable.js";
export default { export default {
name: "CalendarTempus", name: "CalendarTempus",
components: { components: {
FhcCalendar, FhcCalendar
}, },
inject: { inject: {
renderers: { from: "renderers" }, renderers: {from: 'renderers'},
appConfig: { appConfig: {
from: "appConfig", from: 'appConfig',
default: { default: {
visible_status: "all", visible_status: 'all'
}, }
}, }
}, },
directives: { directives: {
draggable, draggable,
}, },
props: { props: {
timezone: { timezone: {
type: String, type: String,
required: true, required: true
}, },
date: { date: {
type: [Date, String, Number, luxon.DateTime], type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local(), default: luxon.DateTime.local()
}, },
mode: { mode: {
type: String, type: String,
default: "Week", default: 'Week'
}, },
getPromiseFunc: { getPromiseFunc: {
type: Function, type: Function,
required: true, required: true
}, },
parkedEvents: { parkedEvents: {
type: Object, type: Object,
default: () => new Set(), default: () => new Set()
}, },
visibleLecturers: { visibleLecturers: {
type: Array, type: Array,
default: null, default: null
}, },
extraBackgrounds: { extraBackgrounds: {
type: Array, type: Array,
default: () => [], default: () => []
}, },
visibleStatus: { visibleStatus: {
type: Array, type: Array,
default: () => ["all"], default: () => ['all']
}, },
}, },
emits: ["update:date", "update:mode", "update:range", "drop", "resize"], emits: [
"update:date",
"update:mode",
"update:range",
"drop",
"resize",
"event-hover",
"event-unhover"
],
data() { data() {
return { return {
@@ -149,6 +157,12 @@ export default {
resetEventLoader() { resetEventLoader() {
this.reset(); this.reset();
}, },
navigatePrev() {
this.$refs.calendar.clickPrev();
},
navigateNext() {
this.$refs.calendar.clickNext();
},
clearOutCalendarEventEmphasis() { clearOutCalendarEventEmphasis() {
this.$refs.calendar.$el this.$refs.calendar.$el
.querySelectorAll( .querySelectorAll(
@@ -234,6 +248,8 @@ export default {
:class="['event-type-' + event.type + ' ' + mode + 'PageContainer', { 'event--parked': parkedEvents.has(String(event.kalender_id)) }]" :class="['event-type-' + event.type + ' ' + mode + 'PageContainer', { 'event--parked': parkedEvents.has(String(event.kalender_id)) }]"
:type="mode == 'day' ? 'button' : undefined" :type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)" :style="eventStyle(event)"
@mouseenter="$emit('event-hover', event)"
@mouseleave="$emit('event-unhover', event)"
> >
<component <component
v-if="mode == 'event'" v-if="mode == 'event'"
@@ -276,5 +292,5 @@ export default {
</div> </div>
</div> </div>
</template> </template>
</fhc-calendar>`, </fhc-calendar>`
}; }
+121 -10
View File
@@ -24,21 +24,56 @@ export default {
return { return {
searchparam: '', searchparam: '',
allCourses: [], allCourses: [],
sortBy: null,
multiWeekIds: new Set()
} }
}, },
computed: { computed: {
courses() { courses() {
const query = (this.searchparam ?? '').trim().toLowerCase(); const query = (this.searchparam ?? '').trim().toLowerCase();
if (!query)
return this.allCourses;
return this.allCourses.filter(course => let result;
course.showname.toLowerCase().includes(query) || if (query)
course.lektoren?.some(l => {
l.name.toLowerCase().includes(query) || result = this.allCourses.filter(course =>
l.kurzbz.toLowerCase().includes(query) course.showname.toLowerCase().includes(query) ||
) course.lektoren?.some(lektor =>
); lektor.name.toLowerCase().includes(query) ||
lektor.kurzbz.toLowerCase().includes(query)
)
);
}
else
result = this.allCourses;
if (this.sortBy === 'lektor-asc' || this.sortBy === 'lektor-desc')
{
let dir = this.sortBy === 'lektor-asc' ? 1 : -1;
return result.sort((a, b) => {
let an = a.lektoren?.[0]?.kurzbz ?? '';
let bn = b.lektoren?.[0]?.kurzbz ?? '';
return an.localeCompare(bn) * dir;
});
}
else if (this.sortBy === 'kw-asc')
{
return result.sort((a, b) => (a.start_kw ?? 0) - (b.start_kw ?? 0));
}
else if (this.sortBy === 'kw-desc')
{
return result.sort((a, b) => (b.start_kw ?? 0) - (a.start_kw ?? 0));
}
else if (this.sortBy === 'stunden-asc')
{
return result.sort((a, b) => (a.offenestunden ?? 0) - (b.offenestunden ?? 0));
}
else if (this.sortBy === 'stunden-desc')
{
return result.sort((a, b) => (b.offenestunden ?? 0) - (a.offenestunden ?? 0));
}
return result;
} }
}, },
watch: { watch: {
@@ -94,6 +129,7 @@ export default {
id: orig.lehreinheit_id, id: orig.lehreinheit_id,
orig: orig, orig: orig,
stundenblockung: course.stundenblockung, stundenblockung: course.stundenblockung,
multiweek: this.isMultiWeek(course),
}; };
}, },
courseStyle(course) { courseStyle(course) {
@@ -104,7 +140,44 @@ export default {
}, },
selectLecturer(lektor) { selectLecturer(lektor) {
this.$emit('select-lecturer', lektor); this.$emit('select-lecturer', lektor);
} },
setSort(field) {
if (this.sortBy === `${field}-asc`)
this.sortBy = `${field}-desc`;
else if (this.sortBy === `${field}-desc`)
this.sortBy = null;
else
this.sortBy = `${field}-asc`;
},
sortIcon(field) {
if (this.sortBy === `${field}-asc`)
return 'fa-arrow-up';
if (this.sortBy === `${field}-desc`)
return 'fa-arrow-down';
return 'fa-sort';
},
isSortActive(field) {
return this.sortBy === `${field}-asc` || this.sortBy === `${field}-desc`;
},
reload()
{
this.loadCoursesByStg(this.stg);
},
toggleMultiWeek(course)
{
let id = course.lehreinheit_id[0];
let weekIds = new Set(this.multiWeekIds);
if (weekIds.has(id))
weekIds.delete(id);
else
weekIds.add(id);
this.multiWeekIds = weekIds;
},
isMultiWeek(course)
{
return this.multiWeekIds.has(course.lehreinheit_id[0]);
},
}, },
template: ` template: `
<div class="course-picker d-flex flex-column h-100"> <div class="course-picker d-flex flex-column h-100">
@@ -114,6 +187,36 @@ export default {
type="text" type="text"
v-model="searchparam" v-model="searchparam"
/> />
<div class="d-flex gap-1 mt-2">
<button
type="button"
class="btn btn-sm btn-outline-secondary"
:disabled="!stg"
@click="reload">
<i class="fa fa-rotate-right"></i>
</button>
<button
type="button"
class="btn btn-sm"
:class="isSortActive('lektor') ? 'btn-primary' : 'btn-outline-secondary'"
@click="setSort('lektor')">
Lkt <i class="fa" :class="sortIcon('lektor')"></i>
</button>
<button
type="button"
class="btn btn-sm"
:class="isSortActive('kw') ? 'btn-primary' : 'btn-outline-secondary'"
@click="setSort('kw')">
KW <i class="fa" :class="sortIcon('kw')"></i>
</button>
<button
type="button"
class="btn btn-sm"
:class="isSortActive('stunden') ? 'btn-primary' : 'btn-outline-secondary'"
@click="setSort('stunden')">
Std <i class="fa" :class="sortIcon('stunden')"></i>
</button>
</div>
</div> </div>
<div v-if="!stg" class="d-flex flex-column align-items-center justify-content-center text-center text-muted py-5 px-3 h-100"> <div v-if="!stg" class="d-flex flex-column align-items-center justify-content-center text-center text-muted py-5 px-3 h-100">
<span class="small fw-semibold mb-1">Keine Lehreinheiten</span> <span class="small fw-semibold mb-1">Keine Lehreinheiten</span>
@@ -123,6 +226,7 @@ export default {
<div <div
v-for="course in courses" v-for="course in courses"
:key="course.lehreinheit_id" :key="course.lehreinheit_id"
style="cursor:grab"
:style="courseStyle(course)" :style="courseStyle(course)"
class="course-picker-row" class="course-picker-row"
v-draggable:move.noimage="dragLehreinheitCollection(course)" v-draggable:move.noimage="dragLehreinheitCollection(course)"
@@ -131,6 +235,13 @@ export default {
<div class="d-flex gap-1"> <div class="d-flex gap-1">
<span class="fw-semibold small w-50" v-tooltip="course.lehrfach_bez">{{ course.lehrfach }} {{ course.lehrform }}</span> <span class="fw-semibold small w-50" v-tooltip="course.lehrfach_bez">{{ course.lehrfach }} {{ course.lehrform }}</span>
<span class="fw-semibold small w-50" v-tooltip="course.raumtypalternativ">{{ course.raumtyp }}</span> <span class="fw-semibold small w-50" v-tooltip="course.raumtypalternativ">{{ course.raumtyp }}</span>
<i
class="fa fa-calendar-week"
:class="isMultiWeek(course) ? 'text-primary' : 'text-muted'"
style="cursor:pointer"
v-tooltip="isMultiWeek(course) ? 'MultiWeek aktiv' : 'MultiWeek Verplanung aktivieren'"
@click.stop="toggleMultiWeek(course)"
></i>
</div> </div>
<!--TODO(david) entfernen, dient nur für das mappen mit der lvverwaltung--> <!--TODO(david) entfernen, dient nur für das mappen mit der lvverwaltung-->
@@ -0,0 +1,56 @@
export default {
name: "KeyboardShortcuts",
props: {
shortcuts: {
type: Array,
default: () => []
},
},
methods: {
isInputFocused(target)
{
if (!target)
return false;
return (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target.isContentEditable)
},
shortcutMatches(shortcut, e)
{
let shortcutWantsCtrl = shortcut.ctrl === true;
let ctrlIsPressed = e.ctrlKey || e.metaKey;
if (shortcutWantsCtrl !== ctrlIsPressed)
return false;
if (e.key !== shortcut.key)
return false;
let shouldSkipWhenTyping = shortcut.skipWhenTyping !== false;
return !(shouldSkipWhenTyping && this.isInputFocused(e.target));
},
handleKeydown(e)
{
for (let shortcut of this.shortcuts)
{
if (!this.shortcutMatches(shortcut, e))
continue;
let shouldPreventDefault = shortcut.preventDefault !== false;
if (shouldPreventDefault)
e.preventDefault();
shortcut.handler(e);
return;
}
}
},
mounted() {
window.addEventListener('keydown', this.handleKeydown);
},
beforeUnmount() {
window.removeEventListener('keydown', this.handleKeydown);
},
render() {
return null;
}
};
@@ -0,0 +1,191 @@
import ApiKalender from '../../api/factory/tempus/kalender.js';
import BsModal from '../../../js/components/Bootstrap/Modal.js';
export default {
components: {
BsModal,
},
props: {
show: { type: Boolean, default: false },
lehreinheitId: { type: [String, Number], default: null },
ortKurzbz: { type: String, default: null },
startTime: { type: String, default: null },
endTime: { type: String, default: null },
},
emits: ['close', 'confirmed'],
data() {
return {
loading: false,
plan: [],
skippedWeeks: [],
errors: [],
restOffen: 0,
};
},
watch: {
show(val) {
if (val) {
this.loadPlan();
this.$refs.bsModal.show();
}
else {
this.$refs.bsModal.hide();
}
},
},
computed: {
countTermin() {
return (this.plan.filter(termin => termin.planen).length);
},
formattedSkippedWeeks() {
return this.skippedWeeks.map(week => luxon.DateTime.fromISO(week).toFormat('dd.MM.yyyy')).join('\n');
}
},
methods: {
async loadPlan() {
this.loading = true;
this.plan = [];
this.errors = [];
this.skippedWeeks = [];
this.restOffen = 0;
await this.$api.call(
ApiKalender.calculateMultiWeekPlan(
this.lehreinheitId,
this.ortKurzbz,
this.startTime,
this.endTime
))
.then(result => result.data)
.then(result => {
this.plan = (result.plan || []).map(termin => ({
...termin,
selected_ort_kurzbz: termin.raum_vorschlaege?.[0]?.ort_kurzbz ?? termin.ort_kurzbz,
planen: (termin.collisions && termin.collisions.length > 0) ? false : true,
}));
this.skippedWeeks = result.skipped_weeks || [];
this.errors = result.errors || [];
this.restOffen = result.rest_offen || 0;
this.loading = false;
})
.catch(error => {
this.$fhcAlert.handleSystemError(error);
this.close();
});
},
async confirm() {
this.loading = true;
await this.$api.call(
ApiKalender.confirmMultiWeekPlan(this.plan.filter(termin => termin.planen), this.lehreinheitId)
);
this.$emit('confirmed');
this.close();
},
close() {
this.loading = false;
this.$emit('close');
},
hasCollision(termin) {
return termin.collisions && termin.collisions.length > 0;
},
roomOptions(termin) {
return termin.raum_vorschlaege || [];
},
formatDate(date)
{
return luxon.DateTime.fromISO(date).toFormat('dd.MM.yyyy');
}
},
template: `
<bs-modal
ref="bsModal"
class="bootstrap-prompt"
dialog-class="modal-xl"
@hidden-bs-modal="close"
>
<template v-slot:title>Multi-Week Verplanung Vorschau</template>
<div v-if="loading" class="text-center py-4">
<i class="fa fa-spinner fa-spin"></i> Wird berechnet...
</div>
<div v-else>
<div v-if="restOffen > 0" class="alert alert-warning small">
Achtung: {{ restOffen }} Stunden konnten nicht verplant werden (Semesterende erreicht).
</div>
<div v-if="skippedWeeks.length" class="alert alert-warning small">
Übersprungende Ferienwochen: <div style="white-space: pre-line;">{{ formattedSkippedWeeks }}</div>
</div>
<div v-if="errors.length" class="alert alert-danger small">
<div v-for="(err, i) in errors" :key="i">{{ err.message }}</div>
</div>
<table class="table table-sm align-middle">
<thead>
<tr>
<th>Datum</th>
<th>Zeit</th>
<th>Collisions</th>
<th>Raum</th>
<th>Planen?</th>
</tr>
</thead>
<tbody>
<tr v-for="(termin, i) in plan" :key="i" :class="hasCollision(termin) ? 'table-danger' : 'table-success'">
<td>{{ formatDate(termin.datum) }}</td>
<td>{{ termin.start.split(' ')[1] }} - {{ termin.ende.split(' ')[1] }}</td>
<td>
<span v-if="hasCollision(termin)" class="text-danger small">
<div style="white-space: pre-line;">{{ termin.collisions.map(c => c.message).join('\\n') }}</div>
</span>
<span v-else class="text-success small">
---
</span>
</td>
<td>
<select
v-if="roomOptions(termin).length"
class="form-select form-select-sm"
v-model="termin.selected_ort_kurzbz"
>
<option
v-for="raum in roomOptions(termin)"
:key="raum.ort_kurzbz"
:value="raum.ort_kurzbz"
>
{{ raum.ort_kurzbz }} (Score: {{ raum.score }})
</option>
</select>
<span v-else class="text-muted small">Kein Raum verfügbar</span>
</td>
<td>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
v-model="termin.planen"
:id="'override-' + i"
>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<template #footer>
<button class="btn btn-outline-secondary" @click="close">Abbrechen</button>
<button class="btn btn-primary" :disabled="loading || !plan.length" @click="confirm">
{{ countTermin }} Termine anlegen
</button>
</template>
</bs-modal>
`
}
@@ -15,6 +15,17 @@ export default {
return classes; return classes;
}, },
topicString() {
return Array.isArray(this.event.topic) ? this.event.topic.join(', ') : this.event.topic;
},
ortString() {
return Array.isArray(this.event.ort_kurzbz) ? this.event.ort_kurzbz.join(', ') : this.event.ort_kurzbz;
},
gruppeString() {
return Array.isArray(this.event.gruppe)
? this.event.gruppe.map(gruppe => gruppe.bezeichnung).join(', ')
: this.event.gruppe;
},
tooltipString() { tooltipString() {
const tooltipArray = []; const tooltipArray = [];
@@ -25,14 +36,21 @@ export default {
tooltipArray.push([ tooltipArray.push([
this.$p.t('profilUpdate/topic'), this.$p.t('profilUpdate/topic'),
this.event.topic this.topicString
].join(": ")); ].join(": "));
tooltipArray.push([ tooltipArray.push([
this.$p.t('person/ort'), this.$p.t('person/ort'),
this.event.ort_kurzbz this.ortString
].join(": ")); ].join(": "));
if (this.gruppeString) {
tooltipArray.push([
this.$p.t('lehre/gruppe'),
this.gruppeString
].join(": "));
}
if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) { if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) {
if (this.event.lektor.length > 3) { if (this.event.lektor.length > 3) {
tooltipArray.push([ tooltipArray.push([
@@ -80,8 +98,9 @@ export default {
<span>{{ end }}</span> <span>{{ end }}</span>
</div> </div>
<div class="event-text" v-tooltip="tooltipString"> <div class="event-text" v-tooltip="tooltipString">
<span class="event-topic">{{ event.topic }}</span> <span class="event-topic">{{ topicString }}</span>
<span data-cy="calendar-event-room" class="event-place">{{ event.ort_kurzbz }}</span> <span class="event-place" data-cy="calendar-event-room">{{ ortString }}</span>
<span v-if="gruppeString" class="event-gruppe">{{ gruppeString }}</span>
<span <span
v-for="(lektor,index) in event.lektor.slice(0, 3)" v-for="(lektor,index) in event.lektor.slice(0, 3)"
class="event-lectors" class="event-lectors"
@@ -49,7 +49,10 @@ export default {
return this.event.ende; return this.event.ende;
} }
return numberPadding(this.event.ende.getHours()) + ":" + numberPadding(this.event.ende.getMinutes()); 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;
},
}, },
methods: { methods: {
mehtodNumberPadding: function (number) { mehtodNumberPadding: function (number) {
@@ -106,7 +109,7 @@ export default {
}}</th> }}</th>
<td> <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" aria-hidden="true" style="color:#00649C"></i></a> <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" aria-hidden="true" style="color:#00649C"></i></a>
{{event.ort_kurzbz}} {{ortString}}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -25,14 +25,21 @@ export default {
tooltipArray.push([ tooltipArray.push([
this.$p.t('profilUpdate/topic'), this.$p.t('profilUpdate/topic'),
this.event.topic this.topicString
].join(": ")); ].join(": "));
tooltipArray.push([ tooltipArray.push([
this.$p.t('person/ort'), this.$p.t('person/ort'),
this.event.ort_kurzbz this.ortString
].join(": ")); ].join(": "));
if (this.gruppeString) {
tooltipArray.push([
this.$p.t('lehre/gruppe'),
this.gruppeString
].join(": "));
}
if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) { if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) {
if (this.event.lektor.length > 3) { if (this.event.lektor.length > 3) {
tooltipArray.push([ tooltipArray.push([
@@ -48,6 +55,21 @@ 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.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])
].join(": "));
} else {
tooltipArray.push([
this.$p.t('lehre/teilnehmer'),
this.event.teilnehmer_person.map(person => `${person.vorname} ${person.nachname}`).join("\n")
].join(": "));
}
}
return tooltipArray.join("\n"); return tooltipArray.join("\n");
}, },
start() { start() {
@@ -59,7 +81,18 @@ export default {
return luxon.Duration return luxon.Duration
.fromISOTime(this.event.ende) .fromISOTime(this.event.ende)
.toISOTime({ suppressSeconds: true }); .toISOTime({ suppressSeconds: true });
} },
ortString() {
return Array.isArray(this.event.ort_kurzbz) ? this.event.ort_kurzbz.join(', ') : this.event.ort_kurzbz;
},
topicString() {
return Array.isArray(this.event.topic) ? this.event.topic.join(', ') : this.event.topic;
},
gruppeString() {
return Array.isArray(this.event.teilnehmer_gruppe)
? this.event.teilnehmer_gruppe.map(gruppe => gruppe.gruppe_kurzbz).join(', ')
: this.event.teilnehmer_gruppe;
},
}, },
template: /* html */` template: /* html */`
<div <div
@@ -73,7 +106,7 @@ export default {
<span>{{ end }}</span> <span>{{ end }}</span>
</div> </div>
<div class="event-text" v-tooltip="tooltipString"> <div class="event-text" v-tooltip="tooltipString">
<span class="event-topic">{{ event.topic }}</span> <span class="event-topic">{{ topicString }}</span>
<span <span
v-for="lektor in event.lektor.slice(0, 3)" v-for="lektor in event.lektor.slice(0, 3)"
class="event-lectors" class="event-lectors"
@@ -86,7 +119,20 @@ export default {
> >
... +{{ event.lektor.length - 3 }} ... +{{ event.lektor.length - 3 }}
</span> </span>
<span data-cy="calendar-event-room" class="event-place">{{ event.ort_kurzbz }}</span> <span
v-for="person in (event.teilnehmer_person || []).slice(0, 3)"
class="event-teilnehmer"
>
{{ person.vorname }} {{ person.nachname }}
</span>
<span
v-if="event.teilnehmer_person && event.teilnehmer_person.length > 3"
class="event-teilnehmer-plus"
>
... +{{ event.teilnehmer_person.length - 3 }}
</span>
<span class="event-place" data-cy="calendar-event-room">{{ ortString }}</span>
<span v-if="gruppeString" class="event-gruppe">{{ gruppeString }}</span>
</div> </div>
</div> </div>
`, `,
@@ -46,7 +46,10 @@ export default {
return this.event.ende; return this.event.ende;
} }
return numberPadding(this.event.ende.getHours()) + ":" + numberPadding(this.event.ende.getMinutes()); 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;
},
}, },
methods: { methods: {
mehtodNumberPadding: function (number) { mehtodNumberPadding: function (number) {
@@ -93,7 +96,7 @@ export default {
}}</th> }}</th>
<td> <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> <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>
{{event.ort_kurzbz}} {{ortString}}
</td> </td>
</tr> </tr>
<tr> <tr>
+175 -66
View File
@@ -37,6 +37,10 @@ import ApiStudiengangTree from "../../api/lehrveranstaltung/studiengangtree.js";
import StvStudiensemester from "../Stv/Studentenverwaltung/Studiensemester.js"; import StvStudiensemester from "../Stv/Studentenverwaltung/Studiensemester.js";
import FormInput from "../../../js/components/Form/Input.js"; import FormInput from "../../../js/components/Form/Input.js";
import Reservierung from "./Reservierung.js"; import Reservierung from "./Reservierung.js";
import { getTempusShortcuts } from "./shortcuts.js";
import KeyboardShortcuts from "./KeyboardShortcuts.js";
import { useContextMenuActions } from "../../composables/Tempus/ContextMenuActions.js";
import MultiWeekPlanModal from "./MultiWeekPlanModal.js";
export default { export default {
name: "Tempus", name: "Tempus",
@@ -56,6 +60,8 @@ export default {
Multiselect: primevue.multiselect, Multiselect: primevue.multiselect,
FormInput, FormInput,
Reservierung, Reservierung,
KeyboardShortcuts,
MultiWeekPlanModal
}, },
props: { props: {
defaultSemester: String, defaultSemester: String,
@@ -78,12 +84,24 @@ export default {
currentSemester: this.defaultSemester, currentSemester: this.defaultSemester,
renderers: Vue.computed(() => this.renderers), renderers: Vue.computed(() => this.renderers),
appConfig: Vue.computed(() => this.appconfig), appConfig: Vue.computed(() => this.appconfig),
contextMenuActions: Vue.computed(() => this.contextMenuActions), contextMenuActions: useContextMenuActions({
openRaumauswahl: (orig) => this.openRaumauswahl(orig),
openResourcesAssignmentModal: (orig) => this.openResourcesAssignmentModal(orig),
openHistory: (orig) => this.openHistory(orig),
deleteEntry: (orig) => this.deleteEntry(orig),
syncToLecturer: (orig) => this.syncToLecturer(orig),
syncToStudent: (orig) => this.syncToStudent(orig),
}),
tableActions: {
deleteEntries: (origList) => this.deleteEntries(origList),
openRaumauswahl: (orig) => this.openRaumauswahl(orig),
},
}; };
}, },
data() { data() {
return { return {
appconfig: {}, appconfig: {},
currentMode: 'week',
configEndpoints: ApiTempusConfig, configEndpoints: ApiTempusConfig,
endpoint: ApiStudiengangTree, endpoint: ApiStudiengangTree,
raumVorschlaege: [], raumVorschlaege: [],
@@ -121,6 +139,7 @@ export default {
events: null, events: null,
minimized: false, minimized: false,
currentlySelectedEvent: null, currentlySelectedEvent: null,
hoveredEvent: null,
//currentDay: new Date(), //currentDay: new Date(),
studiensemesterKurzbz: this.defaultSemester, studiensemesterKurzbz: this.defaultSemester,
lists: { lists: {
@@ -164,64 +183,19 @@ export default {
areFormButtonsDisplayed: false, areFormButtonsDisplayed: false,
}, },
currentlyUpdatedEvent: null, currentlyUpdatedEvent: null,
multiWeekModal: {
show: false,
lehreinheitId: null,
ortKurzbz: null,
startTime: null,
endTime: null,
},
}; };
}, },
computed: { computed: {
contextMenuActions() {
return {
lehreinheit: [
{
label: "Raumauswahl",
icon: "fa-solid fa-door-open",
action: this.openRaumauswahl,
},
{
label: "Ressourcen zuordnen",
icon: "fa-solid fa-list",
action: this.openResourcesAssignmentModal,
},
{
label: "Freischalten für Voransicht",
icon: "fa-solid fa-chalkboard-user",
action: (orig) =>
this.$api
.call(ApiKalender.syncToLecturer(orig.kalender_id))
.then(() => this.$refs.calendar.resetEventLoader()),
},
{
label: "Freischalten für Live",
icon: "fa-solid fa-user-graduate",
action: (orig) =>
this.$api
.call(ApiKalender.syncToStudent(orig.kalender_id))
.then(() => this.$refs.calendar.resetEventLoader()),
},
{
label: "History",
icon: "fa-solid fa-clock-rotate-left",
action: this.openHistory,
},
{
label: "Delete",
icon: "fa-solid fa-calendar-xmark",
action: this.deleteEntry,
},
],
reservierung: [
{
label: "Delete",
icon: "fa-solid fa-calendar-xmark",
action: this.deleteEntry,
},
],
};
},
currentDay() { currentDay() {
return luxon.DateTime.now().setZone(this.config.timezone).toISODate(); return luxon.DateTime.now().setZone(this.config.timezone).toISODate();
}, },
currentMode() {
return "week";
},
visibleLecturerUids() { visibleLecturerUids() {
if (!this.lecturers.length) return null; if (!this.lecturers.length) return null;
return this.lecturers return this.lecturers
@@ -244,6 +218,9 @@ export default {
label: this.visibleStatusArray[status], label: this.visibleStatusArray[status],
})); }));
}, },
keyboardShortcuts() {
return getTempusShortcuts(this);
},
dropdownParsedAvailableResources() { dropdownParsedAvailableResources() {
return this.resourcesAssignmentModal.availableResources return this.resourcesAssignmentModal.availableResources
.map((unit) => { .map((unit) => {
@@ -283,15 +260,33 @@ export default {
this.$refs.resourcesAssignmentModal.show(); this.$refs.resourcesAssignmentModal.show();
}, },
async deleteEntry(orig) { async deleteEntry(orig)
if (!orig?.kalender_id) return; {
if (!orig?.kalender_id)
return;
await this.$api await this.deleteEntryCall(orig);
.call(ApiKalender.deleteEntry(orig?.kalender_id)) this.$refs.calendar.resetEventLoader();
.then((result) => { this.$refs.coursepicker.reload();
this.$refs.calendar.resetEventLoader(); },
}); async deleteEntries(origList)
}, {
let validList = (origList ?? []).filter(orig => orig?.kalender_id);
if (!validList.length)
return;
await Promise.allSettled(validList.map(orig => this.deleteEntryCall(orig)));
this.$refs.calendar.resetEventLoader();
this.$refs.coursepicker.reload();
},
async deleteEntryCall(orig)
{
await this.$api.call(ApiKalender.deleteEntry(
orig.kalender_id
)).then(() => {
this.$refs.parking.unpark({ type: orig.type, id: orig.kalender_id });
});
},
async openHistory(orig) { async openHistory(orig) {
if (!orig?.kalender_id) return; if (!orig?.kalender_id) return;
await this.$api await this.$api
@@ -301,6 +296,18 @@ export default {
this.$refs.historyModel.show(); this.$refs.historyModel.show();
}); });
}, },
syncToLecturer(orig) {
if (!orig?.kalender_id)
return;
return this.$api.call(ApiKalender.syncToLecturer(orig.kalender_id))
.then(() => this.$refs.calendar.resetEventLoader());
},
syncToStudent(orig) {
if (!orig?.kalender_id)
return;
return this.$api.call(ApiKalender.syncToStudent(orig.kalender_id))
.then(() => this.$refs.calendar.resetEventLoader());
},
async selectRaum(ort_kurzbz) { async selectRaum(ort_kurzbz) {
const orig = this.raumModal; const orig = this.raumModal;
await this.$api await this.$api
@@ -368,9 +375,13 @@ export default {
if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid) if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid)
this.calendarDate = newDate.toISODate(); this.calendarDate = newDate.toISODate();
}, },
handleChangeMode() { handleChangeMode(newMode, newDate) {
console.log("handleChangeMode"); if (!newMode)
}, return;
this.currentMode = newMode;
if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid)
this.calendarDate = newDate.toISODate();
},
toggleStatus(selected) { toggleStatus(selected) {
if (!selected || selected.length === 0) { if (!selected || selected.length === 0) {
this.visibleStatus = ["all"]; this.visibleStatus = ["all"];
@@ -567,10 +578,12 @@ export default {
dates.end_time, dates.end_time,
() => { () => {
this.$refs.calendar.resetEventLoader(); this.$refs.calendar.resetEventLoader();
this.$refs.coursepicker.reload();
}, },
() => { () => {
this.$refs.calendar.clearOutCalendarEventEmphasis(); this.$refs.calendar.clearOutCalendarEventEmphasis();
this.$refs.calendar.resetEventLoader(); this.$refs.calendar.resetEventLoader();
this.$refs.coursepicker.reload();
}, },
); );
}, },
@@ -585,6 +598,19 @@ export default {
const obj = item[0]; const obj = item[0];
if (!obj?.type) return alert("Unbekannter Drop-Typ"); if (!obj?.type) return alert("Unbekannter Drop-Typ");
if (payload.targetKalenderId)
{
if (obj.type !== 'lehreinheit' || !obj.orig?.lehreinheit_id)
return alert("Nur Lehreinheiten können einem Termin hinzugefügt werden");
return this.$api.call(
ApiKalender.addToKalenderEvent(payload.targetKalenderId, obj.orig.lehreinheit_id)
).then(() => {
this.$refs.calendar.resetEventLoader();
this.bcc.postMessage('dropped');
});
}
const dates = this._parseDates(start, end); const dates = this._parseDates(start, end);
if (!dates) return; if (!dates) return;
@@ -608,6 +634,18 @@ export default {
this.reservierungPending = true; this.reservierungPending = true;
this.$refs.reservierung.show(start_time, end_time); this.$refs.reservierung.show(start_time, end_time);
} else if (obj.type === "lehreinheit") { } else if (obj.type === "lehreinheit") {
if (obj.multiweek)
{
this.openMultiWeekPreview(
obj.orig.lehreinheit_id,
this.ort_kurzbz ? this.ort_kurzbz : obj.orig.ort_kurzbz,
start_time,
end_time
);
return;
}
return this.$api return this.$api
.call( .call(
ApiKalender.addKalenderEvent( ApiKalender.addKalenderEvent(
@@ -617,8 +655,10 @@ export default {
end_time, end_time,
), ),
) )
.then(result => result.data)
.then(() => { .then(() => {
this.$refs.calendar.resetEventLoader(); this.$refs.calendar.resetEventLoader();
this.$refs.coursepicker.reload();
this.bcc.postMessage("dropped"); this.bcc.postMessage("dropped");
}); });
} else if (obj.type === "kalender") { } else if (obj.type === "kalender") {
@@ -634,6 +674,7 @@ export default {
id: obj.orig.kalender_id, id: obj.orig.kalender_id,
}); });
this.$refs.calendar.resetEventLoader(); this.$refs.calendar.resetEventLoader();
this.$refs.coursepicker.reload();
this.bcc.postMessage("dropped"); this.bcc.postMessage("dropped");
}, },
() => { () => {
@@ -651,6 +692,21 @@ export default {
alert("Unbekannter Drop-Typ: " + obj.type); alert("Unbekannter Drop-Typ: " + obj.type);
} }
}, },
openMultiWeekPreview(lehreinheit_id, ort_kurzbz, start_time, end_time) {
this.multiWeekModal.lehreinheitId = lehreinheit_id;
this.multiWeekModal.ortKurzbz = ort_kurzbz;
this.multiWeekModal.startTime = start_time;
this.multiWeekModal.endTime = end_time;
this.multiWeekModal.show = true;
},
closeMultiWeekModal() {
this.multiWeekModal.show = false;
},
onMultiWeekConfirmed() {
this.$refs.calendar.resetEventLoader();
this.$refs.coursepicker.reload();
this.bcc.postMessage('dropped');
},
handleRange(range) { handleRange(range) {
if (!range?.start || !range?.end) return; if (!range?.start || !range?.end) return;
@@ -1023,6 +1079,47 @@ export default {
}); });
} }
}, },
onEventHover(event) {
this.hoveredEvent = event;
},
onEventUnhover(event)
{
if (this.hoveredEvent?.kalender_id === event?.kalender_id) {
this.hoveredEvent = null;
}
},
parkHoveredEvent()
{
const event = this.hoveredEvent;
if (!event?.kalender_id)
return;
if (this.$refs.parking.isParked(event.kalender_id))
{
this.$refs.parking.unpark({ type: event.type, id: event.kalender_id });
}
else
{
this.$refs.parking.park(null, {
type: 'kalender',
id: event.kalender_id,
orig: event
});
}
},
deleteHoveredEvent() {
const event = this.hoveredEvent;
if (!event?.kalender_id)
return;
this.deleteEntry(event);
},
clearHoveredEvent() {
this.hoveredEvent = null;
},
focusSearchbar() {
this.$refs.searchbar?.$refs?.input.focus()
},
}, },
watch: { watch: {
lecturers: { lecturers: {
@@ -1118,6 +1215,7 @@ export default {
}, },
template: /*html*/ ` template: /*html*/ `
<div data-cy="tempus" class="tempus"> <div data-cy="tempus" class="tempus">
<keyboard-shortcuts :shortcuts="keyboardShortcuts" />
<header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow"> <header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow">
<div class="col-md-4 col-lg-3 col-xl-2 d-flex align-items-center"> <div class="col-md-4 col-lg-3 col-xl-2 d-flex align-items-center">
<button <button
@@ -1313,7 +1411,7 @@ export default {
v-model:parked-keys="parkedKeys" v-model:parked-keys="parkedKeys"
></parking-slot> ></parking-slot>
<fhc-coursepicker :stg="stg" @select-lecturer="setEmp" @select-kw="jumpToKw" :studiensemester="selectedStudiensemester"></fhc-coursepicker> <fhc-coursepicker ref="coursepicker" :stg="stg" @select-lecturer="setEmp" @select-kw="jumpToKw" :studiensemester="selectedStudiensemester"></fhc-coursepicker>
</div> </div>
<stv-studiensemester v-model:studiensemester-kurzbz="selectedStudiensemester"></stv-studiensemester> <stv-studiensemester v-model:studiensemester-kurzbz="selectedStudiensemester"></stv-studiensemester>
@@ -1333,6 +1431,8 @@ export default {
@resize="resizeHandler" @resize="resizeHandler"
@update:date="handleChangeDate" @update:date="handleChangeDate"
@update:mode="handleChangeMode" @update:mode="handleChangeMode"
@event-hover="onEventHover"
@event-unhover="onEventUnhover"
:extra-backgrounds="extraBackgrounds" :extra-backgrounds="extraBackgrounds"
@update:range="handleRange" @update:range="handleRange"
class="responsive-calendar" class="responsive-calendar"
@@ -1464,5 +1564,14 @@ export default {
:ort-kurzbz="ort_kurzbz" :ort-kurzbz="ort_kurzbz"
@saved="reservierungPending = false; $refs.calendar.resetEventLoader()" @saved="reservierungPending = false; $refs.calendar.resetEventLoader()"
></reservierung> ></reservierung>
</div>`, <multi-week-plan-modal
:show="multiWeekModal.show"
:lehreinheit-id="multiWeekModal.lehreinheitId"
:ort-kurzbz="multiWeekModal.ortKurzbz"
:start-time="multiWeekModal.startTime"
:end-time="multiWeekModal.endTime"
@close="closeMultiWeekModal"
@confirmed="onMultiWeekConfirmed"
/>
</div>`
}; };
+56
View File
@@ -0,0 +1,56 @@
export function getTempusShortcuts(self) {
return [
{
key: 'F',
ctrl: true,
skipWhenTyping: false,
handler: () => self.focusSearchbar()
},
{
key: '1',
ctrl: true,
skipWhenTyping: false,
handler: () => self.handleChangeMode('month')
},
{
key: '2',
ctrl: true,
skipWhenTyping: false,
handler: () => self.handleChangeMode('week')
},
{
key: '3',
ctrl: true,
skipWhenTyping: false,
handler: () => self.handleChangeMode('tableList')
},
{
key: 'ArrowLeft',
ctrl: true,
skipWhenTyping: false,
handler: () => self.$refs.calendar.navigatePrev()
},
{
key: 'ArrowRight',
ctrl: true,
skipWhenTyping: false,
handler: () => self.$refs.calendar.navigateNext()
},
{
key: 'p',
skipWhenTyping: true,
handler: () => self.parkHoveredEvent()
},
{
key: 'P',
skipWhenTyping: true,
handler: () => self.parkHoveredEvent()
},
{
key: 'Delete',
skipWhenTyping: true,
handler: () => self.deleteHoveredEvent()
},
];
}
@@ -0,0 +1,44 @@
export function useContextMenuActions(handlers)
{
return Vue.computed(() => ({
lehreinheit: [
{
label: 'Raumauswahl',
icon: 'fa-solid fa-door-open',
action: handlers.openRaumauswahl
},
{
label: 'Ressourcen zuordnen',
icon: 'fa-solid fa-table-list',
action: handlers.openResourcesAssignmentModal
},
{
label: 'Freischalten für Voransicht',
icon: 'fa-solid fa-chalkboard-user',
action: handlers.syncToLecturer
},
{
label: 'Freischalten für Live',
icon: 'fa-solid fa-user-graduate',
action: handlers.syncToStudent
},
{
label: 'History',
icon: 'fa-solid fa-clock-rotate-left',
action: handlers.openHistory
},
{
label: 'Delete',
icon: 'fa-solid fa-calendar-xmark',
action: handlers.deleteEntry
},
],
reservierung: [
{
label: 'Delete',
icon: 'fa-solid fa-calendar-xmark',
action: handlers.deleteEntry
},
]
}));
}
@@ -37,7 +37,9 @@ context("Tempus event mutation tests", () => {
const eventData = JSON.parse(eventJSON); const eventData = JSON.parse(eventJSON);
const eventId = eventData?.id; const eventId = eventData?.id;
const originalRoom = eventData?.orig?.ort_kurzbz; let originalRoom = eventData?.orig?.ort_kurzbz;
if (Array.isArray(originalRoom)) originalRoom = originalRoom.length ? originalRoom[0] : "";
expect(eventId, "planner event id").to.exist; expect(eventId, "planner event id").to.exist;
expect(originalRoom, "original event room").to.be.a("string").and.not.be expect(originalRoom, "original event room").to.be.a("string").and.not.be
.empty; .empty;
@@ -80,7 +82,9 @@ context("Tempus event mutation tests", () => {
const eventData = JSON.parse(eventJSON); const eventData = JSON.parse(eventJSON);
const eventId = eventData?.id; const eventId = eventData?.id;
const originalRoom = eventData?.orig?.ort_kurzbz; let originalRoom = eventData?.orig?.ort_kurzbz;
if (Array.isArray(originalRoom)) originalRoom = originalRoom.length ? originalRoom[0] : "";
expect(eventId, "planner event id").to.exist; expect(eventId, "planner event id").to.exist;
expect(originalRoom, "original event room").to.be.a("string").and.not.be expect(originalRoom, "original event room").to.be.a("string").and.not.be
.empty; .empty;
@@ -16,7 +16,9 @@ context("Tempus filter tests", () => {
.then((eventJSON) => { .then((eventJSON) => {
expect(eventJSON).to.exist; expect(eventJSON).to.exist;
const selectedRoom = JSON.parse(eventJSON)?.orig?.ort_kurzbz; let selectedRoom = JSON.parse(eventJSON)?.orig?.ort_kurzbz;
if (Array.isArray(selectedRoom)) selectedRoom = selectedRoom.length ? selectedRoom[0] : "";
expect(selectedRoom, "first event room").to.be.a("string").and.not.be expect(selectedRoom, "first event room").to.be.a("string").and.not.be
.empty; .empty;
expect( expect(
@@ -41,7 +43,10 @@ context("Tempus filter tests", () => {
tempusPage.getCalendarEvents().each(($event) => { tempusPage.getCalendarEvents().each(($event) => {
const eventData = tempusPage.getCalendarEventData($event); const eventData = tempusPage.getCalendarEventData($event);
expect(eventData?.orig?.ort_kurzbz, "event data room").to.eq( let eventRoom = eventData?.orig?.ort_kurzbz;
if (Array.isArray(eventRoom)) eventRoom = eventRoom.length ? eventRoom[0] : "";
expect(eventRoom, "event data room").to.eq(
selectedRoom, selectedRoom,
); );
cy.wrap($event) cy.wrap($event)
@@ -64,7 +69,9 @@ context("Tempus filter tests", () => {
.then((eventJSON) => { .then((eventJSON) => {
expect(eventJSON).to.exist; expect(eventJSON).to.exist;
const selectedRoom = JSON.parse(eventJSON)?.orig?.ort_kurzbz; let selectedRoom = JSON.parse(eventJSON)?.orig?.ort_kurzbz;
if (Array.isArray(selectedRoom)) selectedRoom = selectedRoom.length ? selectedRoom[0] : "";
console.log("Selected room:", selectedRoom);
expect(selectedRoom, "first event room").to.be.a("string").and.not.be expect(selectedRoom, "first event room").to.be.a("string").and.not.be
.empty; .empty;
expect( expect(
@@ -135,7 +135,10 @@ class TempusPage {
const eventData = JSON.parse( const eventData = JSON.parse(
event.getAttribute("data-fhc-draggable-value"), event.getAttribute("data-fhc-draggable-value"),
); );
return !!eventData?.orig?.ort_kurzbz;
let isArray = Array.isArray(eventData?.orig?.ort_kurzbz);
return isArray ? eventData?.orig?.ort_kurzbz.length > 0 : !!eventData?.orig?.ort_kurzbz;
}); });
getCalendarEventsWithLecturer = () => getCalendarEventsWithLecturer = () =>
this.getCalendarEvents().filter((index, event) => { this.getCalendarEvents().filter((index, event) => {
@@ -435,6 +438,7 @@ class TempusPage {
const event = this.getCalendarEventById(eventId); const event = this.getCalendarEventById(eventId);
return event return event
.scrollIntoView()
.should("be.visible") .should("be.visible")
.drag(this.getCalendarPartDropTarget(partIndex), { .drag(this.getCalendarPartDropTarget(partIndex), {
waitForAnimations: false, waitForAnimations: false,