From 9983a4810297f4b7ca2e50bc03a50e422e08bb87 Mon Sep 17 00:00:00 2001 From: ma0048 Date: Tue, 21 Jul 2026 13:50:17 +0200 Subject: [PATCH 01/33] gui fuer kalender sync hinzugefuegt und syncjob angepasst --- application/controllers/Tempus.php | 36 +- .../api/frontend/v1/tempus/Coursepicker.php | 72 ---- .../api/frontend/v1/tempus/Kalender.php | 3 +- .../api/frontend/v1/tempus/KalenderSync.php | 298 +++++++++++++++ application/libraries/KalenderLib.php | 84 ----- .../libraries/KalenderNotificationLib.php | 3 +- application/libraries/KalenderSyncLib.php | 336 +++++++++++++++++ .../ressource/Kalendersyncstatus_model.php | 14 + application/views/Tempus.php | 4 +- public/js/api/factory/tempus/sync.js | 83 +++++ public/js/apps/Tempus.js | 2 + public/js/components/Tempus/Modal.js | 340 ++++++++++++++++++ public/js/components/Tempus/TempusSync.js | 196 ++++++++++ system/dbupdate_3.4.php | 1 + .../77949_tempus_synchronisation.php | 38 ++ system/phrasesupdate.php | 120 +++++++ 16 files changed, 1463 insertions(+), 167 deletions(-) create mode 100644 application/controllers/api/frontend/v1/tempus/KalenderSync.php create mode 100644 application/libraries/KalenderSyncLib.php create mode 100644 application/models/ressource/Kalendersyncstatus_model.php create mode 100644 public/js/api/factory/tempus/sync.js create mode 100644 public/js/components/Tempus/Modal.js create mode 100644 public/js/components/Tempus/TempusSync.js create mode 100644 system/dbupdate_3.4/77949_tempus_synchronisation.php diff --git a/application/controllers/Tempus.php b/application/controllers/Tempus.php index 139f0b690..175a9ffa4 100644 --- a/application/controllers/Tempus.php +++ b/application/controllers/Tempus.php @@ -16,25 +16,45 @@ class Tempus extends Auth_Controller // Load Libraries $this->load->library('VariableLib', ['uid' => getAuthUID()]); - + // Load Config $this->load->config('calendar'); } - /** - * @return void - */ - public function _remap() + public function index() { + $this->_loadView(); + } + public function sync() + { + $this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel'); + $this->load->model('organisation/Studienplan_model', 'StudienplanModel'); + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); + $this->load->model('ressource/Kalenderstatus_model', 'KalenderStatusModel'); + + $this->StudiengangModel->addOrder('typ'); + $this->StudiengangModel->addOrder('kurzbz'); + $this->StudiengangModel->addJoin('tbl_organisationseinheit', 'oe_kurzbz', 'LEFT'); + $organisationen = getData($this->StudiengangModel->loadWhere(array('tbl_studiengang.aktiv' => 'true'))); + + $this->StudiensemesterModel->addOrder('start', 'DESC'); + $studiensemestern = getData($this->StudiensemesterModel->load()); + + $this->_loadView(array('organisationen' => $organisationen, 'studiensemestern' => $studiensemestern)); + } + + private function _loadView($extraVariables = []) + { $this->load->view('Tempus', [ 'permissions' => [ 'admin' => $this->permissionlib->isBerechtigt('admin') ], - 'variables' => [ + 'variables' => array_merge([ 'semester_aktuell' => $this->variablelib->getVar('semester_aktuell'), 'timezone' => $this->config->item('timezone') - ] + ], $extraVariables) ]); } -} +} \ No newline at end of file diff --git a/application/controllers/api/frontend/v1/tempus/Coursepicker.php b/application/controllers/api/frontend/v1/tempus/Coursepicker.php index 686b39023..b8f9b9269 100644 --- a/application/controllers/api/frontend/v1/tempus/Coursepicker.php +++ b/application/controllers/api/frontend/v1/tempus/Coursepicker.php @@ -8,7 +8,6 @@ class Coursepicker extends FHCAPI_Controller public function __construct() { parent::__construct([ - 'search' => self::PERM_LOGGED, 'getByStg' => self::PERM_LOGGED ]); @@ -21,77 +20,6 @@ class Coursepicker extends FHCAPI_Controller $this->loadPhrases(['ui']); } - - public function search() - { - $query = $this->input->get('query'); - if (is_null($query)) - $this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL); - - $query_words = explode(' ', $query); - - //TODO Where weiter anpassen z.B. Fachbereich - $this->_ci->LehreinheitModel->addSelect('tbl_lehreinheit.lehreinheit_id, - tbl_lehreinheit.lvnr, - tbl_lehreinheit.lehrfach_id, - lehrfach.kurzbz AS lehrfach, - lehrfach.bezeichnung AS lehrfach_bez, - lehrfach.farbe AS lehrfach_farbe, - tbl_lehreinheit.lehrform_kurzbz AS lehrform, - lema.mitarbeiter_uid AS lektor_uid, - tbl_mitarbeiter.kurzbz AS lektor, - tbl_studiengang.studiengang_kz, - upper(tbl_studiengang.typ::character varying::text || tbl_studiengang.kurzbz::text) AS studiengang, - lvb.semester, - lvb.verband, - lvb.gruppe, - lvb.gruppe_kurzbz, - tbl_lehreinheit.raumtyp, - tbl_lehreinheit.raumtypalternativ, - tbl_lehreinheit.stundenblockung, - tbl_lehreinheit.wochenrythmus, - lema.semesterstunden, - lema.planstunden, - tbl_lehreinheit.start_kw, - tbl_lehreinheit.anmerkung, - tbl_lehreinheit.studiensemester_kurzbz'); - - $this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehreinheitmitarbeiter lema', 'tbl_lehreinheit.lehreinheit_id = lema.lehreinheit_id'); - $this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehreinheitgruppe lvb', 'tbl_lehreinheit.lehreinheit_id = lvb.lehreinheit_id'); - $this->_ci->LehreinheitModel->addJoin('public.tbl_studiengang', 'lvb.studiengang_kz = tbl_studiengang.studiengang_kz'); - $this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehrveranstaltung lehrfach', 'tbl_lehreinheit.lehrfach_id = lehrfach.lehrveranstaltung_id'); - $this->_ci->LehreinheitModel->addJoin('public.tbl_mitarbeiter', 'lema.mitarbeiter_uid = tbl_mitarbeiter.mitarbeiter_uid'); - $this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehrform', 'tbl_lehrform.lehrform_kurzbz = tbl_lehreinheit.lehrform_kurzbz'); - - $this->_ci->MitarbeiterModel->db->group_start(); - - foreach ($query_words as $word) - { - $this->_ci->LehreinheitModel->db->group_start(); - $this->_ci->LehreinheitModel->db->where('lema.mitarbeiter_uid ILIKE', "%" . $word . "%"); - $this->_ci->LehreinheitModel->db->or_where('lvb.gruppe_kurzbz ILIKE', "%" . $word . "%"); - $this->_ci->LehreinheitModel->db->or_where('tbl_studiengang.kurzbzlang ILIKE', "%" . $word . "%"); - $this->_ci->LehreinheitModel->db->or_where('lvb.verband ILIKE', "%" . $word . "%"); - $this->_ci->LehreinheitModel->db->or_where('lvb.gruppe ILIKE', "%" . $word . "%"); - $this->_ci->LehreinheitModel->db->or_where('lehrfach.bezeichnung ILIKE', "%" . $word . "%"); - - if (is_numeric($word)) - { - $this->_ci->LehreinheitModel->db->or_where('tbl_studiengang.studiengang_kz', $word); - $this->_ci->LehreinheitModel->db->or_where('lvb.semester', $word); - } - $this->_ci->LehreinheitModel->db->group_end(); - - } - $this->_ci->LehreinheitModel->db->group_end(); - $this->_ci->LehreinheitModel->db->where('tbl_lehreinheit.studiensemester_kurzbz = \'SS2025\''); - $this->_ci->LehreinheitModel->db->where(array('tbl_lehrform.verplanen' => true)); - - $result = $this->_ci->LehreinheitModel->load(); - - $this->terminateWithSuccess(hasData($result) ? getData($result) : array()); - } - public function getByStg() { $this->_ci->form_validation->set_data($_POST); diff --git a/application/controllers/api/frontend/v1/tempus/Kalender.php b/application/controllers/api/frontend/v1/tempus/Kalender.php index 75b8b02f7..833d0c0aa 100644 --- a/application/controllers/api/frontend/v1/tempus/Kalender.php +++ b/application/controllers/api/frontend/v1/tempus/Kalender.php @@ -41,6 +41,7 @@ class Kalender extends FHCAPI_Controller $this->_ci->load->library('LogLib'); $this->_ci->load->library('form_validation'); $this->_ci->load->library('KalenderLib'); + $this->_ci->load->library('KalenderSyncLib'); $this->_ci->load->library('RaumvorschlagLib'); $this->loadPhrases([ 'ui' @@ -273,7 +274,7 @@ class Kalender extends FHCAPI_Controller public function sync() { - $result = $this->_ci->kalenderlib->sync(); + $result = $this->_ci->kalendersynclib->sync(); $this->terminateWithSuccess(getData($result)); } public function syncToLecturer() diff --git a/application/controllers/api/frontend/v1/tempus/KalenderSync.php b/application/controllers/api/frontend/v1/tempus/KalenderSync.php new file mode 100644 index 000000000..ed5b7b145 --- /dev/null +++ b/application/controllers/api/frontend/v1/tempus/KalenderSync.php @@ -0,0 +1,298 @@ + ['admin:r', 'assistenz:r'], + 'loadSync' => ['admin:r', 'assistenz:r'], + 'add' => ['admin:r', 'assistenz:r'], + 'start' => ['admin:r', 'assistenz:r'], + 'delete' => ['admin:r', 'assistenz:r'], + 'updateSync' => ['admin:r', 'assistenz:r'], + 'getStudienplan' => ['admin:r', 'assistenz:r'], + 'getSyncStatus' => ['admin:r', 'assistenz:r'], + ]); + + $this->loadPhrases([ + 'lehre', + ]); + + $this->_ci = &get_instance(); + $this->_ci->load->library('form_validation'); + $this->_ci->load->model('organisation/Studienplan_model', 'StudienplanModel'); + $this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->_ci->load->model('ressource/Kalendersyncstatus_model', 'KalendersyncstatusModel'); + $this->_ci->load->model('ressource/Kalenderstatus_model', 'KalenderStatusModel'); + $this->_ci->load->library('KalenderSyncLib'); + } + + public function getSyncs() + { + $this->_ci->form_validation->set_data($_GET); + $this->_ci->form_validation->set_rules('studiensemester_kurzbz',"studiensemester_kurzbz","required"); + + if($this->_ci->form_validation->run() === FALSE) + $this->terminateWithValidationErrors($this->_ci->form_validation->error_array()); + $studiensemester_kurzbz = $this->_ci->input->get('studiensemester_kurzbz', TRUE); + + $language = getUserLanguage() == 'German' ? 0 : 1; + + $this->_ci->KalenderStatusModel->addSelect('tbl_kalender_syncstatus.*, array_to_json(bezeichnung_mehrsprachig::varchar[])->>' . $language .' AS sync_status_kurzbz, tbl_organisationseinheit.bezeichnung as oebezeichnung, tbl_studienplan.bezeichnung as studienplanbezeichnung'); + $this->_ci->KalenderStatusModel->addJoin('lehre.tbl_kalender_status', 'tbl_kalender_syncstatus.sync_status_kurzbz = tbl_kalender_status.status_kurzbz'); + $this->_ci->KalenderStatusModel->addJoin('public.tbl_organisationseinheit', 'tbl_kalender_syncstatus.oe_kurzbz = tbl_organisationseinheit.oe_kurzbz'); + $this->_ci->KalenderStatusModel->addJoin('lehre.tbl_studienplan', 'tbl_kalender_syncstatus.studienplan_id = tbl_studienplan.studienplan_id', 'LEFT'); + + $data = $this->_ci->KalendersyncstatusModel->loadWhere(array('studiensemester_kurzbz' => $studiensemester_kurzbz)); + + if (isError($data)) + $this->terminateWithError(getError($data)); + + $this->terminateWithSuccess(getData($data)); + } + + public function delete() + { + $this->load->library('form_validation'); + + $this->form_validation->set_rules('kalender_syncstatus_id', 'kalender_syncstatus_id', 'required'); + + if (!$this->form_validation->run()) + $this->terminateWithValidationErrors($this->form_validation->error_array()); + + $kalender_syncstatus_id = $this->input->post('kalender_syncstatus_id'); + + $result = $this->_ci->KalendersyncstatusModel->load($kalender_syncstatus_id); + + if (isError($result)) + $this->terminateWithError(getError($result)); + + if (!hasData($result)) + $this->terminateWithError($this->p->t('lehre', 'errorDeleting')); + + $del_result = $this->_ci->KalendersyncstatusModel->delete(getData($result)[0]->kalender_syncstatus_id); + + if (isError($del_result)) + $this->terminateWithError(getError($del_result)); + $this->terminateWithSuccess($del_result); + } + + public function loadSync() + { + $this->_ci->form_validation->set_data($_GET); + $this->_ci->form_validation->set_rules('kalender_syncstatus_id',"kalender_syncstatus_id","required"); + + if($this->_ci->form_validation->run() === FALSE) + $this->terminateWithValidationErrors($this->_ci->form_validation->error_array()); + $kalender_syncstatus_id = $this->_ci->input->get('kalender_syncstatus_id', TRUE); + $data = $this->_ci->KalendersyncstatusModel->loadWhere(array('kalender_syncstatus_id' => $kalender_syncstatus_id)); + if (isError($data)) + $this->terminateWithError(getError($data)); + + $this->terminateWithSuccess(getData($data)[0]); + } + + public function getSyncStatus() + { + $language = getUserLanguage() == 'German' ? 0 : 1; + + $this->_ci->KalenderStatusModel->addSelect('*, array_to_json(bezeichnung_mehrsprachig::varchar[])->>' . $language .' AS bezeichnung, status_kurzbz'); + $this->_ci->KalenderStatusModel->db->where_in('status_kurzbz', self::ALLOWED_STATUS); + $data = $this->_ci->KalenderStatusModel->load(); + + if (isError($data)) + $this->terminateWithError(getError($data)); + + $this->terminateWithSuccess(getData($data)); + } + + + public function add() + { + $formData = $this->input->post('formData'); + + $this->_ci->form_validation->set_data($formData); + + $this->_ci->form_validation->set_rules('oe_kurzbz',"oe_kurzbz","required"); + $this->_ci->form_validation->set_rules('studiensemester_kurzbz',"studiensemester_kurzbz","required"); + $this->_ci->form_validation->set_rules('sync_status_kurzbz',"sync_status_kurzbz","required"); + + if($this->_ci->form_validation->run() === FALSE) + $this->terminateWithValidationErrors($this->_ci->form_validation->error_array()); + + $ausbildungssemester = isEmptyString($formData['ausbildungssemester']) ? null : $formData['ausbildungssemester']; + $studienplan_id = isEmptyString($formData['studienplan_id']) ? null : $formData['studienplan_id']; + + $existing = $this->_checkIfExist( + $formData['oe_kurzbz'], + $formData['studiensemester_kurzbz'], + $ausbildungssemester, + $studienplan_id + ); + + if (isError($existing)) + $this->terminateWithError(getError($existing)); + + if (hasData($existing)) + { + $this->terminateWithError('Es existiert bereits ein Eintrag.'); + } + + $result = $this->_ci->KalendersyncstatusModel->insert( + array( + 'oe_kurzbz' => $formData['oe_kurzbz'], + 'studiensemester_kurzbz' => $formData['studiensemester_kurzbz'], + 'datum_bis' => $formData['datum_bis'], + 'studienplan_id' => $studienplan_id, + 'ausbildungssemester' => $ausbildungssemester, + 'sync_status_kurzbz' => $formData['sync_status_kurzbz'], + 'mail' => $formData['mail'], + 'insertvon' => getAuthUID() + ) + ); + + $this->terminateWithSuccess($result); + } + + + public function start() + { + $formData = $this->input->post('formData'); + + $this->_ci->form_validation->set_data($formData); + + $this->_ci->form_validation->set_rules('oe_kurzbz',"oe_kurzbz","required"); + $this->_ci->form_validation->set_rules('studiensemester_kurzbz',"studiensemester_kurzbz","required"); + $this->_ci->form_validation->set_rules('sync_status_kurzbz',"sync_status_kurzbz","required"); + + if($this->_ci->form_validation->run() === FALSE) + $this->terminateWithValidationErrors($this->_ci->form_validation->error_array()); + + $result = $this->_ci->kalendersynclib->runManual($formData['oe_kurzbz'], $formData['studiensemester_kurzbz'], $formData['ausbildungssemester'], $formData['studienplan_id'], $formData['mail'], $formData['datum_bis'], $formData['sync_status_kurzbz']); + + if (isError($result)) + $this->terminateWithError(getError($result)); + + $this->terminateWithSuccess($result); + } + public function updateSync() + { + $formData = $this->input->post('formData'); + + $this->_ci->form_validation->set_data($formData); + + $this->_ci->form_validation->set_rules('kalender_syncstatus_id',"kalender_syncstatus_id","required"); + $this->_ci->form_validation->set_rules('oe_kurzbz',"oe_kurzbz","required"); + $this->_ci->form_validation->set_rules('studiensemester_kurzbz',"studiensemester_kurzbz","required"); + $this->_ci->form_validation->set_rules('sync_status_kurzbz',"sync_status_kurzbz","required"); + + if($this->_ci->form_validation->run() === FALSE) + $this->terminateWithValidationErrors($this->_ci->form_validation->error_array()); + + $ausbildungssemester = isEmptyString($formData['ausbildungssemester']) ? null : $formData['ausbildungssemester']; + $studienplan_id = isEmptyString($formData['studienplan_id']) ? null : $formData['studienplan_id']; + + $existing = $this->_checkIfExist( + $formData['oe_kurzbz'], + $formData['studiensemester_kurzbz'], + $ausbildungssemester, + $studienplan_id, + $formData['kalender_syncstatus_id'] + ); + + if (isError($existing)) + $this->terminateWithError(getError($existing)); + + if (hasData($existing)) + { + $this->terminateWithError('Es existiert bereits ein Eintrag.'); + } + + $this->_ci->KalendersyncstatusModel->update( + array('kalender_syncstatus_id' => $formData['kalender_syncstatus_id']), + array( + 'oe_kurzbz' => $formData['oe_kurzbz'], + 'studiensemester_kurzbz' => $formData['studiensemester_kurzbz'], + 'datum_bis' => $formData['datum_bis'], + 'studienplan_id' => $studienplan_id, + 'ausbildungssemester' => $ausbildungssemester, + 'sync_status_kurzbz' => $formData['sync_status_kurzbz'], + 'mail' => $formData['mail'], + 'updatevon' => getAuthUID(), + 'updateamum' => date('c'), + ) + ); + } + public function getStudienplan() + { + $this->_ci->form_validation->set_data($_GET); + $this->_ci->form_validation->set_rules('oe_kurzbz',"oe_kurzbz","required"); + + if($this->_ci->form_validation->run() === FALSE) + $this->terminateWithValidationErrors($this->_ci->form_validation->error_array()); + + $oe_kurzbz = $this->_ci->input->get('oe_kurzbz', TRUE); + $studiensemester_kurzbz = $this->_ci->input->get('studiensemester_kurzbz', TRUE); + $ausbildungssemester = $this->_ci->input->get('ausbildungssemester', TRUE); + + $this->_ci->StudiengangModel->addJoin('tbl_organisationseinheit', 'oe_kurzbz'); + $is_studiengang = $this->_ci->StudiengangModel->loadWhere(array('oe_kurzbz' => $oe_kurzbz)); + + if (hasData($is_studiengang) && !isEmptyString($studiensemester_kurzbz)) + { + $studiengang = getData($is_studiengang)[0]; + + $studienplaene = $this->_ci->StudienplanModel->getStudienplaeneBySemester( + $studiengang->studiengang_kz, + $studiensemester_kurzbz, + isEmptyString($ausbildungssemester) ? null : $ausbildungssemester + ); + + + $data = $this->getDataOrTerminateWithError($studienplaene); + + $unique = array(); + foreach ($data as $row) + { + $unique[$row->studienplan_id] = (object) array( + 'studienplan_id' => $row->studienplan_id, + 'studienordnung_id' => $row->studienordnung_id, + 'bezeichnung' => $row->bezeichnung, + 'orgform_kurzbz' => $row->orgform_kurzbz + ); + } + + $this->terminateWithSuccess(array_values($unique)); + } + $this->terminateWithSuccess(); + } + + private function _checkIfExist($oe_kurzbz, $studiensemester_kurzbz, $ausbildungssemester, $studienplan_id, $kalender_syncstatus_id = null) + { + $this->_ci->KalendersyncstatusModel->db->where('oe_kurzbz', $oe_kurzbz); + $this->_ci->KalendersyncstatusModel->db->where('studiensemester_kurzbz', $studiensemester_kurzbz); + + if (is_null($ausbildungssemester)) + $this->_ci->KalendersyncstatusModel->db->where('ausbildungssemester IS NULL', null, false); + else + $this->_ci->KalendersyncstatusModel->db->where('ausbildungssemester', $ausbildungssemester); + + if (is_null($studienplan_id)) + $this->_ci->KalendersyncstatusModel->db->where('studienplan_id IS NULL', null, false); + else + $this->_ci->KalendersyncstatusModel->db->where('studienplan_id', $studienplan_id); + + if (!is_null($kalender_syncstatus_id)) + $this->_ci->KalendersyncstatusModel->db->where('kalender_syncstatus_id !=', $kalender_syncstatus_id); + return $this->_ci->KalendersyncstatusModel->load(); + } + +} diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index 19c800e3f..efc401ad1 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -1489,88 +1489,4 @@ class KalenderLib return $history_entries; } - //TODO in eine eigen Lib? - public function sync() - { - $this->_ci->load->model('ressource/Kalender_model', 'KalenderModel'); - - $this->_ci->KalenderModel->addSelect('tbl_kalender.*, tbl_kalender_ort.ort_kurzbz, tbl_kalender_ort.location'); - $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'tbl_kalender.kalender_id = tbl_kalender_ort.kalender_id', 'LEFT'); - $this->_ci->KalenderModel->db->where_in('status_kurzbz', array('sync_live', 'sync_preview', 'todelete', 'planning', 'preview')); - $this->_ci->KalenderModel->addOrder('tbl_kalender.kalender_id', 'DESC'); - $to_update = $this->_ci->KalenderModel->load(); - - if (!hasData($to_update)) - return success(); - - $mail_infos = []; - - foreach (getData($to_update) as $entry) - { - if ($entry->status_kurzbz === 'todelete') - { - $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'deleted')); - $mail_infos[] = array('entry' => $entry, 'new_status' => 'deleted', 'notify' => array('lektor', 'student')); - } - - if ($entry->status_kurzbz === 'sync_preview') - { - $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'preview')); - $this->_archiveVorgaenger($entry->vorgaenger_kalender_id, true); - $mail_infos[] = array('entry' => $entry, 'new_status' => 'preview', 'notify' => array('lektor')); - } - - if ($entry->status_kurzbz === 'sync_live' || $entry->status_kurzbz === 'planning') - { - $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'live')); - $this->_archiveVorgaenger($entry->vorgaenger_kalender_id, false); - $mail_infos[] = array('entry' => $entry, 'new_status' => 'live', 'notify' => array('lektor', 'student')); - } - - if ($entry->status_kurzbz === 'preview') - { - $current = getData($this->_ci->KalenderModel->load(array('kalender_id' => $entry->kalender_id)))[0]; - if ($current->status_kurzbz === 'archived') - continue; - - $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'live')); - $this->_archiveVorgaenger($entry->vorgaenger_kalender_id, false); - $mail_infos[] = array('entry' => $entry, 'new_status' => 'live', 'notify' => array('lektor', 'student')); - } - } - - $this->_ci->load->library('KalenderNotificationLib'); - $this->_ci->kalendernotificationlib->sendMails($mail_infos); - return success(); - - } - - private function _archiveVorgaenger($vorgaenger_kalender_id, $stop_at_live = false) - { - if (!$vorgaenger_kalender_id) return; - - $current_id = $vorgaenger_kalender_id; - - while ($current_id) - { - $vorgaenger = $this->_ci->KalenderModel->load(array('kalender_id' => $current_id)); - if (!hasData($vorgaenger)) return; - - $vorgaenger = getData($vorgaenger)[0]; - - if ($stop_at_live && $vorgaenger->status_kurzbz === 'live') - return; - - if (in_array($vorgaenger->status_kurzbz, array('preview', 'live'))) - { - $this->_ci->KalenderModel->update( - array('kalender_id' => $current_id), - array('status_kurzbz' => 'archived') - ); - } - - $current_id = $vorgaenger->vorgaenger_kalender_id; - } - } - } diff --git a/application/libraries/KalenderNotificationLib.php b/application/libraries/KalenderNotificationLib.php index d611eb247..acf14bae7 100644 --- a/application/libraries/KalenderNotificationLib.php +++ b/application/libraries/KalenderNotificationLib.php @@ -10,7 +10,7 @@ class KalenderNotificationLib $this->_ci =& get_instance(); $this->_ci->load->library('MailLib'); $this->_ci->load->config('tempus'); - + $this->_ci->load->model('ressource/Kalender_model', 'KalenderModel'); } public function sendMails($mail_infos) @@ -90,6 +90,7 @@ class KalenderNotificationLib if (!empty($student_deleted)) $student_entries .= $this->_addToList($student_deleted, 'gelöscht') . '
'; + //TODO (david) richtigen Empfänger if (!empty($lektor_entries)) $this->_ci->maillib->send('', 'ma0048@technikum-wien.at', 'Lektor Tempus Update', $lektor_entries); if (!empty($student_entries)) diff --git a/application/libraries/KalenderSyncLib.php b/application/libraries/KalenderSyncLib.php new file mode 100644 index 000000000..ee0197e20 --- /dev/null +++ b/application/libraries/KalenderSyncLib.php @@ -0,0 +1,336 @@ + array('planning', 'sync_preview'), + 'live' => array('planning', 'sync_preview', 'preview', 'sync_live') + ); + + public function __construct() + { + $this->_ci =& get_instance(); + + $this->_ci->load->model('ressource/Kalender_model', 'KalenderModel'); + $this->_ci->load->model('ressource/Kalendersyncstatus_model', 'KalendersyncstatusModel'); + $this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel'); + } + + public function run($studiensemester_kurzbz) + { + $this->_ci->KalendersyncstatusModel->db->where('studiensemester_kurzbz', $studiensemester_kurzbz); + + $configs = $this->_ci->KalendersyncstatusModel->load(); + + if (isError($configs)) + return $configs; + + if (!hasData($configs)) + return success([]); + + $results = []; + $mail_infos_gesamt = []; + + foreach (getData($configs) as $config) + { + + $ziel_status = isEmptyString($config->sync_status_kurzbz) ? 'live' : $config->sync_status_kurzbz; + + $relevante_stati = array_merge($this->status_map[$ziel_status], array('todelete')); + + $kalender_ids_result = $this->getKalenderIds( + $config->oe_kurzbz, + $config->studiensemester_kurzbz, + $relevante_stati, + $config->ausbildungssemester, + $config->studienplan_id, + $config->datum_bis + ); + + if (isError($kalender_ids_result)) + return $kalender_ids_result; + + $kalender_ids = getData($kalender_ids_result); + + $sync_result = $this->_syncKalenderIds($kalender_ids, $ziel_status); + + if (isError($sync_result)) + return $sync_result; + + $sync_data = getData($sync_result); + + if ((bool) $config->mail) + $mail_infos_gesamt = array_merge($mail_infos_gesamt, $sync_data['mail_infos']); + + $results[] = [ + 'kalender_syncstatus_id' => $config->kalender_syncstatus_id, + 'oe_kurzbz' => $config->oe_kurzbz, + 'ausbildungssemester' => $config->ausbildungssemester, + 'studienplan_id' => $config->studienplan_id, + 'ziel_status' => $ziel_status, + 'mail' => (bool) $config->mail, + 'synced' => $sync_data['synced'] + ]; + } + + if (!empty($mail_infos_gesamt)) + { + $this->_ci->load->library('KalenderNotificationLib'); + $this->_ci->kalendernotificationlib->sendMails($mail_infos_gesamt); + } + + return success($results); + } + + public function runManual($oe_kurzbz, $studiensemester_kurzbz, $ausbildungssemester = null, $studienplan_id = null, $mail = true, $datum_bis = null, $ziel_status = 'live') + { + $relevante_stati = array_merge($this->status_map[$ziel_status], array('todelete')); + + $kalender_ids_result = $this->getKalenderIds( + $oe_kurzbz, + $studiensemester_kurzbz, + $relevante_stati, + $ausbildungssemester, + $studienplan_id, + $datum_bis + ); + + if (isError($kalender_ids_result)) + return $kalender_ids_result; + + $kalender_ids = getData($kalender_ids_result); + + $sync_result = $this->_syncKalenderIds($kalender_ids, $ziel_status); + + if (isError($sync_result)) + return $sync_result; + + $sync_data = getData($sync_result); + + if ($mail && !empty($sync_data['mail_infos'])) + { + $this->_ci->load->library('KalenderNotificationLib'); + $this->_ci->kalendernotificationlib->sendMails($sync_data['mail_infos']); + } + + return success(['synced' => $sync_data['synced']]); + } + + public function sync() + { + $this->_ci->KalenderModel->addSelect('tbl_kalender.*, tbl_kalender_ort.ort_kurzbz, tbl_kalender_ort.location'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'tbl_kalender.kalender_id = tbl_kalender_ort.kalender_id', 'LEFT'); + $this->_ci->KalenderModel->db->where_in('status_kurzbz', array('sync_live', 'sync_preview', 'todelete')); + $this->_ci->KalenderModel->addOrder('tbl_kalender.kalender_id', 'DESC'); + $to_update = $this->_ci->KalenderModel->load(); + + if (isError($to_update)) + return $to_update; + + if (!hasData($to_update)) + return success(['synced' => 0]); + + $mail_infos = []; + $synced = 0; + + foreach (getData($to_update) as $entry) + { + if ($entry->status_kurzbz === 'todelete') + { + $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'deleted')); + $mail_infos[] = array('entry' => $entry, 'new_status' => 'deleted', 'notify' => array('lektor', 'student')); + $synced++; + } + + if ($entry->status_kurzbz === 'sync_preview') + { + $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'preview')); + $this->_archiveVorgaenger($entry->vorgaenger_kalender_id, true); + $mail_infos[] = array('entry' => $entry, 'new_status' => 'preview', 'notify' => array('lektor')); + $synced++; + } + + if ($entry->status_kurzbz === 'sync_live') + { + $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'live')); + $this->_archiveVorgaenger($entry->vorgaenger_kalender_id, false); + $mail_infos[] = array('entry' => $entry, 'new_status' => 'live', 'notify' => array('lektor', 'student')); + $synced++; + } + } + + $this->_ci->load->library('KalenderNotificationLib'); + $this->_ci->kalendernotificationlib->sendMails($mail_infos); + + return success(['synced' => $synced]); + } + + public function getKalenderIds($oe_kurzbz, $studiensemester_kurzbz, $status_kurzbz_list, $ausbildungssemester = null, $studienplan_id = null, $datum_bis = null) + { + if (empty($status_kurzbz_list)) + return success([]); + + $studiengang_result = $this->_getStudiengang($oe_kurzbz); + + if (isError($studiengang_result)) + return $studiengang_result; + + $studiengang_kz = getData($studiengang_result); + + $this->_ci->KalenderModel->addDistinct('tbl_kalender.kalender_id'); + $this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit', 'tbl_kalender.kalender_id = tbl_kalender_lehreinheit.kalender_id'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit', 'tbl_kalender_lehreinheit.lehreinheit_id = tbl_lehreinheit.lehreinheit_id'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_lehrveranstaltung', 'tbl_lehreinheit.lehrveranstaltung_id = tbl_lehrveranstaltung.lehrveranstaltung_id'); + + //TODO (david) oe_kurzbz rekursiv? + if (!is_null($studiengang_kz)) + $this->_ci->KalenderModel->db->where('tbl_lehrveranstaltung.studiengang_kz', $studiengang_kz); + else + $this->_ci->KalenderModel->db->where('tbl_lehrveranstaltung.oe_kurzbz', $oe_kurzbz); + + $this->_ci->KalenderModel->db->where('tbl_lehreinheit.studiensemester_kurzbz', $studiensemester_kurzbz); + $this->_ci->KalenderModel->db->where_in('tbl_kalender.status_kurzbz', $status_kurzbz_list); + + $this->_ci->KalenderModel->db->where('NOT EXISTS ( + SELECT 1 FROM lehre.tbl_kalender nachfolger + WHERE nachfolger.vorgaenger_kalender_id = tbl_kalender.kalender_id)', null, false); + + if (!is_null($studienplan_id)) + { + $this->_ci->KalenderModel->addJoin('lehre.tbl_studienplan_lehrveranstaltung', 'tbl_lehrveranstaltung.lehrveranstaltung_id = tbl_studienplan_lehrveranstaltung.lehrveranstaltung_id'); + $this->_ci->KalenderModel->db->where('tbl_studienplan_lehrveranstaltung.studienplan_id', $studienplan_id); + } + + if (!is_null($ausbildungssemester)) + $this->_ci->KalenderModel->db->where('tbl_lehrveranstaltung.semester', $ausbildungssemester); + + if (!is_null($datum_bis)) + { + $end_date = date('Y-m-d', strtotime($datum_bis . ' +1 day')); + $this->_ci->KalenderModel->db->where('tbl_kalender.von <', $end_date); + } + + $result = $this->_ci->KalenderModel->load(); + + if (isError($result)) + return $result; + + if (!hasData($result)) + return success([]); + + return success(array_column(getData($result), 'kalender_id')); + } + + + private function _archiveVorgaenger($vorgaenger_kalender_id, $stop_at_live = false) + { + if (!$vorgaenger_kalender_id) return; + + $current_id = $vorgaenger_kalender_id; + + while ($current_id) + { + $vorgaenger = $this->_ci->KalenderModel->load(array('kalender_id' => $current_id)); + if (!hasData($vorgaenger)) return; + + $vorgaenger = getData($vorgaenger)[0]; + + if ($stop_at_live && $vorgaenger->status_kurzbz === 'live') + return; + + if (in_array($vorgaenger->status_kurzbz, array('preview', 'live'))) + { + $this->_ci->KalenderModel->update( + array('kalender_id' => $current_id), + array('status_kurzbz' => 'archived') + ); + } + + $current_id = $vorgaenger->vorgaenger_kalender_id; + } + } + + private function _syncKalenderIds($kalender_ids, $ziel_status) + { + if (empty($kalender_ids)) + return success(['synced' => 0, 'mail_infos' => []]); + + $this->_ci->KalenderModel->addSelect('tbl_kalender.*, tbl_kalender_ort.ort_kurzbz, tbl_kalender_ort.location'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'tbl_kalender.kalender_id = tbl_kalender_ort.kalender_id', 'LEFT'); + $this->_ci->KalenderModel->db->where_in('tbl_kalender.kalender_id', $kalender_ids); + $this->_ci->KalenderModel->addOrder('tbl_kalender.kalender_id', 'DESC'); + $to_update = $this->_ci->KalenderModel->load(); + + if (isError($to_update)) + return $to_update; + + if (!hasData($to_update)) + return success(['synced' => 0, 'mail_infos' => []]); + + $mail_infos = []; + $synced = 0; + + foreach (getData($to_update) as $entry) + { + if ($entry->status_kurzbz === 'todelete') + { + $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'deleted')); + $mail_infos[] = array('entry' => $entry, 'new_status' => 'deleted', 'notify' => array('lektor', 'student')); + $synced++; + continue; + } + + if ($ziel_status === 'preview' && in_array($entry->status_kurzbz, array('planning', 'sync_preview'))) + { + $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'preview')); + $this->_archiveVorgaenger($entry->vorgaenger_kalender_id, true); + $mail_infos[] = array('entry' => $entry, 'new_status' => 'preview', 'notify' => array('lektor')); + $synced++; + continue; + } + + if ($ziel_status === 'live' && in_array($entry->status_kurzbz, array('planning', 'sync_preview', 'sync_live'))) + { + $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'live')); + $this->_archiveVorgaenger($entry->vorgaenger_kalender_id, false); + $mail_infos[] = array('entry' => $entry, 'new_status' => 'live', 'notify' => array('lektor', 'student')); + $synced++; + continue; + } + + if ($ziel_status === 'live' && $entry->status_kurzbz === 'preview') + { + $current = getData($this->_ci->KalenderModel->load(array('kalender_id' => $entry->kalender_id)))[0]; + if ($current->status_kurzbz === 'archived') + continue; + + $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'live')); + $this->_archiveVorgaenger($entry->vorgaenger_kalender_id, false); + $mail_infos[] = array('entry' => $entry, 'new_status' => 'live', 'notify' => array('lektor', 'student')); + $synced++; + } + } + + return success(['synced' => $synced, 'mail_infos' => $mail_infos]); + } + + + private function _getStudiengang($oe_kurzbz) + { + $this->_ci->StudiengangModel->addJoin('tbl_organisationseinheit', 'oe_kurzbz'); + $result = $this->_ci->StudiengangModel->loadWhere(array('oe_kurzbz' => $oe_kurzbz)); + + if (isError($result)) + return $result; + + if (!hasData($result)) + return success(null); + + return success(getData($result)[0]->studiengang_kz); + } +} \ No newline at end of file diff --git a/application/models/ressource/Kalendersyncstatus_model.php b/application/models/ressource/Kalendersyncstatus_model.php new file mode 100644 index 000000000..f1cd8aedf --- /dev/null +++ b/application/models/ressource/Kalendersyncstatus_model.php @@ -0,0 +1,14 @@ +dbTable = 'lehre.tbl_kalender_syncstatus'; + $this->pk = 'kalender_syncstatus_id'; + } +} diff --git a/application/views/Tempus.php b/application/views/Tempus.php index 346ad56ad..0bd9cb697 100644 --- a/application/views/Tempus.php +++ b/application/views/Tempus.php @@ -7,11 +7,13 @@ 'vue3' => true, 'primevue3' => true, 'tabulator5' => true, + 'filtercomponent' => true, + 'navigationcomponent' => true, 'vuedatepicker11' => true, 'phrases' => array( 'global', 'ui', - 'notiz', + 'lehre' ), 'customCSSs' => [ 'public/css/components/vue-datepicker.css', diff --git a/public/js/api/factory/tempus/sync.js b/public/js/api/factory/tempus/sync.js new file mode 100644 index 000000000..0dd8d9eb5 --- /dev/null +++ b/public/js/api/factory/tempus/sync.js @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2026 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + getSyncs(studiensemester_kurzbz) { + return { + method: 'get', + url: 'api/frontend/v1/tempus/KalenderSync/getSyncs', + params: { studiensemester_kurzbz } + }; + }, + loadSync(kalender_syncstatus_id) { + return { + method: 'get', + url: 'api/frontend/v1/tempus/KalenderSync/loadSync', + params: { kalender_syncstatus_id } + }; + }, + getSyncStatus() { + return { + method: 'get', + url: 'api/frontend/v1/tempus/KalenderSync/getSyncStatus', + }; + }, + add(formData) { + return { + method: 'post', + url: 'api/frontend/v1/tempus/KalenderSync/add', + params: { formData } + }; + }, + delete(kalender_syncstatus_id) { + return { + method: 'post', + url: 'api/frontend/v1/tempus/KalenderSync/delete', + params: { kalender_syncstatus_id } + }; + }, + start(formData) { + return { + method: 'post', + url: 'api/frontend/v1/tempus/KalenderSync/start', + params: { formData } + }; + }, + updateSync(formData) { + return { + method: 'post', + url: 'api/frontend/v1/tempus/KalenderSync/updateSync', + params: { formData } + }; + }, + getStudienplan(oe_kurzbz, studiensemester_kurzbz, ausbildungssemester) { + return { + method: 'get', + url: 'api/frontend/v1/tempus/KalenderSync/getStudienplan', + params: { oe_kurzbz, studiensemester_kurzbz, ausbildungssemester } + }; + }, + + getMaxSemester(studiengang_kzs) { + return { + method: 'post', + url: 'api/frontend/v1/stv/status/getMaxSemester/', + params: { studiengang_kzs } + }; + }, + +}; \ No newline at end of file diff --git a/public/js/apps/Tempus.js b/public/js/apps/Tempus.js index 160aa3383..b1159da02 100644 --- a/public/js/apps/Tempus.js +++ b/public/js/apps/Tempus.js @@ -16,6 +16,7 @@ */ import FhcTempus from "../components/Tempus/Tempus.js"; +import FhcTempusSync from "../components/Tempus/TempusSync.js"; import Phrasen from "../plugins/Phrasen.js"; import {capitalize} from "../helpers/StringHelpers.js"; @@ -27,6 +28,7 @@ const router = VueRouter.createRouter({ history: VueRouter.createWebHistory(), routes: [ { path: `/${ciPath}/Tempus`, component: FhcTempus }, + { path: `/${ciPath}/Tempus/sync`, component: FhcTempusSync }, ] }); diff --git a/public/js/components/Tempus/Modal.js b/public/js/components/Tempus/Modal.js new file mode 100644 index 000000000..981479aea --- /dev/null +++ b/public/js/components/Tempus/Modal.js @@ -0,0 +1,340 @@ +import BsModal from '../Bootstrap/Modal.js'; +import CoreForm from '../Form/Form.js'; +import FormValidation from '../Form/Validation.js'; +import FormInput from '../Form/Input.js'; + +import ApiTempusSync from '../../api/factory/tempus/sync.js'; + +export default { + components: { + BsModal, + CoreForm, + FormValidation, + FormInput + }, + props: { + config: Object, + stsem_kurzbz: String + }, + emits: [ + 'saved' + ], + data() { + return { + mode: 'new', + formData: {}, + studienplaene: [], + max_semester: null, + syncStati: [] + }; + }, + computed: { + semester() { + if (!this.max_semester) + return []; + + return Array.from({length: this.max_semester}, (_, index) => index); + } + }, + watch: { + 'formData.oe_kurzbz'(newOrganisation) { + if (!newOrganisation) { + this.max_semester = null; + this.studienplaene = []; + return; + } + + let filtered_org = this.config.organisationen.filter(org => org.oe_kurzbz === newOrganisation)[0]; + + if (filtered_org && filtered_org.studiengang_kz) + this.getSemester(filtered_org.studiengang_kz); + else + this.max_semester = null; + + this.getStudienplan(); + }, + 'formData.studiensemester_kurzbz'() { + this.getStudienplan(); + }, + 'formData.ausbildungssemester'() { + this.getStudienplan(); + } + }, + methods: { + + openNew() { + this.$refs.modal.hide(); + + this.mode = 'new'; + this.max_semester = null; + this.studienplaene = []; + this.formData = { + oe_kurzbz: null, + studiensemester_kurzbz: this.stsem_kurzbz ?? null, + datum_bis: null, + studienplan_id: null, + ausbildungssemester: null, + sync_status_kurzbz: null, + mail: false + }; + + this.$refs.form.clearValidation() + this.$refs.modal.show() + }, + openEdit(entry) { + if (!entry || entry.kalender_syncstatus_id === undefined || entry.kalender_syncstatus_id === null) + { + return; + } + + this.$refs.modal.hide(); + this.mode = 'edit'; + + this.$api + .call(ApiTempusSync.loadSync(entry.kalender_syncstatus_id)) + .then(result => { + this.formData = result.data; + return this.$refs.form.clearValidation(); + }) + .then(() => this.$refs.modal.show()) + .catch(this.$fhcAlert.handleSystemError); + }, + + openStart() { + this.$refs.modal.hide(); + + this.mode = 'start'; + this.max_semester = null; + this.studienplaene = []; + this.formData = { + oe_kurzbz: null, + studiensemester_kurzbz: this.stsem_kurzbz ?? null, + datum_bis: null, + sync_status_kurzbz: null, + studienplan_id: null, + ausbildungssemester: null, + mail: false + }; + + this.$refs.form.clearValidation() + this.$refs.modal.show() + }, + getSemester(stg_kz) { + this.$api.call(ApiTempusSync.getMaxSemester([stg_kz])) + .then(response => response.data) + .then(response => { + this.max_semester = response; + + let found = this.semester.filter(semester => semester === this.formData.ausbildungssemester); + if (found.length === 0) + this.formData.ausbildungssemester = null; + }) + .catch(error => { + this.$fhcAlert.handleSystemError(error); + }); + }, + getStudienplan() { + if (!this.formData.oe_kurzbz || !this.formData.studiensemester_kurzbz) { + this.studienplaene = []; + return; + } + + this.$api.call(ApiTempusSync.getStudienplan( + this.formData.oe_kurzbz, + this.formData.studiensemester_kurzbz, + this.formData.ausbildungssemester ?? null + )) + .then(response => response.data) + .then(response => { + this.studienplaene = response; + let found = this.studienplaene.filter(plan => plan.studienplan_id === this.formData.studienplan_id); + if (found.length === 0) + this.formData.studienplan_id = null; + }) + .catch(error => { + this.$fhcAlert.handleSystemError(error); + }); + }, + save() { + if (this.mode === 'start') + this.start(); + else if (this.mode === 'new') + this.insert(); + else + this.update(); + }, + insert() { + this.$refs.form + .call(ApiTempusSync.add(this.formData)) + .then(() => { + this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); + this.$emit('saved'); + this.$refs.modal.hide(); + }) + .catch(this.$fhcAlert.handleSystemError); + }, + update() { + this.$refs.form + .call(ApiTempusSync.updateSync(this.formData)) + .then(() => { + this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); + this.$emit('saved'); + this.$refs.modal.hide(); + }) + .catch(this.$fhcAlert.handleSystemError); + }, + start() { + this.$refs.form + .call(ApiTempusSync.start(this.formData)) + .then(() => { + this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); + this.$emit('saved'); + this.$refs.modal.hide(); + }) + .catch(this.$fhcAlert.handleSystemError); + } + }, + created() { + console.log(this.config) + this.$api + .call(ApiTempusSync.getSyncStatus()) + .then(result => this.syncStati = result.data) + .catch(error => this.$fhcAlert.handleSystemError(error)); + }, + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` +}; \ No newline at end of file diff --git a/public/js/components/Tempus/TempusSync.js b/public/js/components/Tempus/TempusSync.js new file mode 100644 index 000000000..c97164984 --- /dev/null +++ b/public/js/components/Tempus/TempusSync.js @@ -0,0 +1,196 @@ +import {CoreFilterCmpt} from '../filter/Filter.js'; +import {CoreNavigationCmpt} from '../navigation/Navigation.js'; +import FormInput from "../Form/Input.js"; + +import CoreBaseLayout from '../../components/layout/BaseLayout.js'; +import ApiTempusSync from '../../api/factory/tempus/sync.js'; +import TempusSyncModal from './Modal.js'; + +export default { + props: { + config: Object, + defaultSemester: String + }, + components: { + CoreFilterCmpt, + CoreBaseLayout, + CoreNavigationCmpt, + FormInput, + TempusSyncModal + }, + data() { + return { + studiensemester_kurzbz: this.defaultSemester ?? null + } + }, + watch: { + studiensemester_kurzbz() + { + this.reloadTable(); + } + }, + computed: { + tabulatorOptions() { + return { + index: "kalender_syncstatus_id", + ajaxURL: 'dummy', + ajaxRequestFunc: () => this.$api.call(ApiTempusSync.getSyncs(this.studiensemester_kurzbz)), + ajaxResponse: (url, params, response) => { + return response.data || []; + }, + layout: 'fitDataStretch', + placeholder: "Keine Daten verfügbar", + persistenceID: "2026_07_21_tempus_sync_v1", + locale: true, + columns: [ + { + formatter: 'rowSelection', + titleFormatter: 'rowSelection', + headerSort: false, + width: 40 + }, + {titlePhrase: 'lehre/organisationseinheit', field: 'oebezeichnung'}, + {titlePhrase: 'lehre/studiensemester', field: 'studiensemester_kurzbz'}, + {titlePhrase: 'lehre/bis_title', field: 'datum_bis', + formatter: (cell) => { + const value = cell.getValue(); + if (!value) return '' + const date = luxon.DateTime.fromSQL(value) + return date.isValid ? date.toFormat('dd.MM.yyyy') : value + }, + minWidth: 100 + }, + {titlePhrase: 'lehre/studienplan', field: 'studienplanbezeichnung', minWidth: 200}, + {titlePhrase: 'lehre/ausbildungssemester', field: 'ausbildungssemester'}, + {titlePhrase: 'global/status', field: 'sync_status_kurzbz'}, + {titlePhrase: 'lehre/mail_benachrichtigung', field: 'mail', formatter: 'tickCross', hozAlign: 'center', minWidth: 80}, + {titlePhrase:'global/insertamum', field: 'insertamum', + formatter: (cell) => { + const value = cell.getValue(); + if (!value) return '' + const date = luxon.DateTime.fromSQL(value) + return date.isValid ? date.toFormat('dd.MM.yyyy HH:mm') : value + }, + visible: false + }, + {titlePhrase: 'global/insertvon', field: 'insertvon', visible: false}, + {titlePhrase: 'global/updateamum', field: 'updateamum', + formatter: (cell) => { + const value = cell.getValue(); + if (!value) return '' + const date = luxon.DateTime.fromSQL(value) + return date.isValid ? date.toFormat('dd.MM.yyyy HH:mm') : value + }, + visible: false + }, + {titlePhrase: 'global/updatevon', field: 'updatevon', visible: false}, + { + titlePhrase: 'global/aktionen', + field: 'actions', + headerSort: false, + hozAlign: 'center', + width: 120, + formatter: (cell) => { + let container = document.createElement('div'); + container.className = "d-flex gap-2"; + let button = document.createElement('button'); + button.className = 'btn btn-outline-secondary'; + button.innerHTML = ''; + button.addEventListener('click', () => + this.$refs.syncModal.openEdit(cell.getRow().getData()) + ); + + container.append(button); + + button = document.createElement('button'); + button.className = 'btn btn-outline-secondary'; + button.innerHTML = ''; + button.addEventListener('click', evt => { + evt.stopPropagation(); + this.$fhcAlert + .confirmDelete() + .then(result => result ? cell.getData().kalender_syncstatus_id : Promise.reject({handled:true})) + .then(kalender_syncstatus_id => this.$api.call(ApiTempusSync.delete(kalender_syncstatus_id))) + .then(() => { + this.reloadTable(); + }) + .catch(this.$fhcAlert.handleSystemError); + }); + container.append(button); + + return container; + } + } + ], + } + } + }, + + methods: { + openNewModal() + { + this.$refs.syncModal.openNew(); + }, + reloadTable() + { + this.$refs.syncTable.reloadTable(); + }, + openStartModal() + { + this.$refs.syncModal.openStart(); + } + }, + template: ` + + + + + + ` +}; \ No newline at end of file diff --git a/system/dbupdate_3.4.php b/system/dbupdate_3.4.php index cc64bb47f..34317ad04 100644 --- a/system/dbupdate_3.4.php +++ b/system/dbupdate_3.4.php @@ -99,6 +99,7 @@ require_once('dbupdate_3.4/75888_reihungstest_mehrfachdurchfuehrung.php'); require_once('dbupdate_3.4/76150_perm_other_lv_plan.php'); require_once('dbupdate_3.4/68957_dashboard_bookmark_neue_Spalte_sort.php'); require_once('dbupdate_3.4/68530_Dashboard_Cleanup.php'); +require_once('dbupdate_3.4/77949_tempus_synchronisation.php'); // *** Pruefung und hinzufuegen der neuen Attribute und Tabellen echo '

Pruefe Tabellen und Attribute!

'; diff --git a/system/dbupdate_3.4/77949_tempus_synchronisation.php b/system/dbupdate_3.4/77949_tempus_synchronisation.php new file mode 100644 index 000000000..5773a31d0 --- /dev/null +++ b/system/dbupdate_3.4/77949_tempus_synchronisation.php @@ -0,0 +1,38 @@ +db_query("SELECT kalender_syncstatus_id FROM lehre.tbl_kalender_syncstatus LIMIT 1")) +{ + $qry = "CREATE TABLE lehre.tbl_kalender_syncstatus + ( + kalender_syncstatus_id bigserial NOT NULL, + oe_kurzbz character varying(32), + studiensemester_kurzbz character varying(32), + datum_bis date, + studienplan_id integer, + ausbildungssemester smallint, + sync_status_kurzbz character varying(32), + mail boolean DEFAULT false NOT NULL, + insertamum timestamp DEFAULT now(), + insertvon character varying(32), + updateamum timestamp, + updatevon character varying(32), + CONSTRAINT tbl_kalender_syncstatus_pk PRIMARY KEY (kalender_syncstatus_id) + ); + + ALTER TABLE lehre.tbl_kalender_syncstatus ADD CONSTRAINT tbl_kalender_syncstatus_oe_kurzbz_fk FOREIGN KEY (oe_kurzbz) REFERENCES public.tbl_organisationseinheit (oe_kurzbz) ON DELETE RESTRICT ON UPDATE CASCADE; + ALTER TABLE lehre.tbl_kalender_syncstatus ADD CONSTRAINT tbl_kalender_syncstatus_studiensemester_kurzbz_fk FOREIGN KEY (studiensemester_kurzbz) REFERENCES public.tbl_studiensemester(studiensemester_kurzbz) ON DELETE RESTRICT ON UPDATE CASCADE; + ALTER TABLE lehre.tbl_kalender_syncstatus ADD CONSTRAINT tbl_kalender_syncstatus_studienplan_id_fk FOREIGN KEY (studienplan_id) REFERENCES lehre.tbl_studienplan (studienplan_id) ON DELETE RESTRICT ON UPDATE CASCADE; + ALTER TABLE lehre.tbl_kalender_syncstatus ADD CONSTRAINT tbl_kalender_syncstatus_status_sync_status_kurzbz_fk FOREIGN KEY (sync_status_kurzbz) REFERENCES lehre.tbl_kalender_status (status_kurzbz) ON DELETE RESTRICT ON UPDATE CASCADE; + + GRANT SELECT, UPDATE ON lehre.tbl_kalender_syncstatus_kalender_syncstatus_id_seq TO vilesci; + GRANT SELECT ON lehre.tbl_kalender_syncstatus_kalender_syncstatus_id_seq TO web; + GRANT SELECT, UPDATE, INSERT, DELETE ON lehre.tbl_kalender_syncstatus to vilesci; + GRANT SELECT ON lehre.tbl_kalender_typ to web;"; + + if(!$db->db_query($qry)) + echo 'lehre.tbl_kalender_syncstatus: '.$db->db_last_error().'
'; + else + echo ' lehre.tbl_kalender_syncstatus: Tabelle hinzugefuegt
'; + +} \ No newline at end of file diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 827e03261..02f550c63 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -3363,6 +3363,126 @@ $phrases = array( ) ) ), + array( + 'app' => 'core', + 'category' => 'lehre', + 'phrase' => 'mail_benachrichtigung', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Mail-Benachrichtigung', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Email notification', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'lehre', + 'phrase' => 'bis_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Bis', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Until', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'lehre', + 'phrase' => 'errorDeleting', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Fehler beim Löschen.', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Error deleting.', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'lehre', + 'phrase' => 'tempus_sync_start', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Synchronisierung manuell starten', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Start synchronization manually', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'lehre', + 'phrase' => 'tempus_sync_edit', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Sync-Eintrag bearbeiten', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Edit sync entry', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'lehre', + 'phrase' => 'tempus_sync_new', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Neuer Sync-Eintrag', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'New sync entry', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), array( 'app' => 'core', 'category' => 'lehre', From 817afa545be6bb5e3c451e505cb35ed6c03c0675 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Wed, 22 Jul 2026 15:52:31 +0200 Subject: [PATCH 02/33] add tempus kalender tags --- application/config/routes.php | 2 + application/config/tempus.php | 9 +- .../api/frontend/v1/tempus/Tags.php | 53 +++++ application/core/Tag_Controller.php | 30 ++- application/libraries/KalenderLib.php | 186 +++++++++------ application/views/Tempus.php | 1 + public/css/components/calendar/events.css | 3 + public/css/tags.css | 7 + public/js/api/factory/tempus/tag.js | 59 +++++ public/js/components/Tag/Tag.js | 6 +- .../Renderer/Lehreinheit/calendarEvent.js | 49 +++- .../Renderer/Lehreinheit/modalContent.js | 54 ++++- public/js/components/Tempus/Tempus.js | 220 ++++++++++++++++-- .../composables/Tempus/ContextMenuActions.js | 5 + system/dbupdate_3.4.php | 1 + .../dbupdate_3.4/78052_tempus_tag_events.php | 19 ++ system/phrasesupdate.php | 80 +++++++ 17 files changed, 694 insertions(+), 90 deletions(-) create mode 100644 application/controllers/api/frontend/v1/tempus/Tags.php create mode 100644 public/js/api/factory/tempus/tag.js create mode 100644 system/dbupdate_3.4/78052_tempus_tag_events.php diff --git a/application/config/routes.php b/application/config/routes.php index 8024f449a..32c2152a9 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -125,6 +125,8 @@ $route['api/frontend/v1/stv/[sS]tudents/([WS]S[0-9]{4})/person/(:num)'] = 'api/f // load routes from extensions, also look for environment-specific configs $subdirs = ['application/config/extensions', 'application/config/' . ENVIRONMENT . '/extensions']; +$route['api/frontend/v1/tempus/Tags/getTagsByCalendar/(:num)'] = 'api/frontend/v1/tempus/Tags/getTagsByCalendar/$1'; + foreach($subdirs as $subdir) { if(is_dir($subdir)) diff --git a/application/config/tempus.php b/application/config/tempus.php index e3e18b954..833f5655f 100644 --- a/application/config/tempus.php +++ b/application/config/tempus.php @@ -2,4 +2,11 @@ $config['send_update_mails'] = false; $config['calendar_start'] = 7; -$config['calendar_end'] = 23; \ No newline at end of file +$config['calendar_end'] = 23; + + +$config['tempus_tags'] = [ + 'prioone' => ['readonly' => false], + 'priotwo' => ['readonly' => true], + 'hinweis' => ['readonly' => false], +]; \ No newline at end of file diff --git a/application/controllers/api/frontend/v1/tempus/Tags.php b/application/controllers/api/frontend/v1/tempus/Tags.php new file mode 100644 index 000000000..c5223cd9a --- /dev/null +++ b/application/controllers/api/frontend/v1/tempus/Tags.php @@ -0,0 +1,53 @@ + self::BERECHTIGUNG_KURZBZ, + 'getTags' => self::BERECHTIGUNG_KURZBZ, + 'getTagsByCalendar' => self::BERECHTIGUNG_KURZBZ, + 'addTag' => self::BERECHTIGUNG_KURZBZ, + 'updateTag' => self::BERECHTIGUNG_KURZBZ, + 'doneTag' => self::BERECHTIGUNG_KURZBZ, + 'deleteTag' => self::BERECHTIGUNG_KURZBZ + ]); + + $this->config->load('tempus'); + } + + public function getTag($readonly_tags = null) + { + parent::getTag($this->config->item('tempus_tags')); + } + public function getTags($tags = null) + { + parent::getTags($this->config->item('tempus_tags')); + } + public function getTagsByCalendar($eindeutige_kalender_gruppen_id = null) + { + parent::getTagsByAssignmentTypeValue('eindeutige_kalender_gruppen_id', $eindeutige_kalender_gruppen_id, $this->config->item('tempus_tags')); + } + public function addTag($withZuordnung = true, $updatable_tags = null) + { + parent::addTag(true, $this->config->item('tempus_tags')); + } + public function updateTag($updatable_tags = null) + { + parent::updateTag($this->config->item('tempus_tags')); + } + public function deleteTag($withZuordnung = true, $updatable_tags = null) + { + parent::deleteTag(true, $this->config->item('tempus_tags')); + } + public function doneTag($updatable_tags = null) + { + parent::doneTag($this->config->item('tempus_tags')); + } +} diff --git a/application/core/Tag_Controller.php b/application/core/Tag_Controller.php index 5b9bac6c5..6a160c75b 100644 --- a/application/core/Tag_Controller.php +++ b/application/core/Tag_Controller.php @@ -106,7 +106,33 @@ class Tag_Controller extends FHCAPI_Controller $notiztypen = $this->NotiztypModel->loadWhere(array('aktiv' => true)); $this->terminateWithSuccess(hasData($notiztypen) ? getData($notiztypen) : array()); } + public function getTagsByAssignmentTypeValue($zuordnung_typ, $zuordnung_id, $tags = null) + { + $this->NotizzuordnungModel->addSelect( + 'tbl_notizzuordnung.notiz_id as notiz_id, + typ_kurzbz as tag_typ_kurzbz, + array_to_json(bezeichnung_mehrsprachig::varchar[])->>0 as bezeichnung, + style, + beschreibung, + tag, + tbl_notiz.erledigt as done + ' + ); + + if (is_array($tags) && !isEmptyArray($tags)) + { + $tags = $this->_filterTag($tags, false); + $this->NotizzuordnungModel->db->where_in('tbl_notiz_typ.typ_kurzbz', $tags); + } + $this->NotizzuordnungModel->addJoin('public.tbl_notiz', 'public.tbl_notizzuordnung.notiz_id = public.tbl_notiz.notiz_id'); + $this->NotizzuordnungModel->addJoin('public.tbl_notiz_typ', 'public.tbl_notiz.typ = public.tbl_notiz_typ.typ_kurzbz'); + + $this->NotizzuordnungModel->addOrder('prioritaet'); + + $notiztypen = $this->NotizzuordnungModel->loadWhere(array('aktiv' => true, $zuordnung_typ => $zuordnung_id)); + $this->terminateWithSuccess(hasData($notiztypen) ? getData($notiztypen) : array()); + } public function addTag($withZuordnung = true, $updatable_tags = null) { $postData = $this->getPostJson(); @@ -126,7 +152,7 @@ class Tag_Controller extends FHCAPI_Controller if (!in_array($postData->tag_typ_kurzbz, $tags)) $this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); } - + if ($withZuordnung) { $return = array(); @@ -135,7 +161,7 @@ class Tag_Controller extends FHCAPI_Controller $this->terminateWithError('Error occurred', self::ERROR_TYPE_GENERAL); $values = array_unique($postData->values); - + foreach ($values as $value) { $insertResult = $this->addNotiz($postData); diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index 3a531102f..70b23e891 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -39,56 +39,56 @@ class KalenderLib $end_date = date('Y-m-d', strtotime($end_date . ' +1 day')); $this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, - tbl_kalender.eindeutige_gruppen_id, - tbl_kalender.status_kurzbz, - tbl_kalender.typ, - tbl_kalender.von, - tbl_kalender.bis, - tbl_kalender_ort.ort_kurzbz, - tbl_lehreinheit.lehreinheit_id, - tbl_lehreinheit.lehrveranstaltung_id, - tbl_lehreinheit.lehrfach_id, - tbl_lehreinheit.lehrform_kurzbz, - tbl_lehrveranstaltung.oe_kurzbz, - lehrfach.kurzbz AS lehrfach_kurzbz, - lehrfach.bezeichnung AS lehrfach_bezeichnung, - lehrfach.farbe, - COALESCE(orginasator.uid, tbl_lehreinheitmitarbeiter.mitarbeiter_uid) as mitarbeiter_uid, - COALESCE(reservierung_person.vorname, tbl_person.vorname) as vorname, - COALESCE(reservierung_person.nachname, tbl_person.nachname) as nachname, - COALESCE(reservierung_ma.kurzbz, tbl_mitarbeiter.kurzbz) AS ma_kurzbz, - teilnehmergruppe.gruppe_kurzbz as teilnehmerg_grp, - COALESCE ( - UPPER(tbl_studiengang.typ || tbl_studiengang.kurzbz) || - COALESCE(verbandgruppe.semester::varchar, \'\') || - COALESCE(verbandgruppe.verband::varchar, \'\') || - COALESCE(verbandgruppe.gruppe, \'\'), - \'\') as verband_grp, - tbl_kalender_event.beschreibung, - tbl_kalender_event.titel, - teilmitarbeiter.kurzbz as teilnehmer_kurzbz, - teilperson.vorname as teilnehmer_vorname, - teilperson.nachname as teilnehmer_nachname, - teilbenutzer.uid as teilnehmer_uid, - CASE - WHEN tbl_lehreinheitgruppe.gruppe_kurzbz IS NULL THEN - COALESCE( - UPPER(le_studiengang.typ || le_studiengang.kurzbz) || - COALESCE(tbl_lehreinheitgruppe.semester::varchar, \'\') || - COALESCE(tbl_lehreinheitgruppe.verband::varchar, \'\') || - COALESCE(tbl_lehreinheitgruppe.gruppe, \'\'), - \'\') - ELSE tbl_lehreinheitgruppe.gruppe_kurzbz - END AS lehreinheit_gruppe_bezeichnung, - tbl_lehreinheitgruppe.gruppe_kurzbz as le_gruppe_kurzbz, - tbl_lehreinheitgruppe.studiengang_kz as le_studiengang_kz, - tbl_lehreinheitgruppe.semester as le_semester, - tbl_lehreinheitgruppe.verband as le_verband, - tbl_lehreinheitgruppe.gruppe as le_gruppe, - le_gruppe.direktinskription as le_direktinskription, - - - '); + tbl_kalender.eindeutige_gruppen_id, + tbl_kalender.status_kurzbz, + tbl_kalender.typ, + tbl_kalender.von, + tbl_kalender.bis, + tbl_kalender_ort.ort_kurzbz, + tbl_lehreinheit.lehreinheit_id, + tbl_lehreinheit.lehrveranstaltung_id, + tbl_lehreinheit.lehrfach_id, + tbl_lehreinheit.lehrform_kurzbz, + tbl_lehrveranstaltung.oe_kurzbz, + lehrfach.kurzbz AS lehrfach_kurzbz, + lehrfach.bezeichnung AS lehrfach_bezeichnung, + lehrfach.farbe, + COALESCE(orginasator.uid, tbl_lehreinheitmitarbeiter.mitarbeiter_uid) as mitarbeiter_uid, + COALESCE(reservierung_person.vorname, tbl_person.vorname) as vorname, + COALESCE(reservierung_person.nachname, tbl_person.nachname) as nachname, + COALESCE(reservierung_ma.kurzbz, tbl_mitarbeiter.kurzbz) AS ma_kurzbz, + teilnehmergruppe.gruppe_kurzbz as teilnehmerg_grp, + COALESCE ( + UPPER(tbl_studiengang.typ || tbl_studiengang.kurzbz) || + COALESCE(verbandgruppe.semester::varchar, \'\') || + COALESCE(verbandgruppe.verband::varchar, \'\') || + COALESCE(verbandgruppe.gruppe, \'\'), + \'\') as verband_grp, + tbl_kalender_event.beschreibung, + tbl_kalender_event.titel, + teilmitarbeiter.kurzbz as teilnehmer_kurzbz, + teilperson.vorname as teilnehmer_vorname, + teilperson.nachname as teilnehmer_nachname, + teilbenutzer.uid as teilnehmer_uid, + CASE + WHEN tbl_lehreinheitgruppe.gruppe_kurzbz IS NULL THEN + COALESCE( + UPPER(le_studiengang.typ || le_studiengang.kurzbz) || + COALESCE(tbl_lehreinheitgruppe.semester::varchar, \'\') || + COALESCE(tbl_lehreinheitgruppe.verband::varchar, \'\') || + COALESCE(tbl_lehreinheitgruppe.gruppe, \'\'), + \'\') + ELSE tbl_lehreinheitgruppe.gruppe_kurzbz + END AS lehreinheit_gruppe_bezeichnung, + tbl_lehreinheitgruppe.gruppe_kurzbz as le_gruppe_kurzbz, + tbl_lehreinheitgruppe.studiengang_kz as le_studiengang_kz, + tbl_lehreinheitgruppe.semester as le_semester, + tbl_lehreinheitgruppe.verband as le_verband, + tbl_lehreinheitgruppe.gruppe as le_gruppe, + le_gruppe.direktinskription as le_direktinskription, + tag_data_agg.tags as tags, + resource_data_agg.resources as resources + '); $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit', 'tbl_kalender.kalender_id = tbl_kalender_lehreinheit.kalender_id', 'LEFT'); $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_event', 'tbl_kalender.kalender_id = tbl_kalender_event.kalender_id', 'LEFT'); @@ -134,6 +134,69 @@ class KalenderLib ); $this->_ci->KalenderModel->addJoin('public.tbl_studiengang le_studiengang', 'le_lehrverband.studiengang_kz = le_studiengang.studiengang_kz', 'LEFT'); + // Include tags in the query + $this->_ci->load->config('tempus'); + $tags = $this->_ci->config->item('tempus_tags'); + + $whereTags = ''; + if (is_array($tags) && !isEmptyArray($tags)) { + $tags = array_keys($tags); + + foreach ($tags as $key => $tag) { + $tags[$key] = $this->_ci->KalenderModel->escape($tag); + } + $whereTags = " AND nt.typ_kurzbz IN (" . implode(",", $tags) . ")"; + } + $subQueryTag = "( + SELECT + tag.eindeutige_kalender_gruppen_id, + COALESCE(json_agg(tag ORDER BY tag.done, tag.prioritaet), '[]'::json) AS tags + FROM ( + SELECT DISTINCT ON (n.notiz_id) + n.notiz_id AS id, + nt.typ_kurzbz, + array_to_json(nt.bezeichnung_mehrsprachig)->>0 AS beschreibung, + n.text AS notiz, + nt.style, + n.erledigt AS done, + nt.prioritaet AS prioritaet, + nz.eindeutige_kalender_gruppen_id + FROM public.tbl_notizzuordnung AS nz + JOIN public.tbl_notiz AS n ON nz.notiz_id = n.notiz_id + JOIN public.tbl_notiz_typ AS nt ON n.typ = nt.typ_kurzbz " . $whereTags . " + ) AS tag + GROUP BY tag.eindeutige_kalender_gruppen_id + ) AS tag_data_agg"; + + $this->_ci->KalenderModel->addJoin($subQueryTag, 'tag_data_agg.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id', 'LEFT'); + // End of tags inclusion + + // Include assigned operational resources in the query + $subQueryResource = "( + SELECT + resource.eindeutige_kalender_gruppen_id, + COALESCE(json_agg(resource ORDER BY resource.beschreibung), '[]'::json) AS resources + FROM ( + SELECT + betriebsmittel_kalender.*, + betriebsmittel.beschreibung, + betriebsmittel.verplanen + FROM lehre.tbl_betriebsmittel_kalender AS betriebsmittel_kalender + JOIN wawi.tbl_betriebsmittel AS betriebsmittel + ON betriebsmittel.betriebsmittel_id = betriebsmittel_kalender.betriebsmittel_id + WHERE betriebsmittel.verplanen = TRUE + ) AS resource + GROUP BY resource.eindeutige_kalender_gruppen_id + ) AS resource_data_agg"; + + $this->_ci->KalenderModel->addJoin( + $subQueryResource, + 'resource_data_agg.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id', + 'LEFT' + ); + // End of operational resources inclusion + + $this->_ci->KalenderModel->db->where('tbl_kalender.von >=', $start_date); $this->_ci->KalenderModel->db->where('tbl_kalender.bis <', $end_date); @@ -157,6 +220,8 @@ class KalenderLib { $von = new DateTime($row->von); $bis = new DateTime($row->bis); + $resources = isset($row->resources) ? $row->resources : []; + $parsedResources = is_string($resources) ? json_decode($resources, true) : $resources; $events[$id] = (object) [ 'eindeutige_gruppen_id' => $row->eindeutige_gruppen_id, @@ -185,7 +250,9 @@ class KalenderLib 'beschreibung' => isset($row->beschreibung) ? $row->beschreibung : '', 'topic' => [], 'collisions' => false, - 'has_assigned_resources' => isset($row->has_assigned_resources) ? $row->has_assigned_resources : false, + 'has_assigned_resources' => !empty($parsedResources), + 'resources' => is_array($parsedResources) ? $parsedResources : [], + 'tags' => isset($row->tags) ? $row->tags : [], ]; } @@ -443,13 +510,6 @@ class KalenderLib $this->_ci->KalenderModel->db->where_not_in('status_kurzbz', array('deleted')); - $this->_ci->KalenderModel->addSelect([ - "( - SELECT COUNT(*) FROM lehre.tbl_betriebsmittel_kalender - WHERE tbl_betriebsmittel_kalender.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id - ) AS has_assigned_resources" - ]); - $data = $this->_ci->KalenderModel->load(); return $this->_mapEvents($data); @@ -961,16 +1021,6 @@ class KalenderLib } } - /* foreach ($specialGroups as $group) - { - $this->_addSpecialGroupToEvent($kalender_id, $group['gruppe_kurzbz'], $group['rolle']); - } - - foreach ($groups as $group) - { - $this->_addGroupToEvent($kalender_id, $group['stg_kz'], $group['semester'], $group['verband'], $group['gruppe'], $group['rolle']); - }*/ - if (isSuccess($kalendereventresult) && !is_null($ort_kurzbz)) { $ortresult = $this->_addKalenderOrt($kalender_id, $ort_kurzbz); diff --git a/application/views/Tempus.php b/application/views/Tempus.php index 346ad56ad..fd6f3a0b7 100644 --- a/application/views/Tempus.php +++ b/application/views/Tempus.php @@ -8,6 +8,7 @@ 'primevue3' => true, 'tabulator5' => true, 'vuedatepicker11' => true, + 'tags' => true, 'phrases' => array( 'global', 'ui', diff --git a/public/css/components/calendar/events.css b/public/css/components/calendar/events.css index 8946c1856..f224c6f7b 100644 --- a/public/css/components/calendar/events.css +++ b/public/css/components/calendar/events.css @@ -86,6 +86,9 @@ pointer-events: none; background-color: transparent !important; border: none !important; + width: fit-content; + margin-left: auto; + margin-right: auto; } .fhc-resize-bar--top { diff --git a/public/css/tags.css b/public/css/tags.css index e92f415b2..61e0d46e9 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -17,6 +17,13 @@ transform: scale(1.15); } +/* add prop that disasbles hover and cursor*/ +.tag.disabled:hover +{ + transform: none; + cursor: default; +} + .tag_rot { background-color: #ff0000ff; } diff --git a/public/js/api/factory/tempus/tag.js b/public/js/api/factory/tempus/tag.js new file mode 100644 index 000000000..6b8a4ce01 --- /dev/null +++ b/public/js/api/factory/tempus/tag.js @@ -0,0 +1,59 @@ +export default { + + getTag(data) + { + return { + method: 'get', + url: '/api/frontend/v1/tempus/Tags/getTag', + params: data + }; + }, + + getTags(data) + { + return { + method: 'get', + url: '/api/frontend/v1/tempus/Tags/getTags' + }; + }, + getTagsByCalendar(calenderGroupId) + { + return { + method: 'get', + url: '/api/frontend/v1/tempus/Tags/getTagsByCalendar/' + calenderGroupId, + }; + }, + addTag(data) + { + return { + method: 'post', + url: '/api/frontend/v1/tempus/Tags/addTag', + params: data + }; + }, + updateTag(data) + { + return { + method: 'post', + url: '/api/frontend/v1/tempus/Tags/updateTag', + params: data + }; + }, + doneTag(data) + { + return { + method: 'post', + url: '/api/frontend/v1/tempus/Tags/doneTag', + params: data + }; + }, + + deleteTag(data) + { + return { + method: 'post', + url: '/api/frontend/v1/tempus/Tags/deleteTag', + params: data + }; + }, +}; diff --git a/public/js/components/Tag/Tag.js b/public/js/components/Tag/Tag.js index 7064c0c17..ecaaa2354 100644 --- a/public/js/components/Tag/Tag.js +++ b/public/js/components/Tag/Tag.js @@ -14,6 +14,10 @@ export default { 'deleted', ], props: { + isListItemShown: { + type: Boolean, + default: true + }, endpoint: { type: Object, required: true @@ -206,7 +210,7 @@ export default { } }, template: ` -
+
+
+ + + + + @@ -1411,6 +1592,17 @@ export default { + Pruefe Tabellen und Attribute!'; diff --git a/system/dbupdate_3.4/78052_tempus_tag_events.php b/system/dbupdate_3.4/78052_tempus_tag_events.php new file mode 100644 index 000000000..64f47d57b --- /dev/null +++ b/system/dbupdate_3.4/78052_tempus_tag_events.php @@ -0,0 +1,19 @@ +db_query("SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'tbl_notizzuordnung' + AND column_name = 'eindeutige_kalender_gruppen_id'"); + +if ($db->db_num_rows($result) === 0) +{ + $qry = "ALTER TABLE public.tbl_notizzuordnung + ADD COLUMN eindeutige_kalender_gruppen_id UUID NULL DEFAULT NULL"; + + if (!$db->db_query($qry)) + echo 'public.tbl_notizzuordnung: '.$db->db_last_error().'
'; + else + echo '
public.tbl_notizzuordnung: eindeutige_kalender_gruppen_id column added'; +} diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 001e9db71..7657b8c36 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -61059,6 +61059,86 @@ I have been informed that I am under no obligation to consent to the transmissio ) ) ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'tags_assignment_modal_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Tag-Zuweisung', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Tag Assignment', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'assigned_tags_subtitle', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Zugewiesene Tags', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Assigned Tags', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'no_assigned_tags', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Keine zugewiesenen Tags', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'No Assigned Tags', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'tags', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Tags', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Tags', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), // generic tabulator phrases END ); From b809392f5d4a8049dc7c89c94dab56ce17b3d90d Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Wed, 22 Jul 2026 16:20:43 +0200 Subject: [PATCH 03/33] add historyModal extraction from tempus component --- public/js/components/Tempus/HistoryModal.js | 68 +++++++++++++++++++++ public/js/components/Tempus/Tempus.js | 28 ++------- 2 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 public/js/components/Tempus/HistoryModal.js diff --git a/public/js/components/Tempus/HistoryModal.js b/public/js/components/Tempus/HistoryModal.js new file mode 100644 index 000000000..9716bc1f1 --- /dev/null +++ b/public/js/components/Tempus/HistoryModal.js @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2024 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import BsModal from "../Bootstrap/Modal.js"; + +export default { + name: "HistoryModal", + components: { + BsModal, + }, + props: { + entries: { + type: Array, + default: () => [], + }, + }, + methods: { + show() { + this.$refs.modal.show(); + }, + hide() { + this.$refs.modal.hide(); + }, + }, + template: ` + + + + + `, +}; diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index a5f28ef73..af762c554 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -46,6 +46,7 @@ import { getTempusShortcuts } from "./shortcuts.js"; import KeyboardShortcuts from "./KeyboardShortcuts.js"; import { useContextMenuActions } from "../../composables/Tempus/ContextMenuActions.js"; import MultiWeekPlanModal from "./MultiWeekPlanModal.js"; +import HistoryModal from "./HistoryModal.js"; import { getTempusSearchbarOptions } from "./Filters/searchbarOptions.js"; import CoreTag from '../Tag/Tag.js'; @@ -71,6 +72,7 @@ export default { Reservierung, KeyboardShortcuts, MultiWeekPlanModal, + HistoryModal, CoreTag, }, props: { @@ -310,7 +312,7 @@ export default { .call(ApiKalender.getHistory(orig.kalender_id)) .then((result) => { this.historyEntries = result.data ?? []; - this.$refs.historyModel.show(); + this.$refs.historyModal.show(); }); }, syncToLecturer(orig) { @@ -1569,29 +1571,7 @@ export default { - - - - + Date: Wed, 22 Jul 2026 16:36:20 +0200 Subject: [PATCH 04/33] add resourcesAssignmentModal extraction from tempus component --- .../Tempus/ResourcesAssignmentModal.js | 282 ++++++++++++++++++ public/js/components/Tempus/Tempus.js | 252 +--------------- 2 files changed, 288 insertions(+), 246 deletions(-) create mode 100644 public/js/components/Tempus/ResourcesAssignmentModal.js diff --git a/public/js/components/Tempus/ResourcesAssignmentModal.js b/public/js/components/Tempus/ResourcesAssignmentModal.js new file mode 100644 index 000000000..fb366f4ba --- /dev/null +++ b/public/js/components/Tempus/ResourcesAssignmentModal.js @@ -0,0 +1,282 @@ +/** + * Copyright (C) 2024 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import BsModal from "../Bootstrap/Modal.js"; +import FormInput from "../Form/Input.js"; +import ApiOperationalResourceToCalender from "../../api/factory/operationalResourceToCalender.js"; + +export default { + name: "ResourcesAssignmentModal", + components: { + BsModal, + FormInput, + }, + emits: ["saveFinished"], + data() { + return { + calendar: null, + availableResources: [], + filteredAvailableResources: [], + selectedAvailableResource: null, + assignedResources: [], + areFormButtonsDisplayed: false, + }; + }, + computed: { + dropdownParsedAvailableResources() { + return this.availableResources + .map((unit) => ({ + label: unit.beschreibung, + value: unit.betriebsmittel_id, + data: unit, + })) + .sort((a, b) => a.label?.localeCompare(b.label)); + }, + }, + watch: { + selectedAvailableResource(newValue) { + if (!newValue) return; + + this.assignedResources.push({ + betriebsmittel_kalender_id: null, + betriebsmittel_id: newValue.data.betriebsmittel_id, + beschreibung: newValue.data.beschreibung, + anmerkung: "", + isNoteTextareaShown: false, + }); + + this.areFormButtonsDisplayed = true; + }, + }, + methods: { + async open(calendar) { + if (!calendar?.kalender_id) return; + + this.calendar = calendar; + this.availableResources = + await this.fetchSchedulableResourcesByCalender(calendar.kalender_id); + this.filteredAvailableResources = [ + ...this.dropdownParsedAvailableResources, + ]; + this.assignedResources = + await this.fetchAssignedResourcesByCalender(calendar.kalender_id); + + this.show(); + }, + show() { + this.$refs.modal.show(); + }, + hide() { + this.$refs.modal.hide(); + }, + reset() { + this.calendar = null; + this.availableResources = []; + this.filteredAvailableResources = []; + this.selectedAvailableResource = null; + this.assignedResources = []; + this.areFormButtonsDisplayed = false; + }, + async fetchAssignedResourcesByCalender(calenderId) { + const result = await this.$api.call( + ApiOperationalResourceToCalender.getAssignedResourcesByCalender( + calenderId, + ), + ); + + if (result.meta.status === "success") { + return result.data + .filter((unit) => !!unit) + .map((unit) => ({ + isNoteTextareaShown: + unit.anmerkung && unit.anmerkung.trim() !== "", + ...unit, + })); + } + + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_assigned_resources_fetch_error_message"), + ); + return []; + }, + async fetchSchedulableResourcesByCalender(calendarId) { + const result = await this.$api.call( + ApiOperationalResourceToCalender.getSchedulableResourcesByCalendar( + calendarId, + ), + ); + + if (result.meta.status === "success") return result.data; + + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_schedulable_resources_fetch_error_message"), + ); + return []; + }, + filterAvailableResources(event) { + const query = event.query.toLowerCase(); + const unassignedResources = this.dropdownParsedAvailableResources.filter( + (unit) => + !this.assignedResources.some( + (assigned) => assigned.betriebsmittel_id === unit.value, + ), + ); + + this.filteredAvailableResources = query + ? unassignedResources.filter((unit) => + unit.label.toLowerCase().includes(query), + ) + : unassignedResources; + }, + toggleAssignedResourceNoteInput(resource) { + const assignedResource = this.assignedResources.find( + (assigned) => assigned.betriebsmittel_id === resource.betriebsmittel_id, + ); + + if (assignedResource) { + assignedResource.isNoteTextareaShown = + !assignedResource.isNoteTextareaShown; + } + + this.areFormButtonsDisplayed = true; + }, + removeAssignedResource(resource) { + this.assignedResources = this.assignedResources.filter( + (assigned) => + assigned.betriebsmittel_id !== resource.betriebsmittel_id, + ); + this.areFormButtonsDisplayed = true; + }, + async refreshResourcesAssignmentModalData() { + if (!this.calendar?.kalender_id) return; + + this.availableResources = + await this.fetchSchedulableResourcesByCalender( + this.calendar.kalender_id, + ); + this.filteredAvailableResources = [ + ...this.dropdownParsedAvailableResources, + ]; + this.assignedResources = await this.fetchAssignedResourcesByCalender( + this.calendar.kalender_id, + ); + this.selectedAvailableResource = null; + this.areFormButtonsDisplayed = false; + }, + async saveAssignedResourcesToCalendarItem() { + if (!this.calendar?.kalender_id) return; + + const result = await this.$api.call( + ApiOperationalResourceToCalender.storeResourcesToCalendarRelationship( + this.calendar.kalender_id, + this.assignedResources, + ), + ); + + if (result.meta.status === "success") { + this.$fhcAlert.alertSuccess( + this.$p.t("ui", "assigned_resources_save_success_message"), + ); + await this.refreshResourcesAssignmentModalData(); + } else { + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_assigned_resources_save_error_message"), + ); + } + + this.$emit("saveFinished"); + this.hide(); + }, + }, + template: ` + + + + + `, +}; diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index af762c554..3774d98f9 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -28,7 +28,6 @@ import ApiSearchbar from "../../api/factory/searchbar.js"; import ApiRenderers from "../../api/factory/renderers.js"; import ApiTempusConfig from "../../api/factory/tempus/config.js"; import ApiBetriebsmittel from "../../api/factory/betriebsmittel.js"; -import ApiOperationalResourceToCalender from "../../api/factory/operationalResourceToCalender.js"; import ApiTempusTag from "../../api/factory/tempus/tag.js"; import AppMenu from "../AppMenu.js"; import drop from "../../directives/drop.js"; @@ -47,6 +46,7 @@ import KeyboardShortcuts from "./KeyboardShortcuts.js"; import { useContextMenuActions } from "../../composables/Tempus/ContextMenuActions.js"; import MultiWeekPlanModal from "./MultiWeekPlanModal.js"; import HistoryModal from "./HistoryModal.js"; +import ResourcesAssignmentModal from "./ResourcesAssignmentModal.js"; import { getTempusSearchbarOptions } from "./Filters/searchbarOptions.js"; import CoreTag from '../Tag/Tag.js'; @@ -73,6 +73,7 @@ export default { KeyboardShortcuts, MultiWeekPlanModal, HistoryModal, + ResourcesAssignmentModal, CoreTag, }, props: { @@ -99,7 +100,7 @@ export default { contextMenuActions: useContextMenuActions({ openRaumauswahl: (orig) => this.openRaumauswahl(orig), openResourcesAssignmentModal: (orig) => - this.openResourcesAssignmentModal(orig), + this.$refs.resourcesAssignmentModal?.open(orig), openTagsModal: (orig) => this.openTagsAssignmentModal(orig), openHistory: (orig) => this.openHistory(orig), deleteEntry: (orig) => this.deleteEntry(orig), @@ -169,14 +170,6 @@ export default { endTime: null, }, studiengaenge_all: [], - resourcesAssignmentModal: { - calendar: null, - availableResources: [], - filteredAvailableResources: [], - selectedAvailableResource: null, - assignedResources: [], - areFormButtonsDisplayed: false, - }, tagsAssignmentModal: { calendar: null, availableTags: [], @@ -220,17 +213,6 @@ export default { searchbaroptions() { return getTempusSearchbarOptions(this); }, - dropdownParsedAvailableResources() { - return this.resourcesAssignmentModal.availableResources - .map((unit) => { - return { - label: unit.beschreibung, - value: unit.betriebsmittel_id, - data: unit, - }; - }) - .sort((a, b) => a.label?.localeCompare(b.label)); - }, dropdownParsedAvailableTags() { return this.tagsAssignmentModal.availableTags .map((tag) => { @@ -254,21 +236,6 @@ export default { this.$refs.raumModal.show(); }); }, - async openResourcesAssignmentModal(orig) { - if (!orig?.kalender_id) return; - - this.resourcesAssignmentModal.calendar = orig; - this.resourcesAssignmentModal.availableResources = - await this.fetchSchedulableResourcesByCalender(orig.kalender_id); - this.resourcesAssignmentModal.filteredAvailableResources = [ - ...this.dropdownParsedAvailableResources, - ]; - - this.resourcesAssignmentModal.assignedResources = - await this.fetchAssignedResourcesByCalender(orig.kalender_id); - - this.$refs.resourcesAssignmentModal.show(); - }, async openTagsAssignmentModal(orig) { console.log("openTagsAssignmentModal called with orig:", orig); if (!orig?.kalender_id) return; @@ -827,138 +794,6 @@ export default { this.extraBackgrounds = res; }, - async fetchAssignedResourcesByCalender(calenderId) { - let getAssignedResources = await this.$api.call( - ApiOperationalResourceToCalender.getAssignedResourcesByCalender( - calenderId, - ), - ); - if (getAssignedResources.meta.status === "success") { - return getAssignedResources.data - .filter((unit) => !!unit) - .map((unit) => { - return { - isNoteTextareaShown: - unit.anmerkung && unit.anmerkung.trim() !== "", - ...unit, - }; - }) - .filter((unit) => !!unit); - } else { - this.$fhcAlert.alertError( - this.$p.t("ui", "failed_assigned_resources_fetch_error_message"), - ); - } - - return []; - }, - async fetchSchedulableResourcesByCalender(calendarID) { - let getSchedulableResourcesByCalendar = await this.$api.call( - ApiOperationalResourceToCalender.getSchedulableResourcesByCalendar( - calendarID, - ), - ); - if (getSchedulableResourcesByCalendar.meta.status === "success") { - return getSchedulableResourcesByCalendar.data; - } else { - this.$fhcAlert.alertError( - this.$p.t("ui", "failed_schedulable_resources_fetch_error_message"), - ); - } - - return []; - }, - filterAvailableResources(event) { - this.resourcesAssignmentModal.filteredAvailableResources; - const query = event.query.toLowerCase(); - if (!query) { - return (this.resourcesAssignmentModal.filteredAvailableResources = [ - ...this.dropdownParsedAvailableResources.filter((unit) => { - return !this.resourcesAssignmentModal.assignedResources.some( - (assigned) => assigned.betriebsmittel_id === unit.value, - ); - }), - ]); - } - - return (this.resourcesAssignmentModal.filteredAvailableResources = - this.dropdownParsedAvailableResources - .filter((unit) => { - return !this.resourcesAssignmentModal.assignedResources.some( - (assigned) => assigned.betriebsmittel_id === unit.value, - ); - }) - .filter((unit) => { - return unit.label.toLowerCase().includes(query); - })); - }, - toggleAssignedResourceNoteInput(resource) { - const index = this.resourcesAssignmentModal.assignedResources.findIndex( - (assigned) => assigned.betriebsmittel_id === resource.betriebsmittel_id, - ); - if (index !== -1) { - this.resourcesAssignmentModal.assignedResources[ - index - ].isNoteTextareaShown = - !this.resourcesAssignmentModal.assignedResources[index] - .isNoteTextareaShown; - } - - this.resourcesAssignmentModal.areFormButtonsDisplayed = true; - }, - removeAssignedResource(resource) { - this.resourcesAssignmentModal.assignedResources = - this.resourcesAssignmentModal.assignedResources.filter( - (assigned) => - assigned.betriebsmittel_id !== resource.betriebsmittel_id, - ); - - this.resourcesAssignmentModal.areFormButtonsDisplayed = true; - }, - async refreshResourcesAssignmentModalData(calenderItem) { - this.resourcesAssignmentModal.availableResources = - await this.fetchSchedulableResourcesByCalender( - calenderItem.kalender_id, - ); - this.resourcesAssignmentModal.filteredAvailableResources = [ - ...this.dropdownParsedAvailableResources, - ]; - - this.resourcesAssignmentModal.assignedResources = - await this.fetchAssignedResourcesByCalender(calenderItem.kalender_id); - this.resourcesAssignmentModal.selectedAvailableResource = null; - this.resourcesAssignmentModal.areFormButtonsDisplayed = false; - }, - async saveAssignedResourcesToCalendarItem(calenderItem, assignedResources) { - let getSchedulableResourcesByCalendar = await this.$api.call( - ApiOperationalResourceToCalender.storeResourcesToCalendarRelationship( - calenderItem.kalender_id, - assignedResources, - ), - ); - if (getSchedulableResourcesByCalendar.meta.status === "success") { - this.$fhcAlert.alertSuccess( - this.$p.t("ui", "assigned_resources_save_success_message"), - ); - await this.refreshResourcesAssignmentModalData(calenderItem); - } else { - this.$fhcAlert.alertError( - this.$p.t("ui", "failed_assigned_resources_save_error_message"), - ); - } - - this.$refs.calendar.resetEventLoader(); - this.$refs.resourcesAssignmentModal.hide(); - }, - closeResourcesAssignmentModal() { - this.resourcesAssignmentModal = { - availableResources: [], - filteredAvailableResources: [], - selectedAvailableResource: null, - assignedResources: [], - areFormButtonsDisplayed: false, - }; - }, async fetchAvailableTags() { let getAvailableTags = await this.$api.call( ApiTempusTag.getTags(), @@ -1136,19 +971,6 @@ export default { handler() { this.$refs.calendar.resetEventLoader(); }, - }, - "resourcesAssignmentModal.selectedAvailableResource": function (newVal) { - if (!newVal) return; - - this.resourcesAssignmentModal.assignedResources.push({ - betriebsmittel_kalender_id: null, - betriebsmittel_id: newVal.data.betriebsmittel_id, - beschreibung: newVal.data.beschreibung, - anmerkung: "", - isNoteTextareaShown: false, - }); - - this.resourcesAssignmentModal.areFormButtonsDisplayed = true; }, }, mounted() { @@ -1439,7 +1261,6 @@ export default { - - - - - + @save-finished="$refs.calendar.resetEventLoader()" + /> Date: Wed, 22 Jul 2026 17:01:11 +0200 Subject: [PATCH 05/33] add tagsAssignmentModal extraction from tempus component --- .../components/Tempus/TagsAssignmentModal.js | 193 ++++++++++++++++ public/js/components/Tempus/Tempus.js | 210 +----------------- 2 files changed, 199 insertions(+), 204 deletions(-) create mode 100644 public/js/components/Tempus/TagsAssignmentModal.js diff --git a/public/js/components/Tempus/TagsAssignmentModal.js b/public/js/components/Tempus/TagsAssignmentModal.js new file mode 100644 index 000000000..3c57b208d --- /dev/null +++ b/public/js/components/Tempus/TagsAssignmentModal.js @@ -0,0 +1,193 @@ +/** + * Copyright (C) 2024 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import BsModal from "../Bootstrap/Modal.js"; +import FormInput from "../Form/Input.js"; +import CoreTag from "../Tag/Tag.js"; +import ApiTempusTag from "../../api/factory/tempus/tag.js"; + +export default { + name: "TagsAssignmentModal", + components: { + BsModal, + FormInput, + CoreTag, + }, + emits: ["tagsChanged"], + data() { + return { + calendar: null, + availableTags: [], + filteredAvailableTags: [], + selectedAvailableTag: null, + assignedTags: [], + tagEndpoint: ApiTempusTag, + }; + }, + computed: { + dropdownParsedAvailableTags() { + return this.availableTags.map((tag) => ({ + label: tag.bezeichnung, + value: tag.tag_typ_kurzbz, + data: tag, + })); + }, + }, + methods: { + async open(calendar) { + if (!calendar?.kalender_id) return; + + this.calendar = calendar; + this.availableTags = await this.fetchAvailableTags(); + this.filteredAvailableTags = [...this.dropdownParsedAvailableTags]; + this.assignedTags = await this.fetchAssignedTagsByCalender( + calendar.eindeutige_gruppen_id, + ); + + this.show(); + }, + show() { + this.$refs.modal.show(); + }, + hide() { + this.$refs.modal.hide(); + }, + reset() { + this.calendar = null; + this.availableTags = []; + this.filteredAvailableTags = []; + this.selectedAvailableTag = null; + this.assignedTags = []; + }, + async fetchAvailableTags() { + const result = await this.$api.call(ApiTempusTag.getTags()); + + if (result.meta.status === "success") return result.data; + + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_available_tags_fetch_error_message"), + ); + return []; + }, + async fetchAssignedTagsByCalender(calendarGroupId) { + const result = await this.$api.call( + ApiTempusTag.getTagsByCalendar(calendarGroupId), + ); + + if (result.meta.status === "success") { + return result.data.filter((tag) => !!tag); + } + + this.$fhcAlert.alertError( + this.$p.t("ui", "failed_assigned_tags_fetch_error_message"), + ); + return []; + }, + filterAvailableTags(event) { + const query = event.query.toLowerCase(); + const unassignedTags = this.dropdownParsedAvailableTags.filter( + (tag) => + !this.assignedTags.some( + (assigned) => assigned.tag_id === tag.value, + ), + ); + + this.filteredAvailableTags = query + ? unassignedTags.filter((tag) => + tag.label.toLowerCase().includes(query), + ) + : unassignedTags; + }, + selectTag(event) { + this.selectedAvailableTag = event.value; + this.$refs.tagComponent?.openModal(event.value.data); + }, + editTag(tag) { + this.$refs.tagComponent?.editTag(tag.notiz_id); + }, + async handleCalendarTagChange() { + const calendarGroupId = this.calendar?.eindeutige_gruppen_id; + if (!calendarGroupId) return; + + this.assignedTags = + await this.fetchAssignedTagsByCalender(calendarGroupId); + this.$emit("tagsChanged"); + }, + }, + template: /* html */` + + + + + + `, +}; diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index 3774d98f9..f6effc5c6 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -28,7 +28,6 @@ import ApiSearchbar from "../../api/factory/searchbar.js"; import ApiRenderers from "../../api/factory/renderers.js"; import ApiTempusConfig from "../../api/factory/tempus/config.js"; import ApiBetriebsmittel from "../../api/factory/betriebsmittel.js"; -import ApiTempusTag from "../../api/factory/tempus/tag.js"; import AppMenu from "../AppMenu.js"; import drop from "../../directives/drop.js"; import AppConfig from "../AppConfig.js"; @@ -39,7 +38,6 @@ import StvVerband from "../Stv/Studentenverwaltung/Verband.js"; import ApiStudiengangTree from "../../api/factory/tempus/studiengangtree.js"; import ApiInfo from "../../api/factory/tempus/info.js"; import StvStudiensemester from "../Stv/Studentenverwaltung/Studiensemester.js"; -import FormInput from "../../../js/components/Form/Input.js"; import Reservierung from "./Reservierung.js"; import { getTempusShortcuts } from "./shortcuts.js"; import KeyboardShortcuts from "./KeyboardShortcuts.js"; @@ -47,8 +45,8 @@ import { useContextMenuActions } from "../../composables/Tempus/ContextMenuActio import MultiWeekPlanModal from "./MultiWeekPlanModal.js"; import HistoryModal from "./HistoryModal.js"; import ResourcesAssignmentModal from "./ResourcesAssignmentModal.js"; +import TagsAssignmentModal from "./TagsAssignmentModal.js"; import { getTempusSearchbarOptions } from "./Filters/searchbarOptions.js"; -import CoreTag from '../Tag/Tag.js'; export default { name: "Tempus", @@ -68,13 +66,12 @@ export default { StvVerband, StvStudiensemester, Multiselect: primevue.multiselect, - FormInput, Reservierung, KeyboardShortcuts, MultiWeekPlanModal, HistoryModal, ResourcesAssignmentModal, - CoreTag, + TagsAssignmentModal, }, props: { defaultSemester: String, @@ -101,7 +98,7 @@ export default { openRaumauswahl: (orig) => this.openRaumauswahl(orig), openResourcesAssignmentModal: (orig) => this.$refs.resourcesAssignmentModal?.open(orig), - openTagsModal: (orig) => this.openTagsAssignmentModal(orig), + openTagsModal: (orig) => this.$refs.tagsAssignmentModal?.open(orig), openHistory: (orig) => this.openHistory(orig), deleteEntry: (orig) => this.deleteEntry(orig), syncToLecturer: (orig) => this.syncToLecturer(orig), @@ -170,15 +167,6 @@ export default { endTime: null, }, studiengaenge_all: [], - tagsAssignmentModal: { - calendar: null, - availableTags: [], - filteredAvailableTags: [], - selectedAvailableTag: null, - assignedTags: [], - areFormButtonsDisplayed: false, - }, - tagEndpoint: ApiTempusTag, }; }, computed: { @@ -213,16 +201,6 @@ export default { searchbaroptions() { return getTempusSearchbarOptions(this); }, - dropdownParsedAvailableTags() { - return this.tagsAssignmentModal.availableTags - .map((tag) => { - return { - label: tag.bezeichnung, - value: tag.tag_typ_kurzbz, - data: tag, - }; - }); - }, }, methods: { async openRaumauswahl(orig) { @@ -236,19 +214,6 @@ export default { this.$refs.raumModal.show(); }); }, - async openTagsAssignmentModal(orig) { - console.log("openTagsAssignmentModal called with orig:", orig); - if (!orig?.kalender_id) return; - - this.tagsAssignmentModal.calendar = orig; - this.tagsAssignmentModal.availableTags = await this.fetchAvailableTags(); - this.tagsAssignmentModal.filteredAvailableTags = [ - ...this.dropdownParsedAvailableTags, - ]; - - this.tagsAssignmentModal.assignedTags = await this.fetchAssignedTagsByCalender(orig.eindeutige_gruppen_id); - this.$refs.tagsAssignmentModal.show(); - }, async deleteEntry(orig) { if (!orig?.kalender_id) return; @@ -794,114 +759,6 @@ export default { this.extraBackgrounds = res; }, - async fetchAvailableTags() { - let getAvailableTags = await this.$api.call( - ApiTempusTag.getTags(), - ); - if (getAvailableTags.meta.status === "success") { - return getAvailableTags.data; - } else { - this.$fhcAlert.alertError( - this.$p.t("ui", "failed_available_tags_fetch_error_message"), - ); - } - - return []; - }, - async fetchAssignedTagsByCalender(calenderId) { - let getAssignedTags = await this.$api.call( - ApiTempusTag.getTagsByCalendar(calenderId), - ); - if (getAssignedTags.meta.status === "success") { - return getAssignedTags.data.filter((unit) => !!unit); - } else { - this.$fhcAlert.alertError( - this.$p.t("ui", "failed_assigned_tags_fetch_error_message"), - ); - } - - return []; - }, - filterAvailableTags(event) { - this.tagsAssignmentModal.filteredAvailableTags; - const query = event.query.toLowerCase(); - if (!query) { - return (this.tagsAssignmentModal.filteredAvailableTags = [ - ...this.dropdownParsedAvailableTags.filter((tag) => { - return !this.tagsAssignmentModal.assignedTags.some( - (assigned) => assigned.tag_id === tag.value, - ); - }), - ]); - } - - return (this.tagsAssignmentModal.filteredAvailableTags = - this.dropdownParsedAvailableTags - .filter((tag) => { - return !this.tagsAssignmentModal.assignedTags.some( - (assigned) => assigned.tag_id === tag.value, - ); - }) - .filter((tag) => { - return tag.label.toLowerCase().includes(query); - })); - }, - removeAssignedTag(tag) { - this.tagsAssignmentModal.assignedTags = - this.tagsAssignmentModal.assignedTags.filter( - (assigned) => assigned.tag_id !== tag.tag_id, - ); - - this.tagsAssignmentModal.areFormButtonsDisplayed = true; - }, - async refreshTagsAssignmentModalData(calenderItem) { - this.tagsAssignmentModal.availableTags = - await this.fetchAvailableTags(); - this.tagsAssignmentModal.filteredAvailableTags = [ - ...this.dropdownParsedAvailableTags, - ]; - - this.tagsAssignmentModal.assignedTags = - await this.fetchAssignedTagsByCalender(calenderItem.eindeutige_gruppen_id); - this.tagsAssignmentModal.selectedAvailableTag = null; - this.tagsAssignmentModal.areFormButtonsDisplayed = false; - }, - async saveAssignedTagsToCalendarItem(calenderItem, assignedTags) { - let saveAssignedTagsByCalender = await this.$api.call( - ApiTempusTag.getTags( - calenderItem.eindeutige_gruppen_id, - assignedTags, - ), - ); - if (saveAssignedTagsByCalender.meta.status === "success") { - this.$fhcAlert.alertSuccess( - this.$p.t("ui", "assigned_tags_save_success_message"), - ); - await this.refreshTagsAssignmentModalData(calenderItem); - } else { - this.$fhcAlert.alertError( - this.$p.t("ui", "failed_assigned_tags_save_error_message"), - ); - } - - this.$refs.calendar.resetEventLoader(); - this.$refs.tagsAssignmentModal.hide(); - }, - closeTagsAssignmentModal() { - this.tagsAssignmentModal = { - availableTags: [], - filteredAvailableTags: [], - selectedAvailableTag: null, - assignedTags: [], - areFormButtonsDisplayed: false, - }; - }, - async handleCalenderTagChange(calenderID) { - this.tagsAssignmentModal.assignedTags = - await this.fetchAssignedTagsByCalender(calenderID); - - this.$refs.calendar.resetEventLoader(); - }, removeLecturer(uid) { if (uid == null) { for (const lect of this.lecturers) delete this.overlayCache[lect.uid]; @@ -1283,66 +1140,11 @@ export default { ref="resourcesAssignmentModal" @save-finished="$refs.calendar.resetEventLoader()" /> - - - - + @tags-changed="$refs.calendar.resetEventLoader()" + /> - Date: Fri, 24 Jul 2026 09:16:40 +0200 Subject: [PATCH 06/33] Add smoke test for calendar event tag assignment --- system/seeders/015_kalender.sql | 11 +++++++++++ system/seeders/016_tag.sql | 5 +++++ .../cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js | 14 ++++++++++++++ tests/cypress/cypress/support/pages/tempus.po.js | 10 ++++++++++ 4 files changed, 40 insertions(+) create mode 100644 system/seeders/016_tag.sql diff --git a/system/seeders/015_kalender.sql b/system/seeders/015_kalender.sql index 345e003a3..4aa274060 100644 --- a/system/seeders/015_kalender.sql +++ b/system/seeders/015_kalender.sql @@ -177,6 +177,17 @@ VALUES (51006, 31), (51004, 32); +-- Calendar setup for tags tests +INSERT INTO lehre.tbl_kalender +(von, bis, typ, status_kurzbz, vorgaenger_kalender_id, eindeutige_gruppen_id, insertamum, insertvon, updateamum, updatevon) +VALUES +(date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '16:55:00', date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '17:40:00', 'lehreinheit', 'live', NULL, 'ec1bd001-facf-4b21-a671-bcb2d4f037fd', now(), 'demoadmin', NULL, 'demoadmin'); + +INSERT INTO lehre.tbl_kalender_lehreinheit +(lehreinheit_id, kalender_id) +VALUES +(51004, 33); + -- INSERT INTO public.tbl_variablenname -- (name, defaultwert) -- VALUES('ignore_resources_collisions', 'false'); diff --git a/system/seeders/016_tag.sql b/system/seeders/016_tag.sql new file mode 100644 index 000000000..9fef2cf1c --- /dev/null +++ b/system/seeders/016_tag.sql @@ -0,0 +1,5 @@ +INSERT INTO public.tbl_notiz_typ +(typ_kurzbz, bezeichnung_mehrsprachig, beschreibung, automatisiert, aktiv, zuordnung, tag, style, vorrueckung, prioritaet/*, taglib*/) +VALUES +('prioone', '{Prio 1,Prio 1}', '', false, true, NULL, false, 'tag_rot', false, 1/*, NULL*/), +('priotwo', '{Prio 2,Prio 2}', '', false, true, NULL, false, 'tag_gelb', false, 2/*, NULL*/); \ No newline at end of file diff --git a/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js b/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js index 454c8686a..45fde5243 100644 --- a/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js +++ b/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js @@ -71,6 +71,20 @@ context("Tempus smoke tests", () => { tempusPage.getResourcesModal().should("be.visible"); }); + it("shows tags modal when selecting Tags from event context menu", () => { + tempusPage.waitForCalendarToFinishLoading(); + tempusPage + .getCalendarEventsByWeekdayAndStartTime("Sunday", "16:55:00") + .should("have.length.greaterThan", 0); + + tempusPage.getCalendarEventsByWeekdayAndStartTime("Sunday", "16:55:00").first().rightclick(); + tempusPage.getEventContextMenuOption("Tags").click({ force: true }); + waitForOk("@fetchTags"); + waitForOk("@fetchTagsByCalendar"); + + tempusPage.getTagsModal().should("be.visible"); + }); + it("shows history modal when selecting History from event context menu", () => { tempusPage.waitForCalendarToFinishLoading(); tempusPage.getCalendarEvents().should("have.length.greaterThan", 0); diff --git a/tests/cypress/cypress/support/pages/tempus.po.js b/tests/cypress/cypress/support/pages/tempus.po.js index 7eeeab146..3f8dd1e42 100644 --- a/tests/cypress/cypress/support/pages/tempus.po.js +++ b/tests/cypress/cypress/support/pages/tempus.po.js @@ -72,6 +72,14 @@ class TempusPage { method: "GET", url: "**/tempus/OperationalResourceToCalenderAPI/getAssignedResourcesByCalender/**", }).as("fetchAssignedResources"); + cy.intercept({ + method: "GET", + url: "**/tempus/Tags/getTags**", + }).as("fetchTags"); + cy.intercept({ + method: "GET", + url: "**/tempus/Tags/getTagsByCalendar/**", + }).as("fetchTagsByCalendar"); cy.intercept({ method: "POST", url: "**/tempus/coursepicker/getByStg**", @@ -345,6 +353,8 @@ class TempusPage { cy .get("[data-cy='resourcesAssignmentModal']") .closest(".bootstrap-modal.show"); + getTagsModal = () => + cy.get("[data-cy='tagsAssignmentModal']").closest(".bootstrap-modal.show") getCalendarEventRoom = (id) => this.getCalendarEventById(id).find("[data-cy='calendar-event-room']"); getStundenrasterToggle = () => From e08ae7b3319cefe388f1d62a862629761828df48 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Fri, 24 Jul 2026 09:26:05 +0200 Subject: [PATCH 07/33] Add extraction of header component from tempus component --- public/js/components/Tempus/Header.js | 109 ++++++++++++++++++++++++++ public/js/components/Tempus/Tempus.js | 95 +++------------------- 2 files changed, 120 insertions(+), 84 deletions(-) create mode 100644 public/js/components/Tempus/Header.js diff --git a/public/js/components/Tempus/Header.js b/public/js/components/Tempus/Header.js new file mode 100644 index 000000000..7109932fa --- /dev/null +++ b/public/js/components/Tempus/Header.js @@ -0,0 +1,109 @@ +import CoreSearchbar from "../searchbar/searchbar.js"; +import NavLanguage from "../navigation/Language.js"; + +export default { + name: "TempusHeader", + components: { + CoreSearchbar, + NavLanguage, + }, + props: { + tempusRoot: String, + avatarUrl: String, + logoutUrl: String, + searchbaroptions: { + type: Object, + required: true, + }, + searchfunction: { + type: Function, + required: true, + }, + }, + emits: ["focus-searchbar"], + methods: { + focusSearchbar() { + this.$refs.searchbar?.$refs?.input?.focus(); + this.$emit("focus-searchbar"); + }, + }, + template: /* html */ ` + + `, +}; diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index f6effc5c6..0b83340b2 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -14,8 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import CoreSearchbar from "../searchbar/searchbar.js"; -import NavLanguage from "../navigation/Language.js"; import VerticalSplit from "../verticalsplit/verticalsplit.js"; import FhcCalendar from "../Calendar/Tempus.js"; import FhcCoursepicker from "./Coursepicker.js"; @@ -47,11 +45,11 @@ import HistoryModal from "./HistoryModal.js"; import ResourcesAssignmentModal from "./ResourcesAssignmentModal.js"; import TagsAssignmentModal from "./TagsAssignmentModal.js"; import { getTempusSearchbarOptions } from "./Filters/searchbarOptions.js"; +import TempusHeader from "./Header.js"; export default { name: "Tempus", components: { - CoreSearchbar, VerticalSplit, FhcCalendar, FhcCoursepicker, @@ -61,7 +59,7 @@ export default { ParkingSlot, AppConfig, AppMenu, - NavLanguage, + TempusHeader, BsModal, StvVerband, StvStudiensemester, @@ -810,7 +808,7 @@ export default { this.hoveredEvent = null; }, focusSearchbar() { - this.$refs.searchbar?.$refs?.input.focus(); + this.$refs.header?.focusSearchbar(); }, }, watch: { @@ -910,85 +908,14 @@ export default { data-cy="tempus" > - +