From 9983a4810297f4b7ca2e50bc03a50e422e08bb87 Mon Sep 17 00:00:00 2001 From: ma0048 Date: Tue, 21 Jul 2026 13:50:17 +0200 Subject: [PATCH 01/15] 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 6282cb04029671d22faae8745bd53911eb924fa2 Mon Sep 17 00:00:00 2001 From: ma0048 Date: Wed, 5 Aug 2026 11:12:33 +0200 Subject: [PATCH 02/15] hinzugefuegt: - alle anzeigen option - jetzt starten knopf - oe rekursiv --- application/controllers/Tempus.php | 16 +++++-- .../api/frontend/v1/tempus/KalenderSync.php | 10 +++- application/libraries/KalenderSyncLib.php | 14 +++++- application/libraries/RaumvorschlagLib.php | 1 - public/js/components/Tempus/Modal.js | 48 +++++++++++-------- public/js/components/Tempus/TempusSync.js | 27 +++++++++-- system/phrasesupdate.php | 20 ++++++++ 7 files changed, 104 insertions(+), 32 deletions(-) diff --git a/application/controllers/Tempus.php b/application/controllers/Tempus.php index 175a9ffa4..78efde1b4 100644 --- a/application/controllers/Tempus.php +++ b/application/controllers/Tempus.php @@ -34,10 +34,18 @@ class Tempus extends Auth_Controller $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->OrganisationseinheitModel->addJoin('tbl_studiengang', 'oe_kurzbz', 'LEFT'); + + $this->OrganisationseinheitModel->addSelect('public.tbl_organisationseinheit.organisationseinheittyp_kurzbz, + public.tbl_organisationseinheit.bezeichnung, + public.tbl_studiengang.studiengang_kz, + public.tbl_organisationseinheit.oe_kurzbz, + tbl_studiengang.typ, + COALESCE(public.tbl_studiengang.bezeichnung, \'\') as stgbezeichnung'); + $this->OrganisationseinheitModel->addOrder('tbl_organisationseinheit.aktiv', 'DESC'); + $this->OrganisationseinheitModel->addOrder('tbl_organisationseinheit.organisationseinheittyp_kurzbz'); + $organisationen = getData($this->OrganisationseinheitModel->loadWhere(array("tbl_organisationseinheit.aktiv" => true))); + $this->StudiensemesterModel->addOrder('start', 'DESC'); $studiensemestern = getData($this->StudiensemesterModel->load()); diff --git a/application/controllers/api/frontend/v1/tempus/KalenderSync.php b/application/controllers/api/frontend/v1/tempus/KalenderSync.php index ed5b7b145..13e4cc012 100644 --- a/application/controllers/api/frontend/v1/tempus/KalenderSync.php +++ b/application/controllers/api/frontend/v1/tempus/KalenderSync.php @@ -46,12 +46,18 @@ class KalenderSync extends FHCAPI_Controller $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->addSelect('tbl_kalender_syncstatus.*, array_to_json(bezeichnung_mehrsprachig::varchar[])->>' . $language .' AS sync_status, 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'); + $this->_ci->KalenderStatusModel->addJoin('public.tbl_studiensemester', 'tbl_kalender_syncstatus.studiensemester_kurzbz = tbl_studiensemester.studiensemester_kurzbz'); + $this->_ci->KalenderStatusModel->addOrder('public.tbl_studiensemester.start', 'DESC'); - $data = $this->_ci->KalendersyncstatusModel->loadWhere(array('studiensemester_kurzbz' => $studiensemester_kurzbz)); + + if ($studiensemester_kurzbz === 'all') + $data = $this->_ci->KalendersyncstatusModel->load(); + else + $data = $this->_ci->KalendersyncstatusModel->loadWhere(array('tbl_kalender_syncstatus.studiensemester_kurzbz' => $studiensemester_kurzbz)); if (isError($data)) $this->terminateWithError(getError($data)); diff --git a/application/libraries/KalenderSyncLib.php b/application/libraries/KalenderSyncLib.php index ee0197e20..284a13274 100644 --- a/application/libraries/KalenderSyncLib.php +++ b/application/libraries/KalenderSyncLib.php @@ -18,6 +18,7 @@ class KalenderSyncLib $this->_ci->load->model('ressource/Kalender_model', 'KalenderModel'); $this->_ci->load->model('ressource/Kalendersyncstatus_model', 'KalendersyncstatusModel'); $this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->_ci->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); } public function run($studiensemester_kurzbz) @@ -181,17 +182,26 @@ class KalenderSyncLib $studiengang_kz = getData($studiengang_result); + if (!hasData($studiengang_result)) + { + $organisationseinheiten = $this->_ci->OrganisationseinheitModel->getChilds($oe_kurzbz); + $oe_kurzbz = hasData($organisationseinheiten) ? array_column(getData($organisationseinheiten), 'oe_kurzbz') : []; + } + $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_in('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); diff --git a/application/libraries/RaumvorschlagLib.php b/application/libraries/RaumvorschlagLib.php index 8cb4aa8a4..7e4d254b9 100644 --- a/application/libraries/RaumvorschlagLib.php +++ b/application/libraries/RaumvorschlagLib.php @@ -23,7 +23,6 @@ class RaumvorschlagLib $this->_ci->load->model('organisation/gruppe_model', 'GruppeModel'); $this->_ci->load->model('organisation/Lehrverband_model', 'LehrverbandModel'); - $this->_ci->load->model('education/Lehreinheitgruppe_model', 'LehreinheitgruppeModel'); $this->_ci->load->model('education/Lehreinheitgruppe_model', 'LehreinheitgruppeModel'); $this->_ci->load->library('CollisionChecker'); diff --git a/public/js/components/Tempus/Modal.js b/public/js/components/Tempus/Modal.js index 981479aea..8a6232feb 100644 --- a/public/js/components/Tempus/Modal.js +++ b/public/js/components/Tempus/Modal.js @@ -34,29 +34,32 @@ export default { return []; return Array.from({length: this.max_semester}, (_, index) => index); + }, + studienplanTrigger() { + return [ + this.formData.oe_kurzbz, + this.formData.studiensemester_kurzbz, + this.formData.ausbildungssemester + ].join('|'); } }, watch: { - 'formData.oe_kurzbz'(newOrganisation) { + '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]; + 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'() { + studienplanTrigger() { this.getStudienplan(); } }, @@ -123,7 +126,7 @@ export default { this.$api.call(ApiTempusSync.getMaxSemester([stg_kz])) .then(response => response.data) .then(response => { - this.max_semester = response; + this.max_semester = response ?? null; let found = this.semester.filter(semester => semester === this.formData.ausbildungssemester); if (found.length === 0) @@ -146,7 +149,7 @@ export default { )) .then(response => response.data) .then(response => { - this.studienplaene = 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; @@ -168,7 +171,8 @@ export default { .call(ApiTempusSync.add(this.formData)) .then(() => { this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); - this.$emit('saved'); + if (this.stsem_kurzbz !== this.formData.studiensemester_kurzbz) + this.$emit('saved', this.formData.studiensemester_kurzbz); this.$refs.modal.hide(); }) .catch(this.$fhcAlert.handleSystemError); @@ -178,31 +182,35 @@ export default { .call(ApiTempusSync.updateSync(this.formData)) .then(() => { this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); - this.$emit('saved'); + if (this.stsem_kurzbz !== this.formData.studiensemester_kurzbz) + this.$emit('saved', this.formData.studiensemester_kurzbz); this.$refs.modal.hide(); }) .catch(this.$fhcAlert.handleSystemError); }, - start() { + start(data) { this.$refs.form - .call(ApiTempusSync.start(this.formData)) + .call(ApiTempusSync.start(data ?? this.formData)) .then(() => { - this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); - this.$emit('saved'); + this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successStart')); this.$refs.modal.hide(); }) .catch(this.$fhcAlert.handleSystemError); + }, + hideModal() + { + this.formData = {} } }, created() { - console.log(this.config) + this.$api .call(ApiTempusSync.getSyncStatus()) - .then(result => this.syncStati = result.data) + .then(result => this.syncStati = result.data ?? []) .catch(error => this.$fhcAlert.handleSystemError(error)); }, template: ` - + diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 138136891..e4e8023b1 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -39126,6 +39126,26 @@ array( ) ) ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'successStart', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '"Erfolgreich gestartet', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Started successfully', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), array( 'app' => 'core', 'category' => 'ui', From a74f6799175bcd598b13ded178123fd6d4eee815 Mon Sep 17 00:00:00 2001 From: ma0048 Date: Wed, 5 Aug 2026 12:14:53 +0200 Subject: [PATCH 03/15] =?UTF-8?q?fixed=20Sync=20Bug=20-=20Planning=20->=20?= =?UTF-8?q?Voransicht=20->=20Live=20->=20Planning=20->=20Voransicht=20->?= =?UTF-8?q?=20Delete=20l=C3=A4sst=20eintr=C3=A4ge=20in=20der=20History=20a?= =?UTF-8?q?uf=20Live?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/libraries/KalenderSyncLib.php | 100 ++++++++++++++-------- public/js/components/Tempus/Modal.js | 6 +- 2 files changed, 65 insertions(+), 41 deletions(-) diff --git a/application/libraries/KalenderSyncLib.php b/application/libraries/KalenderSyncLib.php index 284a13274..7c10c2add 100644 --- a/application/libraries/KalenderSyncLib.php +++ b/application/libraries/KalenderSyncLib.php @@ -123,51 +123,76 @@ class KalenderSyncLib 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(); + $mail_infos_gesamt = []; + $synced_gesamt = 0; - if (isError($to_update)) - return $to_update; + $todelete_ids_result = $this->_getManuallySetStatus(array('todelete')); - if (!hasData($to_update)) - return success(['synced' => 0]); + if (isError($todelete_ids_result)) + return $todelete_ids_result; - $mail_infos = []; - $synced = 0; + $todelete_result = $this->_syncKalenderIds(getData($todelete_ids_result), null); - foreach (getData($to_update) as $entry) + if (isError($todelete_result)) + return $todelete_result; + + $todelete_data = getData($todelete_result); + $mail_infos_gesamt = array_merge($mail_infos_gesamt, $todelete_data['mail_infos']); + $synced_gesamt += $todelete_data['synced']; + + $preview_ids_result = $this->_getManuallySetStatus(array('sync_preview')); + + if (isError($preview_ids_result)) + return $preview_ids_result; + + $preview_result = $this->_syncKalenderIds(getData($preview_ids_result), 'preview'); + + if (isError($preview_result)) + return $preview_result; + + $preview_data = getData($preview_result); + $mail_infos_gesamt = array_merge($mail_infos_gesamt, $preview_data['mail_infos']); + $synced_gesamt += $preview_data['synced']; + + $live_ids_result = $this->_getManuallySetStatus(array('sync_live')); + + if (isError($live_ids_result)) + return $live_ids_result; + + $live_result = $this->_syncKalenderIds(getData($live_ids_result), 'live'); + + if (isError($live_result)) + return $live_result; + + $live_data = getData($live_result); + $mail_infos_gesamt = array_merge($mail_infos_gesamt, $live_data['mail_infos']); + $synced_gesamt += $live_data['synced']; + + if (!empty($mail_infos_gesamt)) { - 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_gesamt); } - $this->_ci->load->library('KalenderNotificationLib'); - $this->_ci->kalendernotificationlib->sendMails($mail_infos); + return success(['synced' => $synced_gesamt]); + } - return success(['synced' => $synced]); + private function _getManuallySetStatus($status_kurzbz_list) + { + if (empty($status_kurzbz_list)) + return success([]); + + $this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id'); + $this->_ci->KalenderModel->db->where_in('status_kurzbz', $status_kurzbz_list); + $result = $this->_ci->KalenderModel->load(); + + if (isError($result)) + return $result; + + if (!hasData($result)) + return success([]); + + return success(array_column(getData($result), 'kalender_id')); } public function getKalenderIds($oe_kurzbz, $studiensemester_kurzbz, $status_kurzbz_list, $ausbildungssemester = null, $studienplan_id = null, $datum_bis = null) @@ -290,6 +315,7 @@ class KalenderSyncLib if ($entry->status_kurzbz === 'todelete') { $this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'deleted')); + $this->_archiveVorgaenger($entry->vorgaenger_kalender_id, false); $mail_infos[] = array('entry' => $entry, 'new_status' => 'deleted', 'notify' => array('lektor', 'student')); $synced++; continue; diff --git a/public/js/components/Tempus/Modal.js b/public/js/components/Tempus/Modal.js index 8a6232feb..67b482c68 100644 --- a/public/js/components/Tempus/Modal.js +++ b/public/js/components/Tempus/Modal.js @@ -171,8 +171,7 @@ export default { .call(ApiTempusSync.add(this.formData)) .then(() => { this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); - if (this.stsem_kurzbz !== this.formData.studiensemester_kurzbz) - this.$emit('saved', this.formData.studiensemester_kurzbz); + this.$emit('saved', this.formData.studiensemester_kurzbz); this.$refs.modal.hide(); }) .catch(this.$fhcAlert.handleSystemError); @@ -182,8 +181,7 @@ export default { .call(ApiTempusSync.updateSync(this.formData)) .then(() => { this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); - if (this.stsem_kurzbz !== this.formData.studiensemester_kurzbz) - this.$emit('saved', this.formData.studiensemester_kurzbz); + this.$emit('saved', this.formData.studiensemester_kurzbz); this.$refs.modal.hide(); }) .catch(this.$fhcAlert.handleSystemError); From 626bc121d1727a35af4818b534108af2520a7d05 Mon Sep 17 00:00:00 2001 From: ma0048 Date: Wed, 5 Aug 2026 14:45:28 +0200 Subject: [PATCH 04/15] listenansicht datepicker range hinzugefuegt --- public/js/components/Calendar/Base.js | 8 ++ public/js/components/Calendar/Base/Header.js | 8 +- .../Calendar/Base/Header/Datepicker.js | 99 +++++++++++-------- public/js/components/Calendar/Mode/Table.js | 92 ++++------------- .../js/components/Calendar/Mode/Table/View.js | 22 ++--- public/js/components/Calendar/Tempus.js | 11 +++ public/js/components/Tempus/Tempus.js | 27 +++-- 7 files changed, 131 insertions(+), 136 deletions(-) diff --git a/public/js/components/Calendar/Base.js b/public/js/components/Calendar/Base.js index 85c64ac18..360c9afa2 100644 --- a/public/js/components/Calendar/Base.js +++ b/public/js/components/Calendar/Base.js @@ -137,12 +137,14 @@ export default { "update:date", "update:mode", "update:range", + "update:date-range", "drop" ], data() { return { internalView: null, internalDate: null, + pickedRangeEnd: null, modalEvent: null }; }, @@ -272,6 +274,10 @@ export default { } }, + handleDateRange({ start, end }) { + this.pickedRangeEnd = end; + this.$emit('update:date-range', { start, end }); + }, showEventModal(eventObj) { this.modalEvent = eventObj; this.$refs.modal.show(); @@ -300,6 +306,7 @@ export default { class="card-header" v-model:date="cDate" v-model:mode="cMode" + @update:date-range="handleDateRange" @prev="clickPrev" @next="clickNext" @click:mode="$emit('click:mode', $event)" @@ -316,6 +323,7 @@ export default { :is="modes ? modes[cMode] : null || 'div'" ref="mode" v-model:current-date="cDate" + :range-end="pickedRangeEnd" @update:range="$emit('update:range', $event)" @request-modal-open="showEventModal" @request-modal-close="hideEventModal" diff --git a/public/js/components/Calendar/Base/Header.js b/public/js/components/Calendar/Base/Header.js index 39eea114b..7c3fcafcb 100644 --- a/public/js/components/Calendar/Base/Header.js +++ b/public/js/components/Calendar/Base/Header.js @@ -32,7 +32,8 @@ export default { "prev", "click:mode", "update:date", - "update:mode" + "update:mode", + "update:date-range" ], data() { return { @@ -109,6 +110,7 @@ export default { class="btn btn-outline-secondary border-0" @click="$emit('prev')" :disabled="open" + v-if="mode !== 'tableList'" > @@ -116,6 +118,7 @@ export default { :mode="mode" :date="date" @update:date="$emit('update:date', $event)" + @update:date-range="$emit('update:date-range', $event)" @open="open = true" @closed="open = false" :list-length="modeOptions.length" @@ -124,6 +127,7 @@ export default { class="btn btn-outline-secondary border-0" @click="$emit('next')" :disabled="open" + v-if="mode !== 'tableList'" > @@ -131,4 +135,4 @@ export default { ` -} +} \ No newline at end of file diff --git a/public/js/components/Calendar/Base/Header/Datepicker.js b/public/js/components/Calendar/Base/Header/Datepicker.js index 98a26945b..3e2ec8327 100644 --- a/public/js/components/Calendar/Base/Header/Datepicker.js +++ b/public/js/components/Calendar/Base/Header/Datepicker.js @@ -24,8 +24,14 @@ export default { } }, emits: [ - "update:date" + "update:date", + "update:date-range" ], + data() { + return { + rangeEnd: null + }; + }, computed: { convertedDate() { // convert to target TZ then strip TZ Information @@ -34,34 +40,37 @@ export default { }, current() { switch (this.mode) { - case "month": - return {month: this.convertedDate.month-1, year: this.convertedDate.year}; - case "list": - return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.listLength }).ts - 1]; - case "week": - case "tableList": - return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts]; - case "day": - return this.convertedDate; - default: - return null; + case "month": + return {month: this.convertedDate.month-1, year: this.convertedDate.year}; + case "list": + return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.listLength }).ts - 1]; + case "week": + return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts]; + case "tableList": + return [this.convertedDate.startOf('day').ts, (this.rangeEnd || this.convertedDate).endOf('day').ts]; + case "day": + return this.convertedDate; + default: + return null; } }, title() { switch (this.mode) { - case "month": - return this.date.toLocaleString({ month: 'long', year: 'numeric' }); - case "week": - case "tableList": - var year = this.date.localWeekYear; - var week = this.date.toFormat('nn'); - return this.$p.t('calendar/year_kw', { year, week }); - case "list": - return this.date.toLocaleString(luxon.DateTime.DATE_FULL) + '-' + this.date.plus({ days: this.listLength - 1 }).toLocaleString(luxon.DateTime.DATE_FULL); - case "day": - return this.date.toLocaleString(luxon.DateTime.DATE_FULL); - default: - return 'View not Supported'; + case "month": + return this.date.toLocaleString({ month: 'long', year: 'numeric' }); + case "week": + var year = this.date.localWeekYear; + var week = this.date.toFormat('nn'); + return this.$p.t('calendar/year_kw', { year, week }); + case "tableList": + const end = this.rangeEnd ? this.rangeEnd.setZone(this.timezone, { keepLocalTime: true }) : this.date; + return this.date.toLocaleString(luxon.DateTime.DATE_FULL) + '-' + end.toLocaleString(luxon.DateTime.DATE_FULL); + case "list": + return this.date.toLocaleString(luxon.DateTime.DATE_FULL) + '-' + this.date.plus({ days: this.listLength - 1 }).toLocaleString(luxon.DateTime.DATE_FULL); + case "day": + return this.date.toLocaleString(luxon.DateTime.DATE_FULL); + default: + return 'View not Supported'; } }, weekStart() { @@ -72,20 +81,28 @@ export default { update(value) { let date; switch (this.mode) { - case "month": - value.month++; - date = luxon.DateTime.fromObject(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale); - break; - case "list": - case "week": - case "tableList": - date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale); - break; - case "day": - date = luxon.DateTime.fromJSDate(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale); - break; - default: - return; // Don't update if the value is invalid! + case "month": + value.month++; + date = luxon.DateTime.fromObject(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale); + break; + case "list": + case "week": + date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale); + break; + case "tableList": + if (!Array.isArray(value) || !value[0] || !value[1]) + return; + this.rangeEnd = luxon.DateTime.fromJSDate(value[1]); + const start = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale); + const end = this.rangeEnd.setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale); + this.$emit('update:date', start); + this.$emit('update:date-range', { start, end }); + return; + case "day": + date = luxon.DateTime.fromJSDate(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale); + break; + default: + return; // Don't update if the value is invalid! } this.$emit("update:date", date); }, @@ -99,8 +116,8 @@ export default { @update:model-value="update" :format="() => title" :month-picker="mode == 'month'" - :week-picker="mode == 'week' || mode == 'tableList'" - :range="mode == 'list' ? { autoRange: listLength - 1 } : false" + :week-picker="mode == 'week'" + :range="mode == 'list' ? { autoRange: listLength - 1 } : (mode == 'tableList' ? { partialRange: false } : false)" :text-input="mode == 'day'" :week-start="weekStart" :week-numbers="{ type: weekNumbers }" diff --git a/public/js/components/Calendar/Mode/Table.js b/public/js/components/Calendar/Mode/Table.js index 3676114af..529927d5f 100644 --- a/public/js/components/Calendar/Mode/Table.js +++ b/public/js/components/Calendar/Mode/Table.js @@ -1,109 +1,57 @@ -import BaseSlider from '../Base/Slider.js'; import TableView from './Table/View.js'; export default { name: "ModeTable", components: { - BaseSlider, TableView }, props: { currentDate: { type: luxon.DateTime, required: true + }, + rangeEnd: { + type: luxon.DateTime, + default: null } }, emits: [ - "update:currentDate", "update:range", - "click", - "requestModalOpen" ], data() { return { focusDate: this.currentDate, - rangeOffset: 0 + focusEnd: this.rangeEnd || this.currentDate }; }, computed: { range() { - let first = this.focusDate.startOf('week', { useLocaleWeeks: true }); - let last = this.focusDate.endOf('week', { useLocaleWeeks: true }); - - if (this.rangeOffset != 0) { - if (this.rangeOffset < 0) { - first = first.plus({ weeks: this.rangeOffset }); - } else { - last = last.plus({ weeks: this.rangeOffset }); - } - } - - return luxon.Interval.fromDateTimes(first, last); + return luxon.Interval.fromDateTimes(this.focusDate.startOf('day'), this.focusEnd.endOf('day')); } }, watch: { - currentDate() { - if (this.currentDate.locale != this.focusDate.locale) { - this.focusDate = this.currentDate; - this.$emit('update:range', this.range); - } else { - this.rangeOffset = this.currentDate.startOf('week', { useLocaleWeeks: true }).diff(this.focusDate.startOf('week', { useLocaleWeeks: true }), 'weeks').weeks; - if (this.rangeOffset) { - this.$emit('update:range', this.range); - this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage); - } - } + currentDate(newDate) { + this.focusDate = newDate; + if (!this.rangeEnd) + this.focusEnd = newDate; + this.$emit('update:range', this.range); + }, + rangeEnd(newEnd) { + this.focusEnd = newEnd || this.focusDate; + this.$emit('update:range', this.range); } }, methods: { - prevPage() { - this.rangeOffset = this.$refs.slider.target - 1; - this.$emit('update:range', this.range); - this.$refs.slider.prevPage().then(this.updatePage); - }, - nextPage() { - this.rangeOffset = this.$refs.slider.target + 1; - this.$emit('update:range', this.range); - this.$refs.slider.nextPage().then(this.updatePage); - }, - updatePage(weeks) { - const newFocusDate = this.focusDate.plus({ weeks }); - this.focusDate = newFocusDate; - this.rangeOffset = 0; - this.$emit('update:currentDate', this.focusDate); - this.$emit('update:range', this.range); - }, - viewAttrs(weeks) { - const day = this.focusDate.plus({ weeks }); - return { ...this.$attrs, day }; - }, - handleClickDefaults(evt) { - switch (evt.detail.source) { - case 'day': - // default: Set current-date - this.$emit('update:currentDate', evt.detail.value); - break; - case 'event': - // default: Request Modal - this.$emit('requestModalOpen', { event: evt.detail.value }); - break; - } - } }, mounted() { this.$emit('update:range', this.range); }, template: ` -
- - - - - +
+ + +
` -} +} \ No newline at end of file diff --git a/public/js/components/Calendar/Mode/Table/View.js b/public/js/components/Calendar/Mode/Table/View.js index cee1bfe7d..6f3553f7b 100644 --- a/public/js/components/Calendar/Mode/Table/View.js +++ b/public/js/components/Calendar/Mode/Table/View.js @@ -1,8 +1,6 @@ import {CoreFilterCmpt} from "../../../../components/filter/Filter.js"; import BsModal from '../../../Bootstrap/Modal.js'; import FormInput from "../../../Form/Input.js"; -import ApiDetails from "../../../../api/lehrveranstaltung/details.js"; - export default { name: "TableView", @@ -20,6 +18,10 @@ export default { day: { type: luxon.DateTime, required: true + }, + end: { + type: luxon.DateTime, + required: true } }, data() @@ -30,10 +32,13 @@ export default { }, computed: { start() { - return this.day.startOf('week', { useLocaleWeeks: true }); + return this.day.startOf('day'); + }, + tableEnd() { + return this.end.endOf('day'); }, preparedEvents() { - const end = this.start.plus({ days: 7 }); + const end = this.tableEnd; return this.events .filter(e => e.start < end && e.end > this.start) .sort((a, b) => a.start.ts - b.start.ts) @@ -112,15 +117,6 @@ export default { this.$refs.tableViewTable?.tabulator?.setData(newData); } }, -/* mounted() { - - this.$api.call(ApiDetails.getRaumtyp()) - .then(result => { - this.raumtyp_array = result.data; - }) - .catch(this.$fhcAlert.handleSystemError); - - },*/ template: /* html */`