Compare commits

...
Author SHA1 Message Date
Ivymaster cde51fa165 BUG: Reintroduced status icon on tempus calender event 2026-09-07 14:31:40 +02:00
Ivymaster 22fd4f199e BUG: Rework of setEmp in tempus component 2026-09-03 11:27:11 +02:00
Ivymaster 3e9788dcbf BUG: Removed duplicates of setEmp in tempus component 2026-09-03 11:21:03 +02:00
Ivymaster faa8f0446c Conflicts resolved 2026-09-03 11:11:10 +02:00
ma0048 4c2cd86b6d restore deleted setemp function 2026-09-03 11:06:25 +02:00
Ivymaster 563fda603a BUG: Room suggestions preview 2026-09-03 11:02:02 +02:00
ma0048 492c9a0140 fix: rebuild Raumvorschlag after adding event 2026-09-02 15:28:05 +02:00
Ivymaster 02ff8d0e3c Conflicts resolved 2026-09-02 15:01:56 +02:00
ma0048 90f62a7a9e - merged master
- added raumvorschlag slots
2026-09-02 10:54:36 +02:00
Ivymaster 3c333af184 Minor bug fixes 2026-09-01 13:15:29 +02:00
ma0048 eeb49e1967 Merge remote-tracking branch 'origin/feature-78287/tempus_raumzuordnung' into feature-68298/Tempus_Grundstruktur
# Conflicts:
#	application/libraries/KalenderLib.php
#	application/libraries/RaumvorschlagLib.php
2026-09-01 13:08:38 +02:00
ma0048 1d61652b67 Merge remote-tracking branch 'origin/feature-78211/tempus_kollisionscheck_auf_lehreinheit_id_umbauen_' into feature-68298/Tempus_Grundstruktur
# Conflicts:
#	application/libraries/KalenderLib.php
2026-09-01 12:59:22 +02:00
ma0048 8a448fde3c Merge remote-tracking branch 'origin/feature-78288/tempus_usability' into feature-68298/Tempus_Grundstruktur
# Conflicts:
#	public/css/components/horizontalsplit.css
#	public/js/components/Tempus/Filters/searchbarOptions.js
2026-09-01 12:48:07 +02:00
ma0048 1361c3fc00 Merge remote-tracking branch 'origin/feature-78286/tempus_berechtigungen' into feature-68298/Tempus_Grundstruktur
# Conflicts:
#	application/controllers/api/frontend/v1/tempus/Coursepicker.php
#	application/libraries/KalenderLib.php
2026-09-01 12:42:16 +02:00
ma0048 d727012806 Merge remote-tracking branch 'origin/feature-78285/tempus_stundenraster_ausschalten_nur_mit_eigener_berechtigung' into feature-68298/Tempus_Grundstruktur
# Conflicts:
#	system/dbupdate_3.4.php
2026-09-01 12:32:38 +02:00
ma0048 b91ba9fdbb Merge remote-tracking branch 'origin/feature-78324/tempus_coursepicker_lektor' into feature-68298/Tempus_Grundstruktur
# Conflicts:
#	application/controllers/api/frontend/v1/tempus/Coursepicker.php
#	public/js/components/Tempus/Coursepicker.js
2026-09-01 12:28:32 +02:00
ma0048 e311d7ee00 Merge remote-tracking branch 'origin/feature-78289/tempus_parken_features' into feature-68298/Tempus_Grundstruktur 2026-09-01 12:22:25 +02:00
ma0048 357a51bdcb Merge remote-tracking branch 'origin/feature-78317/tempus_delete_status' into feature-68298/Tempus_Grundstruktur 2026-09-01 12:21:53 +02:00
ma0048 61ef27e5eb Merge branch 'feature-77949/tempus_synchronisation' into feature-68298/Tempus_Grundstruktur
# Conflicts:
#	application/libraries/KalenderLib.php
#	system/dbupdate_3.4.php
2026-09-01 12:20:12 +02:00
ma0048 a26b75bc3c Merge remote-tracking branch 'origin/feature-78179/tempus_listenansicht_datumfilter' into feature-68298/Tempus_Grundstruktur 2026-09-01 12:17:16 +02:00
Ivymaster 61d7268f90 Conflicts resolved 2026-09-01 11:52:25 +02:00
ma0048 fb36b2a548 - merged master
- updaed Verbandsbaum to work with the new version
2026-08-31 14:58:27 +02:00
ma0048 51886960bb Merge branch 'master' into feature-68298/Tempus_Grundstruktur
# Conflicts:
#	application/controllers/api/frontend/v1/lv/StgTree.php
#	public/js/components/Stv/Studentenverwaltung/Verband.js
#	system/dbupdate_3.4.php
#	tests/cypress/.gitignore
2026-08-31 12:49:28 +02:00
ma0048 98be0b43b7 - permissions geandert 2026-08-27 08:08:34 +02:00
ma0048 4cbf37d727 - ueberpruefung der berechtigung auf den studiengang von der lv beim verschieben/hinzufuegen/loeschen der lehreinheit 2026-08-27 08:07:39 +02:00
ma0048 623e8007b0 - sichtbarkeitsstatus hinzugefuegt
- tage ausblendbar
- lektor zur lvverwaltung verlinkt
- vertical splitter hinzugefuegt
- anzahl der einheiten hinzugefuegt
2026-08-26 09:01:47 +02:00
ma0048 8184555e12 location hinzugefuegt
raumlose planung aktivierbar/deaktivierbar
automatische zuteilung zum priorraum aktivierbar/deaktivierbar
raum zuteilung ueber dialog aktivierbar/deaktivierbar
2026-08-13 13:36:35 +02:00
ma0048 b2aa4c4922 stundenraster mit eigener berechtigung basis/tempus_stundenraster deaktivierbar 2026-08-12 11:39:17 +02:00
ma0048 b5ac68840b fixed bug after column rename 2026-08-12 11:31:05 +02:00
ma0048 33b50bf479 course picker
- kurse vom lektor koennen angezeigt werden
- searchbar button hinzuefuegt um kurse vom lektor im course picker zu laden
2026-08-12 11:10:07 +02:00
ma0048 b0cb79d316 - geparkte Stunden im Parkfenster koennen entfernt werden
- sprung zur Woche
- farbe der lv
2026-08-12 08:26:40 +02:00
ma0048 015c8ca0f1 kollisionscheck auf lehreinheit_id umgebaut
raumvorschlag by lehreinheit hinzuegfuegt
2026-08-11 11:48:29 +02:00
ma0048 b58b4446a9 todelete auf to_delete umebannnt
to_delete_live & to_delete_preview hinugefuegt
history logik umbau
2026-08-10 10:57:34 +02:00
ma0048 c6f85b493a loader hinzugefuegt 2026-08-10 10:54:28 +02:00
Ivymaster 96526be231 Add kalender column rename from eindeutige_gruppen_id to eindeutige_kalender_gruppen_id 2026-08-06 13:43:20 +02:00
Ivymaster a912fcf019 Add kalender column rename from eindeutige_gruppen_id to eindeutige_kalender_gruppen_id 2026-08-06 13:20:51 +02:00
ma0048 626bc121d1 listenansicht datepicker range hinzugefuegt 2026-08-05 14:45:28 +02:00
ma0048 785246f58f fixed Sync Bug - Planning -> Voransicht -> Live -> Planning -> Voransicht -> Delete lässt einträge in der History auf Live 2026-08-05 12:35:41 +02:00
ma0048 a74f679917 fixed Sync Bug - Planning -> Voransicht -> Live -> Planning -> Voransicht -> Delete lässt einträge in der History auf Live 2026-08-05 12:14:53 +02:00
ma0048 6282cb0402 hinzugefuegt:
- alle anzeigen option
- jetzt starten knopf
- oe rekursiv
2026-08-05 11:12:33 +02:00
Ivymaster 45b0268105 BUG: Added reload of events on lang change, for tag display update 2026-08-04 11:35:06 +02:00
ma0048 3e34ea3917 fixed drops in pauses 2026-08-03 14:27:39 +02:00
ma0048 8636364c08 Merge branch 'feature-68298/Tempus_Grundstruktur' into feature-77949/tempus_synchronisation
# Conflicts:
#	system/dbupdate_3.4.php
2026-08-03 12:31:07 +02:00
ma0048 5ff43e0abd getoffenestunden archived, deleted, todelete nicht beruecksichtigen 2026-08-03 09:46:29 +02:00
ma0048 cc3b47a10e Raumzuteilung Entfernen / Hinzufuegen von Raeumen
Lehreinheiten entfernen
2026-07-31 10:34:15 +02:00
Ivymaster 98cf44de7f BUG: Fix exc tempus tag modal closing, and tag translations 2026-07-29 17:18:51 +02:00
ma0048 f907a12d19 - added room selection for reservations
- fixed reservation bug without a room
- added KW search to the course picker
- fixed room updates so no assigned rooms are lost
2026-07-28 09:20:20 +02:00
ma0048 32a7e28c80 fix: correct drop time when dropping onto an existing event 2026-07-24 12:37:38 +02:00
Ivymaster 1f2da90eb3 Add extraction of sidebar menu component from tempus component 2026-07-24 09:57:40 +02:00
Ivymaster 12ddcfff48 Add extraction of app and verband menu components from tempus component 2026-07-24 09:33:36 +02:00
Ivymaster e08ae7b331 Add extraction of header component from tempus component 2026-07-24 09:26:05 +02:00
Ivymaster 4a9f41160f Add smoke test for calendar event tag assignment 2026-07-24 09:16:40 +02:00
Ivymaster 4e21bb37d3 add tagsAssignmentModal extraction from tempus component 2026-07-22 17:01:11 +02:00
Ivymaster 4e58705107 add resourcesAssignmentModal extraction from tempus component 2026-07-22 16:36:20 +02:00
Ivymaster b809392f5d add historyModal extraction from tempus component 2026-07-22 16:20:43 +02:00
Ivymaster 817afa545b add tempus kalender tags 2026-07-22 15:52:31 +02:00
ma0048 9b273fa498 tab instead of space 2026-07-22 13:38:16 +02:00
Ivymaster 02a81fd52e BUG: fix for available tempus event resource suggestions 2026-07-22 08:58:58 +02:00
ma0048 9983a48102 gui fuer kalender sync hinzugefuegt und syncjob angepasst 2026-07-21 13:50:17 +02:00
Ivymaster a30affba3a add tempus cypress tests 2026-07-17 09:10:08 +02:00
Ivymaster e09fea72a4 add resources assignment to tempus kalender entries 2026-07-17 09:09:49 +02:00
ma0048 9abaf30ed3 Merge branch 'master' into feature-68298/Tempus_Grundstruktur
# Conflicts:
#	public/js/components/Calendar/Base/DragAndDrop.js
#	public/js/components/Calendar/Base/Grid/Line.js
#	public/js/composables/EventLoader.js
2026-07-15 15:48:44 +02:00
ma0048 81c16a3bf2 - wording angepasst lehreinheit -> lvteil
- Kollisionscheck an/aus wird angezeigt
- Shortcut für Ignore-Collision (F9) hinzugefuegt
- coursepicker:
 - design angepasst
 - favorisieren ermoeglicht
 - phrases
- bei lv immer alle raeume/lektoren/gruppen anzeigen unabhaengig vom filter
2026-07-15 14:58:41 +02:00
ma0048 a413ae17b8 suche und filter erweiterungen 2026-07-09 10:58:56 +02:00
ma0048 8172df5782 - migration script:
- auf cli job geandert
 - bisdatum nicht mehr pflicht
 - mehrere orte werden uebernommen
 - mehrere gruppen werden uebernommen

- coursepicker
 - sortierung hinzugefuegt
 - multiweekverplanung ermoeglicht
 - offene stunden berechnungn hinzugefuegt

- roomcollision auf ort array geaendert

- keyboardshortcuts hinzugefuegt
2026-07-07 11:00:25 +02:00
ma0048 6565399633 - added room suggestion priority
- return erro codes
- config for from/to without grid
- bug fixes and optimizations
2026-06-15 11:47:36 +02:00
ma0048 72692812ff lecture collision check bug fix 2026-06-01 15:08:29 +02:00
ma0048 80dabf5ce6 added join to hours for zeitsperren collision check 2026-06-01 14:28:27 +02:00
ma0048 e4a1264072 fixed zeitsperren collision check 2026-06-01 13:59:50 +02:00
ma0048 517f1a9d93 formatting 2026-06-01 13:39:59 +02:00
ma0048 e62848c773 added collision checks on creation 2026-06-01 11:18:53 +02:00
ma0048 9ac9804563 implement first version of collision checks 2026-05-26 14:09:58 +02:00
ma0048 24a0e74281 Fix: prevent duplicate entries 2026-05-21 12:27:55 +02:00
ma0048 57597dac29 - added reservierungen
- added updatemails
- added reservierungssync
- updated status kurzbz
- reservierung renderer for tempus
2026-04-28 20:17:50 +02:00
ma0048 7240b0c798 first version of history logic 2026-04-08 14:40:33 +02:00
ma0048 ab5294de2f stylingaenderungen
coursepicker umgebaut, keine backend suche mehr
hinzugefuegt:
- resizeHanlder funktion
- broadcastchannel postMessage "dropped"
- tbl_kalender_status column bezeichnung_mehrsprachig
- tbl_kalender_status column sort
2026-04-01 10:53:31 +02:00
ma0048 8dd42361a0 Merge branch 'master' into feature-68298/Tempus_Grundstruktur
# Conflicts:
#	public/js/components/Cis/Renderer/Lehreinheit/calendarEvent.js
#	system/dbupdate_3.4.php
2026-03-30 09:18:31 +02:00
ma0048 229de14f9c tempus pre-alpha version - add missing file 2026-03-25 14:42:36 +01:00
ma0048 e990bb3d81 tempus pre-alpha version 2026-03-25 14:41:13 +01:00
ma0048 43a1d163a3 Merge branch 'master' into feature-68298/Tempus_Grundstruktur 2026-01-20 09:21:47 +01:00
Andreas Österreicher 93388ff06c Merge branch 'master' into feature-68298/Tempus_Grundstruktur 2025-11-12 13:55:44 +01:00
Andreas Österreicher 352fc53e74 Erster Prototyp für Tempus Neu DB und GUI 2025-10-20 11:01:50 +02:00
162 changed files with 22988 additions and 280 deletions
+22
View File
@@ -33,5 +33,27 @@ Events::on('loadRenderers', function ($renderers) {
);
});
//Tempus Renderers:
Events::on('loadTempusRenderers', function ($renderers) {
$fhc_core_renderers =& $renderers();
$fhc_core_renderers["reservierung"] = array(
'calendarEvent' => absoluteJsImportUrl('public/js/components/Tempus/Renderer/Reservierungen/calendarEvent.js'),
'modalTitle' => absoluteJsImportUrl('public/js/components/Tempus/Renderer/Reservierungen/modalTitle.js'),
'modalContent' => absoluteJsImportUrl('public/js/components/Tempus/Renderer/Reservierungen/modalContent.js'),
'calendarEventStyles' => APP_ROOT . 'public/css/Cis4/CoreCalendarEvents.css'
);
});
Events::on('loadTempusRenderers', function ($renderers) {
$fhc_core_renderers =& $renderers();
$fhc_core_renderers["lehreinheit"] = array(
'calendarEvent' => absoluteJsImportUrl('public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js'),
'modalTitle' => absoluteJsImportUrl('public/js/components/Tempus/Renderer/Lehreinheit/modalTitle.js'),
'modalContent' => absoluteJsImportUrl('public/js/components/Tempus/Renderer/Lehreinheit/modalContent.js'),
'calendarEventStyles' => APP_ROOT . 'public/css/Cis4/CoreCalendarEvents.css'
);
});
+2
View File
@@ -140,6 +140,8 @@ $route['api/frontend/v1/treemenudata/lvverwaltung/stdsem/(:any)/emp/(:any)/stg/(
// 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))
+12
View File
@@ -0,0 +1,12 @@
<?php
$config['send_update_mails'] = false;
$config['calendar_start'] = 7;
$config['calendar_end'] = 23;
$config['tempus_tags'] = [
'prioone' => ['readonly' => false],
'priotwo' => ['readonly' => true],
'hinweis' => ['readonly' => false],
];
+5
View File
@@ -27,3 +27,8 @@ $config['lvv'] = [
"library" => "treemenu/LvVwLib",
"permissions" => ["admin:r", "assistenz:r"]
];
$config['tempus'] = [
"library" => "treemenu/TempusLib",
"permissions" => ["admin:r", "assistenz:r", "lehre/lvplan:r"]
];
@@ -0,0 +1,32 @@
<?php
/**
* Copyright (C) 2026 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (!defined('BASEPATH')) exit('No direct script access allowed');
$config['root'] = [
'stg' => [
'semester' => [
'group',
'verband' => [
'group'
]
],
'orgform'
]
];
+69
View File
@@ -0,0 +1,69 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*
*/
class Tempus extends Auth_Controller
{
public function __construct()
{
$permissions = [];
$router = load_class('Router');
$permissions[$router->method] = ['admin:r', 'assistenz:r'];
parent::__construct($permissions);
// Load Libraries
$this->load->library('VariableLib', ['uid' => getAuthUID()]);
// Load Config
$this->load->config('calendar');
}
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->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());
$this->_loadView(array('organisationen' => $organisationen, 'studiensemestern' => $studiensemestern));
}
private function _loadView($extraVariables = [])
{
$this->load->view('Tempus', [
'permissions' => [
'admin' => $this->permissionlib->isBerechtigt('admin'),
'stundenraster' => $this->permissionlib->isBerechtigt('basis/tempus_stundenraster'),
],
'variables' => array_merge([
'semester_aktuell' => $this->variablelib->getVar('semester_aktuell'),
'timezone' => $this->config->item('timezone')
], $extraVariables)
]);
}
}
@@ -31,7 +31,7 @@ class RendererLoader extends FHCAPI_Controller
parent::__construct([
'GetRenderers' => self::PERM_LOGGED,
'GetTempusRenderers' => self::PERM_LOGGED,
]);
$this->load->library('LogLib');
@@ -66,6 +66,26 @@ class RendererLoader extends FHCAPI_Controller
$this->terminateWithSuccess($renderer_paths);
}
public function GetTempusRenderers(){
$renderer_paths = [];
Events::trigger(
'loadRenderers',
function & () use (&$renderer_paths)
{
return $renderer_paths;
}
);
Events::trigger(
'loadTempusRenderers',
function & () use (&$renderer_paths)
{
return $renderer_paths;
}
);
$this->terminateWithSuccess($renderer_paths);
}
@@ -184,54 +184,17 @@ class Gruppe extends FHCAPI_Controller
$query_words = explode(' ', $query);
$this->_ci->GruppeModel->addSelect('gruppe_kurzbz,
studiengang_kz,
semester,
bezeichnung,
gid,
\'false\' as lehrverband');
$this->_ci->GruppeModel->db->where(array('sichtbar' => true, 'aktiv' => true, 'lehre' => true, 'direktinskription' => false, 'semester IS NOT NULL' => null));
$this->_ci->GruppeModel->db->group_start();
foreach ($query_words as $word)
{
$this->_ci->GruppeModel->db->group_start();
$this->_ci->GruppeModel->db->where('gruppe_kurzbz ILIKE', "%" . $word . "%");
$this->_ci->GruppeModel->db->or_where('bezeichnung ILIKE', "%" . $word . "%");
$this->_ci->GruppeModel->db->group_end();
}
$this->_ci->GruppeModel->db->group_end();
$gruppen_result = $this->_ci->GruppeModel->load();
$gruppen_array = array();
$gruppen_result = $this->_ci->GruppeModel->search($query_words);
if (isError($gruppen_result))
$this->terminateWithError(getError($gruppen_result), self::ERROR_TYPE_GENERAL);
$gruppen_array = array();
if (hasData($gruppen_result))
$gruppen_array = getData($gruppen_result);
$this->_ci->LehrverbandModel->addSelect('CONCAT(UPPER(CONCAT(typ, kurzbz)), \'\', semester, verband, COALESCE(gruppe,\'\')) as gruppe_kurzbz,
studiengang_kz,
semester,
tbl_lehrverband.bezeichnung,
gid,
\'true\' as lehrverband');
$this->_ci->LehrverbandModel->addJoin('public.tbl_studiengang', 'studiengang_kz');
$this->_ci->LehrverbandModel->addOrder('verband');
$this->_ci->LehrverbandModel->addOrder('gruppe');
$this->_ci->LehrverbandModel->db->where(array('tbl_lehrverband.aktiv' => true));
$this->_ci->LehrverbandModel->db->group_start();
foreach ($query_words as $word)
{
$this->_ci->LehrverbandModel->db->group_start();
$this->_ci->LehrverbandModel->db->where('CONCAT(CONCAT(typ, kurzbz), \'\', semester, verband, COALESCE(gruppe,\'\')) ILIKE', "%" . $word . "%");
$this->_ci->LehrverbandModel->db->or_where('tbl_lehrverband.bezeichnung ILIKE', "%" . $word . "%");
$this->_ci->LehrverbandModel->db->group_end();
}
$this->_ci->LehrverbandModel->db->group_end();
$lehrverband_result = $this->_ci->LehrverbandModel->load();
$lehrverband_result = $this->_ci->LehrverbandModel->search($query_words);
$lehrverband_array = array();
@@ -0,0 +1,157 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class Config extends FHCAPI_Controller
{
private $_ci;
public function __construct()
{
parent::__construct([
'get' => ['admin:r', 'assistenz:rw', 'lehre/lvplan:rw'],
'getHeader' => ['admin:r', 'assistenz:rw', 'lehre/lvplan:rw'],
'set' => ['admin:r', 'assistenz:rw', 'lehre/lvplan:rw'],
'updateCollision' => ['admin:r', 'assistenz:rw', 'lehre/lvplan:rw'],
]);
// Load Phrases
$this->loadPhrases([
'ui',
]);
$this->_ci = &get_instance();
$this->_ci->load->model('ressource/Kalenderstatus_model', 'KalenderStatusModel');
$this->_ci->load->model('system/Variable_model', 'VariableModel');
$this->_ci->load->library('VariableLib', array('uid' => getAuthUID()));
}
public function get()
{
$config = [];
$var_array = array(
'ignore_kollision',
'kollision_student',
'ignore_reservierung',
'ignore_zeitsperre',
'ignore_resources_collisions',
'roomless_planning',
'priority_room_planning',
'dialog_room_planning'
);
$result = $this->_ci->VariableModel->getVariables(getAuthUID(), $var_array);
$data = $this->getDataOrTerminateWithError($result);
$config['ignore_kollision'] = [
"type" => "checkbox",
"label" => $this->p->t('ui', 'ignore_kollision'),
"value" => ($data['ignore_kollision'] ?? 'false') === 'true'
];
$config['kollision_student'] = [
"type" => "checkbox",
"label" => $this->p->t('ui', 'kollision_student'),
"value" => ($data['kollision_student'] ?? 'false') === 'true'
];
$config['ignore_reservierung'] = [
"type" => "checkbox",
"label" => $this->p->t('ui', 'ignore_reservierung'),
"value" => ($data['ignore_reservierung'] ?? 'false') === 'true'
];
$config['ignore_zeitsperre'] = [
"type" => "checkbox",
"label" => $this->p->t('ui', 'ignore_zeitsperre'),
"value" => ($data['ignore_zeitsperre'] ?? 'false') === 'true'
];
$config['ignore_resources_collisions'] = [
"type" => "checkbox",
"label" => $this->p->t('ui', 'ignore_resources_collisions'),
"value" => ($data['ignore_resources_collisions'] ?? 'false') === 'true'
];
$config['roomless_planning'] = [
"type" => "checkbox",
"label" => $this->p->t('ui', 'roomless_planning'),
"value" => ($data['roomless_planning'] ?? 'false') === 'true'
];
$config['priority_room_planning'] = [
"type" => "checkbox",
"label" => $this->p->t('ui', 'priority_room_planning'),
"value" => ($data['priority_room_planning'] ?? 'false') === 'true'
];
$config['dialog_room_planning'] = [
"type" => "checkbox",
"label" => $this->p->t('ui', 'dialog_room_planning'),
"value" => ($data['dialog_room_planning'] ?? 'false') === 'true'
];
$this->terminateWithSuccess($config);
}
public function getHeader()
{
$language = getUserLanguage() == 'German' ? 0 : 1;
$this->_ci->KalenderStatusModel->addSelect('*, array_to_json(bezeichnung_mehrsprachig::varchar[])->>' . $language .' AS status');
$this->_ci->KalenderStatusModel->addOrder('sort');
$this->_ci->KalenderStatusModel->db->where_not_in('status_kurzbz', array('archived', 'deleted'));
$visible_status = $this->_ci->KalenderStatusModel->load();
$visible_status = getData($visible_status);
$config['visible_status']['all'] = 'Alle';
foreach ($visible_status as $status)
{
$config['visible_status'][$status->status_kurzbz] = $status->status;
}
$this->terminateWithSuccess($config);
}
public function set()
{
$configs = array(
'ignore_kollision',
'kollision_student',
'ignore_reservierung',
'ignore_zeitsperre',
'ignore_resources_collisions',
'roomless_planning',
'priority_room_planning',
'dialog_room_planning'
);
foreach ($configs as $config)
{
$this->_ci->VariableModel->setVariable(
getAuthUID(),
$config,
$this->input->post($config) === true ? 'true' : 'false'
);
}
$this->terminateWithSuccess();
}
public function updateCollision()
{
$original_ignore = $this->_ci->variablelib->getVar('ignore_kollision');
$this->_ci->VariableModel->setVariable(
getAuthUID(),
'ignore_kollision',
$original_ignore === 'true' ? 'false' : 'true'
);
}
}
@@ -0,0 +1,276 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Coursepicker extends FHCAPI_Controller
{
private $_ci;
const ALLOWED_COURSE_FILTER = ['uid', 'stg'];
public function __construct()
{
parent::__construct([
'getCourses' => 'lehre/lvplan:rw',
]);
$this->_ci = &get_instance();
$this->load->library('form_validation');
$this->_ci->load->model('education/lehreinheit_model', 'LehreinheitModel');
$this->_ci->load->model('education/Lehreinheitmitarbeiter_model', 'LehreinheitmitarbeiterModel');
$this->loadPhrases(['ui']);
}
public function getCourses()
{
$this->_ci->form_validation->set_data($_POST);
$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->post('studiensemester_kurzbz', TRUE);
$filter = $this->_checkFilter(self::ALLOWED_COURSE_FILTER);
$this->_getBasePlan($studiensemester_kurzbz);
$this->_ci->LehreinheitModel->db->group_start();
if (isset($filter->stg))
$this->_addStgWhere($filter->stg);
if (isset($filter->uid))
{
$uid_array = (array) $filter->uid;
$this->_ci->LehreinheitModel->db->or_where_in('tbl_benutzer.uid', $uid_array);
}
$this->_ci->LehreinheitModel->db->group_end();
$this->_ci->LehreinheitModel->addOrder('lehreinheit_id');
$result = $this->_ci->LehreinheitModel->load();
$result = hasData($result) ? getData($result) : array();
$this->terminateWithSuccess($this->_mapEvents($result));
}
private function _getBasePlan($studiensemester_kurzbz)
{
$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,
ma.kurzbz AS lektor,
tbl_person.vorname,
tbl_person.nachname,
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', 'tbl_lehreinheit.lehrveranstaltung_id = tbl_lehrveranstaltung.lehrveranstaltung_id');
$this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehrveranstaltung lehrfach', 'tbl_lehreinheit.lehrfach_id = lehrfach.lehrveranstaltung_id');
$this->_ci->LehreinheitModel->addJoin('public.tbl_mitarbeiter ma', 'lema.mitarbeiter_uid = ma.mitarbeiter_uid');
$this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehrform', 'tbl_lehrform.lehrform_kurzbz = tbl_lehreinheit.lehrform_kurzbz');
$this->_ci->LehreinheitModel->addJoin('public.tbl_benutzer', 'ma.mitarbeiter_uid = tbl_benutzer.uid');
$this->_ci->LehreinheitModel->addJoin('public.tbl_person', 'tbl_benutzer.person_id = tbl_person.person_id');
$this->_ci->LehreinheitModel->db->group_start();
$this->_ci->LehreinheitModel->db->where('tbl_lehrform.verplanen', true);
$this->_ci->LehreinheitModel->db->where('tbl_lehreinheit.studiensemester_kurzbz', $studiensemester_kurzbz);
$this->_ci->LehreinheitModel->db->group_end();
}
private function _addStgWhere($studiengaenge)
{
$this->_ci->LehreinheitModel->db->group_start();
$first = true;
foreach ($studiengaenge as $studiengang)
{
if ($first)
$this->_ci->LehreinheitModel->db->group_start();
else
{
$this->_ci->LehreinheitModel->db->or_group_start();
}
$first = false;
$this->_ci->LehreinheitModel->db->where('tbl_lehrveranstaltung.studiengang_kz', $studiengang['stg_kz']);
if (isset($studiengang['semester']))
$this->_ci->LehreinheitModel->db->where('tbl_lehrveranstaltung.semester', $studiengang['semester']);
if (isset($studiengang['orgform_kurzbz']))
$this->_ci->LehreinheitModel->db->where('tbl_lehrveranstaltung.orgform_kurzbz', $studiengang['orgform_kurzbz']);
$this->_ci->LehreinheitModel->db->group_end();
}
$this->_ci->LehreinheitModel->db->group_end();
}
private function _mapEvents($result)
{
$lehreinheit_ids = array_values(array_unique(array_column($result, 'lehreinheit_id')));
$offen_map = array();
if (!empty($lehreinheit_ids))
{
$offen_result = $this->_ci->LehreinheitModel->getOffeneStunden($lehreinheit_ids);
if (hasData($offen_result))
{
foreach (getData($offen_result) as $row)
{
$offen_map[$row->lehreinheit_id . '_' . $row->mitarbeiter_uid] = $row;
}
}
}
$grouped = array();
foreach ($result as $row)
{
$lehreinheit_id = $row->lehreinheit_id;
if (!isset($grouped[$lehreinheit_id]))
{
$grouped[$lehreinheit_id] = (object)array(
'lehrfach_id' => $row->lehrfach_id,
'lehrfach_bez' => $row->lehrfach_bez,
'lehrfach_farbe' => $row->lehrfach_farbe,
'studiengang_kz' => $row->studiengang_kz,
'studiengang' => $row->studiengang,
'semester' => $row->semester,
'verband' => $row->verband,
'gruppe' => $row->gruppe,
'gruppe_kurzbz' => $row->gruppe_kurzbz,
'raumtyp' => $row->raumtyp,
'raumtypalternativ' => $row->raumtypalternativ,
'anmerkung' => $row->anmerkung,
'studiensemester_kurzbz' => $row->studiensemester_kurzbz,
'fachbereich_kurzbz' => isset($row->fachbereich_kurzbz) ? $row->fachbereich_kurzbz : null,
'lektoren' => array(),
'lehreinheit_id' => $lehreinheit_id,
'lvnr' => array(),
'lehrfach' => array(),
'lehrform' => array(),
'stundenblockung' => array(),
'wochenrythmus' => array(),
'planstunden' => array(),
'start_kw' => array(),
'verplant' => array(),
'offenestunden' => array(),
'lehrverband' => array(),
'lem' => array(),
'verplant_gesamt' => 0,
);
}
$group = $grouped[$lehreinheit_id];
$group->lektoren[$row->lektor_uid] = (object)array(
'uid' => $row->lektor_uid,
'kurzbz' => trim($row->lektor),
'name' => $row->vorname . ' ' . $row->nachname,
);
$group->lvnr[] = $row->lvnr;
$group->lehrfach[] = $row->lehrfach;
$group->lehrform[] = $row->lehrform;
$group->stundenblockung[] = $row->stundenblockung;
$group->wochenrythmus[] = $row->wochenrythmus;
$group->planstunden[] = $row->planstunden;
$group->start_kw[] = $row->start_kw;
$offen_key = $lehreinheit_id . '_' . $row->lektor_uid;
$offen = isset($offen_map[$offen_key]) ? $offen_map[$offen_key] : null;
$verplant_wert = $offen ? $offen->verplant : 0;
$offenestunden_wert = $offen ? $offen->offenestunden : 0;
$group->verplant[] = $verplant_wert;
$group->offenestunden[] = $offenestunden_wert;
$group->verplant_gesamt += $verplant_wert;
$lvb = $row->studiengang . '-' . $row->semester;
if ($row->verband != '' && $row->verband != ' ' && $row->verband != '0' && $row->verband != null)
$lvb .= $row->verband;
if ($row->gruppe != '' && $row->gruppe != ' ' && $row->gruppe != '0' && $row->gruppe != null)
$lvb .= $row->gruppe;
$group->lehrverband[] = ($row->gruppe_kurzbz != '' && $row->gruppe_kurzbz != null) ? $row->gruppe_kurzbz : $lvb;
$group->lem[] = array(
'lehreinheit_id' => $row->lehreinheit_id,
'mitarbeiter_uid' => $row->lektor_uid,
);
}
foreach ($grouped as $group)
{
$group->lektoren = array_values($group->lektoren);
$group->lehrverband = array_values(array_unique($group->lehrverband));
$group->lehrfach = $this->_formatArr($group->lehrfach);
$group->lehrform = $this->_formatArr($group->lehrform);
$group->stundenblockung = $this->_formatArr($group->stundenblockung);
$group->wochenrythmus = $this->_formatArr($group->wochenrythmus);
$group->planstunden = $this->_formatArr($group->planstunden);
$group->start_kw = $this->_formatArr($group->start_kw);
$group->verplant = $this->_formatArr($group->verplant);
$group->offenestunden = $this->_formatArr($group->offenestunden);
}
return array_values($grouped);
}
private function _formatArr($arr)
{
$values = array_values(array_unique($arr));
$formatted = implode(' ', $values);
if (count($values) > 1)
$formatted .= ' ?';
return $formatted;
}
private function _checkFilter($filters)
{
$filter_valid = false;
$filter_object = new stdClass();
foreach ($filters as $filter)
{
if ($this->_ci->input->post($filter))
{
$filter_valid = true;
$filter_object->$filter = $this->_ci->input->post($filter);
}
}
if (!$filter_valid)
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
return $filter_object;
}
}
@@ -0,0 +1,47 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Favorites extends FHCAPI_Controller
{
public function __construct()
{
parent::__construct([
'index' => 'lehre/lvplan:rw',
'set' => 'lehre/lvplan:rw'
]);
// Load models
$this->load->model('system/Variable_model', 'VariableModel');
}
public function index()
{
$result = $this->VariableModel->getVariables(getAuthUID(), ['tempus_favorites']);
$data = $this->getDataOrTerminateWithError($result);
if (!$data)
$this->terminateWithSuccess(null);
else
$this->terminateWithSuccess(isset($data['tempus_favorites']) ? $data['tempus_favorites'] : null);
}
public function set()
{
$this->load->library('form_validation');
$this->form_validation->set_rules('favorites', 'Favorites', 'required');
if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array());
$favorites = $this->input->post('favorites');
$result = $this->VariableModel->setVariable(getAuthUID(), 'tempus_favorites', $favorites);
$this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess(true);
}
}
@@ -0,0 +1,37 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Info extends FHCAPI_Controller
{
private $_ci;
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getStudiengaenge' => 'lehre/lvplan:rw',
]);
$this->_ci =& get_instance();
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->loadPhrases([
'ui'
]);
}
public function getStudiengaenge()
{
$this->_ci->StudiengangModel->addDistinct();
$this->_ci->StudiengangModel->addSelect('studiengang_kz');
$this->_ci->StudiengangModel->addSelect("CONCAT(kurzbzlang, ' (', UPPER(CONCAT(typ, kurzbz)), ') - ', tbl_studiengang.bezeichnung) AS name");
$result = $this->getDataOrTerminateWithError($this->_ci->StudiengangModel->load());
$this->terminateWithSuccess($result);
}
}
@@ -0,0 +1,509 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Kalender extends FHCAPI_Controller
{
private $_ci;
const ALLOWED_PLAN_FILTER = ['ort', 'uid', 'stg'];
const ALLOWED_ROOM_FILTER = ['lehreinheit_id', 'kalender_id'];
const ALLOWED_TO_UPDATE = ['start_time', 'end_time', 'orte'];
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getStunden' => 'lehre/lvplan:rw',
'getCalendarHours' => 'lehre/lvplan:rw',
'getPlan' => 'lehre/lvplan:rw',
'getPlanByOrt' => 'lehre/lvplan:rw',
'getRaumvorschlag' => 'lehre/lvplan:rw',
'getRaumvorschlagSlots' => 'lehre/lvplan:rw',
'getLehreinheiten' => 'lehre/lvplan:rw',
'getHistory' => 'lehre/lvplan:rw',
'deleteEntry' => 'lehre/lvplan:rw',
'syncToLecturer' => 'lehre/lvplan:rw',
'syncToStudent' => 'lehre/lvplan:rw',
'getPlanLecturer' =>'lehre/lvplan:rw',
'getPlanStudent' => 'lehre/lvplan:rw',
'getZeitwuensche' => 'lehre/lvplan:rw',
'getZeitsperren' => 'lehre/lvplan:rw',
'updateKalenderEvent' => 'lehre/lvplan:rw',
'deleteOrtEntry' => 'lehre/lvplan:rw',
'deleteFromKalenderEvent' => 'lehre/lvplan:rw',
'addKalenderEvent' => 'lehre/lvplan:rw',
'calculateMultiWeekPlan' => 'lehre/lvplan:rw',
'confirmMultiWeekPlan' => 'lehre/lvplan:rw',
'addToKalenderEvent' => 'lehre/lvplan:rw',
'sync' => 'lehre/lvplan:rw',
]);
$this->_ci =& get_instance();
$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'
]);
$this->_ci->load->config('tempus');
$this->_ci->loglib->setConfigs(array(
'classIndex' => 5,
'functionIndex' => 5,
'lineIndex' => 4,
'dbLogType' => 'API', // required
'dbExecuteUser' => 'RESTful API'
));
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* fetches Stunden layout from database
* @access public
*
*/
public function getStunden()
{
$this->load->model('ressource/Stunde_model', 'StundeModel');
$this->_ci->StundeModel->addOrder('stunde', 'ASC');
$stunden = $this->_ci->StundeModel->load();
$stunden = $this->getDataOrTerminateWithError($stunden);
$this->terminateWithSuccess($stunden);
}
public function getCalendarHours()
{
$calender_start = $this->_ci->config->item('calendar_start') ?? 7;
$calender_end = $this->_ci->config->item('calendar_end') ?? 23;
$this->terminateWithSuccess(array(
'start' => $calender_start,
'end' => $calender_end
));
}
public function getPlan()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->post('start_date', TRUE);
$end_date = $this->_ci->input->post('end_date', TRUE);
$filter = $this->_checkFilter(self::ALLOWED_PLAN_FILTER);
$stundenplan_data = $this->_ci->kalenderlib->getPlanForPlanner(
$start_date,
$end_date,
isset($filter->ort) ? $filter->ort : null,
isset($filter->uid) ? $filter->uid : null,
isset($filter->stg) ? $filter->stg : null
);
$this->terminateWithSuccess($stundenplan_data);
}
public function getPlanStudent()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$stundenplan_data = $this->_ci->kalenderlib->getPlanForStudent(
$start_date,
$end_date
);
$this->terminateWithSuccess($stundenplan_data);
}
public function getPlanLecturer()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$stundenplan_data = $this->_ci->kalenderlib->getPlanForLecturer(
$start_date,
$end_date
);
$this->terminateWithSuccess($stundenplan_data);
}
public function getPlanByOrt($start_date = null, $end_date = null, $ort = null)
{
if (!isset($start_date) || !isset($end_date) || !isset($ort))
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
$this->_ci->form_validation->set_rules('ort',"ort","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$ort = $this->_ci->input->get('ort', TRUE);
}
$this->terminateWithSuccess($this->_ci->kalenderlib->getPlanByOrt($start_date, $end_date, $ort));
}
public function getZeitsperren()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
$this->_ci->form_validation->set_rules('emp',"emp","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$emp = $this->_ci->input->get('emp', TRUE);
$stundenplan_data = $this->_ci->kalenderlib->getZeitsperren($start_date, $end_date, $emp);
$this->terminateWithSuccess($stundenplan_data);
}
public function getZeitwuensche()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
$this->_ci->form_validation->set_rules('emp',"emp","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$emp = $this->_ci->input->get('emp', TRUE);
$stundenplan_data = $this->_ci->kalenderlib->getZeitwuensche($start_date, $end_date, $emp);
$this->terminateWithSuccess($stundenplan_data);
}
public function updateKalenderEvent()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('kalender_id',"kalender_id","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$updateFields = $this->_checkUpdate($this->_ci->input->post('updatedInfos', TRUE));
$kalender_id = $this->_ci->input->post('kalender_id', TRUE);
$result = $this->_ci->kalenderlib->updateKalenderEvent($kalender_id, $updateFields->orte ?? null, $updateFields->start_time ?? null, $updateFields->end_time ?? null, $updateFields->location ?? null);
if (isError($result))
$this->terminateWithError(getError($result), $result->code);
$this->terminateWithSuccess(getData($result));
}
public function deleteOrtEntry()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('kalender_id',"kalender_id","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$kalender_id = $this->_ci->input->post('kalender_id', TRUE);
$result = $this->_ci->kalenderlib->updateOrt($kalender_id, []);
if (isError($result))
$this->terminateWithError(getError($result), $result->code);
$this->terminateWithSuccess(getData($result));
}
public function getRaumvorschlag()
{
$this->_ci->form_validation->set_data($_POST);
$filter = $this->_checkFilter(self::ALLOWED_ROOM_FILTER);
$this->terminateWithSuccess($this->_ci->raumvorschlaglib->getVorschlaege($filter->kalender_id));
}
public function getRaumvorschlagSlots()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required");
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$lehreinheit_id = $this->_ci->input->get('lehreinheit_id', TRUE);
$start_date = $this->_ci->input->get('start_date', TRUE);
$end_date = $this->_ci->input->get('end_date', TRUE);
$this->terminateWithSuccess(getData($this->_ci->raumvorschlaglib->getVorschlaegeSlots($lehreinheit_id, $start_date, $end_date)));
}
public function getLehreinheiten()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('kalender_id', "kalender_id", "required");
$kalender_id = $this->_ci->input->get('kalender_id', TRUE);
$result = $this->_ci->kalenderlib->getLehreinheiten($kalender_id);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(hasData($result) ? getData($result) : []);
}
public function getHistory()
{
$this->_ci->form_validation->set_data($_GET);
$this->_ci->form_validation->set_rules('kalender_id',"kalender_id","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$kalender_id = $this->_ci->input->get('kalender_id', TRUE);
$result = $this->_ci->kalenderlib->getHistory($kalender_id);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function deleteEntry()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('kalender_id', "kalender_id", "required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$kalender_id = $this->_ci->input->post('kalender_id', TRUE);
$result = $this->_ci->kalenderlib->deleteEntry($kalender_id);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function sync()
{
$result = $this->_ci->kalendersynclib->sync();
$this->terminateWithSuccess(getData($result));
}
public function syncToLecturer()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('kalender_id', "kalender_id", "required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$kalender_id = $this->_ci->input->post('kalender_id', TRUE);
$result = $this->_ci->kalenderlib->updateStatus($kalender_id, 'sync_preview');
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function syncToStudent()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('kalender_id', "kalender_id", "required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$kalender_id = $this->_ci->input->post('kalender_id', TRUE);
$result = $this->_ci->kalenderlib->updateStatus($kalender_id, 'sync_live');
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function addKalenderEvent()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required");
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$lehreinheit_id = $this->_ci->input->post('lehreinheit_id', TRUE);
$ort_kurzbz = $this->_ci->input->post('ort_kurzbz', TRUE);
$start_date = $this->_ci->input->post('start_date', TRUE);
$end_date = $this->_ci->input->post('end_date', TRUE);
$result = $this->_ci->kalenderlib->addKalenderEvent($start_date, $end_date, $lehreinheit_id, $ort_kurzbz);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function calculateMultiWeekPlan()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required");
$this->_ci->form_validation->set_rules('start_date',"start_date","required");
$this->_ci->form_validation->set_rules('end_date',"end_date","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$lehreinheit_id = $this->_ci->input->post('lehreinheit_id', TRUE);
$ort_kurzbz = $this->_ci->input->post('ort_kurzbz', TRUE);
$start_date = $this->_ci->input->post('start_date', TRUE);
$end_date = $this->_ci->input->post('end_date', TRUE);
$result = $this->_ci->kalenderlib->calculateMultiWeekPlan($start_date, $end_date, $lehreinheit_id, $ort_kurzbz);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function confirmMultiWeekPlan()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('plan[]',"plan","required");
$this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$plan = $this->_ci->input->post('plan', TRUE);
$lehreinheit_id = $this->_ci->input->post('lehreinheit_id', TRUE);
$result = $this->_ci->kalenderlib->confirmMultiWeekPlan($plan, $lehreinheit_id);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function addToKalenderEvent()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('target_kalender_id',"target_kalender_id","required");
$this->_ci->form_validation->set_rules('lehreinheit_id',"lehreinheit_id","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$target_kalender_id = $this->_ci->input->post('target_kalender_id', TRUE);
$lehreinheit_id = $this->_ci->input->post('lehreinheit_id', TRUE);
$result = $this->_ci->kalenderlib->addToKalenderEvent($target_kalender_id, $lehreinheit_id);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
public function deleteFromKalenderEvent()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('kalender_id',"kalender_id","required");
$this->_ci->form_validation->set_rules('lehreinheit_ids[]',"lehreinheit_ids","required");
if($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$kalender_id = $this->_ci->input->post('kalender_id', TRUE);
$lehreinheit_ids = $this->_ci->input->post('lehreinheit_ids', TRUE);
$result = $this->_ci->kalenderlib->deleteFromKalenderEvent($kalender_id, $lehreinheit_ids);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(getData($result));
}
private function _checkFilter($filters)
{
$filter_valid = true;
$filter_object = new stdClass();
foreach ($filters as $filter)
{
if ($this->_ci->input->post($filter))
{
$filter_valid = true;
$filter_object->$filter = $this->_ci->input->post($filter);
}
}
if (!$filter_valid)
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
return $filter_object;
}
private function _checkUpdate($updateInfos)
{
$update_valid = false;
$update_object = new stdClass();
foreach (self::ALLOWED_TO_UPDATE as $filter)
{
if (isset($updateInfos[$filter]))
{
$update_valid = true;
$update_object->$filter = $updateInfos[$filter];
}
}
if (!$update_valid)
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
return $update_object;
}
}
@@ -0,0 +1,304 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class KalenderSync extends FHCAPI_Controller
{
private $_ci;
const ALLOWED_STATUS = ['preview', 'live'];
public function __construct()
{
parent::__construct([
'getSyncs' => ['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, 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');
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));
$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();
}
}
@@ -0,0 +1,84 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class OperationalResourceToCalenderAPI extends FHCAPI_Controller
{
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getAssignedResourcesByCalender' => ['admin:r', 'assistenz:r', 'lehre/lvplan:rw'],
'storeResourcesToCalendarRelationship' => ['admin:w', 'assistenz:w', 'lehre/lvplan:rw'],
'getSchedulableResourcesByCalendar' => ['admin:r', 'assistenz:r', 'lehre/lvplan:rw'],
]);
$this->load->model('ressource/BetriebsmittelKalender_model', 'BetriebsmittelKalenderModel');
$this->load->model('ressource/Betriebsmittel_model', 'BetriebsmittelModel');
$this->load->model('ressource/Kalender_model', 'KalenderModel');
$this->load->library('CollisionChecker');
$this->load->library('KalenderLib');
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
public function getSchedulableResourcesByCalendar($calenderID)
{
if (!isset($calenderID)) $this->terminateWithError("Missing required parameter 'kalender_id'");
$result = $this->KalenderModel->loadWhere(['kalender_id' => $calenderID]);
if (isError($result)) $this->terminateWithError("Calendar with id '$calenderID' does not have a valid group id");
$calender = $this->getDataOrTerminateWithError($result)[0];
if (!isset($calender)) $this->terminateWithError("Calendar with id '$calenderID' not found");
$result = $this->BetriebsmittelModel->getSchedulableEntriesByDatetimeInterval($calender->von, $calender->bis);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function getAssignedResourcesByCalender($calenderID)
{
if (!isset($calenderID)) $this->terminateWithError("Missing required parameter 'kalender_id'");
$result = $this->KalenderModel->loadWhere(['kalender_id' => $calenderID]);
if (empty($result)) $this->terminateWithError("Calendar with id '$calenderID' not found");
$calenderGroupID = $this->getDataOrTerminateWithError($result)[0]->eindeutige_kalender_gruppen_id;
if (!isset($calenderGroupID)) $this->terminateWithError("Calendar with id '$calenderID' does not have a valid group id");
$this->BetriebsmittelKalenderModel->addSelect(['tbl_betriebsmittel_kalender.*', 'tbl_betriebsmittel.beschreibung', 'tbl_betriebsmittel.verplanen']);
$this->BetriebsmittelKalenderModel->addJoin('wawi.tbl_betriebsmittel', 'betriebsmittel_id');
$result = $this->BetriebsmittelKalenderModel->loadWhere([
'eindeutige_kalender_gruppen_id' => $calenderGroupID,
'tbl_betriebsmittel.verplanen' => true,
]);
$this->terminateWithSuccess($this->getDataOrTerminateWithError($result));
}
public function storeResourcesToCalendarRelationship()
{
$calenderID = $this->input->post('kalender_id');
$assignedResources = $this->input->post('assignedResources');
if (!isset($calenderID)) $this->terminateWithError("Missing required parameter 'kalender_id'", 'general');
if (!isset($assignedResources)) $this->terminateWithError("Missing required parameter 'assignedResources'", 'general');
$result = $this->KalenderModel->loadWhere(['kalender_id' => $calenderID]);
if (empty($result)) $this->terminateWithError("Calendar with id '$calenderID' not found");
$calendar = $this->getDataOrTerminateWithError($result)[0];
$calenderGroupID = $calendar->eindeutige_kalender_gruppen_id;
if (!isset($calenderGroupID)) $this->terminateWithError("Calendar with id '$calenderID' does not have a valid group id");
$result = $this->kalenderlib->addOperationalResourcesToKalenderEvent($calendar, $assignedResources);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess(['message' => 'Resources assigned successfully']);
}
}
@@ -0,0 +1,233 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Reservierung extends FHCAPI_Controller
{
private $_ci;
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'addReservierung' => 'lehre/lvplan:rw',
'getRollen' => 'lehre/lvplan:rw',
'getInformation' => 'lehre/lvplan:rw',
'getLektor' => 'lehre/lvplan:rw',
'searchGroup' => 'lehre/lvplan:rw',
]);
$this->_ci =& get_instance();
$this->_ci->load->library('LogLib');
$this->_ci->load->library('form_validation');
$this->_ci->load->library('KalenderLib');
$this->_ci->load->model('ressource/Ort_model', 'OrtModel');
$this->_ci->load->model('ressource/Kalender_Event_Rolle_model', 'KalenderEventRolleModel');
$this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->_ci->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$this->_ci->load->model('organisation/gruppe_model', 'GruppeModel');
$this->loadPhrases([
'ui'
]);
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
public function getInformation()
{
$return_array = array('berechtigt' => false, 'studiengaenge' => []);
$this->_ci->OrtModel->db->join("
(select ort,standort_id,strasse, plz
FROM public.tbl_standort
LEFT JOIN public.tbl_adresse USING(adresse_id)
) standort", "standort_id", "LEFT", false);
$raeume = $this->_ci->OrtModel->loadWhere(array('aktiv' => true, 'reservieren' => true));
$return_array['raeume'] = hasData($raeume) ? getData($raeume) : [];
if (!$this->_ci->permissionlib->isBerechtigt('lehre/reservierung'))
$this->terminateWithSuccess($return_array);
$stg_berechtigungen = $this->_ci->permissionlib->getSTG_isEntitledFor('lehre/reservierung');
if (isEmptyArray($stg_berechtigungen))
$this->terminateWithSuccess($return_array);
$this->_ci->StudiengangModel->addSelect('studiengang_kz, UPPER(CONCAT(typ, kurzbz)) as kuerzel, kurzbzlang');
$this->_ci->StudiengangModel->addOrder('typ, kurzbz');
$this->_ci->StudiengangModel->db->where_in('studiengang_kz', $stg_berechtigungen);
$studiengaenge = $this->_ci->StudiengangModel->loadWhere(array('aktiv' => true));
if (isError($studiengaenge))
$this->terminateWithError(getError($studiengaenge));
$language = getUserLanguage() == 'German' ? 0 : 1;
$this->_ci->KalenderEventRolleModel->addOrder('sort');
$this->_ci->KalenderEventRolleModel->addSelect('rolle_kurzbz, array_to_json(bezeichnung_mehrsprachig::varchar[])->>'. $language. ' as bezeichnung');
$rollen = $this->_ci->KalenderEventRolleModel->load();
$this->_ci->StudiensemesterModel->addOrder('start', 'DESC');
$studiensemester = $this->_ci->StudiensemesterModel->load();
$return_array['studiengaenge'] = hasData($studiengaenge) ? getData($studiengaenge) : [];
$return_array['berechtigt'] = true;
$return_array['rollen'] = hasData($rollen) ? getData($rollen) : [];
$return_array['studiensemester'] = hasData($studiensemester) ? getData($studiensemester) : [];
$this->terminateWithSuccess($return_array);
}
public function getRaeume()
{
$this->_ci->OrtModel->db->join("
(select ort,standort_id,strasse, plz
FROM public.tbl_standort
LEFT JOIN public.tbl_adresse USING(adresse_id)
) standort", "standort_id", "LEFT", false);
$result = $this->_ci->OrtModel->loadWhere(array('aktiv' => true, 'reservieren' => true));
$this->terminateWithSuccess(hasData($result) ? getData($result) : []);
}
public function searchGroup()
{
$query = $this->input->get('query');
if (is_null($query))
$this->terminateWithError($this->_ci->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
$stg_berechtigungen = $this->_ci->permissionlib->getSTG_isEntitledFor('lehre/reservierung');
if (isEmptyArray($stg_berechtigungen))
$this->terminateWithSuccess([]);
$query_words = explode(' ', urldecode($query));
$gruppen_result = $this->_ci->GruppeModel->search($query_words);
if (isError($gruppen_result))
$this->terminateWithError(getError($gruppen_result), self::ERROR_TYPE_GENERAL);
$gruppen_array = array();
if (hasData($gruppen_result))
$gruppen_array = getData($gruppen_result);
$lehrverband_result = $this->_ci->LehrverbandModel->search($query_words);
$lehrverband_array = array();
if (isError($lehrverband_result))
$this->terminateWithError(getError($lehrverband_result), self::ERROR_TYPE_GENERAL);
if (hasData($lehrverband_result))
$lehrverband_array = getData($lehrverband_result);
$all_gruppen = array_merge($gruppen_array, $lehrverband_array);
$gefilterte_gruppen = array_filter($all_gruppen, function($gruppe) use ($stg_berechtigungen)
{
return in_array($gruppe->studiengang_kz, $stg_berechtigungen);
});
$this->terminateWithSuccess($gefilterte_gruppen);
}
public function getRollen()
{
$language = getUserLanguage() == 'German' ? 0 : 1;
$this->_ci->KalenderEventRolleModel->addOrder('sort');
$this->_ci->KalenderEventRolleModel->addSelect('rolle_kurzbz, array_to_json(bezeichnung_mehrsprachig::varchar[])->>'. $language. ' as bezeichnung');
$result = $this->_ci->KalenderEventRolleModel->load();
$this->terminateWithSuccess(hasData($result) ? getData($result) : []);
}
public function addReservierung()
{
$this->_ci->form_validation->set_rules('titel', "titel", "required", ['required' => $this->p->t('ui', 'error_fieldRequired')]);
$this->_ci->form_validation->set_rules('beschreibung', "beschreibung", "required", ['required' => $this->p->t('ui', 'error_fieldRequired')]);
$this->_ci->form_validation->set_rules('start_date', "start_date", "required", ['required' => $this->p->t('ui', 'error_fieldRequired')]);
$this->_ci->form_validation->set_rules('end_date', "end_date", "required", ['required' => $this->p->t('ui', 'error_fieldRequired')]);
if ($this->_ci->form_validation->run() === FALSE)
$this->terminateWithValidationErrors($this->_ci->form_validation->error_array());
$orte = $this->_ci->input->post('orte', TRUE);
if (empty($orte) || (empty($orte['ort_kurzbz']) && empty($orte['location'])))
{
$this->terminateWithValidationErrors(['ort_kurzbz' => $this->p->t('ui', 'error_fieldRequired', ['field' => 'Raum'])]);
}
$titel = $this->_ci->input->post('titel', TRUE);
$beschreibung = $this->_ci->input->post('beschreibung', TRUE);
$start_date = $this->_ci->input->post('start_date', TRUE);
$end_date = $this->_ci->input->post('end_date', TRUE);
$teilnehmer = $this->_ci->input->post('teilnehmer', TRUE);
$specialGroups = $this->_ci->input->post('specialGroups', TRUE);
$specialFinalGroups = $this->_ci->input->post('specialFinalGroups', TRUE);
$groups = $this->_ci->input->post('groups', TRUE);
if ($this->_ci->permissionlib->isBerechtigt('lehre/reservierung'))
{
if (empty($teilnehmer) || !is_array($teilnehmer))
{
$teilnehmer[] = array('uid' => getAuthUID(), 'rolle' => 'organisator');
}
}
else
$teilnehmer[] = array('uid' => getAuthUID(), 'rolle' => 'organisator');
$result = $this->_ci->kalenderlib->addReservierung($titel, $beschreibung, $orte, $start_date, $end_date, $teilnehmer, $specialFinalGroups, $specialGroups, $groups);
if (isError($result))
$this->terminateWithError(getError($result));
$this->terminateWithSuccess($result);
}
public function getLektor()
{
$query = $this->input->get('query');
if (is_null($query))
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
$query_words = explode(' ', $query);
$this->_ci->MitarbeiterModel->addSelect('uid, person_id, vorname, nachname');
$this->_ci->MitarbeiterModel->addJoin('public.tbl_benutzer', 'uid = mitarbeiter_uid');
$this->_ci->MitarbeiterModel->addJoin('public.tbl_person', 'person_id');
$this->_ci->MitarbeiterModel->db->where('public.tbl_benutzer.aktiv', true);
$this->_ci->MitarbeiterModel->db->group_start();
foreach ($query_words as $word)
{
$this->_ci->MitarbeiterModel->db->group_start();
$this->_ci->MitarbeiterModel->db->where('tbl_person.vorname ILIKE', "%" . $word . "%");
$this->_ci->MitarbeiterModel->db->or_where('tbl_person.nachname ILIKE', "%" . $word . "%");
$this->_ci->MitarbeiterModel->db->or_where('uid ILIKE', "%" . $word . "%");
$this->_ci->MitarbeiterModel->db->group_end();
}
$this->_ci->MitarbeiterModel->db->group_end();
$this->_ci->MitarbeiterModel->addOrder('nachname');
$this->_ci->MitarbeiterModel->addOrder('vorname');
$result = $this->_ci->MitarbeiterModel->load();
$this->terminateWithSuccess(hasData($result) ? getData($result) : array());
}
}
@@ -0,0 +1,53 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class Tags extends Tag_Controller
{
const BERECHTIGUNG_KURZBZ = ['admin:rw', 'assistenz:rw'];
public function __construct()
{
parent::__construct([
'getTag' => 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'));
}
}
@@ -0,0 +1,27 @@
<?php
if (!defined("BASEPATH")) exit("No direct script access allowed");
class TempusJob extends JOB_Controller
{
private $_ci;
public function __construct()
{
parent::__construct();
$this->_ci =& get_instance();
$this->_ci->load->helper('hlp_sancho_helper');
$this->_ci->load->library('KalenderLib');
}
public function sync()
{
$this->_ci->logInfo('Start job FHC-Core->Tempus->sync');
$this->_ci->kalenderlib->sync();
$this->_ci->logInfo('End job FHC-Core->Tempus->sync');
}
}
@@ -0,0 +1,610 @@
<?php
/*
* Job zur einmaligen Migration des Stundenplans
*
* Aufruf
* php index.ci.php system/MigrateKalender
*/
if (! defined('BASEPATH')) exit('No direct script access allowed');
class MigrateKalender extends CLI_Controller
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->load->model('ressource/Kalender_model', 'KalenderModel');
$this->load->model('ressource/Kalender_Lehreinheit_model', 'KalenderLehreinheitModel');
$this->load->model('ressource/Kalender_Ort_model', 'KalenderOrtModel');
$this->load->model('ressource/Stundenplandev_Kalender_model', 'SyncModel');
$this->load->model('ressource/Reservierung_Kalender_model', 'SyncReservierungModel');
$this->load->model('ressource/Kalender_Event_Teilnehmer_model', 'KalenderEventTeilnehmerModel');
$this->load->model('ressource/Kalender_Event_model', 'KalenderEventModel');
$this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$this->load->model('person/Benutzer_model', 'BenutzerModel');
}
/**
* Everything has a beginning
*/
public function migrateStundenplan($von, $bis = null, $studiengang_kz = null)
{
if (is_null($von))
return error('Parameter "von" ist erforderlich');
if (is_null($bis))
$bis = '2100-12-31';
$db = new DB_Model();
$stpldevsql = '
WITH eindeutige_stunden AS (
SELECT DISTINCT unr, datum, stunde
FROM lehre.tbl_stundenplandev
WHERE datum >= ? AND datum <= ?';
$params = [$von, $bis];
if (!is_null($studiengang_kz))
{
$stpldevsql .= ' AND studiengang_kz = ?';
$params[] = $studiengang_kz;
}
$stpldevsql .= '),
block_keys AS (
SELECT
unr,
datum,
stunde,
stunde - ROW_NUMBER() OVER (PARTITION BY unr, datum ORDER BY stunde) AS block_nr
FROM eindeutige_stunden
),
blocks AS (
SELECT
bk.unr,
bk.datum,
bk.block_nr,
MIN(bk.stunde) AS stunde_von,
MAX(bk.stunde) AS stunde_bis,
array_agg(DISTINCT sp.lehreinheit_id) AS lehreinheit_id,
array_agg(DISTINCT sp.ort_kurzbz) AS ort_kurzbz,
array_agg(sp.stundenplandev_id ORDER BY bk.stunde) AS stundenplandev_ids,
MIN(sp.insertamum) AS insertamum,
(array_agg(sp.insertvon ORDER BY sp.insertamum ASC))[1] AS insertvon,
MAX(sp.updateamum) AS updateamum,
(array_agg(sp.updatevon ORDER BY sp.updateamum DESC))[1] AS updatevon
FROM block_keys bk JOIN lehre.tbl_stundenplandev sp ON sp.unr = bk.unr AND sp.datum = bk.datum AND sp.stunde = bk.stunde
WHERE sp.datum >= ? AND sp.datum <= ?
GROUP BY bk.unr, bk.datum, bk.block_nr
)
SELECT
b.stundenplandev_ids,
b.unr,
b.datum,
b.block_nr,
b.lehreinheit_id,
b.ort_kurzbz,
b.datum + stundevon.beginn AS von,
b.datum + stundebis.ende AS bis,
b.insertamum,
b.insertvon,
b.updateamum,
b.updatevon
FROM blocks b
JOIN lehre.tbl_stunde stundevon ON stundevon.stunde = b.stunde_von
JOIN lehre.tbl_stunde stundebis ON stundebis.stunde = b.stunde_bis
ORDER BY b.datum, b.unr, b.block_nr;';
array_push($params, $von, $bis);
$stpldev = $db->execReadOnlyQuery($stpldevsql, $params);
if (hasData($stpldev))
{
// Pruefen ob der Eintrag schon in Sync Tabelle vorhanden ist
// Wenn neuere Änderungen vorhanden dann Update
// Wenn keine Änderungen seit leztem Sync dann Ueberspringen
// Wenn noch nicht vorhanden neu anlegen
// Danach ggf pruefen welceh Eintraege in der zwischenzeit geloescht wurden und
// in der neuen Tabelle auch archivieren oder loeschen
$data = getData($stpldev);
foreach($data as $block)
{
$ids = is_array($block->stundenplandev_ids) ? $block->stundenplandev_ids : explode(',', $block->stundenplandev_ids);
/*$ids = array_map('intval', $ids);*/
$this->SyncModel->db->group_start();
$where_ids_chunks = array_chunk($ids,25);
foreach($where_ids_chunks as $where_ids)
{
$this->SyncModel->db->or_where_in('stundenplandev_id', $where_ids);
}
$this->SyncModel->db->group_end();
$sync_result = $this->SyncModel->load();
if (!hasData($sync_result))
{
$kalender_id = $this->_insertKalender($block, 'lehreinheit');
if ($kalender_id)
{
$this->_insertSync($block->stundenplandev_ids, $kalender_id);
}
}
else
{
$syncData = getData($sync_result);
$kalender_id = $syncData[0]->kalender_id;
$last_sync = $syncData[0]->lastupdate;
$synced_ids = array_column($syncData, 'stundenplandev_id');
if ($block->updateamum > $last_sync)
{
$this->_updateKalender($kalender_id, $block);
$this->_updateSync($synced_ids, $kalender_id);
}
$fehlende = array_diff($block->stundenplandev_ids, $synced_ids);
if (!empty($fehlende))
{
$this->_insertSync($fehlende, $kalender_id);
}
}
}
}
}
public function migrateReservierung($von, $bis = null, $ort_kurzbz = null)
{
if (is_null($von))
return error('Parameter "von" ist erforderlich');
if (is_null($bis))
$bis = '2100-12-31';
$db = new DB_Model();
$qry = "WITH eindeutige_stunden AS (
SELECT DISTINCT titel, beschreibung, datum, stunde
FROM campus.tbl_reservierung
WHERE datum >= ? AND datum <= ?";
$params = array($von, $bis);
if (!is_null($ort_kurzbz))
{
$qry .= " AND ort_kurzbz = ?";
$params[] = $ort_kurzbz;
}
$qry .= "),
block_keys AS (
SELECT
titel, beschreibung, datum, stunde,
stunde - ROW_NUMBER() OVER (PARTITION BY titel, beschreibung, datum ORDER BY stunde) AS block_nr
FROM eindeutige_stunden
),
blocks AS (
SELECT
bk.titel,
bk.beschreibung,
bk.datum,
bk.block_nr,
MIN(bk.stunde) AS stunde_von,
MAX(bk.stunde) AS stunde_bis,
array_agg(DISTINCT r.reservierung_id::text) AS reservierung_ids,
array_agg(DISTINCT r.uid) AS uids,
array_agg(DISTINCT r.gruppe_kurzbz) AS gruppen_kurzbz,
array_agg(DISTINCT ROW(r.semester, r.verband, r.gruppe, r.studiengang_kz)::text) AS svg_kombis,
array_agg(DISTINCT r.ort_kurzbz) AS ort_kurzbz,
MIN(r.studiengang_kz) AS studiengang_kz,
MIN(r.reservierung_id) AS reservierung_id,
MAX(r.insertamum) AS insertamum,
(array_agg(r.insertvon ORDER BY r.insertamum ASC))[1] AS insertvon
FROM block_keys bk
JOIN campus.tbl_reservierung r
ON r.titel = bk.titel AND r.beschreibung = bk.beschreibung AND r.datum = bk.datum AND r.stunde = bk.stunde
WHERE r.datum >= ? AND r.datum <= ?
GROUP BY bk.titel, bk.beschreibung, bk.datum, bk.block_nr
)
SELECT
b.*,
(b.datum + s_von.beginn) AS von,
(b.datum + s_bis.ende) AS bis
FROM blocks b
JOIN lehre.tbl_stunde s_von ON s_von.stunde = b.stunde_von
JOIN lehre.tbl_stunde s_bis ON s_bis.stunde = b.stunde_bis
ORDER BY b.reservierung_id DESC;";
array_push($params, $von, $bis);
$reservierung_data = $db->execReadOnlyQuery($qry, $params);
if (hasData($reservierung_data))
{
$data = getData($reservierung_data);
foreach($data as $block)
{
$ids = is_array($block->reservierung_ids) ? $block->reservierung_ids : explode(',', $block->reservierung_ids);
$this->SyncReservierungModel->db->group_start();
$where_ids_chunks = array_chunk($ids,25);
foreach($where_ids_chunks as $where_ids)
{
$this->SyncReservierungModel->db->or_where_in('reservierung_id', $where_ids);
}
$this->SyncReservierungModel->db->group_end();
$sync_result = $this->SyncReservierungModel->load();
if (!hasData($sync_result))
{
$kalender_id = $this->_insertKalender($block, 'reservierung');
if ($kalender_id)
{
$this->_insertReservierungSync($block->reservierung_ids, $kalender_id);
}
}
else
{
$syncData = getData($sync_result);
$kalender_id = $syncData[0]->kalender_id;
$last_sync = $syncData[0]->lastupdate;
$synced_ids = array_column($syncData, 'reservierung_id');
if ($block->insertamum > $last_sync)
{
$this->_updateKalender($kalender_id, $block);
$this->_updateReservierungSync($synced_ids, $kalender_id);
}
$fehlende = array_diff($block->reservierung_ids, $synced_ids);
if (!empty($fehlende))
{
$this->_insertReservierungSync($fehlende, $kalender_id);
}
}
}
}
}
public function migrateStundenplanBetriebsmittelEntries() {
$this->setKalendarEntriesGroupIDs();
$this->setKalendarEntriesGroupIDsForChildren();
$dbModel = new DB_Model();
$deleteOldImportedTempusEntriesQuery = "DELETE from lehre.tbl_betriebsmittel_kalender WHERE quelle != 'tempus_neu' OR quelle IS NULL;";
$createHelperTypeQuery = "DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_type
WHERE typname = 'betriebsmittel_info'
) THEN
CREATE TYPE betriebsmittel_info AS (
betriebsmittel_id bigint,
insertamum timestamp,
insertvon text
);
END IF;
END $$;";
$query = $deleteOldImportedTempusEntriesQuery .
$createHelperTypeQuery .
"WITH test AS (
SELECT
tk.eindeutige_kalender_gruppen_id AS eindeutige_kalender_gruppen_id,
array_agg(tk.kalender_id),
array_agg(
ROW(tsb.betriebsmittel_id, tsb.insertamum, tsb.insertvon)::betriebsmittel_info
) AS betriebsmittel_data
FROM sync.tbl_stundenplandev_kalender AS sk
JOIN lehre.tbl_kalender tk ON tk.kalender_id = sk.kalender_id
JOIN lehre.tbl_stundenplan_betriebsmittel tsb ON tsb.stundenplandev_id = sk.stundenplandev_id
GROUP BY tk.eindeutige_kalender_gruppen_id
)
INSERT INTO lehre.tbl_betriebsmittel_kalender (
eindeutige_kalender_gruppen_id,
betriebsmittel_id,
insertamum,
insertvon
)
SELECT
t.eindeutige_kalender_gruppen_id,
bm.betriebsmittel_id,
bm.insertamum,
bm.insertvon
FROM test t
CROSS JOIN LATERAL unnest(t.betriebsmittel_data) AS bm
ON CONFLICT (eindeutige_kalender_gruppen_id, betriebsmittel_id) DO NOTHING;
;";
$dbModel->db->query($query);
}
private function setKalendarEntriesGroupIDs() {
$dbModel = new DB_Model();
$query = "UPDATE lehre.tbl_kalender
SET eindeutige_kalender_gruppen_id = gen_random_uuid()
WHERE vorgaenger_kalender_id IS NULL
AND eindeutige_kalender_gruppen_id IS NULL;";
$dbModel->db->query($query);
}
private function setKalendarEntriesGroupIDsForChildren() {
$dbModel = new DB_Model();
$query = "WITH RECURSIVE tree AS
(
SELECT
kalender_id,
vorgaenger_kalender_id,
kalender_id AS root_id,
eindeutige_kalender_gruppen_id as root_eindeutige_kalender_gruppen_id
FROM lehre.tbl_kalender
WHERE vorgaenger_kalender_id IS NULL
UNION ALL
SELECT
i.kalender_id,
i.vorgaenger_kalender_id,
t.root_id,
t.root_eindeutige_kalender_gruppen_id
FROM lehre.tbl_kalender i
JOIN tree t
ON i.vorgaenger_kalender_id = t.kalender_id
where i.eindeutige_kalender_gruppen_id is NULL
)
UPDATE lehre.tbl_kalender k
SET eindeutige_kalender_gruppen_id = t.root_eindeutige_kalender_gruppen_id
FROM tree t
WHERE k.kalender_id = t.kalender_id
AND k.eindeutige_kalender_gruppen_id IS NULL;";
$dbModel->db->query($query);
}
private function _insertKalender($block, $typ)
{
$result = $this->KalenderModel->insert(
array (
'von' => $block->von,
'bis' => $block->bis,
'typ' => $typ,
'status_kurzbz'=> 'live',
'insertamum' => $block->insertamum,
'insertvon' => $block->insertvon,
'updateamum' => $block->updateamum ?? null,
'updatevon' => $block->updatevon ?? null
)
);
if(!isSuccess($result))
return null;
$kalender_id = getData($result);
if ($typ === 'lehreinheit')
{
$lehreinheit_ids = is_array($block->lehreinheit_id) ? $block->lehreinheit_id : explode(',', $block->lehreinheit_id);
foreach ($lehreinheit_ids as $lehreinheit_id)
{
$leResult = $this->KalenderLehreinheitModel->insert(
array (
'kalender_id' => $kalender_id,
'lehreinheit_id'=> $lehreinheit_id
)
);
if (!isSuccess($leResult))
return null;
}
}
else if ($typ === 'reservierung')
{
$eventResult = $this->KalenderEventModel->insert(array(
'kalender_id' => $kalender_id,
'titel' => $block->titel,
'beschreibung' => $block->beschreibung
));
if (!isSuccess($eventResult))
return null;
if ($block->insertvon)
{
$user = $this->BenutzerModel->load(array('uid' => $block->insertvon));
if (hasData($user))
{
$this->KalenderEventTeilnehmerModel->insert(array(
'kalender_id' => $kalender_id,
'uid' => getData($user)[0]->uid,
'rolle_kurzbz' => 'organisator'
));
}
}
$uids = is_array($block->uids) ? $block->uids : explode(',', $block->uids);
foreach ($uids as $uid)
{
$this->KalenderEventTeilnehmerModel->insert(array(
'kalender_id' => $kalender_id,
'uid' => $uid,
'rolle_kurzbz' => 'teilnehmer'
));
}
$semester_range = $this->StudiensemesterModel->getByDateRange($block->von, $block->bis);
if (isError($semester_range))
return null;
$studiensemester_kurzbz = getData($semester_range)[0]->studiensemester_kurzbz ?? null;
$gruppen = is_array($block->gruppen_kurzbz) ? $block->gruppen_kurzbz : explode(',', $block->gruppen_kurzbz ?? '');
foreach ($gruppen as $gruppe_kurzbz)
{
$gruppe_kurzbz = trim($gruppe_kurzbz);
if (!empty($gruppe_kurzbz))
{
$this->KalenderEventTeilnehmerModel->insert(array(
'kalender_id' => $kalender_id,
'gruppe_kurzbz' => $gruppe_kurzbz,
'studiengang_kz' => $block->studiengang_kz,
'studiensemester_kurzbz' => $studiensemester_kurzbz,
'rolle_kurzbz' => 'teilnehmer'
));
}
}
foreach ($block->svg_kombis as $kombi_str)
{
$kombi_str = trim($kombi_str, '()');
list($sem, $verb, $grp, $stg_kz) = explode(',', $kombi_str);
$sem = trim($sem) === '' ? null : trim($sem);
$verb = trim($verb) === '' ? null : trim($verb);
$grp = trim($grp) === '' ? null : trim($grp);
$stg_kz = trim($stg_kz) === '' ? null : trim($stg_kz);
if (is_null($sem) && is_null($verb) && is_null($grp) && is_null($stg_kz))
continue;
$this->KalenderEventTeilnehmerModel->insert(array(
'kalender_id' => $kalender_id,
'studiengang_kz' => $stg_kz,
'semester' => $sem,
'verband' => $verb,
'gruppe' => $grp,
'studiensemester_kurzbz' => $studiensemester_kurzbz,
'rolle_kurzbz' => 'teilnehmer'
));
}
}
$orte = is_array($block->ort_kurzbz) ? $block->ort_kurzbz : explode(',', $block->ort_kurzbz ?? '');
foreach ($orte as $ort)
{
$ort = trim($ort);
if (!empty($ort))
{
$this->KalenderOrtModel->insert(
array (
'kalender_id' => $kalender_id,
'ort_kurzbz' => $ort
)
);
}
}
return $kalender_id;
}
private function _insertSync($ids, $kalender_id)
{
foreach($ids as $id)
{
$this->SyncModel->insert(
array (
'stundenplandev_id' => $id,
'kalender_id' => $kalender_id,
'lastupdate' => date('Y-m-d H:i:s')
)
);
}
}
private function _insertReservierungSync($ids, $kalender_id)
{
foreach($ids as $id)
{
$this->SyncReservierungModel->insert(
array (
'reservierung_id' => $id,
'kalender_id' => $kalender_id,
'lastupdate' => date('Y-m-d H:i:s')
)
);
}
}
private function _updateKalender($kalender_id, $block)
{
$this->KalenderModel->update(
array (
'kalender_id' => $kalender_id
),
array (
'von' => $block->von,
'bis' => $block->bis,
'updateamum'=> $block->updateamum,
'updatevon' => $block->updatevon
)
);
$orte = is_array($block->ort_kurzbz) ? $block->ort_kurzbz : explode(',', $block->ort_kurzbz ?? '');
$orte = array_filter(array_map('trim', $orte));
$this->KalenderOrtModel->delete(array('kalender_id' => $kalender_id));
foreach ($orte as $ort)
{
$this->KalenderOrtModel->insert(
array (
'kalender_id' => $kalender_id,
'ort_kurzbz' => $ort
)
);
}
}
private function _updateSync($ids, $kalender_id)
{
foreach($ids as $id)
{
$this->SyncModel->update(
array (
'stundenplandev_id' => $id,
'kalender_id' => $kalender_id
),
array (
'lastupdate' => date('Y-m-d H:i:s')
)
);
}
}
private function _updateReservierungSync($ids, $kalender_id)
{
foreach($ids as $id)
{
$this->SyncReservierungModel->update(
array (
'reservierung_id' => $id,
'kalender_id' => $kalender_id
),
array (
'lastupdate' => date('Y-m-d H:i:s')
)
);
}
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
interface ICollisionCheck
{
public function getName();
public function check($data);
public function checkAll($kalender_ids);
}
+31 -3
View File
@@ -44,7 +44,6 @@ class Tag_Controller extends FHCAPI_Controller
{
$language = $this->_getLanguageIndex();
$index_bezeichnung_mehrsprachig = $language - 1;
$id = $this->input->get('id');
if (is_array($readonly_tags) && !isEmptyArray($readonly_tags))
@@ -123,7 +122,36 @@ 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)
{
$language = $this->_getLanguageIndex();
$index_bezeichnung_mehrsprachig = $language - 1;
$this->NotizzuordnungModel->addSelect(
'tbl_notizzuordnung.notiz_id as notiz_id,
typ_kurzbz as tag_typ_kurzbz,
array_to_json(bezeichnung_mehrsprachig::varchar[])->>'. $index_bezeichnung_mehrsprachig. ' 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();
@@ -143,7 +171,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();
@@ -152,7 +180,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);
+10
View File
@@ -570,3 +570,13 @@ function buildDropdownEntryPrintArray($id, $name, $parameterurl, $uid=null, $ord
];
}
function generateUUID()
{
$data = openssl_random_pseudo_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
@@ -0,0 +1,67 @@
<?php
defined('BASEPATH') || exit('No direct script access allowed');
use CI3_Events as Events;
class CollisionChecker
{
private $_checks = [];
private $_ci;
public function __construct()
{
$this->_ci =& get_instance();
$this->_ci->load->library('collision/checks/RoomCollisionCheck');
$this->_ci->load->library('collision/checks/LectureCollisionCheck');
$this->_ci->load->library('collision/checks/VerbandCollisionCheck');
$this->_ci->load->library('collision/checks/StudentCollisionCheck');
$this->_ci->load->library('collision/checks/ResourcesCollisionCheck');
$this->register($this->_ci->roomcollisioncheck);
$this->register($this->_ci->lecturecollisioncheck);
$this->register($this->_ci->verbandcollisioncheck);
$this->register($this->_ci->studentcollisioncheck);
$this->register($this->_ci->resourcescollisioncheck);
Events::trigger('collision_register', $this);
}
public function register(ICollisionCheck $check)
{
$this->_checks[$check->getName()] = $check;
}
public function run($data)
{
$errors = [];
foreach ($this->_checks as $check)
{
$result = $check->check($data);
if (!empty($result))
{
$errors = array_merge($errors, $result);
}
}
return $errors;
}
public function runAll($kalender_ids)
{
$results = array_fill_keys($kalender_ids, []);
foreach ($this->_checks as $check)
{
$batchResult = $check->checkAll($kalender_ids);
foreach ($batchResult as $kalender_id => $errors)
{
$results[$kalender_id] = array_merge($results[$kalender_id], $errors);
}
}
return $results;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,171 @@
<?php
if (! defined("BASEPATH")) exit("No direct script access allowed");
class KalenderNotificationLib
{
private $_ci;
public function __construct()
{
$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)
{
if (!$this->_ci->config->item('send_update_mails'))
return true;
$lektor_added = array();
$lektor_changed = array();
$lektor_deleted = array();
$student_added = array();
$student_changed = array();
$student_deleted = array();
foreach ($mail_infos as $info)
{
$entry = $info['entry'];
$new_status = $info['new_status'];
$notify = $info['notify'];
$old_entry = null;
if ($entry->vorgaenger_kalender_id)
{
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'tbl_kalender.kalender_id = tbl_kalender_ort.kalender_id', 'LEFT');
$vorgaenger = $this->_ci->KalenderModel->load(array('tbl_kalender.kalender_id' => $entry->vorgaenger_kalender_id));
if (hasData($vorgaenger))
$old_entry = getData($vorgaenger)[0];
}
if ($new_status === 'deleted')
$row = $this->_buildMailDeleted($entry);
else if ($old_entry)
$row = $this->_buildMailChanged($old_entry, $entry);
else
$row = $this->_buildMailNew($entry);
if (in_array('lektor', $notify))
{
if ($new_status === 'deleted')
$lektor_deleted[] = $row;
else if ($old_entry)
$lektor_changed[] = $row;
else
$lektor_added[] = $row;
}
if (in_array('student', $notify))
{
if ($new_status === 'deleted')
$student_deleted[] = $row;
else if ($old_entry)
$student_changed[] = $row;
else
$student_added[] = $row;
}
}
$lektor_entries = '';
$student_entries = '';
if (!empty($lektor_added))
$lektor_entries .= $this->_addToList($lektor_added, 'hinzugefügt') . '<hr/>';
if (!empty($lektor_changed))
$lektor_entries .= $this->_addToList($lektor_changed, 'geändert') . '<hr/>';
if (!empty($lektor_deleted))
$lektor_entries .= $this->_addToList($lektor_deleted, 'gelöscht') . '<hr/>';
if (!empty($student_added))
$student_entries .= $this->_addToList($student_added, 'hinzugefügt') . '<hr/>';
if (!empty($student_changed))
$student_entries .= $this->_addToList($student_changed, 'geändert') . '<hr/>';
if (!empty($student_deleted))
$student_entries .= $this->_addToList($student_deleted, 'gelöscht') . '<hr/>';
//TODO (david) richtigen Empfänger
if (!empty($lektor_entries))
$this->_ci->maillib->send('', '[email protected]', 'Lektor Tempus Update', $lektor_entries);
if (!empty($student_entries))
$this->_ci->maillib->send('', '[email protected]', 'Student Tempus Update', $student_entries);
}
private function _addToList($entries, $status)
{
return 'Folgende Einträge wurden <b>'. $status .'</b>: <ul>' . implode('', $entries) . '</ul>';
}
private function _buildMailNew($entry)
{
$von = date('d.m.Y H:i', strtotime($entry->von));
$bis = date('H:i', strtotime($entry->bis));
return '<li>
<b>Kalender ID ' . ($entry->kalender_id ?? '-') . '</b>
<ul>
<li><b>Uhrzeit:</b> ' . $von . ' - ' . $bis . '</li>
<li><b>Ort:</b> ' . ($entry->ort_kurzbz ?? '-') . '</li>
</ul>
</li>';
}
private function _buildMailChanged($old_entry, $new_entry)
{
$old_von = date('d.m.Y H:i', strtotime($old_entry->von));
$old_bis = date('H:i', strtotime($old_entry->bis));
$new_von = date('d.m.Y H:i', strtotime($new_entry->von));
$new_bis = date('H:i', strtotime($new_entry->bis));
$old_ort = $old_entry->ort_kurzbz ?? '-';
$new_ort = $new_entry->ort_kurzbz ?? '-';
$uhrzeit_changed = ($old_von . $old_bis) !== ($new_von . $new_bis);
$ort_changed = $old_ort !== $new_ort;
$changes = '';
if ($uhrzeit_changed)
{
$changes .= '<li>
<b>Uhrzeit:</b>
<s style="color:red;">' . $old_von . ' - ' . $old_bis . '</s>
<span style="color:green;">' . $new_von . ' - ' . $new_bis . '</span>
</li>';
}
if ($ort_changed)
{
$changes .= '<li>
<b>Ort:</b>
<s style="color:red;">' . $old_ort . '</s>
<span style="color:green;">' . $new_ort . '</span>
</li>';
}
return '<li>
<b>Kalender ID ' . ($new_entry->kalender_id ?? '-') . '</b>
<ul>' . $changes . '</ul>
</li>';
}
private function _buildMailDeleted($entry)
{
$von = date('d.m.Y H:i', strtotime($entry->von));
$bis = date('H:i', strtotime($entry->bis));
return '<li style="color:red;">
<b><s>Kalender ID ' . ($entry->kalender_id ?? '-') . '</s></b>
<ul>
<li><s>' . $von . ' - ' . $bis . '</s></li>
<li><s>' . ($entry->ort_kurzbz ?? '-') . '</s></li>
</ul>
</li>';
}
}
+429
View File
@@ -0,0 +1,429 @@
<?php
if (! defined("BASEPATH")) exit("No direct script access allowed");
class KalenderSyncLib
{
private $_ci;
private $status_map = array(
'preview' => 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');
$this->_ci->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel');
$this->_ci->load->library('KalenderLib');
}
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('to_delete', 'to_delete_live', 'to_delete_preview'));
$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('to_delete', 'to_delete_live', 'to_delete_preview'));
$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()
{
$mail_infos_gesamt = [];
$synced_gesamt = 0;
$todelete_ids_result = $this->_getManuallySetStatus(array('to_delete', 'to_delete_live', 'to_delete_preview'));
if (isError($todelete_ids_result))
return $todelete_ids_result;
$todelete_result = $this->_syncKalenderIds(getData($todelete_ids_result), null);
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))
{
$this->_ci->load->library('KalenderNotificationLib');
$this->_ci->kalendernotificationlib->sendMails($mail_infos_gesamt);
}
return success(['synced' => $synced_gesamt]);
}
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)
{
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);
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');
if (!is_null($studiengang_kz))
{
$this->_ci->KalenderModel->db->where('tbl_lehrveranstaltung.studiengang_kz', $studiengang_kz);
}
else
{
$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);
$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)
{
$return_array = array();
if (!$vorgaenger_kalender_id) return success($return_array);
$current_id = $vorgaenger_kalender_id;
while ($current_id)
{
$vorgaenger = $this->_ci->KalenderModel->load(array('kalender_id' => $current_id));
if (!hasData($vorgaenger)) return success($return_array);
$vorgaenger = getData($vorgaenger)[0];
if ($stop_at_live && $vorgaenger->status_kurzbz === 'live')
return success($return_array);
if (in_array($vorgaenger->status_kurzbz, array('preview', 'live')))
{
$this->_ci->KalenderModel->update(
array('kalender_id' => $current_id),
array('status_kurzbz' => 'archived', 'status_kurzbz_max' => $vorgaenger->status_kurzbz)
);
$return_array[] = $vorgaenger;
}
$current_id = $vorgaenger->vorgaenger_kalender_id;
}
return success($return_array);
}
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 (in_array($entry->status_kurzbz, array('to_delete', 'to_delete_live', 'to_delete_preview')))
{
$notify = array();
if ($entry->status_kurzbz === 'to_delete_live')
{
$status_max = 'live';
$notify = array('lektor', 'student');
}
else if ($entry->status_kurzbz === 'to_delete_preview')
{
$status_max = 'preview';
$notify = array('lektor');
}
else if ($entry->status_kurzbz === 'to_delete')
{
$status_max = 'planning';
}
$this->_ci->KalenderModel->update(array('kalender_id' => $entry->kalender_id), array('status_kurzbz' => 'deleted', 'status_kurzbz_max' => $status_max));
$mail_infos[] = array('entry' => $entry, 'new_status' => 'deleted', 'notify' => $notify);
$vorgaenger_array = $this->_archiveVorgaenger($entry->vorgaenger_kalender_id);
if (hasData($vorgaenger_array))
{
foreach(getData($vorgaenger_array) as $vorgaenger)
{
$archive_notify = array();
if ($vorgaenger->status_kurzbz === 'preview')
$archive_notify = array('lektor');
else if ($vorgaenger->status_kurzbz === 'live')
{
$archive_notify = array('student', 'lektor');
$history = $this->_ci->kalenderlib->getHistory($vorgaenger->kalender_id, false, false);
if (hasData($history))
{
$history = getData($history);
foreach ($history as $entry)
{
if ($entry->status_kurzbz === 'deleted' && $entry->status_kurzbz_max === 'preview')
{
$archive_notify = array('student');
break;
}
}
}
}
$mail_infos[] = array('entry' => $vorgaenger, 'new_status' => 'deleted', 'notify' => $archive_notify);
}
}
$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);
}
}
+563
View File
@@ -0,0 +1,563 @@
<?php
if (! defined("BASEPATH")) exit("No direct script access allowed");
use CI3_Events as Events;
class RaumvorschlagLib
{
private $_ci;
public function __construct()
{
$this->_ci =& get_instance();
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->model('ressource/Kalender_Lehreinheit_model', 'KalenderLehreinheitModel');
$this->_ci->load->model('ressource/Kalender_Event_model', 'KalenderEventModel');
$this->_ci->load->model('ressource/Kalender_Event_Teilnehmer_model', 'KalenderEventTeilnehmerModel');
$this->_ci->load->model('ressource/Kalender_Ort_model', 'KalenderOrtModel');
$this->_ci->load->model('education/Lehreinheit_model', 'LehreinheitModel');
$this->_ci->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
$this->_ci->load->model('education/LehreinheitMitarbeiter_model', 'LehreinheitMitarbeiterModel');
$this->_ci->load->model('ressource/Ort_model', 'OrtModel');
$this->_ci->load->model('organisation/gruppe_model', 'GruppeModel');
$this->_ci->load->model('organisation/Lehrverband_model', 'LehrverbandModel');
$this->_ci->load->model('ressource/Zeitsperre_model', 'ZeitsperreModel');
$this->_ci->load->model('ressource/Stunde_model', 'StundeModel');
$this->_ci->load->model('education/Lehreinheitgruppe_model', 'LehreinheitgruppeModel');
$this->_ci->load->library('CollisionChecker');
$this->_ci->load->library('KalenderLib');
$this->_ci->load->library('PhrasesLib', array('ui'));
$this->_ci->load->library('VariableLib', array('uid' => getAuthUID()));
}
public function getVorschlaege($kalender_id)
{
$event = $this->_ci->kalenderlib->getByKalenderId($kalender_id);
$event = $event[0];
return $this->_getVorschlaegeForEvent($event);
}
public function getVorschlaegeByLehreinheit($lehreinheit_id, $von, $bis)
{
$teilnehmer = $this->_getLehreinheitTeilnehmer($lehreinheit_id);
$event = (object) [
'kalender_id' => null,
'lehreinheit_id' => [$lehreinheit_id],
'isostart' => (new DateTime($von))->format('c'),
'isoend' => (new DateTime($bis))->format('c'),
'datum' => (new DateTime($von))->format('Y-m-d'),
'lektor' => $teilnehmer->lektor_data,
'gruppe' => $teilnehmer->gruppen_data,
];
return $this->_getVorschlaegeForEvent($event);
}
public function getVorschlaegeSlots($lehreinheit_id, $start_date, $end_date)
{
$lehreinheit_result = $this->_ci->LehreinheitModel->load($lehreinheit_id);
if (!hasData($lehreinheit_result))
return error('Lehreinheit nicht gefunden');
$lehreinheit = getData($lehreinheit_result)[0];
$block = $lehreinheit->stundenblockung;
if ($block <= 0)
return error('Stundenblockung ist ungültig');
$this->_ci->StundeModel->addOrder('stunde', 'ASC');
$stunden_result = $this->_ci->StundeModel->load();
$stunden = hasData($stunden_result) ? array_values(getData($stunden_result)) : [];
if (empty($stunden))
return error('Kein Stunden-Raster gefunden');
$teilnehmer = $this->_getLehreinheitTeilnehmer($lehreinheit_id);
$grp = $this->_splitGruppen($teilnehmer->gruppen_data);
$lektor_uids = array_column($teilnehmer->lektor_data, 'mitarbeiter_uid');
$verplante_events = $this->_ci->kalenderlib->getForRaumvorschlag(
$start_date,
$end_date,
$lektor_uids,
$grp->gruppen_kurzbz,
$grp->lehrverband_gruppen
);
$start_day = strtotime($start_date);
$end_day = strtotime($end_date);
$moegliche_slots = array();
$verplante_zeitsperren = $this->_getZeitsperen($start_date, $end_date, $lektor_uids);
while ($start_day <= $end_day)
{
$tag = date('Y-m-d', $start_day);
$verplante_tages_events = array_values(array_filter($verplante_events, function($event) use ($tag)
{
return $event->datum === $tag;
}));
foreach ($stunden as $index => $start_stunde)
{
if (!isset($stunden[$index + $block - 1]))
continue;
$end_stunde = $stunden[$index + $block - 1];
$slot_start = $tag . ' ' . $start_stunde->beginn;
$slot_end = $tag . ' ' . $end_stunde->ende;
if (!$this->_isFrei($slot_start, $slot_end, $verplante_zeitsperren, $lektor_uids, 'lektor'))
continue;
if (!$this->_isFrei($slot_start, $slot_end, $verplante_tages_events, $lektor_uids, 'lektor'))
continue;
if (!$this->_isFrei($slot_start, $slot_end, $verplante_tages_events, $grp->gruppen_kurzbz, 'gruppe'))
continue;
if (!$this->_isFrei($slot_start, $slot_end, $verplante_tages_events, $grp->lehrverband_gruppen_bezeichnung, 'lehrverband'))
continue;
$moegliche_slots[] = [
'isostart' => (new DateTime($slot_start))->format('c'),
'isoend' => (new DateTime($slot_end))->format('c'),
'marker_isoend' => (new DateTime($tag . ' ' . $start_stunde->ende))->format('c'),
];
}
$start_day = strtotime('+1 day', $start_day);
}
$slots = [];
foreach ($moegliche_slots as $slot)
{
$event_obj = (object) [
'kalender_id' => null,
'lehreinheit_id' => [$lehreinheit_id],
'isostart' => $slot['isostart'],
'isoend' => $slot['isoend'],
];
$raumkandidaten = $this->_getRaumkandidaten($event_obj);
if (empty($raumkandidaten))
continue;
$ratings = $this->_getRatings($verplante_events, $slot, $raumkandidaten, $lektor_uids, $grp->gruppen_kurzbz, $grp->lehrverband_gruppen_bezeichnung);
$best = $ratings[0];
$anzahl_weitere = count($ratings) - 1;
$slots[] = [
'isostart' => $slot['isostart'],
'isoend' => $slot['isoend'],
'marker_isoend' => $slot['marker_isoend'],
'rating' => $this->_scoreToRating($best['score']),
'label' => $anzahl_weitere > 0 ? $best['ort_kurzbz'] . ' (' . $best['score'] . ') + ' . $anzahl_weitere . ' weitere' : $best['ort_kurzbz'] . ' (' . $best['score'] . ')',
'raeume' => $ratings,
];
}
return success($slots);
}
private function _getZeitsperen($start_date, $end_date, $uids)
{
if (empty($uids)) return [];
if (($this->_ci->variablelib->getVar('ignore_kollision') === 'true') || ($this->_ci->variablelib->getVar('ignore_zeitsperre') === 'true')) return [];
$this->_ci->ZeitsperreModel->addSelect('mitarbeiter_uid, vondatum, vonstunde_z.beginn as von_beginn, bisdatum, bisstunde_z.ende as bis_ende');
$this->_ci->ZeitsperreModel->addJoin('lehre.tbl_stunde vonstunde_z', 'vonstunde_z.stunde = tbl_zeitsperre.vonstunde', 'LEFT');
$this->_ci->ZeitsperreModel->addJoin('lehre.tbl_stunde bisstunde_z', 'bisstunde_z.stunde = tbl_zeitsperre.bisstunde', 'LEFT');
$this->_ci->ZeitsperreModel->db->where('zeitsperretyp_kurzbz !=', 'ZVerfueg');
$this->_ci->ZeitsperreModel->db->where('(tbl_zeitsperre.vondatum + COALESCE(vonstunde_z.beginn, \'00:00\'))::timestamp <', $end_date);
$this->_ci->ZeitsperreModel->db->where('(tbl_zeitsperre.bisdatum + COALESCE(bisstunde_z.ende, \'23:59\'))::timestamp >', $start_date);
$this->_ci->ZeitsperreModel->db->where_in('mitarbeiter_uid', $uids);
$result = $this->_ci->ZeitsperreModel->load();
if (!hasData($result)) return [];
$events = [];
foreach (getData($result) as $row)
{
$von = new DateTime($row->vondatum . ' ' . ($row->von_beginn ?? '00:00'));
$bis = new DateTime($row->bisdatum . ' ' . ($row->bis_ende ?? '23:59'));
$events[] = (object)[
'isostart' => $von->format('c'),
'isoend' => $bis->format('c'),
'lektor' => [['mitarbeiter_uid' => $row->mitarbeiter_uid]]
];
}
return $events;
}
private function _getLehreinheitTeilnehmer($lehreinheit_id)
{
$lektoren_result = $this->_ci->LehreinheitMitarbeiterModel->loadWhere(['lehreinheit_id' => $lehreinheit_id]);
$lektor_data = [];
if (hasData($lektoren_result))
{
$lektor_data = getData($lektoren_result);
}
$gruppen_result = $this->_ci->LehreinheitgruppeModel->getByLehreinheit($lehreinheit_id);
$gruppen_data = [];
if (hasData($gruppen_result))
{
$gruppen_rows = getData($gruppen_result);
foreach ($gruppen_rows as $gruppe)
{
$gruppen_data[] = (array) $gruppe;
}
}
return (object)[
'lektor_data' => $lektor_data,
'gruppen_data' => $gruppen_data,
];
}
private function _splitGruppen($gruppen_data)
{
$gruppen_kurzbz = array_values(array_filter(array_column($gruppen_data, 'gruppe_kurzbz')));
$lehrverband_gruppen = array_values(array_filter($gruppen_data, function($gruppe)
{
return empty($gruppe['gruppe_kurzbz']);
}));
$lehrverband_gruppen_bezeichnung = array_column($lehrverband_gruppen, 'bezeichnung');
return (object)[
'gruppen_kurzbz' => $gruppen_kurzbz,
'lehrverband_gruppen' => $lehrverband_gruppen,
'lehrverband_gruppen_bezeichnung' => $lehrverband_gruppen_bezeichnung
];
}
//TODO (david) umbauen auf CollisionChecks
private function _isFrei($slot_start, $slot_end, $events, $check_array, $type)
{
if (empty($check_array)) return true;
$slot_start_ts = strtotime($slot_start);
$slot_end_ts = strtotime($slot_end);
foreach ($events as $event)
{
$event_start_ts = strtotime($event->isostart);
$event_end_ts = strtotime($event->isoend);
if ($event_end_ts <= $slot_start_ts || $event_start_ts >= $slot_end_ts)
continue;
$event_uids = $this->_getIds($event, $type);
if (!empty(array_intersect($event_uids, $check_array)))
return false;
}
return true;
}
private function _scoreToRating($score)
{
if ($score >= 90)
return 'good';
if ($score >= 60)
return 'mid';
return 'bad';
}
private function _getVorschlaegeForEvent($event)
{
$raumkandidaten = $this->_getRaumkandidaten($event);
if (empty($raumkandidaten)) return [];
$lektor_uids = array_column($event->lektor, 'mitarbeiter_uid');
$grp = $this->_splitGruppen($event->gruppe);
$tages_events = $this->_ci->kalenderlib->getForRaumvorschlag(
$event->datum,
$event->datum,
$lektor_uids,
$grp->gruppen_kurzbz,
$grp->lehrverband_gruppen
);
return $this->_getRatings($tages_events, $event, $raumkandidaten, $lektor_uids, $grp->gruppen_kurzbz, $grp->lehrverband_gruppen_bezeichnung);
}
private function _getRatings($events, $event, $raumkandidaten, $lektor_uids, $gruppen_kurzbz, $lehrverband_gruppen_bezeichnung)
{
$event = (object)$event;
$lektor_davor = $this->_getEventDavor($events, $event->isostart, $lektor_uids, 'lektor');
$gruppen_davor = $this->_getEventDavor($events, $event->isostart, $gruppen_kurzbz, 'gruppe');
$lehrverband_davor = $this->_getEventDavor($events, $event->isostart, $lehrverband_gruppen_bezeichnung, 'lehrverband');
$lektor_davor_ort = $lektor_davor ? $this->_getOrtDetails($lektor_davor->ort_kurzbz) : null;
$gruppen_davor_ort = $gruppen_davor ? $this->_getOrtDetails($gruppen_davor->ort_kurzbz) : null;
$lehrverband_davor_ort = $lehrverband_davor ? $this->_getOrtDetails($lehrverband_davor->ort_kurzbz) : null;
$unique = [];
foreach ($raumkandidaten as $raum)
{
$unique[$raum->ort_kurzbz] = $raum;
}
$kandidaten = array_values($unique);
$ratings = [];
foreach ($kandidaten as $raum)
{
$rating = ['ort_kurzbz' => $raum->ort_kurzbz, 'score' => 100, 'details' => [],
'raumtyp_kurzbz' => $raum->raumtyp_kurzbz, 'max_person' => $raum->max_person];
$this->_rateLektor($rating, $raum, $lektor_davor_ort);
$this->_rateGruppen($rating, $raum, $gruppen_davor_ort);
$this->_rateGruppen($rating, $raum, $lehrverband_davor_ort);
Events::trigger('room_rating',
function & () use (&$rating) {
return $rating;
},
$raum,
$event
);
$ratings[] = $rating;
}
usort($ratings, function($a, $b)
{
return $b['score'] <=> $a['score'];
});
return $ratings;
}
private function _getOrtDetails($ort_kurzbz_array)
{
$ort_kurzbz_array = (array) $ort_kurzbz_array;
if (empty($ort_kurzbz_array)) return [];
$this->_ci->OrtModel->addSelect('ort_kurzbz, stockwerk, standort_id');
$this->_ci->OrtModel->db->where_in('ort_kurzbz', $ort_kurzbz_array);
$result = $this->_ci->OrtModel->load();
return hasData($result) ? getData($result) : [];
}
private function _rateLektor(&$rating, $raum, $lektor_davor_orte)
{
if (empty($lektor_davor_orte)) return;
foreach ($lektor_davor_orte as $ort)
{
if ($ort->ort_kurzbz === $raum->ort_kurzbz)
{
$rating['score'] += 20;
$rating['details'][] = '+20 ' . $this->_ci->phraseslib->t('ui', 'lecturer_already_here');
return;
}
}
$gleiches_gebaeude = array_filter($lektor_davor_orte, function($ort) use ($raum)
{
return $ort->standort_id === $raum->standort_id;
});
if (empty($gleiches_gebaeude))
{
$rating['score'] -= 20;
$rating['details'][] = '-20 '. $this->_ci->phraseslib->t('ui', 'lecturer_building_change');
return;
}
$diffs = array_map(function($ort) use ($raum)
{
return abs($ort->stockwerk - $raum->stockwerk);
}, $gleiches_gebaeude);
$diff = min($diffs);
if ($diff > 0)
{
$rating['score'] -= $diff * 5;
$rating['details'][] = '-' . ($diff * 5) . ' ' . $this->_ci->phraseslib->t('ui', 'lecturer_floor_change');
}
}
private function _rateGruppen(&$rating, $raum, $gruppen_davor_orte)
{
if (empty($gruppen_davor_orte)) return;
foreach ($gruppen_davor_orte as $ort)
{
if ($ort->ort_kurzbz === $raum->ort_kurzbz)
{
$rating['score'] += 10;
$rating['details'][] = '+10 ' . $this->_ci->phraseslib->t('ui', 'student_already_here');
return;
}
}
$gleiches_gebaeude = array_filter($gruppen_davor_orte, function($ort) use ($raum)
{
return $ort->standort_id === $raum->standort_id;
});
if (empty($gleiches_gebaeude))
{
$rating['score'] -= 20;
$rating['details'][] = '-20 '. $this->_ci->phraseslib->t('ui', 'student_building_change');
return;
}
$diffs = array_map(function($ort) use ($raum)
{
return abs($ort->stockwerk - $raum->stockwerk);
}, $gleiches_gebaeude);
$diff = min($diffs);
if ($diff > 0)
{
$rating['score'] -= $diff * 5;
$rating['details'][] = '-' . ($diff * 5) . ' '. $this->_ci->phraseslib->t('ui', 'student_floor_change');
}
}
private function _getEventDavor($events, $von, $uids, $type)
{
$kandidat = null;
foreach ($events as $event)
{
if ($event->isoend > $von)
continue;
//Wenn zwischen zwei Events eine 30+ Minuten Pause liegt, wird das Event davor nicht berücksichtigt
if ((strtotime($von) - strtotime($event->isoend)) > 30 * 60)
continue;
if (empty($event->ort_kurzbz))
continue;
$event_uids = $this->_getIds($event, $type);
if (empty(array_intersect($event_uids, $uids)))
continue;
if ($kandidat === null || $event->isoend > $kandidat->isoend)
$kandidat = $event;
}
return $kandidat;
}
private function _getIds($event, $type)
{
$event_uids = array();
if ($type === 'lektor')
$event_uids = array_column($event->lektor, 'mitarbeiter_uid');
else if ($type === 'gruppe')
$event_uids = array_column($event->gruppe, 'gruppe_kurzbz');
else if ($type === 'lehrverband')
$event_uids = array_column($event->gruppe, 'bezeichnung');
return $event_uids;
}
private function _getRaumkandidaten($event)
{
$raumtyp = null;
$raumtypalternativ = null;
if (!empty($event->lehreinheit_id))
{
$lehreinheit = $this->_ci->LehreinheitModel->load($event->lehreinheit_id[0]);
if (hasData($lehreinheit))
{
$lehreinheit = getData($lehreinheit)[0];
$raumtyp = $lehreinheit->raumtyp ?? null;
$raumtypalternativ = $lehreinheit->raumtypalternativ ?? null;
}
}
$this->_ci->KalenderModel->addDistinct('tbl_kalender_ort.ort_kurzbz');
$this->_ci->KalenderModel->addSelect('tbl_kalender_ort.ort_kurzbz');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'tbl_kalender.kalender_id = tbl_kalender_ort.kalender_id');
$this->_ci->KalenderModel->db->where('tbl_kalender.von <', $event->isoend);
$this->_ci->KalenderModel->db->where('tbl_kalender.bis >', $event->isostart);
if (!is_null($event->kalender_id))
{
$this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id !=', $event->kalender_id);
}
$this->_ci->KalenderModel->db->where_not_in('tbl_kalender.status_kurzbz', ['deleted', 'to_delete', 'to_delete_live', 'to_delete_preview', 'archived']);
$this->_ci->KalenderModel->db->where('tbl_kalender_ort.ort_kurzbz IS NOT NULL', null, false);
$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
);
$belegte = $this->_ci->KalenderModel->load();
$belegte_orte = hasData($belegte) ? array_column(getData($belegte), 'ort_kurzbz') : [];
if (empty($raumtyp))
{
$raeume = $this->_getFreieRaeume(null, $belegte_orte);
return hasData($raeume) ? getData($raeume) : [];
}
$vorschlaege = [];
$raeume = $this->_getFreieRaeume($raumtyp, $belegte_orte);
if (hasData($raeume))
$vorschlaege = getData($raeume);
if (count($vorschlaege) < 5 && !empty($raumtypalternativ))
{
$bereits_gefunden = array_merge($belegte_orte, array_column($vorschlaege, 'ort_kurzbz'));
$alternativ = $this->_getFreieRaeume($raumtypalternativ, $bereits_gefunden);
if (!isError($alternativ) && hasData($alternativ))
$vorschlaege = array_merge($vorschlaege, getData($alternativ));
}
return $vorschlaege;
}
private function _getFreieRaeume($raumtyp, $belegte_orte)
{
$this->_ci->OrtModel->addSelect('ort_kurzbz, stockwerk, standort_id, max_person, raumtyp_kurzbz');
$this->_ci->OrtModel->addJoin('public.tbl_ortraumtyp', 'ort_kurzbz');
if (!empty($raumtyp))
$this->_ci->OrtModel->db->where('raumtyp_kurzbz', $raumtyp);
else
$this->_ci->OrtModel->db->where('reservieren', true);
$this->_ci->OrtModel->db->where('aktiv', true);
$this->_ci->OrtModel->db->where("ort_kurzbz NOT LIKE '\_%'", null, false);
if (!empty($belegte_orte))
$this->_ci->OrtModel->db->where_not_in('ort_kurzbz', $belegte_orte);
$this->_ci->OrtModel->addOrder('hierarchie, ort_kurzbz');
return $this->_ci->OrtModel->load();
}
}
@@ -0,0 +1,303 @@
<?php
class LectureCollisionCheck implements ICollisionCheck
{
private $_ci;
public function __construct()
{
$this->_ci =& get_instance();
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->model('ressource/Zeitsperre_model', 'ZeitsperreModel');
$this->_ci->load->model('education/Lehreinheitmitarbeiter_model', 'LehreinheitmitarbeiterModel');
$this->_ci->load->library('VariableLib', array('uid' => getAuthUID()));
$this->_ci->load->library('PhrasesLib', array('ui'));
}
public function getName()
{
return 'lecture';
}
public function check($data)
{
if (!isset($data->von, $data->bis)) return [];
if (empty($data->kalender_id) && empty($data->lehreinheit_id) && empty($data->uids)) return [];
if ($this->_ci->variablelib->getVar('ignore_kollision') === 'true') return [];
if (!empty($data->kalender_id))
$uids = $this->_getUids($data->kalender_id);
else if (!empty($data->lehreinheit_id))
$uids = $this->_getUidsFromLehreinheit($data->lehreinheit_id);
else
$uids = $data->uids;
if (empty($uids)) return [];
$collisions = [];
$collisions = array_merge($collisions, $this->_checkLehreinheit($uids, $data));
$collisions = array_merge($collisions, $this->_checkReservierung($uids, $data));
$collisions = array_merge($collisions, $this->_checkZeitsperre($uids, $data));
return $collisions;
}
public function checkAll($kalender_ids)
{
if (empty($kalender_ids)) return [];
$kollisionsfreie_user = unserialize(KOLLISIONSFREIE_USER);
$grouped = [];
$this->_ci->KalenderModel->addSelect('DISTINCT ON (tbl_kalender.kalender_id) tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit current_kalender_le', 'current_kalender_le.kalender_id = tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit current_lehreinheit', 'current_lehreinheit.lehreinheit_id = current_kalender_le.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter current_lehreinheit_ma', 'current_lehreinheit_ma.lehreinheit_id = current_lehreinheit.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter other_lehreinheithreinheit_ma', 'other_lehreinheithreinheit_ma.mitarbeiter_uid = current_lehreinheit_ma.mitarbeiter_uid');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit other_lehreinheit', 'other_lehreinheit.lehreinheit_id = other_lehreinheithreinheit_ma.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit other_kalender_le', 'other_kalender_le.lehreinheit_id = other_lehreinheit.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender other_kalender', 'other_kalender.kalender_id = other_kalender_le.kalender_id');
$this->_ci->KalenderModel->db->where('other_kalender.kalender_id != tbl_kalender.kalender_id', null, false);
$this->_ci->KalenderModel->db->where('other_kalender.von < tbl_kalender.bis', null, false);
$this->_ci->KalenderModel->db->where('other_kalender.bis > tbl_kalender.von', null, false);
$this->_ci->KalenderModel->db->where_not_in('other_kalender.status_kurzbz', array('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview'));
$this->_ci->KalenderModel->db->where_not_in('current_lehreinheit_ma.mitarbeiter_uid', $kollisionsfreie_user);
$this->_ci->KalenderModel->db->group_start();
$where_ids_chunks = array_chunk($kalender_ids,25);
foreach($where_ids_chunks as $where_ids)
{
$this->_ci->KalenderModel->db->or_where_in('tbl_kalender.kalender_id', $where_ids);
}
$this->_ci->KalenderModel->db->group_end();
$this->_ci->KalenderModel->db->where(
'other_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)',
null, false
);
$result = $this->_ci->KalenderModel->load();
if (!isError($result) && hasData($result))
{
foreach (getData($result) as $row)
{
$grouped[$row->kalender_id][] = true;
}
}
$this->_ci->KalenderModel->addSelect('DISTINCT ON (tbl_kalender.kalender_id) tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit current_kalender_le', 'current_kalender_le.kalender_id = tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit current_lehreinheit', 'current_lehreinheit.lehreinheit_id = current_kalender_le.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter current_lehreinheit_ma', 'current_lehreinheit_ma.lehreinheit_id = current_lehreinheit.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_event_teilnehmer other_t', 'other_t.uid = current_lehreinheit_ma.mitarbeiter_uid');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_event other_e', 'other_e.kalender_id = other_t.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender other_kalender', 'other_kalender.kalender_id = other_e.kalender_id');
$this->_ci->KalenderModel->db->where('other_kalender.kalender_id != tbl_kalender.kalender_id', null, false);
$this->_ci->KalenderModel->db->where('other_kalender.von < tbl_kalender.bis', null, false);
$this->_ci->KalenderModel->db->where('other_kalender.bis > tbl_kalender.von', null, false);
$this->_ci->KalenderModel->db->where_not_in('other_kalender.status_kurzbz', ['archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview']);
$this->_ci->KalenderModel->db->where_not_in('current_lehreinheit_ma.mitarbeiter_uid', $kollisionsfreie_user);
$this->_ci->KalenderModel->db->group_start();
$where_ids_chunks = array_chunk($kalender_ids,25);
foreach($where_ids_chunks as $where_ids)
{
$this->_ci->KalenderModel->db->or_where_in('tbl_kalender.kalender_id', $where_ids);
}
$this->_ci->KalenderModel->db->group_end();
$this->_ci->KalenderModel->db->where(
'other_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)',
null, false
);
$result = $this->_ci->KalenderModel->load();
if (!isError($result) && hasData($result))
{
foreach (getData($result) as $row)
{
$grouped[$row->kalender_id][] = true;
}
}
$this->_ci->KalenderModel->addSelect('DISTINCT ON (tbl_kalender.kalender_id) tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit current_kalender_le', 'current_kalender_le.kalender_id = tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit current_lehreinheit', 'current_lehreinheit.lehreinheit_id = current_kalender_le.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter current_lehreinheit_ma', 'current_lehreinheit_ma.lehreinheit_id = current_lehreinheit.lehreinheit_id');
$this->_ci->KalenderModel->addJoin('campus.tbl_zeitsperre z',
"z.mitarbeiter_uid = current_lehreinheit_ma.mitarbeiter_uid
AND z.zeitsperretyp_kurzbz != 'ZVerfueg'");
$this->_ci->KalenderModel->addJoin('lehre.tbl_stunde vonstunde_z', 'vonstunde_z.stunde = z.vonstunde', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_stunde bisstunde_z', 'bisstunde_z.stunde = z.bisstunde', 'LEFT');
$this->_ci->KalenderModel->db->where('(z.vondatum + COALESCE(vonstunde_z.beginn, \'00:00\'))::timestamp < tbl_kalender.bis', null, false);
$this->_ci->KalenderModel->db->where('(z.bisdatum + COALESCE(bisstunde_z.ende, \'23:59\'))::timestamp > tbl_kalender.von', null, false);
$this->_ci->KalenderModel->db->where_not_in('current_lehreinheit_ma.mitarbeiter_uid', $kollisionsfreie_user);
$this->_ci->KalenderModel->db->group_start();
$where_ids_chunks = array_chunk($kalender_ids,25);
foreach($where_ids_chunks as $where_ids)
{
$this->_ci->KalenderModel->db->or_where_in('tbl_kalender.kalender_id', $where_ids);
}
$this->_ci->KalenderModel->db->group_end();
$result = $this->_ci->KalenderModel->load();
if (!isError($result) && hasData($result))
{
foreach (getData($result) as $row)
{
$grouped[$row->kalender_id][] = true;
}
}
return $grouped;
}
private function _getUids($kalender_id)
{
$kollisionsfreie_user = unserialize(KOLLISIONSFREIE_USER);
$this->_ci->KalenderModel->addDistinct('mitarbeiter_uid, tbl_kalender_event_teilnehmer.uid');
$this->_ci->KalenderModel->addSelect('mitarbeiter_uid, tbl_kalender_event_teilnehmer.uid');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit', 'kalender_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit', 'lehreinheit_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter', 'lehreinheit_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_event', 'kalender_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_event_teilnehmer', 'tbl_kalender_event.kalender_id = tbl_kalender_event_teilnehmer.kalender_id', 'LEFT');
$this->_ci->KalenderModel->db->group_start();
$this->_ci->KalenderModel->db->where_not_in('mitarbeiter_uid', $kollisionsfreie_user);
$this->_ci->KalenderModel->db->or_where('mitarbeiter_uid IS NULL', null, false);
$this->_ci->KalenderModel->db->group_end();
$result = $this->_ci->KalenderModel->loadWhere(array(
'tbl_kalender.kalender_id' => $kalender_id
));
if (isError($result) || !hasData($result)) return [];
$data = getData($result);
$mitarbeiter_uids = array_filter(array_column($data, 'mitarbeiter_uid'));
$event_teilnehmer = array_filter(array_column($data, 'uid'));
return array_unique(array_merge($mitarbeiter_uids, $event_teilnehmer));
}
private function _getUidsFromLehreinheit($lehreinheit_id)
{
$kollisionsfreie_user = unserialize(KOLLISIONSFREIE_USER);
$result = $this->_ci->LehreinheitmitarbeiterModel->loadWhere(array('lehreinheit_id' => $lehreinheit_id));
if (isError($result) || !hasData($result)) return [];
$uids = array_column(getData($result), 'mitarbeiter_uid');
$uids = array_diff(array_filter($uids), $kollisionsfreie_user);
return array_values(array_unique($uids));
}
private function _checkLehreinheit($uids, $data)
{
$kollisionsfreie_user = unserialize(KOLLISIONSFREIE_USER);
$this->_ci->KalenderModel->addDistinct('mitarbeiter_uid, tbl_kalender.von, tbl_kalender.bis');
$this->_ci->KalenderModel->addSelect('mitarbeiter_uid, tbl_kalender.von, tbl_kalender.bis');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_lehreinheit', 'kalender_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheit', 'lehreinheit_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_lehreinheitmitarbeiter', 'lehreinheit_id', 'LEFT');
$this->_ci->KalenderModel->db->where_in('mitarbeiter_uid', $uids);
if (!empty($data->kalender_id))
$this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id !=', $data->kalender_id);
$this->_ci->KalenderModel->db->where_not_in('tbl_kalender.status_kurzbz', array('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview'));
$this->_ci->KalenderModel->db->where_not_in('mitarbeiter_uid', $kollisionsfreie_user);
$this->_ci->KalenderModel->db->where(
'tbl_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)',
null, false
);
$result = $this->_ci->KalenderModel->loadWhere(array(
'von <' => $data->bis,
'bis >' => $data->von,
));
if (isError($result) || !hasData($result)) return [];
return array_map(function($row)
{
return [
'message' => $this->_ci->phraseslib->t('ui', 'ma_le_kollision') . ': ' . $row->mitarbeiter_uid . ' (' . date('d.m.Y H:i', strtotime($row->von)) . ' - ' . date('d.m.Y H:i', strtotime($row->bis)) . ')',
'errorCode' => 'lector_collision',
];
}, getData($result));
}
private function _checkReservierung($uids, $data)
{
if ($this->_ci->variablelib->getVar('ignore_reservierung') === 'true') return [];
$kollisionsfreie_user = unserialize(KOLLISIONSFREIE_USER);
$this->_ci->KalenderModel->addDistinct('tbl_kalender_event_teilnehmer.uid, tbl_kalender.von, tbl_kalender.bis');
$this->_ci->KalenderModel->addSelect('tbl_kalender_event_teilnehmer.uid, tbl_kalender.von, tbl_kalender.bis');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_event', 'kalender_id', 'LEFT');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_event_teilnehmer', 'tbl_kalender_event.kalender_id = tbl_kalender_event_teilnehmer.kalender_id', 'LEFT');
$this->_ci->KalenderModel->db->where_in('tbl_kalender_event_teilnehmer.uid', $uids);
if (!empty($data->kalender_id))
$this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id !=', $data->kalender_id);
$this->_ci->KalenderModel->db->where_not_in('tbl_kalender.status_kurzbz', array('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview'));
$this->_ci->KalenderModel->db->where_not_in('uid', $kollisionsfreie_user);
$this->_ci->KalenderModel->db->where(
'tbl_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)',
null, false
);
$result = $this->_ci->KalenderModel->loadWhere(array(
'von <' => $data->bis,
'bis >' => $data->von,
));
if (isError($result) || !hasData($result)) return [];
return array_map(function($row)
{
return [
'message' => $this->_ci->phraseslib->t('ui', 'reservierung_kollision') . ': ' . $row->uid . ' (' . date('d.m.Y H:i', strtotime($row->von)) . ' - ' . date('d.m.Y H:i', strtotime($row->bis)) . ')',
'errorCode' => 'reservation_collision',
];
}, getData($result));
}
private function _checkZeitsperre($uids, $data)
{
if ($this->_ci->variablelib->getVar('ignore_zeitsperre') === 'true') return [];
$this->_ci->ZeitsperreModel->addSelect('mitarbeiter_uid, vondatum, vonstunde_z.beginn as von_beginn, bisdatum, bisstunde_z.ende as bis_ende');
$this->_ci->ZeitsperreModel->addJoin('lehre.tbl_stunde vonstunde_z', 'vonstunde_z.stunde = tbl_zeitsperre.vonstunde', 'LEFT');
$this->_ci->ZeitsperreModel->addJoin('lehre.tbl_stunde bisstunde_z', 'bisstunde_z.stunde = tbl_zeitsperre.bisstunde', 'LEFT');
$this->_ci->ZeitsperreModel->db->where('zeitsperretyp_kurzbz !=', 'ZVerfueg');
$this->_ci->ZeitsperreModel->db->where('(tbl_zeitsperre.vondatum + COALESCE(vonstunde_z.beginn, \'00:00\'))::timestamp <', $data->bis);
$this->_ci->ZeitsperreModel->db->where('(tbl_zeitsperre.bisdatum + COALESCE(bisstunde_z.ende, \'23:59\'))::timestamp >', $data->von);
$this->_ci->ZeitsperreModel->db->where_in('mitarbeiter_uid', $uids);
$result = $this->_ci->ZeitsperreModel->load();
if (isError($result) || !hasData($result)) return [];
return array_map(function($row)
{
return [
'message' => $this->_ci->phraseslib->t('ui', 'ma_zeitsperre_kollision') . ': ' . $row->mitarbeiter_uid . ' (' . date('d.m.Y H:i', strtotime($row->vondatum . ' ' . $row->von_beginn)) . ' - ' . date('d.m.Y H:i', strtotime($row->bisdatum . ' ' . $row->bis_ende)) . ')',
'errorCode' => 'absences_collision',
];
}, getData($result));
}
}
@@ -0,0 +1,94 @@
<?php
class ResourcesCollisionCheck implements ICollisionCheck
{
private $_ci;
public function __construct()
{
$this->_ci =& get_instance();
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->library('VariableLib', array('uid' => getAuthUID()));
$this->_ci->load->library('PhrasesLib', array('ui'));
}
public function getName()
{
return 'resources';
}
public function check($data)
{
if (!isset($data->betriebsmittel_ids, $data->von, $data->bis)) return [];
if (empty($data->betriebsmittel_ids)) return [];
if ($this->_ci->variablelib->getVar('ignore_kollision') === 'true') return [];
if ($this->_ci->variablelib->getVar('ignore_resources_collisions') === 'true') return [];
$this->_ci->KalenderModel->addSelect('kalender_id, von, bis, bm.beschreibung AS betriebsmittel_beschreibung');
$this->_ci->KalenderModel->addJoin('lehre.tbl_betriebsmittel_kalender bk', 'bk.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_kalender_gruppen_id');
$this->_ci->KalenderModel->addJoin('wawi.tbl_betriebsmittel bm', 'bm.betriebsmittel_id = bk.betriebsmittel_id');
if (!empty($data->kalender_id))
$this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id !=', $data->kalender_id);
$this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)', null, false);
$this->_ci->KalenderModel->db->where_not_in('tbl_kalender.status_kurzbz', ['archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview']);
$this->_ci->KalenderModel->db->where_in('bk.betriebsmittel_id', $data->betriebsmittel_ids);
$result = $this->_ci->KalenderModel->loadWhere(array(
'von <' => $data->bis,
'bis >' => $data->von,
));
if (isError($result)) return [];
if (!hasData($result)) return [];
return array_map(function($row)
{
return [
'errorCode' => 'resource_collision',
'message' => $this->_ci->phraseslib->t('ui', 'resource_collision') . ': ' . $row->betriebsmittel_beschreibung . ' (' . date('d.m.Y H:i', strtotime($row->von)) . ' - ' . date('d.m.Y H:i', strtotime($row->bis)) . ')'
];
}, getData($result));
}
public function checkAll($kalender_ids)
{
if (empty($kalender_ids)) return [];
$this->_ci->KalenderModel->addSelect('DISTINCT ON (tbl_kalender.kalender_id) tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_betriebsmittel_kalender current_bk', 'current_bk.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_kalender_gruppen_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_betriebsmittel_kalender other_bk', 'other_bk.betriebsmittel_id = current_bk.betriebsmittel_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender other_kalender', 'other_kalender.eindeutige_kalender_gruppen_id = other_bk.eindeutige_kalender_gruppen_id');
$this->_ci->KalenderModel->db->where('other_kalender.kalender_id != tbl_kalender.kalender_id', null, false);
$this->_ci->KalenderModel->db->where('other_kalender.von < tbl_kalender.bis', null, false);
$this->_ci->KalenderModel->db->where('other_kalender.bis > tbl_kalender.von', null, false);
$this->_ci->KalenderModel->db->where_not_in('other_kalender.status_kurzbz', ['archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview']);
$this->_ci->KalenderModel->db->group_start();
$where_ids_chunks = array_chunk($kalender_ids,25);
foreach($where_ids_chunks as $where_ids)
{
$this->_ci->KalenderModel->db->or_where_in('tbl_kalender.kalender_id', $where_ids);
}
$this->_ci->KalenderModel->db->group_end();
$this->_ci->KalenderModel->db->where('other_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)', null, false);
$result = $this->_ci->KalenderModel->load();
if (isError($result) || !hasData($result)) return [];
$grouped = [];
foreach (getData($result) as $row)
{
$grouped[$row->kalender_id][] = true;
}
return $grouped;
}
}
@@ -0,0 +1,90 @@
<?php
class RoomCollisionCheck implements ICollisionCheck
{
private $_ci;
public function __construct()
{
$this->_ci =& get_instance();
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->library('VariableLib', array('uid' => getAuthUID()));
$this->_ci->load->library('PhrasesLib', array('ui'));
}
public function getName()
{
return 'room';
}
public function check($data)
{
if (!isset($data->ort_kurzbz, $data->von, $data->bis) || isEmptyArray($data->ort_kurzbz)) return [];
if ($this->_ci->variablelib->getVar('ignore_kollision') === 'true') return [];
$this->_ci->KalenderModel->addSelect('kalender_id, ort_kurzbz, von, bis');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort', 'kalender_id');
if (!empty($data->kalender_id))
$this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id !=', $data->kalender_id);
$this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)', null, false);
$this->_ci->KalenderModel->db->where_not_in('tbl_kalender.status_kurzbz', array('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview'));
$this->_ci->KalenderModel->db->where_in('ort_kurzbz', $data->ort_kurzbz);
$result = $this->_ci->KalenderModel->loadWhere(array(
'von <' => $data->bis,
'bis >' => $data->von,
));
if (isError($result)) return [];
if (!hasData($result)) return [];
return array_map(function($row)
{
return [
'errorCode' => 'room_collision',
'message' => $this->_ci->phraseslib->t('ui', 'raum_kollision') . ': ' . $row->ort_kurzbz . ' (' . date('d.m.Y H:i', strtotime($row->von)) . ' - ' . date('d.m.Y H:i', strtotime($row->bis)) . ')'
];
}, getData($result));
}
public function checkAll($kalender_ids)
{
if (empty($kalender_ids)) return [];
$this->_ci->KalenderModel->addSelect('DISTINCT ON (tbl_kalender.kalender_id) tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort current_ort', 'current_ort.kalender_id = tbl_kalender.kalender_id');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender_ort other_ort', 'other_ort.ort_kurzbz = current_ort.ort_kurzbz');
$this->_ci->KalenderModel->addJoin('lehre.tbl_kalender other_kalender', 'other_kalender.kalender_id = other_ort.kalender_id');
$this->_ci->KalenderModel->db->where('other_kalender.kalender_id != tbl_kalender.kalender_id', null, false);
$this->_ci->KalenderModel->db->where('other_kalender.von < tbl_kalender.bis', null, false);
$this->_ci->KalenderModel->db->where('other_kalender.bis > tbl_kalender.von', null, false);
$this->_ci->KalenderModel->db->where_not_in('other_kalender.status_kurzbz', array('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview'));
$this->_ci->KalenderModel->db->group_start();
$where_ids_chunks = array_chunk($kalender_ids,25);
foreach($where_ids_chunks as $where_ids)
{
$this->_ci->KalenderModel->db->or_where_in('tbl_kalender.kalender_id', $where_ids);
}
$this->_ci->KalenderModel->db->group_end();
$this->_ci->KalenderModel->db->where('other_kalender.kalender_id NOT IN (SELECT vorgaenger_kalender_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NOT NULL)', null, false);
$result = $this->_ci->KalenderModel->load();
if (isError($result) || !hasData($result)) return [];
$grouped = [];
foreach (getData($result) as $row)
{
$grouped[$row->kalender_id][] = true;
}
return $grouped;
}
}
@@ -0,0 +1,286 @@
<?php
class StudentCollisionCheck implements ICollisionCheck
{
private $_ci;
public function __construct()
{
$this->_ci =& get_instance();
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->library('VariableLib', array('uid' => getAuthUID()));
$this->_ci->load->library('PhrasesLib', array('ui'));
}
public function getName()
{
return 'student';
}
public function check($data)
{
if (!isset($data->von, $data->bis)) return [];
if (!isset($data->kalender_id) && !isset($data->lehreinheit_id)) return [];
if ($this->_ci->variablelib->getVar('ignore_kollision') === 'true') return [];
if ($this->_ci->variablelib->getVar('kollision_student') !== 'true') return [];
$kollisionsfreie_user = unserialize(KOLLISIONSFREIE_USER);
$placeholders = implode(',', array_fill(0, count($kollisionsfreie_user), '?'));
$dbModel = new DB_Model();
if (isset($data->kalender_id))
{
$qry1 = "
SELECT DISTINCT tbl_benutzergruppe.uid
FROM lehre.tbl_kalender
JOIN lehre.tbl_kalender_lehreinheit USING(kalender_id)
JOIN lehre.tbl_lehreinheit ON tbl_lehreinheit.lehreinheit_id = tbl_kalender_lehreinheit.lehreinheit_id
JOIN lehre.tbl_lehreinheitgruppe ON tbl_lehreinheitgruppe.lehreinheit_id = tbl_lehreinheit.lehreinheit_id
JOIN public.tbl_gruppe
ON tbl_gruppe.studiengang_kz = tbl_lehreinheitgruppe.studiengang_kz
AND tbl_gruppe.semester = tbl_lehreinheitgruppe.semester
AND tbl_gruppe.gruppe_kurzbz = tbl_lehreinheitgruppe.gruppe_kurzbz
JOIN public.tbl_benutzergruppe ON tbl_benutzergruppe.gruppe_kurzbz = tbl_gruppe.gruppe_kurzbz
AND tbl_benutzergruppe.studiensemester_kurzbz = tbl_lehreinheit.studiensemester_kurzbz
WHERE tbl_kalender.kalender_id = ?
AND tbl_benutzergruppe.uid NOT IN ($placeholders)
UNION ALL
SELECT DISTINCT tbl_studentlehrverband.student_uid AS uid
FROM lehre.tbl_kalender
JOIN lehre.tbl_kalender_lehreinheit USING(kalender_id)
JOIN lehre.tbl_lehreinheit ON tbl_lehreinheit.lehreinheit_id = tbl_kalender_lehreinheit.lehreinheit_id
JOIN lehre.tbl_lehreinheitgruppe ON tbl_lehreinheitgruppe.lehreinheit_id = tbl_lehreinheit.lehreinheit_id
JOIN public.tbl_studentlehrverband
ON tbl_studentlehrverband.studiengang_kz = tbl_lehreinheitgruppe.studiengang_kz
AND tbl_studentlehrverband.semester = tbl_lehreinheitgruppe.semester
AND tbl_studentlehrverband.studiensemester_kurzbz = tbl_lehreinheit.studiensemester_kurzbz
AND (tbl_lehreinheitgruppe.verband = tbl_studentlehrverband.verband OR tbl_lehreinheitgruppe.verband IS NULL OR btrim(tbl_lehreinheitgruppe.verband::text) = '' OR tbl_studentlehrverband.verband IS NULL)
AND (tbl_lehreinheitgruppe.gruppe = tbl_studentlehrverband.gruppe OR tbl_lehreinheitgruppe.gruppe IS NULL OR btrim(tbl_lehreinheitgruppe.gruppe::text) = '' OR tbl_studentlehrverband.gruppe IS NULL)
WHERE tbl_kalender.kalender_id = ?
AND tbl_studentlehrverband.student_uid NOT IN ($placeholders)
";
$qry_1_param = $data->kalender_id;
}
else
{
$qry1 = "
SELECT DISTINCT tbl_benutzergruppe.uid
FROM lehre.tbl_lehreinheit
JOIN lehre.tbl_lehreinheitgruppe ON tbl_lehreinheitgruppe.lehreinheit_id = tbl_lehreinheit.lehreinheit_id
JOIN public.tbl_gruppe
ON tbl_gruppe.studiengang_kz = tbl_lehreinheitgruppe.studiengang_kz
AND tbl_gruppe.semester = tbl_lehreinheitgruppe.semester
AND tbl_gruppe.gruppe_kurzbz = tbl_lehreinheitgruppe.gruppe_kurzbz
JOIN public.tbl_benutzergruppe ON tbl_benutzergruppe.gruppe_kurzbz = tbl_gruppe.gruppe_kurzbz
AND tbl_benutzergruppe.studiensemester_kurzbz = tbl_lehreinheit.studiensemester_kurzbz
WHERE tbl_lehreinheit.lehreinheit_id = ?
AND tbl_benutzergruppe.uid NOT IN ($placeholders)
UNION ALL
SELECT DISTINCT tbl_studentlehrverband.student_uid AS uid
FROM lehre.tbl_lehreinheit
JOIN lehre.tbl_lehreinheitgruppe ON tbl_lehreinheitgruppe.lehreinheit_id = tbl_lehreinheit.lehreinheit_id
JOIN public.tbl_studentlehrverband
ON tbl_studentlehrverband.studiengang_kz = tbl_lehreinheitgruppe.studiengang_kz
AND tbl_studentlehrverband.semester = tbl_lehreinheitgruppe.semester
AND tbl_studentlehrverband.studiensemester_kurzbz = tbl_lehreinheit.studiensemester_kurzbz
AND (tbl_lehreinheitgruppe.verband = tbl_studentlehrverband.verband OR tbl_lehreinheitgruppe.verband IS NULL OR btrim(tbl_lehreinheitgruppe.verband::text) = '' OR tbl_studentlehrverband.verband IS NULL)
AND (tbl_lehreinheitgruppe.gruppe = tbl_studentlehrverband.gruppe OR tbl_lehreinheitgruppe.gruppe IS NULL OR btrim(tbl_lehreinheitgruppe.gruppe::text) = '' OR tbl_studentlehrverband.gruppe IS NULL)
WHERE tbl_lehreinheit.lehreinheit_id = ?
AND tbl_studentlehrverband.student_uid NOT IN ($placeholders)
";
$qry_1_param = $data->lehreinheit_id;
}
$result1 = $dbModel->execReadOnlyQuery($qry1, array_merge(
[$qry_1_param],
$kollisionsfreie_user,
[$qry_1_param],
$kollisionsfreie_user
));
if (isError($result1) || !hasData($result1)) return [];
$curUids = array_flip(array_column(getData($result1), 'uid'));
$exclude_kalender = isset($data->kalender_id) ? "AND tbl_kalender.kalender_id != ?" : "";
$exclude_kalender_param = isset($data->kalender_id) ? [$data->kalender_id] : [];
$qry2 = "
SELECT DISTINCT tbl_kalender.kalender_id, tbl_kalender.von, tbl_kalender.bis, tbl_benutzergruppe.uid
FROM lehre.tbl_kalender
JOIN lehre.tbl_kalender_lehreinheit ON tbl_kalender_lehreinheit.kalender_id = tbl_kalender.kalender_id
JOIN lehre.tbl_lehreinheit ON tbl_lehreinheit.lehreinheit_id = tbl_kalender_lehreinheit.lehreinheit_id
JOIN lehre.tbl_lehreinheitgruppe ON tbl_lehreinheitgruppe.lehreinheit_id = tbl_lehreinheit.lehreinheit_id
JOIN public.tbl_gruppe
ON tbl_gruppe.studiengang_kz = tbl_lehreinheitgruppe.studiengang_kz
AND tbl_gruppe.semester = tbl_lehreinheitgruppe.semester
AND tbl_gruppe.gruppe_kurzbz = tbl_lehreinheitgruppe.gruppe_kurzbz
JOIN public.tbl_benutzergruppe ON tbl_benutzergruppe.gruppe_kurzbz = tbl_gruppe.gruppe_kurzbz
AND tbl_benutzergruppe.studiensemester_kurzbz = tbl_lehreinheit.studiensemester_kurzbz
WHERE tbl_kalender.von < ?
AND tbl_kalender.bis > ?
$exclude_kalender
AND tbl_kalender.status_kurzbz NOT IN ('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview')
AND tbl_benutzergruppe.uid NOT IN ($placeholders)
AND NOT EXISTS (
SELECT 1 FROM lehre.tbl_kalender vorgaenger
WHERE vorgaenger.vorgaenger_kalender_id = tbl_kalender.kalender_id
)
UNION ALL
SELECT DISTINCT tbl_kalender.kalender_id, tbl_kalender.von, tbl_kalender.bis, tbl_studentlehrverband.student_uid AS uid
FROM lehre.tbl_kalender
JOIN lehre.tbl_kalender_lehreinheit ON tbl_kalender_lehreinheit.kalender_id = tbl_kalender.kalender_id
JOIN lehre.tbl_lehreinheit ON tbl_lehreinheit.lehreinheit_id = tbl_kalender_lehreinheit.lehreinheit_id
JOIN lehre.tbl_lehreinheitgruppe ON tbl_lehreinheitgruppe.lehreinheit_id = tbl_lehreinheit.lehreinheit_id
JOIN public.tbl_studentlehrverband
ON tbl_studentlehrverband.studiengang_kz = tbl_lehreinheitgruppe.studiengang_kz
AND tbl_studentlehrverband.semester = tbl_lehreinheitgruppe.semester
AND tbl_studentlehrverband.studiensemester_kurzbz = tbl_lehreinheit.studiensemester_kurzbz
AND (tbl_lehreinheitgruppe.verband = tbl_studentlehrverband.verband OR tbl_lehreinheitgruppe.verband IS NULL OR btrim(tbl_lehreinheitgruppe.verband::text) = '' OR tbl_studentlehrverband.verband IS NULL)
AND (tbl_lehreinheitgruppe.gruppe = tbl_studentlehrverband.gruppe OR tbl_lehreinheitgruppe.gruppe IS NULL OR btrim(tbl_lehreinheitgruppe.gruppe::text) = '' OR tbl_studentlehrverband.gruppe IS NULL)
WHERE tbl_kalender.von < ?
AND tbl_kalender.bis > ?
$exclude_kalender
AND tbl_kalender.status_kurzbz NOT IN ('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview')
AND tbl_studentlehrverband.student_uid NOT IN ($placeholders)
AND NOT EXISTS (
SELECT 1 FROM lehre.tbl_kalender vorgaenger
WHERE vorgaenger.vorgaenger_kalender_id = tbl_kalender.kalender_id
)
";
$result2 = $dbModel->execReadOnlyQuery($qry2, array_merge(
[$data->bis, $data->von], $exclude_kalender_param, $kollisionsfreie_user,
[$data->bis, $data->von], $exclude_kalender_param, $kollisionsfreie_user
));
if (isError($result2) || !hasData($result2)) return [];
$conflicts = [];
foreach (getData($result2) as $row)
{
if (isset($curUids[$row->uid]))
{
$conflicts[] = [
'message' =>
$this->_ci->phraseslib->t('ui', 'student_kollision')
. ': ' . $row->uid
. ' (' . date('d.m.Y H:i', strtotime($row->von))
. ' - ' . date('d.m.Y H:i', strtotime($row->bis)) . ')',
'errorCode' => 'student_collision'
];
}
}
return $conflicts;
}
public function checkAll($kalender_ids)
{
if (empty($kalender_ids)) return [];
if ($this->_ci->variablelib->getVar('kollision_student') !== 'true') return [];
$dbModel = new DB_Model();
$placeholders = implode(',', array_fill(0, count($kalender_ids), '?'));
$sql = "
SELECT DISTINCT current_kalender.kalender_id, current_benutzergruppe.uid
FROM lehre.tbl_kalender current_kalender
JOIN lehre.tbl_kalender_lehreinheit current_kalender_le ON current_kalender_le.kalender_id = current_kalender.kalender_id
JOIN lehre.tbl_lehreinheit current_lehreinheit ON current_lehreinheit.lehreinheit_id = current_kalender_le.lehreinheit_id
JOIN lehre.tbl_lehreinheitgruppe current_lehreinheitgruppe ON current_lehreinheitgruppe.lehreinheit_id = current_lehreinheit.lehreinheit_id
JOIN public.tbl_gruppe current_gruppe
ON current_gruppe.studiengang_kz = current_lehreinheitgruppe.studiengang_kz
AND current_gruppe.semester = current_lehreinheitgruppe.semester
AND current_gruppe.gruppe_kurzbz = current_lehreinheitgruppe.gruppe_kurzbz
JOIN public.tbl_benutzergruppe current_benutzergruppe ON current_benutzergruppe.gruppe_kurzbz = current_gruppe.gruppe_kurzbz
AND current_benutzergruppe.studiensemester_kurzbz = current_lehreinheit.studiensemester_kurzbz
JOIN lehre.tbl_kalender other_kalender
ON other_kalender.kalender_id != current_kalender.kalender_id
AND other_kalender.von < current_kalender.bis
AND other_kalender.bis > current_kalender.von
AND other_kalender.status_kurzbz NOT IN ('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview')
AND NOT EXISTS (
SELECT 1 FROM lehre.tbl_kalender vorgaenger
WHERE vorgaenger.vorgaenger_kalender_id = other_kalender.kalender_id
)
JOIN lehre.tbl_kalender_lehreinheit other_kalender_le ON other_kalender_le.kalender_id = other_kalender.kalender_id
JOIN lehre.tbl_lehreinheit other_lehreinheit ON other_lehreinheit.lehreinheit_id = other_kalender_le.lehreinheit_id
JOIN lehre.tbl_lehreinheitgruppe other_lehreinheitgruppe ON other_lehreinheitgruppe.lehreinheit_id = other_lehreinheit.lehreinheit_id
JOIN public.tbl_gruppe other_gruppe
ON other_gruppe.studiengang_kz = other_lehreinheitgruppe.studiengang_kz
AND other_gruppe.semester = other_lehreinheitgruppe.semester
AND other_gruppe.gruppe_kurzbz = other_lehreinheitgruppe.gruppe_kurzbz
JOIN public.tbl_benutzergruppe other_benutzergruppe
ON other_benutzergruppe.gruppe_kurzbz = other_gruppe.gruppe_kurzbz
AND other_benutzergruppe.uid = current_benutzergruppe.uid
AND other_benutzergruppe.studiensemester_kurzbz = other_lehreinheit.studiensemester_kurzbz
WHERE current_kalender.kalender_id IN ($placeholders)
UNION ALL
SELECT DISTINCT current_kalender.kalender_id, current_studentlehrverband.student_uid AS uid
FROM lehre.tbl_kalender current_kalender
JOIN lehre.tbl_kalender_lehreinheit current_kalender_le ON current_kalender_le.kalender_id = current_kalender.kalender_id
JOIN lehre.tbl_lehreinheit current_lehreinheit ON current_lehreinheit.lehreinheit_id = current_kalender_le.lehreinheit_id
JOIN lehre.tbl_lehreinheitgruppe current_lehreinheitgruppe ON current_lehreinheitgruppe.lehreinheit_id = current_lehreinheit.lehreinheit_id
JOIN public.tbl_studentlehrverband current_studentlehrverband
ON current_studentlehrverband.studiengang_kz = current_lehreinheitgruppe.studiengang_kz
AND current_studentlehrverband.semester = current_lehreinheitgruppe.semester
AND current_studentlehrverband.studiensemester_kurzbz = current_lehreinheit.studiensemester_kurzbz
AND (current_lehreinheitgruppe.verband = current_studentlehrverband.verband OR current_lehreinheitgruppe.verband IS NULL OR btrim(current_lehreinheitgruppe.verband::text) = '' OR current_studentlehrverband.verband IS NULL)
AND (current_lehreinheitgruppe.gruppe = current_studentlehrverband.gruppe OR current_lehreinheitgruppe.gruppe IS NULL OR btrim(current_lehreinheitgruppe.gruppe::text) = '' OR current_studentlehrverband.gruppe IS NULL)
JOIN lehre.tbl_kalender other_kalender
ON other_kalender.kalender_id != current_kalender.kalender_id
AND other_kalender.von < current_kalender.bis
AND other_kalender.bis > current_kalender.von
AND other_kalender.status_kurzbz NOT IN ('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview')
AND NOT EXISTS (
SELECT 1 FROM lehre.tbl_kalender vorgaenger
WHERE vorgaenger.vorgaenger_kalender_id = other_kalender.kalender_id
)
JOIN lehre.tbl_kalender_lehreinheit other_kalender_le ON other_kalender_le.kalender_id = other_kalender.kalender_id
JOIN lehre.tbl_lehreinheit other_lehreinheit ON other_lehreinheit.lehreinheit_id = other_kalender_le.lehreinheit_id
JOIN lehre.tbl_lehreinheitgruppe other_lehreinheitgruppe ON other_lehreinheitgruppe.lehreinheit_id = other_lehreinheit.lehreinheit_id
JOIN public.tbl_studentlehrverband other_slv
ON other_slv.studiengang_kz = other_lehreinheitgruppe.studiengang_kz
AND other_slv.semester = other_lehreinheitgruppe.semester
AND other_slv.studiensemester_kurzbz = other_lehreinheit.studiensemester_kurzbz
AND other_slv.student_uid = current_studentlehrverband.student_uid
AND (other_lehreinheitgruppe.verband = other_slv.verband OR other_lehreinheitgruppe.verband IS NULL OR btrim(other_lehreinheitgruppe.verband::text) = '' OR other_slv.verband IS NULL)
AND (other_lehreinheitgruppe.gruppe = other_slv.gruppe OR other_lehreinheitgruppe.gruppe IS NULL OR btrim(other_lehreinheitgruppe.gruppe::text) = '' OR other_slv.gruppe IS NULL)
WHERE current_kalender.kalender_id IN ($placeholders)
";
$result = $dbModel->execReadOnlyQuery($sql, array_merge($kalender_ids, $kalender_ids));
if (isError($result) || !hasData($result)) return [];
$grouped = [];
foreach (getData($result) as $row)
{
$grouped[$row->kalender_id][] = true;
}
return $grouped;
}
}
@@ -0,0 +1,413 @@
<?php
class VerbandCollisionCheck implements ICollisionCheck
{
private $_ci;
public function __construct()
{
$this->_ci =& get_instance();
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->library('VariableLib', array('uid' => getAuthUID()));
$this->_ci->load->library('PhrasesLib', array('ui'));
}
public function getName()
{
return 'verband';
}
public function check($data)
{
if (!isset($data->von, $data->bis)) return [];
if (!isset($data->kalender_id) && !isset($data->lehreinheit_id)) return [];
if ($this->_ci->variablelib->getVar('ignore_kollision') === 'true') return [];
$kollision_student = $this->_ci->variablelib->getVar('kollision_student') === 'false';
$kollision_reservierung = $this->_ci->variablelib->getVar('ignore_reservierung') === 'false';
if (!isset($data->kalender_id)) $kollision_reservierung = false;
if (!$kollision_student && !$kollision_reservierung) return [];
$dbModel = new DB_Model();
$collisions = [];
$group_matching_qry = "
(
(
current_lehreinheitguppe.gruppe_kurzbz IS NULL
AND other_lehreinheitguppe.gruppe_kurzbz IS NULL
AND (
current_lehreinheitguppe.verband IS NULL
OR (
current_lehreinheitguppe.verband = other_lehreinheitguppe.verband
AND (current_lehreinheitguppe.gruppe IS NULL OR other_lehreinheitguppe.gruppe IS NULL OR current_lehreinheitguppe.gruppe = other_lehreinheitguppe.gruppe)
)
)
)
OR
(
current_lehreinheitguppe.gruppe_kurzbz IS NOT NULL
AND other_lehreinheitguppe.gruppe_kurzbz IS NOT NULL
AND current_gruppe.direktinskription IS NOT TRUE
AND other_gruppe.direktinskription IS NOT TRUE
)
OR
(
(
current_lehreinheitguppe.gruppe_kurzbz IS NULL
AND other_lehreinheitguppe.gruppe_kurzbz IS NOT NULL
AND other_gruppe.direktinskription IS NOT TRUE
)
OR
(
current_lehreinheitguppe.gruppe_kurzbz IS NOT NULL
AND other_lehreinheitguppe.gruppe_kurzbz IS NULL
AND current_gruppe.direktinskription IS NOT TRUE
)
)
)
";
if ($kollision_student)
{
$union_event = "";
if ($kollision_reservierung)
{
$union_event = "
UNION
SELECT tbl_kalender_event_teilnehmer.studiengang_kz, tbl_kalender_event_teilnehmer.semester, tbl_kalender_event_teilnehmer.verband, tbl_kalender_event_teilnehmer.gruppe, tbl_kalender_event_teilnehmer.gruppe_kurzbz, tbl_kalender_event_teilnehmer.kalender_id
FROM lehre.tbl_kalender_event_teilnehmer
WHERE tbl_kalender_event_teilnehmer.rolle_kurzbz = 'teilnehmer'
";
}
$other_lehreinheitgruppe_subquery = "
SELECT tbl_lehreinheitgruppe.studiengang_kz, tbl_lehreinheitgruppe.semester, tbl_lehreinheitgruppe.verband, tbl_lehreinheitgruppe.gruppe,
tbl_lehreinheitgruppe.gruppe_kurzbz, tbl_kalender_lehreinheit.kalender_id
FROM lehre.tbl_kalender_lehreinheit
JOIN lehre.tbl_lehreinheit ON tbl_lehreinheit.lehreinheit_id = tbl_kalender_lehreinheit.lehreinheit_id
JOIN lehre.tbl_lehreinheitgruppe ON tbl_lehreinheitgruppe.lehreinheit_id = tbl_lehreinheit.lehreinheit_id
". $union_event ."
";
if (isset($data->kalender_id))
{
$sql_gruppen = "
SELECT
other_kalender.von,
other_kalender.bis,
COALESCE(
other_lehreinheitguppe.gruppe_kurzbz,
UPPER(stg.typ::text || stg.kurzbz::text) || '-' || other_lehreinheitguppe.semester ||
COALESCE(other_lehreinheitguppe.verband::text, '') ||
COALESCE(other_lehreinheitguppe.gruppe::text, '')
) AS gruppenname
FROM lehre.tbl_kalender current_kalender
JOIN (
". $other_lehreinheitgruppe_subquery ."
) current_lehreinheitguppe ON current_lehreinheitguppe.kalender_id = current_kalender.kalender_id
LEFT JOIN public.tbl_gruppe current_gruppe
ON current_gruppe.gruppe_kurzbz = current_lehreinheitguppe.gruppe_kurzbz
JOIN lehre.tbl_kalender other_kalender
ON other_kalender.kalender_id != current_kalender.kalender_id
AND other_kalender.von < ?
AND other_kalender.bis > ?
JOIN (
". $other_lehreinheitgruppe_subquery ."
) other_lehreinheitguppe ON other_lehreinheitguppe.kalender_id = other_kalender.kalender_id
LEFT JOIN public.tbl_gruppe other_gruppe
ON other_gruppe.gruppe_kurzbz = other_lehreinheitguppe.gruppe_kurzbz
LEFT JOIN public.tbl_studiengang stg
ON stg.studiengang_kz = other_lehreinheitguppe.studiengang_kz
WHERE current_kalender.kalender_id = ?
AND other_kalender.status_kurzbz NOT IN ('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview')
AND current_lehreinheitguppe.studiengang_kz = other_lehreinheitguppe.studiengang_kz
AND current_lehreinheitguppe.semester = other_lehreinheitguppe.semester
AND ". $group_matching_qry ."
AND other_kalender.kalender_id NOT IN (
SELECT vorgaenger_kalender_id
FROM lehre.tbl_kalender
WHERE vorgaenger_kalender_id IS NOT NULL
)
";
$params = [$data->bis, $data->von, $data->kalender_id];
}
else
{
$sql_gruppen = "
SELECT
other_kalender.von,
other_kalender.bis,
COALESCE(
other_lehreinheitguppe.gruppe_kurzbz,
UPPER(stg.typ::text || stg.kurzbz::text) || '-' || other_lehreinheitguppe.semester ||
COALESCE(other_lehreinheitguppe.verband::text, '') ||
COALESCE(other_lehreinheitguppe.gruppe::text, '')
) AS gruppenname
FROM lehre.tbl_lehreinheitgruppe current_lehreinheitguppe
LEFT JOIN public.tbl_gruppe current_gruppe
ON current_gruppe.gruppe_kurzbz = current_lehreinheitguppe.gruppe_kurzbz
JOIN lehre.tbl_kalender other_kalender
ON other_kalender.von < ?
AND other_kalender.bis > ?
JOIN (
". $other_lehreinheitgruppe_subquery ."
) other_lehreinheitguppe ON other_lehreinheitguppe.kalender_id = other_kalender.kalender_id
LEFT JOIN public.tbl_gruppe other_gruppe
ON other_gruppe.gruppe_kurzbz = other_lehreinheitguppe.gruppe_kurzbz
LEFT JOIN public.tbl_studiengang stg
ON stg.studiengang_kz = other_lehreinheitguppe.studiengang_kz
WHERE current_lehreinheitguppe.lehreinheit_id = ?
AND other_kalender.status_kurzbz NOT IN ('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview')
AND current_lehreinheitguppe.studiengang_kz = other_lehreinheitguppe.studiengang_kz
AND current_lehreinheitguppe.semester = other_lehreinheitguppe.semester
AND ". $group_matching_qry ."
AND other_kalender.kalender_id NOT IN (
SELECT vorgaenger_kalender_id
FROM lehre.tbl_kalender
WHERE vorgaenger_kalender_id IS NOT NULL
)
";
$params = [$data->bis, $data->von, $data->lehreinheit_id];
}
$result = $dbModel->execReadOnlyQuery($sql_gruppen, $params);
if (!isError($result) && hasData($result))
{
foreach (getData($result) as $row)
{
$collisions[] = [
'message' => $this->_ci->phraseslib->t('ui', 'verband_kollision') . ': ' . $row->gruppenname . ' (' . date('d.m.Y H:i', strtotime($row->von)) . ' - ' . date('d.m.Y H:i', strtotime($row->bis)) . ')',
'errorCode' => 'verband_collision',
];
}
}
}
if ($kollision_reservierung && !$kollision_student)
{
$sql_reservierung = "
SELECT
other_kalender.von,
other_kalender.bis,
COALESCE(
other_event_teilnehmer.gruppe_kurzbz,
UPPER(stg.typ::text || stg.kurzbz::text) || '-' || other_event_teilnehmer.semester ||
COALESCE(other_event_teilnehmer.verband::text, '') ||
COALESCE(other_event_teilnehmer.gruppe::text, '')
) AS gruppenname
FROM lehre.tbl_kalender_event_teilnehmer current_event_teilnehmer
LEFT JOIN public.tbl_gruppe current_gruppe
ON current_gruppe.gruppe_kurzbz = current_event_teilnehmer.gruppe_kurzbz
JOIN lehre.tbl_kalender other_kalender
ON other_kalender.kalender_id != ?
AND other_kalender.von < ?
AND other_kalender.bis > ?
JOIN lehre.tbl_kalender_event_teilnehmer other_event_teilnehmer
ON other_event_teilnehmer.kalender_id = other_kalender.kalender_id
AND other_event_teilnehmer.rolle_kurzbz = 'teilnehmer'
LEFT JOIN public.tbl_gruppe other_gruppe
ON other_gruppe.gruppe_kurzbz = other_event_teilnehmer.gruppe_kurzbz
LEFT JOIN public.tbl_studiengang stg
ON stg.studiengang_kz = other_event_teilnehmer.studiengang_kz
WHERE current_event_teilnehmer.kalender_id = ?
AND current_event_teilnehmer.rolle_kurzbz = 'teilnehmer'
AND other_kalender.status_kurzbz NOT IN ('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview')
AND current_event_teilnehmer.studiengang_kz = other_event_teilnehmer.studiengang_kz
AND current_event_teilnehmer.semester = other_event_teilnehmer.semester
AND (
(
current_event_teilnehmer.gruppe_kurzbz IS NULL
AND other_event_teilnehmer.gruppe_kurzbz IS NULL
AND (
current_event_teilnehmer.verband IS NULL
OR (
current_event_teilnehmer.verband = other_event_teilnehmer.verband
AND (current_event_teilnehmer.gruppe IS NULL OR other_event_teilnehmer.gruppe IS NULL OR current_event_teilnehmer.gruppe = other_event_teilnehmer.gruppe)
)
)
)
OR
(
current_event_teilnehmer.gruppe_kurzbz IS NOT NULL
AND other_event_teilnehmer.gruppe_kurzbz IS NOT NULL
AND current_gruppe.direktinskription IS NOT TRUE
AND other_gruppe.direktinskription IS NOT TRUE
)
OR
(
(
current_event_teilnehmer.gruppe_kurzbz IS NULL
AND other_event_teilnehmer.gruppe_kurzbz IS NOT NULL
AND other_gruppe.direktinskription IS NOT TRUE
)
OR
(
current_event_teilnehmer.gruppe_kurzbz IS NOT NULL
AND other_event_teilnehmer.gruppe_kurzbz IS NULL
AND current_gruppe.direktinskription IS NOT TRUE
)
)
)
AND other_kalender.kalender_id NOT IN (
SELECT vorgaenger_kalender_id
FROM lehre.tbl_kalender
WHERE vorgaenger_kalender_id IS NOT NULL
)
";
$result = $dbModel->execReadOnlyQuery($sql_reservierung, [
$data->kalender_id,
$data->bis,
$data->von,
$data->kalender_id,
]);
if (!isError($result) && hasData($result))
{
foreach (getData($result) as $row)
{
$collisions[] = [
'errorCode' => 'reservation_collision',
'message' => $this->_ci->phraseslib->t('ui', 'reservierung_kollision') . ': ' . $row->gruppenname . ' (' . date('d.m.Y H:i', strtotime($row->von)) . ' - ' . date('d.m.Y H:i', strtotime($row->bis)) . ')'
];
}
}
}
return $collisions;
}
public function checkAll($kalender_ids)
{
if (empty($kalender_ids)) return [];
$dbModel = new DB_Model();
$placeholders = implode(',', array_fill(0, count($kalender_ids), '?'));
$sql = "
SELECT DISTINCT ON (current_kalender.kalender_id) current_kalender.kalender_id
FROM lehre.tbl_kalender current_kalender
JOIN (
SELECT tbl_lehreinheitgruppe.studiengang_kz, tbl_lehreinheitgruppe.semester, tbl_lehreinheitgruppe.verband, tbl_lehreinheitgruppe.gruppe,
tbl_lehreinheitgruppe.gruppe_kurzbz, tbl_kalender_lehreinheit.kalender_id
FROM lehre.tbl_kalender_lehreinheit
JOIN lehre.tbl_lehreinheit ON tbl_lehreinheit.lehreinheit_id = tbl_kalender_lehreinheit.lehreinheit_id
JOIN lehre.tbl_lehreinheitgruppe ON tbl_lehreinheitgruppe.lehreinheit_id = tbl_lehreinheit.lehreinheit_id
UNION
SELECT tbl_kalender_event_teilnehmer.studiengang_kz, tbl_kalender_event_teilnehmer.semester, tbl_kalender_event_teilnehmer.verband, tbl_kalender_event_teilnehmer.gruppe,
tbl_kalender_event_teilnehmer.gruppe_kurzbz, tbl_kalender_event_teilnehmer.kalender_id
FROM lehre.tbl_kalender_event_teilnehmer
) current_lehreinheitguppe ON current_lehreinheitguppe.kalender_id = current_kalender.kalender_id
LEFT JOIN public.tbl_gruppe current_gruppe
ON current_gruppe.gruppe_kurzbz = current_lehreinheitguppe.gruppe_kurzbz
JOIN lehre.tbl_kalender other_kalender
ON other_kalender.kalender_id != current_kalender.kalender_id
AND other_kalender.von < current_kalender.bis
AND other_kalender.bis > current_kalender.von
JOIN (
SELECT tbl_lehreinheitgruppe.studiengang_kz, tbl_lehreinheitgruppe.semester, tbl_lehreinheitgruppe.verband, tbl_lehreinheitgruppe.gruppe,
tbl_lehreinheitgruppe.gruppe_kurzbz, tbl_kalender_lehreinheit.kalender_id
FROM lehre.tbl_kalender_lehreinheit
JOIN lehre.tbl_lehreinheit ON tbl_lehreinheit.lehreinheit_id = tbl_kalender_lehreinheit.lehreinheit_id
JOIN lehre.tbl_lehreinheitgruppe ON tbl_lehreinheitgruppe.lehreinheit_id = tbl_lehreinheit.lehreinheit_id
UNION
SELECT tbl_kalender_event_teilnehmer.studiengang_kz, tbl_kalender_event_teilnehmer.semester, tbl_kalender_event_teilnehmer.verband, tbl_kalender_event_teilnehmer.gruppe,
tbl_kalender_event_teilnehmer.gruppe_kurzbz, tbl_kalender_event_teilnehmer.kalender_id
FROM lehre.tbl_kalender_event_teilnehmer
) other_lehreinheitguppe ON other_lehreinheitguppe.kalender_id = other_kalender.kalender_id
LEFT JOIN public.tbl_gruppe other_gruppe
ON other_gruppe.gruppe_kurzbz = other_lehreinheitguppe.gruppe_kurzbz
WHERE current_kalender.kalender_id IN ({$placeholders})
AND other_kalender.status_kurzbz NOT IN ('archived', 'deleted', 'to_delete', 'to_delete_live', 'to_delete_preview')
AND current_lehreinheitguppe.studiengang_kz = other_lehreinheitguppe.studiengang_kz
AND current_lehreinheitguppe.semester = other_lehreinheitguppe.semester
AND (
(
current_lehreinheitguppe.gruppe_kurzbz IS NULL
AND other_lehreinheitguppe.gruppe_kurzbz IS NULL
AND (
current_lehreinheitguppe.verband IS NULL
OR (
current_lehreinheitguppe.verband = other_lehreinheitguppe.verband
AND (current_lehreinheitguppe.gruppe IS NULL OR other_lehreinheitguppe.gruppe IS NULL OR current_lehreinheitguppe.gruppe = other_lehreinheitguppe.gruppe)
)
)
)
OR
(
current_lehreinheitguppe.gruppe_kurzbz IS NOT NULL
AND other_lehreinheitguppe.gruppe_kurzbz IS NOT NULL
AND current_gruppe.direktinskription IS NOT TRUE
AND other_gruppe.direktinskription IS NOT TRUE
)
OR
(
(
current_lehreinheitguppe.gruppe_kurzbz IS NULL
AND other_lehreinheitguppe.gruppe_kurzbz IS NOT NULL
AND other_gruppe.direktinskription IS NOT TRUE
)
OR
(
current_lehreinheitguppe.gruppe_kurzbz IS NOT NULL
AND other_lehreinheitguppe.gruppe_kurzbz IS NULL
AND current_gruppe.direktinskription IS NOT TRUE
)
)
)
AND other_kalender.kalender_id NOT IN (
SELECT vorgaenger_kalender_id
FROM lehre.tbl_kalender
WHERE vorgaenger_kalender_id IS NOT NULL
)
";
$result = $dbModel->execReadOnlyQuery($sql, $kalender_ids);
if (isError($result) || !hasData($result)) return [];
$grouped = [];
foreach (getData($result) as $row)
{
$grouped[$row->kalender_id][] = true;
}
return $grouped;
}
}
@@ -0,0 +1,148 @@
<?php
/**
* Copyright (C) 2026 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
* Tempus Menu library
*/
class TempusLib
{
protected $_ci = null;
public $config = [];
public function __construct()
{
// Get code igniter instance
$this->_ci =& get_instance();
// Load Tree-config
$this->_ci->load->config('treemenu/custom/tempus');
$this->config = $this->_ci->config->item('root');
// Load Libraries
$this->_ci->load->library('treemenu/base/StgLib');
// Load Model
$this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel');
}
public function stg($path_template)
{
$permittedStgs = $this->getPermittedStudiengangKzs();
if (!$permittedStgs)
return [];
return $this->_ci->stglib->studiengang($path_template, $permittedStgs);
}
public function semester($path_template, $has_children, $stg)
{
$stg = $this->getStgFromUrl($stg);
if ($stg === null)
return [];
if (!$has_children)
$this->_ci->StudiengangModel->db->select('TRUE AS leaf', false);
return $this->_ci->stglib->semester($path_template, $stg->studiengang_kz);
}
public function verband($path_template, $stg, $semester)
{
$stg = $this->getStgFromUrl($stg);
if ($stg === null)
return [];
$this->_ci->StudiengangModel->addSelect("CASE WHEN MAX(gruppe)='' OR MAX(gruppe)=' ' THEN TRUE ELSE FALSE END AS leaf");
$this->_ci->StudiengangModel->addGroupBy('v.studiengang_kz, v.semester');
return $this->_ci->stglib->verband($path_template, $stg->studiengang_kz, $semester);
}
public function group($path_template, $stg, $semester, $verband)
{
$stg = $this->getStgFromUrl($stg);
if ($stg === null)
return [];
$this->_ci->StudiengangModel->db->select('TRUE AS leaf', false);
if ($verband === null) {
$this->_ci->load->model('organisation/Gruppe_model', 'GruppeModel');
$this->_ci->GruppeModel->addSelect('semester');
return $this->_ci->stglib->group($path_template, $stg->studiengang_kz, $semester);
}
return $this->_ci->stglib->verbandsgroup($path_template, $stg->studiengang_kz, $semester, $verband);
}
public function orgform($path_template, $stg)
{
$stg = $this->getStgFromUrl($stg);
if ($stg === null)
return [];
if (!$stg->mischform)
return [];
$this->_ci->StudiengangModel->db->select('TRUE AS leaf', false);
$this->_ci->load->model('organisation/Studienordnung_model', 'StudienordnungModel');
$this->_ci->StudienordnungModel->addSelect("p.orgform_kurzbz");
return $this->_ci->stglib->orgform($path_template, $stg->studiengang_kz);
}
protected function getPermittedStudiengangKzs()
{
$permittedStgs = $this->_ci->permissionlib->getSTG_isEntitledFor('admin') ?: [];
$permittedStgs = array_merge($permittedStgs, $this->_ci->permissionlib->getSTG_isEntitledFor('assistenz') ?: []);
$permittedStgs = array_merge($permittedStgs, $this->_ci->permissionlib->getSTG_isEntitledFor('lehre/lvplan') ?: []);
return $permittedStgs;
}
protected function getStgFromUrl($stg)
{
$permittedStgs = $this->getPermittedStudiengangKzs();
if (!$permittedStgs)
return null;
$this->_ci->StudiengangModel->db->where_in('studiengang_kz', $permittedStgs);
$stg = $this->_ci->StudiengangModel->loadWhere([
'LOWER(CONCAT(typ, kurzbz)) =' => $stg
]);
if (!hasData($stg))
return null;
return current(getData($stg));
}
}
@@ -395,6 +395,80 @@ EOSQL;
return $this->execReadOnlyQuery($qry, $params);
}
public function getOffeneStunden($lehreinheit_ids)
{
if (!is_array($lehreinheit_ids))
$lehreinheit_ids = [$lehreinheit_ids];
if (empty($lehreinheit_ids))
return success([]);
$db = new DB_Model();
$placeholders = implode(',', array_fill(0, count($lehreinheit_ids), '?'));
$qry = "WITH eventdauer AS (
SELECT
tbl_kalender_lehreinheit.lehreinheit_id,
tbl_kalender.von,
tbl_kalender.bis,
tbl_kalender.kalender_id
FROM lehre.tbl_lehreinheit
JOIN lehre.tbl_kalender_lehreinheit USING(lehreinheit_id)
JOIN lehre.tbl_kalender USING(kalender_id)
WHERE tbl_lehreinheit.lehreinheit_id IN ($placeholders)
AND NOT EXISTS (SELECT 1 FROM lehre.tbl_kalender nachfolger
WHERE nachfolger.vorgaenger_kalender_id = tbl_kalender.kalender_id)
AND tbl_kalender.status_kurzbz NOT IN ('deleted', 'todelete', 'archived')
GROUP BY tbl_kalender_lehreinheit.lehreinheit_id, tbl_kalender.von, tbl_kalender.bis, tbl_kalender.kalender_id
),
raster AS (
SELECT
eventdauer.lehreinheit_id,
eventdauer.kalender_id,
eventdauer.von,
eventdauer.bis,
vonstunde.stunde AS vonstunde_nr,
bisstunde.stunde AS bisstunde_nr
FROM eventdauer
LEFT JOIN lehre.tbl_stunde vonstunde ON eventdauer.von::time = vonstunde.beginn
LEFT JOIN lehre.tbl_stunde bisstunde ON eventdauer.bis::time = bisstunde.ende
),
unterrichtsdauer AS (
SELECT
raster.lehreinheit_id,
raster.kalender_id,
sum(EXTRACT(EPOCH FROM (LEAST(stunde.ende, raster.bis::time) - GREATEST(stunde.beginn, raster.von::time))) / 60) AS minuten
FROM raster JOIN lehre.tbl_stunde stunde ON stunde.beginn < raster.bis::time AND stunde.ende > raster.von::time
WHERE raster.vonstunde_nr IS NULL OR raster.bisstunde_nr IS NULL
GROUP BY raster.lehreinheit_id, raster.kalender_id
),
verplantestunden AS (
SELECT raster.lehreinheit_id, raster.kalender_id,
CASE
WHEN raster.vonstunde_nr IS NOT NULL AND raster.bisstunde_nr IS NOT NULL
THEN (raster.bisstunde_nr - raster.vonstunde_nr + 1)
ELSE coalesce(unterrichtsdauer.minuten, 0) / 45
END AS verplantestunde
FROM raster LEFT JOIN unterrichtsdauer ON unterrichtsdauer.lehreinheit_id = raster.lehreinheit_id AND unterrichtsdauer.kalender_id = raster.kalender_id
),
verplant_gesamt AS (
SELECT lehreinheit_id, SUM(verplantestunde) AS verplant
FROM verplantestunden
GROUP BY lehreinheit_id
)
SELECT
lema.lehreinheit_id,
lema.mitarbeiter_uid,
lema.planstunden,
ROUND(coalesce(verplant_gesamt.verplant, 0), 2) AS verplant,
ROUND((lema.planstunden - coalesce(verplant_gesamt.verplant, 0)), 2) AS offenestunden
FROM lehre.tbl_lehreinheitmitarbeiter lema
LEFT JOIN verplant_gesamt ON verplant_gesamt.lehreinheit_id = lema.lehreinheit_id
WHERE lema.lehreinheit_id IN ($placeholders)";
return $db->execReadOnlyQuery($qry, array_merge($lehreinheit_ids, $lehreinheit_ids));
}
private function getLVTmp($stg_kz = null)
{
$qry = "SELECT DISTINCT ON(lehrveranstaltung_id) *,
@@ -11,4 +11,26 @@ class Gruppe_model extends DB_Model
$this->dbTable = 'public.tbl_gruppe';
$this->pk = 'gruppe_kurzbz';
}
public function search($query_words)
{
$this->addSelect('gruppe_kurzbz,
studiengang_kz,
semester,
bezeichnung,
gid,
\'false\' as lehrverband');
$this->db->where(array('sichtbar' => true, 'aktiv' => true, 'lehre' => true, 'direktinskription' => false, 'semester IS NOT NULL' => null));
$this->db->group_start();
foreach ($query_words as $word)
{
$this->db->group_start();
$this->db->where('gruppe_kurzbz ILIKE', "%" . $word . "%");
$this->db->or_where('bezeichnung ILIKE', "%" . $word . "%");
$this->db->group_end();
}
$this->db->group_end();
return $this->load();
}
}
@@ -41,4 +41,29 @@ class Lehrverband_model extends DB_Model
return $result;
}
public function search($query_words)
{
$this->addSelect('CONCAT(UPPER(CONCAT(typ, kurzbz)), \'\', semester, verband, COALESCE(gruppe,\'\')) as gruppe_kurzbz,
studiengang_kz,
semester,
tbl_lehrverband.bezeichnung,
gid,
\'true\' as lehrverband');
$this->addJoin('public.tbl_studiengang', 'studiengang_kz');
$this->addOrder('verband');
$this->addOrder('gruppe');
$this->db->where(array('tbl_lehrverband.aktiv' => true));
$this->db->group_start();
foreach ($query_words as $word)
{
$this->db->group_start();
$this->db->where('CONCAT(CONCAT(typ, kurzbz), \'\', semester, verband, COALESCE(gruppe,\'\')) ILIKE', "%" . $word . "%");
$this->db->or_where('tbl_lehrverband.bezeichnung ILIKE', "%" . $word . "%");
$this->db->group_end();
}
$this->db->group_end();
return $this->load();
}
}
@@ -0,0 +1,14 @@
<?php
class BetriebsmittelKalender_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'lehre.tbl_betriebsmittel_kalender';
$this->pk = 'betriebsmittel_kalender_id';
}
}
@@ -31,4 +31,26 @@ class Betriebsmittel_model extends DB_Model
return $this->execQuery($qry);
}
public function getSchedulableEntriesByDatetimeInterval($from, $to)
{
$qry = "SELECT
*
FROM
wawi.tbl_betriebsmittel
WHERE
verplanen=true
AND
NOT EXISTS(
SELECT 1 FROM lehre.tbl_betriebsmittel_kalender
JOIN lehre.tbl_kalender ON tbl_kalender.eindeutige_kalender_gruppen_id = tbl_betriebsmittel_kalender.eindeutige_kalender_gruppen_id
WHERE
betriebsmittel_id=tbl_betriebsmittel.betriebsmittel_id
AND tbl_kalender.von < ?
AND tbl_kalender.bis > ?
)";
return $this->execQuery($qry, array($to, $from));
}
}
@@ -0,0 +1,15 @@
<?php
class Kalender_Event_Rolle_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'lehre.tbl_kalender_event_rolle';
$this->pk = array('rolle_kurzbz');
$this->hasSequence = false;
}
}
@@ -0,0 +1,16 @@
<?php
class
Kalender_Event_Teilnehmer_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'lehre.tbl_kalender_event_teilnehmer';
$this->pk = array('kalender_event_teilnehmer_id');
$this->hasSequence = false;
}
}
@@ -0,0 +1,15 @@
<?php
class Kalender_Event_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'lehre.tbl_kalender_event';
$this->pk = array('kalender_id');
$this->hasSequence = false;
}
}
@@ -0,0 +1,15 @@
<?php
class Kalender_Lehreinheit_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'lehre.tbl_kalender_lehreinheit';
$this->pk = array('kalender_id','lehreinheit_id');
$this->hasSequence = false;
}
}
@@ -0,0 +1,14 @@
<?php
class Kalender_Ort_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'lehre.tbl_kalender_ort';
$this->pk = 'kalender_ort_id';
}
}
@@ -0,0 +1,25 @@
<?php
class Kalender_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'lehre.tbl_kalender';
$this->pk = 'kalender_id';
}
public function generateUniqueGroupId()
{
while (true) {
$uniqueGroupId = generateUUID();
$result = $this->loadWhere(['eindeutige_kalender_gruppen_id' => $uniqueGroupId]);
if (!hasData($result)) {
return $uniqueGroupId;
}
}
}
}
@@ -0,0 +1,14 @@
<?php
class Kalenderstatus_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'lehre.tbl_kalender_status';
$this->pk = 'status_kurzbz';
}
}
@@ -0,0 +1,14 @@
<?php
class Kalendersyncstatus_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'lehre.tbl_kalender_syncstatus';
$this->pk = 'kalender_syncstatus_id';
}
}
@@ -0,0 +1,14 @@
<?php
class Reservierung_Kalender_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'sync.tbl_reservierung_kalender';
$this->pk = 'reservierung_kalender_id';
}
}
@@ -0,0 +1,14 @@
<?php
class Stundenplandev_Kalender_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'sync.tbl_stundenplandev_kalender';
$this->pk = 'stundenplandev_kalender_id';
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
$includesArray = array(
'title' => 'Tempus',
'axios027' => true,
'bootstrap5' => true,
'fontawesome6' => true,
'vue3' => true,
'primevue3' => true,
'tabulator5' => true,
'filtercomponent' => true,
'navigationcomponent' => true,
'vuedatepicker11' => true,
'tags' => true,
'phrases' => array(
'global',
'ui',
'lehre'
),
'customCSSs' => [
'public/css/components/vue-datepicker.css',
'public/css/components/primevue.css',
'public/css/components/calendar.css',
'public/css/Tempus.css',
'public/css/Studentenverwaltung.css',
'public/css/components/function.css'
],
'customJSs' => [
'vendor/moment/luxonjs/luxon.min.js',
'vendor/npm-asset/primevue/listbox/listbox.min.js'
],
'customJSModules' => [
'public/js/apps/Tempus.js'
]
);
$this->load->view('templates/FHC-Header', $includesArray);
?>
<div id="main">
<router-view
default-semester="<?= $variables['semester_aktuell']; ?>"
active-addons="<?= defined('ACTIVE_ADDONS') ? ACTIVE_ADDONS : ''; ?>"
tempus-root="<?= site_url('Tempus'); ?>"
cis-root="<?= CIS_ROOT; ?>"
avatar-url="<?= site_url('Cis/Pub/bild/person/' . getAuthPersonId()); ?>"
logout-url="<?= site_url('Cis/Auth/logout'); ?>"
:permissions="<?= htmlspecialchars(json_encode($permissions)); ?>"
:config="<?= htmlspecialchars(json_encode($variables)); ?>"
>
</router-view>
</div>
<?php $this->load->view('templates/FHC-Footer', $includesArray); ?>
+12
View File
@@ -97,3 +97,15 @@
display: none;
}
.weekPageContainer .calendar-event-collisions
{
display: grid;
grid-template-columns: auto 1fr;
background: repeating-linear-gradient(
135deg,
transparent,
transparent 16px,
rgba(0, 0, 0, 0.3) 16px,
rgba(0, 0, 0, 0.3) 32px
);
}
+2
View File
@@ -8,6 +8,8 @@
@import './components/Notiz.css';
@import './components/Messages.css';
@import './components/AppMenu.css';
@import './components/horizontalsplit.css';
html {
font-size: .875em;
+216
View File
@@ -0,0 +1,216 @@
@import './Fhc.css';
@import './components/searchbar/searchbar.css';
@import './components/verticalsplit.css';
@import './components/FilterComponent.css';
@import './components/Tabs.css';
@import './components/Notiz.css';
html {
font-size: .875em;
height: 100%;
}
body {
/*display: flex;*/
height: 100%;
}
.heightfull {
height: 95%;
}
.navbar-dark .navbar-brand:focus {
box-shadow: 0 0 0 .25rem rgba(13,110,253,.25);
z-index: 3;
}
#main {
height: 100%;
}
.tempus {
height: 100%;
}
.searchbar {
margin-right: 0!important;
}
.searchbar > .input-group {
margin-right: 0!important;
}
.searchbar > .input-group > * {
border-radius: 0!important;
}
#sidebarMenu {
width: 0%;
}
.tabulator-row.disabled.tabulator-row-odd .tabulator-cell {
color: var(--gray-400);
}
.tabulator-row.disabled.tabulator-row-even .tabulator-cell {
color: var(--gray-500);
}
/* Dropdown Toolbar Interessent, submenu */
.dropend .dropdown-toggle.d-flex::after {
height: 0;
}
@media (min-width: 768px) {
#sidebarMenu {
visibility: visible!important;
transform: none;
position: inherit;
z-index: 1;
}
}
.toast.toast-success {
color: #0f5132;
background-color: #d1e7dd!important;
border-color: #badbcc!important;
}
.toast.toast-danger {
color: #842029;
background-color: #f8d7da!important;
border-color: #f5c2c7!important;
}
.has-filter .fa-filter {
color: var(--bs-success);
}
#parkingslot {
border: 1px dashed;
min-height: 5vh;
max-height: 15vh;
color: #AAAAAA;
text-align: center;
font-size: large;
margin-top: 1.25rem;
overflow-y: auto;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.course-picker {
border: 1px dashed #AAAAAA;
display: flex;
flex-direction: column;
flex: 1 1 0;
min-height: 0;
}
.course-picker-row {
padding: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid var(--bs-border-color);
background-color: var(--event-bg);
}
.course-parker {
position: relative;
border: 1px dashed !important;
padding: 0.5rem;
margin: 0.5rem;
background-color: var(--event-bg);
color: var(--event-color, var(--fhc-calendar-color-event, #000));
}
.unpark-btn {
position: absolute;
top: 2px;
right: 2px;
padding: 0 4px;
border: none;
background: transparent;
}
:root{
--fhc-calendar-pane-height: calc(100vh - 120px);
}
.eckerltest {
box-shadow: 3px 3px 3px #ccc;
}
.verband-selection {
border: 1px dashed #AAAAAA;
margin-bottom: 0.5rem;
max-height: 450px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.lecture-selection {
border: 1px dashed #AAAAAA;
margin-bottom: 0.5rem;
max-height: 20vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.room-selection {
border: 1px dashed #AAAAAA;
margin-bottom: 0.5rem;
padding: 0.5rem 0 0.5rem 0.5rem;
}
.btn-link.text-danger {
text-decoration: none;
font-weight: 600;
}
.btn-link.text-danger:hover {
color: #dc3545;
transform: scale(1.1);
}
.bg-lecturer-wish {
opacity: .15;
}
.bg-lecturer-block {
background: rgba(255, 0, 0, 0.15);
}
.bg-raumvorschlag-slot {
opacity: .70;
}
.bg-raumvorschlag-slot.rating-good {
background-color: var(--bs-success);
}
.bg-raumvorschlag-slot.rating-mid {
background-color: var(--bs-warning);
}
.bg-raumvorschlag-slot.rating-bad {
background-color: var(--bs-danger);
}
.wish-w--2 {
background-color: #FF2200;
}
.wish-w--1 {
background-color: #FF9922;
}
.wish-w-1 {
background-color: #CCFFCC;
}
.wish-w-2 {
background-color: #48FA66;
}
.room-option {
display: grid;
grid-template-columns: 20px minmax(80px, 110px) minmax(100px, 1fr) auto 20px 50px;
align-items: center;
width: 100%;
}
+40
View File
@@ -2,6 +2,10 @@
cursor: pointer;
font-size: var(--fhc-calendar-fontsize-event, .875rem);
}
.event--parked {
opacity: 0.45 !important;
}
.fhc-calendar-mode-day .fhc-calendar-base-grid-line,
.fhc-calendar-mode-week .fhc-calendar-base-grid-line {
padding: 0 var(--fhc-calendar-gap-events, var(--fhc-calendar-gap, 1px));
@@ -69,3 +73,39 @@
.fhc-calendar-base .event > *:hover {
filter: brightness(120%);
}
.fhc-resize-bar {
display: flex;
align-items: center;
justify-content: center;
height: 15px;
flex: 0 0 12px;
cursor: ns-resize;
position: relative;
visibility: hidden;
pointer-events: none;
background-color: transparent !important;
border: none !important;
width: fit-content;
margin-left: auto;
margin-right: auto;
}
.fhc-resize-bar--top {
margin-bottom: -16px;
}
.fhc-resize-bar--bottom {
margin-top: -16px;
}
.fhc-calendar-base-grid-line-event:hover > .fhc-resize-bar {
visibility: visible;
pointer-events: auto;
}
.fhc-calendar-base-grid-line-event.event:hover {
z-index: 50;
}
.fhc-resize-bar {
z-index: 2;
}
+7
View File
@@ -68,9 +68,16 @@
/* Grid */
.fhc-calendar-base-grid {
--fhc-calendar-axis-collapsible: .2fr;
--fhc-calendar-axis-collapsible-manual: .1fr;
background-color: var(--fhc-calendar-bg-gap);
grid-gap: var(--fhc-calendar-gap-header, calc(var(--fhc-calendar-gap, 1px) * 2));
}
.fhc-calendar-base-grid-line-collapsed {
overflow: hidden;
}
.fhc-calendar-base-grid .grid-header {
background-color: var(--fhc-calendar-bg-header-gap, var(--fhc-calendar-bg-gap));
}
+7
View File
@@ -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;
}
@@ -0,0 +1,38 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getSchedulableResourcesByCalendar(calendarID) {
return {
method: "get",
url: "/api/frontend/v1/tempus/OperationalResourceToCalenderAPI/getSchedulableResourcesByCalendar/" + calendarID,
};
},
getAssignedResourcesByCalender(calenderID) {
return {
method: "get",
url: "/api/frontend/v1/tempus/OperationalResourceToCalenderAPI/getAssignedResourcesByCalender/" + calenderID,
};
},
storeResourcesToCalendarRelationship(calenderID, assignedResources) {
return {
method: "post",
url: "/api/frontend/v1/tempus/OperationalResourceToCalenderAPI/storeResourcesToCalendarRelationship",
params: { kalender_id: calenderID, assignedResources: assignedResources },
};
}
};
+8
View File
@@ -9,4 +9,12 @@ export default {
}
};
},
loadTempusRenderers() {
return {
method: 'get',
url: '/api/frontend/v1/RendererLoader/GetTempusRenderers',
params: {
}
};
},
}
+44
View File
@@ -0,0 +1,44 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
get() {
return {
method: 'get',
url: 'api/frontend/v1/tempus/config/get'
};
},
getHeader() {
return {
method: 'get',
url: 'api/frontend/v1/tempus/config/getHeader'
};
},
set(params) {
return {
method: 'post',
url: 'api/frontend/v1/tempus/config/set',
params
};
},
updateCollision () {
return {
method: 'post',
url: 'api/frontend/v1/tempus/config/updateCollision'
};
}
};
@@ -0,0 +1,10 @@
export default {
getCourses(filter, studiensemester_kurzbz) {
return {
method: 'post',
url: 'api/frontend/v1/tempus/coursepicker/getCourses',
params: { ...filter, studiensemester_kurzbz }
};
},
};
+8
View File
@@ -0,0 +1,8 @@
export default {
getStudiengaenge() {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Info/getStudiengaenge',
};
},
};
+165
View File
@@ -0,0 +1,165 @@
export default {
getPlan(filter, start_date, end_date)
{
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/getPlan',
params: { ...filter, start_date, end_date }
};
},
getRaumvorschlagSlots(lehreinheit_id, start_date, end_date)
{
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getRaumvorschlagSlots',
params: { lehreinheit_id, start_date, end_date }
};
},
getPlanLecturer(start_date, end_date)
{
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getPlanLecturer',
params: { start_date, end_date }
};
},
getPlanStudent(start_date, end_date)
{
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getPlanStudent',
params: { start_date, end_date }
};
},
syncToLecturer(kalender_id)
{
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/syncToLecturer',
params: { kalender_id }
};
},
syncToStudent(kalender_id)
{
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/syncToStudent',
params: { kalender_id }
};
},
sync()
{
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/sync',
};
},
getLektorZeitsperren(emp, start_date, end_date) {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getZeitsperren',
params: { emp, start_date, end_date }
};
},
getLektorZeitwuensche(emp, start_date, end_date) {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getZeitwuensche',
params: { emp, start_date, end_date }
};
},
getStunden() {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getStunden',
};
},
getCalendarHours() {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getCalendarHours',
};
},
updateKalenderEvent(kalender_id, updatedInfos) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/updateKalenderEvent',
params: { kalender_id, updatedInfos}
};
},
deleteOrtEntry(kalender_id) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/deleteOrtEntry',
params: { kalender_id}
};
},
addKalenderEvent(lehreinheit_id, ort_kurzbz, start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/addKalenderEvent',
params: { lehreinheit_id, ort_kurzbz, start_date, end_date}
};
},
calculateMultiWeekPlan(lehreinheit_id, ort_kurzbz, start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/calculateMultiWeekPlan',
params: { lehreinheit_id, ort_kurzbz, start_date, end_date }
};
},
confirmMultiWeekPlan(plan, lehreinheit_id ) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/confirmMultiWeekPlan',
params: { plan, lehreinheit_id }
};
},
addToKalenderEvent(target_kalender_id, lehreinheit_id ) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/addToKalenderEvent',
params: { target_kalender_id, lehreinheit_id }
};
},
getRaumvorschlag(kalender_id) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/getRaumvorschlag',
params: { kalender_id }
};
},
getLehreinheiten(kalender_id) {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getLehreinheiten',
params: { kalender_id }
};
},
getHistory(kalender_id) {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getHistory',
params: { kalender_id }
};
},
deleteEntry(kalender_id) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/deleteEntry',
params: { kalender_id }
};
},
deleteFromKalenderEvent(kalender_id, lehreinheit_ids) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/deleteFromKalenderEvent',
params: { kalender_id, lehreinheit_ids }
};
},
};
@@ -0,0 +1,59 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getInformation() {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Reservierung/getInformation',
};
},
searchTeilnehmer(query)
{
return {
method: 'get',
url: `/api/frontend/v1/tempus/Reservierung/getLektor?query=${encodeURIComponent(query)}`
};
},
searchGroup(query)
{
return {
method: 'get',
url: `/api/frontend/v1/tempus/Reservierung/searchGroup?query=${encodeURIComponent(query)}`
};
},
getGruppen(query) {
return {
method: 'get',
url: `/api/frontend/v1/tempus/Reservierung/getGruppen?query=${encodeURIComponent(query)}`
};
},
getRollen() {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Reservierung/getRollen',
};
},
addReservierung(titel, beschreibung, orte, start_date, end_date, teilnehmer, specialFinalGroups, specialGroups, groups) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Reservierung/addReservierung',
params: { titel, beschreibung, orte, start_date, end_date, teilnehmer, specialFinalGroups, specialGroups, groups}
};
},
};
@@ -0,0 +1,43 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
get(path) {
let url = 'api/frontend/v1/lv/StgTree';
if (path)
url += '/' + path;
return {
method: 'get',
url
};
},
favorites: {
get() {
return {
method: 'get',
url: 'api/frontend/v1/tempus/favorites'
};
},
set(favorites) {
return {
method: 'post',
url: 'api/frontend/v1/tempus/favorites/set',
params: { favorites }
};
}
}
};
+83
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 }
};
},
};
+59
View File
@@ -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
};
},
};
+48
View File
@@ -0,0 +1,48 @@
/**
* 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 <https://www.gnu.org/licenses/>.
*/
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";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes: [
{ path: `/${ciPath}/Tempus`, component: FhcTempus },
{ path: `/${ciPath}/Tempus/sync`, component: FhcTempusSync },
]
});
const app = Vue.createApp();
app.config.globalProperties.$capitalize = capitalize;
app
.use(router)
.use(primevue.config.default, {
zIndex: {
overlay: 1100,
tooltip: 99999
}
})
.use(Phrasen)
.directive('tooltip', primevue.tooltip)
.mount('#main');
+21 -17
View File
@@ -79,26 +79,30 @@ export default {
this.$fhcAlert.alertSuccess(this.$p.t('ui/settings_saved'));
})
.catch(this.$fhcAlert.handleSystemErrors);
},
reload()
{
this.$api
.call(this.endpoints.get())
.then(res => {
Object.keys(res.data).forEach(key => {
const binding = { ...res.data[key] };
delete binding.value;
delete binding.options;
const options = res.data[key].options;
this.tempValues[key] = res.data[key].value;
this.setup[key] = {
binding,
options
};
});
this.$emit('update:modelValue', { ...this.tempValues });
})
.catch(this.$fhcAlert.handleSystemErrors);
}
},
created() {
this.$api
.call(this.endpoints.get())
.then(res => {
Object.keys(res.data).forEach(key => {
const binding = { ...res.data[key] };
delete binding.value;
delete binding.options;
const options = res.data[key].options;
this.tempValues[key] = res.data[key].value;
this.setup[key] = {
binding,
options
};
});
this.$emit('update:modelValue', { ...this.tempValues });
})
.catch(this.$fhcAlert.handleSystemErrors);
this.reload();
},
template: /* html */`
<fhc-form class="stv-config" ref="form" @submit.prevent="update">
+53 -18
View File
@@ -1,26 +1,35 @@
import BaseDraganddrop from './Base/DragAndDrop.js';
import BaseHeader from './Base/Header.js';
import BaseSlider from './Base/Slider.js';
import BsModal from '../Bootstrap/Modal.js';
import CalClick from '../../directives/Calendar/Click.js';
import DragClick from '../../directives/dragClick.js';
import Draggable from '../../directives/draggable.js';
import Drop from '../../directives/drop.js';
export default {
name: "CalendarBase",
components: {
BaseDraganddrop,
BaseHeader,
BaseSlider,
BsModal
},
directives: {
CalClick
CalClick,
DragClick,
Draggable,
Drop
},
provide() {
return {
events: Vue.computed(() => this.convertedEvents),
backgrounds: Vue.computed(() => this.convertedBackgrounds),
dropAllowed: Vue.computed(() => !!this.onDrop),
onDrop: Vue.computed(() => this.onDrop || null),
locale: Vue.computed(() => this.locale),
timezone: Vue.computed(() => this.timezone),
timeGrid: Vue.computed(() => this.timeGrid),
hoursPlan: Vue.computed(() => this.hoursPlan),
draggableEvents: Vue.computed(() => {
if (!this.draggableEvents)
return () => false;
@@ -43,8 +52,20 @@ export default {
return () => true;
}),
resizableEvents: Vue.computed(() => {
if (!this.resizableEvents)
return () => false;
if (Array.isArray(this.resizableEvents))
return event => this.resizableEvents.includes(event.type);
if (this.resizableEvents instanceof Function)
return this.resizableEvents;
return () => true;
}),
hasDragoverFunc: Vue.computed(() => this.onDragover),
mode: Vue.computed(() => this.mode)
mode: Vue.computed(() => this.mode),
onResize: Vue.computed(() => this.onResize || null),
};
},
props: {
@@ -93,11 +114,18 @@ export default {
type: Boolean,
default: undefined
},
btnTableList: {
type: Boolean,
default: undefined
},
timeGrid: Array,
hoursPlan: Object,
draggableEvents: [Boolean, Array, Function],
dropableEvents: [Boolean, Array, Function],
resizableEvents: [Boolean, Array, Function],
onDragover: Function,
onDrop: Function
onDrop: Function,
onResize: Function
},
emits: [
"click:next",
@@ -109,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
};
},
@@ -136,7 +166,7 @@ export default {
res.start = luxon.DateTime.fromMillis(res.start, { zone: this.timezone, locale: this.locale });
else if (res.start instanceof Date)
res.start = luxon.DateTime.fromJSDate(res.start, { zone: this.timezone, locale: this.locale });
else if (typeof res.start ===
else if (typeof res.start ===
'string' || res.start instanceof String)
res.start = luxon.DateTime.fromISO(res.start, { zone: this.timezone, locale: this.locale });
}
@@ -145,7 +175,7 @@ export default {
res.end = luxon.DateTime.fromMillis(res.end, { zone: this.timezone, locale: this.locale });
else if (res.end instanceof Date)
res.end = luxon.DateTime.fromJSDate(res.end, { zone: this.timezone, locale: this.locale });
else if (typeof res.end ===
else if (typeof res.end ===
'string' || res.end instanceof String)
res.end = luxon.DateTime.fromISO(res.end, { zone: this.timezone, locale: this.locale });
}
@@ -171,7 +201,10 @@ export default {
// choose default mode
let mode = this.mode;
if (mode)
mode = mode.toLowerCase();
{
const lower = mode.toLowerCase();
mode = Object.keys(this.modes).find(key => key.toLowerCase() === lower) || null;
}
if (!mode || !this.modes[mode])
mode = Object.keys(this.modes).find(Boolean); // start with first entry as active mode
return mode || '';
@@ -188,7 +221,7 @@ export default {
},
watch: {
sDate(n, o) {
if (this.sDate.isValid && !this.sDate.hasSame(this.internalDate, 'day'))
if (this.sDate.isValid && (!this.internalDate || !this.sDate.hasSame(this.internalDate, 'day')))
this.internalDate = this.sDate;
},
sMode() {
@@ -240,8 +273,10 @@ export default {
break;
}
},
onDropItem(evt, start, end) {
this.$emit('drop', evt, start, end);
handleDateRange({ start, end }) {
this.pickedRangeEnd = end;
this.$emit('update:date-range', { start, end });
},
showEventModal(eventObj) {
this.modalEvent = eventObj;
@@ -263,11 +298,7 @@ export default {
},
template: /* html */`
<div class="fhc-calendar-base h-100">
<base-draganddrop
class="card h-100"
:events="convertedEvents"
:backgrounds="convertedBackgrounds"
@drop="onDropItem"
<div class="card h-100"
v-cal-click:container
@cal-click-default.capture="handleClickDefaults"
>
@@ -275,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)"
@@ -282,6 +314,7 @@ export default {
:btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))"
:btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))"
:btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))"
:btn-table-list="!!modes['tableList'] && (btnTableList || (showBtns && btnTableList !== false))"
:mode-options="modeOptions ? modeOptions[cMode] : undefined"
>
<slot name="actions" />
@@ -290,14 +323,16 @@ 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"
@drop="$emit('drop', $event)"
v-bind="modeOptions ? modeOptions[cMode] : null || {}"
>
<template v-slot="slot"><slot v-bind="slot" /></template>
</component>
</base-draganddrop>
</div>
<bs-modal ref="modal" dialog-class="modal-lg" body-class="" @hidden-bs-modal="onModalHidden">
<template #title>
<slot v-if="modalEvent" v-bind="{mode: 'eventheader', event: modalEvent.event}" />
@@ -308,4 +343,4 @@ export default {
</bs-modal>
</div>
`
}
}
+233 -26
View File
@@ -1,7 +1,8 @@
import GridLine from './Grid/Line.js';
import GridLineEvent from './Grid/Line/Event.js';
import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
import drop from '../../../directives/drop.js';
import { useResizeHandler } from '../../../helpers/Tempus/ResizeHandler.js';
export default {
name: "CalendarGrid",
@@ -10,17 +11,24 @@ export default {
GridLineEvent
},
directives: {
CalDnd
drop
},
inject: {
originalEvents: "events",
originalBackgrounds: "backgrounds",
dropAllowed: "dropAllowed"
dropAllowed: "dropAllowed",
onDrop: "onDrop",
onResize: "onResize",
timeGrid: {
from: "timeGrid",
default: () => []
}
},
provide() {
return {
flipAxis: Vue.computed(() => this.flipAxis),
axisRow: Vue.computed(() => this.axisRow)
axisRow: Vue.computed(() => this.axisRow),
onDropEvent: Vue.computed(() => this.onDropEvent),
};
},
props: {
@@ -53,14 +61,18 @@ export default {
flipAxis: Boolean,
allDayEvents: Boolean,
axisMainCollapsible: Boolean,
snapToGrid: Boolean
snapToGrid: Boolean,
dayVisibility: {
type: Array,
default: null
}
},
data() {
return {
dragging: false,
resizeObserver: null,
mutationObserver: null,
userScroll: true
userScroll: true,
isDragging: false
};
},
computed: {
@@ -152,7 +164,17 @@ export default {
hasValidEvents() {
return this.events.find(e => e.length);
},
styleGridCols() {
styleGridCols()
{
if (this.dayVisibility)
{
let anyVisible = this.dayVisibility.some(day => day);
if (anyVisible)
{
return this.axisMain.map((day, i) => this.isDayCollapsed(i) ? 'var(--fhc-calendar-axis-collapsible-manual, 0.1fr)' : '1fr').join(' ');
}
}
let cols = 'repeat(' + this.axisMain.length + ', 1fr)';
if (this.axisMainCollapsible) {
if (this.hasValidEvents)
@@ -252,17 +274,27 @@ export default {
pageLeft += this.getPageLeft(el.offsetParent);
return pageLeft;
},
getTimestampFromMouse(evt, dayTimestamp) {
getTimestampFromMouse(evt, dayTimestamp)
{
let mouse, mouseFrac;
if (this.flipAxis) {
mouse = evt.pageX - this.getPageLeft(this.$refs.body) + this.$refs.main.scrollLeft;
const grabOffsetY = parseFloat(evt.dataTransfer.getData('fhc-grab-offset-y')) || 0;
const grabOffsetX = parseFloat(evt.dataTransfer.getData('fhc-grab-offset-x')) || 0;
if (this.flipAxis)
{
mouse = evt.pageX - this.getPageLeft(this.$refs.body) + this.$refs.scroller.scrollLeft - grabOffsetX;
mouseFrac = mouse / this.$refs.body.offsetWidth;
} else {
mouse = evt.pageY - this.getPageTop(this.$refs.body) + this.$refs.main.scrollTop;
}
else
{
mouse = evt.pageY - this.getPageTop(this.$refs.body) + this.$refs.scroller.scrollTop - grabOffsetY;
mouseFrac = mouse / this.$refs.body.offsetHeight;
}
return dayTimestamp + this.start + Math.floor((this.end - this.start) * mouseFrac);
let rawTimestamp = dayTimestamp + this.start + Math.floor((this.end - this.start) * mouseFrac);
let fiveMinutes = 5 * 60 * 1000;
return Math.round(rawTimestamp / fiveMinutes) * fiveMinutes;
},
/* SCROLLING */
@@ -308,8 +340,175 @@ export default {
} else {
this.$refs.scroller.scrollTo(0, 0);
}
},
calculateNettoDuration(start, end) {
const startDay = start.startOf('day');
const blocks = this.axisPartsWithBreaks.filter(p => p.index !== undefined);
let nettoDuration = luxon.Duration.fromMillis(0);
for (const block of blocks)
{
const blockStart = startDay.plus(block.start);
const blockEnd = startDay.plus(block.end);
const overlapStart = blockStart > start ? blockStart : start;
const overlapEnd = blockEnd < end ? blockEnd : end;
if (overlapStart < overlapEnd) {
nettoDuration = nettoDuration.plus(overlapEnd.diff(overlapStart));
}
}
return nettoDuration;
},
calculateDropEnd(dropStart, durationMs) {
const duration = luxon.Duration.fromMillis(durationMs);
const blocks = this.axisPartsWithBreaks.filter(p => p.index !== undefined);
let accumulated = luxon.Duration.fromMillis(0);
for (const block of blocks)
{
const blockStart = dropStart.startOf('day').plus(block.start);
const blockEnd = dropStart.startOf('day').plus(block.end);
if (blockEnd <= dropStart) continue;
const relevantStart = blockStart > dropStart ? blockStart : dropStart;
const relevantDuration = blockEnd.diff(relevantStart);
accumulated = accumulated.plus(relevantDuration);
if (accumulated >= duration)
{
const overflow = accumulated.minus(duration);
return blockEnd.minus(overflow);
}
}
const lastBlock = blocks[blocks.length - 1];
return dropStart.startOf('day').plus(lastBlock.end);
},
onDropEvent(evt, items, date)
{
if (this.snapToGrid)
this.onDropSnap(evt, items, date)
else
this.onDropFree(evt, items, date)
},
onDropSnap(evt, items, date, part) {
let obj = items;
if (!obj?.orig) return;
const dayStr = evt?.currentTarget?.dataset?.day;
const dropDay = dayStr ? luxon.DateTime.fromISO(dayStr) : date;
const rawTimestamp = this.getTimestampFromMouse(evt, dropDay.toMillis());
const grabTime = luxon.DateTime.fromMillis(rawTimestamp);
const blocks = this.axisPartsWithBreaks.filter(p => p.index !== undefined);
const grabOffset = grabTime.diff(dropDay);
let snappedPart = blocks.find(b => grabOffset >= b.start && grabOffset < b.end);
if (!snappedPart)
snappedPart = blocks.find(b => b.start >= grabOffset) || blocks[blocks.length - 1];
const dropStart = snappedPart ? dropDay.plus(snappedPart.start) : grabTime;
let nettoDuration = this._getNettoDurationForDrop(obj);
let dropEnd = this.calculateDropEnd(dropStart, nettoDuration);
this.onDrop?.({
item: [obj],
start: dropStart.toISO(),
end: dropEnd.toISO(),
ctrlKey: !!(evt?.ctrlKey || evt?.metaKey)
});
},
_getNettoDurationForDrop(obj) {
if (obj.orig?.isostart && obj.orig?.isoend)
{
const s = luxon.DateTime.fromISO(obj.orig.isostart);
const e = luxon.DateTime.fromISO(obj.orig.isoend);
if (s.isValid && e.isValid)
return this.calculateNettoDuration(s, e);
}
if (obj.stundenblockung)
{
let blocks = this.axisPartsWithBreaks.filter(p => p.index !== undefined);
let firstBlock = blocks[0];
let blockMinutes = luxon.Duration.fromISO(firstBlock.end).minus(luxon.Duration.fromISO(firstBlock.start)).as('minutes');
if (!Number.isFinite(blockMinutes) || blockMinutes <= 0) blockMinutes = 45;
return luxon.Duration.fromObject({ minutes: obj.stundenblockung * blockMinutes });
}
return luxon.Duration.fromObject({ minutes: 45 });
},
onDropFree(evt, items, date)
{
let obj = items;
if (!obj?.orig)
return;
const timestamp = this.getTimestampFromMouse(evt, date);
const dropStart = luxon.DateTime.fromMillis(timestamp);
let nettoDuration = this._getNettoDurationForDrop(obj);
let dropEnd = this.calculateDropEnd(dropStart, nettoDuration);
this.onDrop?.({
item: [obj],
start: dropStart.toISO(),
end: dropEnd.toISO(),
ctrlKey: !!(evt?.ctrlKey || evt?.metaKey)
});
},
handleResizeStart({ edge, evt, el, event })
{
const gridEl = this.$refs.body;
if (!gridEl)
return;
this.resizeHandler.startResize(edge, evt, {
el,
gridEl,
event,
timeGrid: this.timeGrid,
onEnd: ({ event, newStart, newEnd }) => {
const orig = event?.orig;
if (!orig || !newStart || !newEnd)
return;
this.onResize?.({
item: [{ type: 'kalender', id: orig.kalender_id, orig }],
start: newStart,
end: newEnd
});
}
});
},
isDayCollapsed(index)
{
if (this.dayVisibility)
{
let anyVisible = this.dayVisibility.some(day => day);
if (anyVisible)
return !this.dayVisibility[index];
}
return this.axisMainCollapsible && this.hasValidEvents && !this.events[index].length;
}
},
setup()
{
const resizeHandler = useResizeHandler();
return { resizeHandler };
},
beforeUnmount() {
this.disableAutoScroll();
},
@@ -318,6 +517,7 @@ export default {
class="fhc-calendar-base-grid"
style="display:grid;width:100%;height:100%;overflow:auto"
:style="'grid-template-' + axisRow + 's:auto' + (allDayEvents ? ' auto ' : ' ') + '1fr;grid-template-' + axisCol + 's:auto ' + styleGridCols"
data-cy="calendar-base-grid"
>
<div
class="grid-header"
@@ -328,7 +528,7 @@ export default {
v-for="(date, index) in axisMain"
:key="index"
class="main-header"
:class="{'collapsed-header': axisMainCollapsible && hasValidEvents && !events[index].length}"
:class="{'collapsed-header': isDayCollapsed(index)}"
:style="'grid-' + axisCol + ':' + (2+index)"
>
<slot name="main-header" v-bind="{ index, date }" />
@@ -382,11 +582,11 @@ export default {
ref="body"
class="grid-body"
style="display:grid;grid-template-rows:subgrid;grid-template-columns:subgrid"
@dragenter="isDragging = true"
@dragleave.self="isDragging = false"
@dragend="isDragging = false"
@drop="isDragging = false"
:style="'grid-' + axisCol + ':2/-1;grid-' + axisRow + ':1/-1'"
v-cal-dnd:dropcage
@calendar-dragenter="dragging = true"
@calendar-dragleave="dragging = false"
@dragover="dropAllowed ? $event.preventDefault() : null"
>
<template
v-for="(date, index) in axisMain"
@@ -398,12 +598,16 @@ export default {
class="part-body"
style="position:relative"
:style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i"
:data-drop-index="index * axisPartsSave.length + i + 1"
data-cy="calendar-grid-part"
>
<slot name="part-body" v-bind="{ index, part }" />
<div
v-if="snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone.once="{date: date.plus(part.start || part), ends: ends.slice(ends.findIndex(end => end > date))}"
v-if="snapToGrid"
style="position:absolute;inset:0"
:style="{ zIndex: isDragging ? 10 : 1 }"
:data-day="date.toFormat('yyyy-MM-dd')"
v-drop:move.lehreinheit.kalender.reservierung="(evt, item) => onDropSnap(evt, item, date, part)"
></div>
</div>
<grid-line
@@ -412,7 +616,9 @@ export default {
:date="date"
:events="eventsNormal[index]"
:backgrounds="backgrounds[index]"
:class="{ 'fhc-calendar-base-grid-line-collapsed': isDayCollapsed(index) }"
style="position:relative"
@resize-start="handleResizeStart"
:style="'grid-' + axisRow + ':1/-1;grid-' + axisCol + ':' + (1+index)"
>
<template #event="slot">
@@ -420,9 +626,10 @@ export default {
</template>
<template #dropzone>
<div
v-if="!snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone="evt => getTimestampFromMouse(evt, date)"
v-if="!snapToGrid"
style="position:absolute;inset:0"
:style="{ zIndex: isDragging ? 10 : 1 }"
v-drop:move.lehreinheit.kalender.reservierung="(evt, item) => onDropFree(evt, item, date)"
></div>
</template>
</grid-line>
@@ -432,4 +639,4 @@ export default {
</div>
</div>
`
}
}
@@ -12,7 +12,17 @@ export default {
LineEvent,
LineBackground,
},
inject: ["axisRow", "shouldCompactEvents", "compactibleEventTypes"],
inject: {
axisRow: "axisRow",
shouldCompactEvents: {
from: "shouldCompactEvents",
default: undefined,
},
compactibleEventTypes: {
from: "compactibleEventTypes",
default: undefined,
},
},
props: {
date: {
type: luxon.DateTime,
@@ -137,6 +147,7 @@ export default {
v-if="!event.display || event.display === 'default'"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
:event="event"
@resize-start="$emit('resize-start', $event)"
>
<template v-slot="slot">
<slot name="event" v-bind="slot" />
@@ -1,15 +1,45 @@
import CalDnd from '../../../../../directives/Calendar/DragAndDrop.js';
import draggable from '../../../../../directives/draggable.js';
import drop from '../../../../../directives/drop.js';
import CalClick from '../../../../../directives/Calendar/Click.js';
export default {
name: "GridLineEvent",
directives: {
CalDnd,
draggable,
drop,
CalClick
},
emits: [
'resize-start'
],
data() {
return {
contextMenu: {
show: false,
x: 0,
y: 0
}
};
},
inject: {
draggableEvents: "draggableEvents",
mode: "mode"
resizableEvents: {
from: "resizableEvents",
default: () => () => false
},
mode: "mode",
contextMenuActions: {
from: "contextMenuActions",
default: () => ({})
},
onDrop: {
from: "onDrop",
default: () => null
},
onDropEvent: {
from: "onDropEvent",
default: () => () => {}
}
},
props: {
event: {
@@ -27,6 +57,9 @@ export default {
draggable() {
return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode);
},
resizable() {
return !this.isHeaderOrFooter && this.resizableEvents(this.event.orig, this.mode);
},
classes() {
const classes = [];
if (this.isHeaderOrFooter) {
@@ -37,21 +70,130 @@ export default {
if (this.event.endsHere)
classes.push('event-end');
}
return classes
classes.push(`calender_id-${this.event.orig.kalender_id}`);
return classes;
},
dragKalenderCollection() {
const orig = this.event.orig;
return {
type: 'kalender',
id: orig?.kalender_id ?? null,
orig,
};
},
activeContextActions() {
if (this.isHeaderOrFooter) return [];
const type = this.event.orig?.type ?? 'lehreinheit';
const actions = this.contextMenuActions[type] ?? this.contextMenuActions['default'] ?? [];
return actions.filter(action => !action.visible || action.visible(this.event.orig));
}
},
template: /* html */`
methods: {
onResizeStart(edge, evt) {
this.$emit('resize-start', {
edge,
evt,
el: this.$refs.eventEl,
event: this.event
});
},
onRightClick(evt) {
this.contextMenu.show = true;
this.contextMenu.x = evt.clientX;
this.contextMenu.y = evt.clientY;
},
onContextAction(action) {
this.contextMenu.show = false;
action(this.event.orig);
},
closeContextMenu() {
this.contextMenu.show = false;
},
onDragStart(evt) {
const rect = this.$refs.eventEl.getBoundingClientRect();
evt.dataTransfer.setData('fhc-grab-offset-y', evt.clientY - rect.top);
evt.dataTransfer.setData('fhc-grab-offset-x', evt.clientX - rect.left);
},
onDropOnCard(evt, items) {
if (this.isHeaderOrFooter || !this.onDrop)
return;
const list = Array.isArray(items) ? items : [items];
const obj = list[0];
if (!obj)
return;
if ((evt.ctrlKey || evt.metaKey) && obj.type === 'lehreinheit')
{
return this.onDrop({
item: [obj],
ctrlKey: true,
targetKalenderId: this.event.orig?.kalender_id ?? null
});
}
return this.onDropEvent(evt, items, this.event.start.startOf('day'));
},
},
template:`
<div
class="fhc-calendar-base-grid-line-event event"
:class="classes"
style="z-index: 1"
style="z-index: 11"
:draggable="draggable"
v-cal-dnd:draggable="event"
ref="eventEl"
@dragstart="onDragStart"
v-draggable:move.noimage="draggable ? dragKalenderCollection : {}"
v-drop:move.lehreinheit.kalender.reservierung="onDropOnCard"
v-cal-click:event="isHeaderOrFooter ? event : event.orig"
@contextmenu.prevent="onRightClick"
:data-id="'event-' + event.orig.kalender_id"
:data-group-id="'event-group-' + event.orig.eindeutige_kalender_gruppen_id"
data-cy="calendar-event"
>
<div
v-if="resizable"
class="fhc-resize-bar fhc-resize-bar--top"
@pointerdown.prevent.stop="onResizeStart('start', $event)"
@click.stop
>
<i class="fa-solid fa-grip-lines text-muted"></i>
</div>
<slot :event="isHeaderOrFooter ? event : event.orig">
{{ event.orig }}
</slot>
<div
v-if="resizable"
class="fhc-resize-bar fhc-resize-bar--bottom"
@pointerdown.prevent.stop="onResizeStart('end', $event)"
@click.stop
>
<i class="fa-solid fa-grip-lines text-muted"></i>
</div>
<teleport to="body">
<div
v-if="contextMenu.show"
style="position:fixed; inset:0; z-index:9998"
@click="closeContextMenu"
@contextmenu.prevent="closeContextMenu"
/>
<ul
v-if="contextMenu.show"
class="dropdown-menu show"
:style="{ position: 'fixed', top: contextMenu.y + 'px', left: contextMenu.x + 'px', zIndex: 9999 }"
data-cy="eventContextMenu"
>
<li v-for="action in activeContextActions" :key="action.label">
<button class="dropdown-item" type="button" @click.stop="onContextAction(action.action)">
<i v-if="action.icon" :class="action.icon + ' me-2'"></i>
{{ action.label }}
</button>
</li>
</ul>
</teleport>
</div>
`
}
}
@@ -0,0 +1,67 @@
import draggable from '../../../../../directives/draggable.js';
export default {
name: 'EventCard',
directives: {
draggable,
},
props: {
event: { type: Object, required: true },
parked: Boolean
},
emits: [
'select-kw',
'unpark'
],
computed: {
dragKalenderCollection() {
return this.event
},
topicString() {
return Array.isArray(this.event.orig.topic) ? this.event.orig.topic.join(', ') : this.event.orig.topic;
},
datum() {
return luxon.DateTime.fromISO(this.event.orig.datum).toFormat('dd.MM.yyyy')
},
kw() {
return luxon.DateTime.fromISO(this.event.orig.datum).startOf('week', { useLocaleWeeks: true }).localWeekNumber
},
},
methods: {
eventStyle(event) {
if (!event.farbe)
return undefined;
return '--event-bg:#' + event.farbe;
},
jumpToKw()
{
this.$emit('select-kw', this.kw);
},
unpark(evt)
{
this.$emit('unpark', this.event);
}
},
template: `
<div
class="course-parker event"
v-draggable:move.noimage="dragKalenderCollection"
:style="eventStyle(event.orig)"
data-cy="calendar-event"
>
<button v-if="parked" class="unpark-btn" type="button" @click="unpark">
<i class="fa-solid fa-xmark"></i>
</button>
<div class="title">
{{ topicString || event.orig.titel || event.orig.lehrfach }}
</div>
<div>
<span class="text-decoration-underline" @click="jumpToKw">KW: {{ kw }}</span> {{ datum }}
</div>
<div>
{{ event.orig.beginn }}–{{ event.orig.ende }}
</div>
</div>
`
};
+18 -3
View File
@@ -24,14 +24,16 @@ export default {
btnMonth: Boolean,
btnWeek: Boolean,
btnDay: Boolean,
btnList: Boolean
btnList: Boolean,
btnTableList: Boolean
},
emits: [
"next",
"prev",
"click:mode",
"update:date",
"update:mode"
"update:mode",
"update:date-range"
],
data() {
return {
@@ -89,6 +91,16 @@ export default {
>
<i class="fa fa-table-list"></i>
</button>
<button
v-if="btnTableList"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'tableList'}"
@click="clickMode($event, 'tableList')"
>
<i class="fa fa-table-list"></i>
</button>
</div>
</div>
</div>
@@ -98,6 +110,7 @@ export default {
class="btn btn-outline-secondary border-0"
@click="$emit('prev')"
:disabled="open"
v-if="mode !== 'tableList'"
>
<i class="fa fa-chevron-left"></i>
</button>
@@ -105,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"
@@ -113,6 +127,7 @@ export default {
class="btn btn-outline-secondary border-0"
@click="$emit('next')"
:disabled="open"
v-if="mode !== 'tableList'"
>
<i class="fa fa-chevron-right"></i>
</button>
@@ -120,4 +135,4 @@ export default {
</div>
</div>
`
}
}
@@ -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,32 +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":
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":
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() {
@@ -70,19 +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":
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);
},
@@ -97,7 +117,7 @@ export default {
:format="() => title"
:month-picker="mode == 'month'"
:week-picker="mode == 'week'"
:range="mode == 'list' ? { autoRange: listLength - 1 } : false"
:range="mode == 'list' ? { autoRange: listLength - 1 } : (mode == 'tableList' ? { partialRange: false } : false)"
:text-input="mode == 'day'"
:week-start="weekStart"
:week-numbers="{ type: weekNumbers }"
@@ -1,7 +1,8 @@
import LabelDay from '../../Base/Label/Day.js';
import LabelDow from '../../Base/Label/Dow.js';
import CalDnd from '../../../../directives/Calendar/DragAndDrop.js';
import draggable from '../../../../directives/draggable.js';
import CalClick from '../../../../directives/Calendar/Click.js';
// TODO(chris): drag and drop
@@ -13,7 +14,7 @@ export default {
LabelDow
},
directives: {
CalDnd,
draggable,
CalClick
},
inject: {
@@ -86,7 +87,7 @@ export default {
v-else
class="event"
:draggable="draggable(event)"
v-cal-dnd:draggable="event"
v-draggable="event.orig"
v-cal-click:event="event.orig"
>
<slot :event="event.orig" mode="list" />
@@ -36,7 +36,9 @@ export default {
}
}
return events;
})
}),
draggableEvents: () => false,
resizableEvents: () => false
};
},
inject: {
@@ -0,0 +1,57 @@
import TableView from './Table/View.js';
export default {
name: "ModeTable",
components: {
TableView
},
props: {
currentDate: {
type: luxon.DateTime,
required: true
},
rangeEnd: {
type: luxon.DateTime,
default: null
}
},
emits: [
"update:range",
],
data() {
return {
focusDate: this.currentDate,
focusEnd: this.rangeEnd || this.currentDate
};
},
computed: {
range() {
return luxon.Interval.fromDateTimes(this.focusDate.startOf('day'), this.focusEnd.endOf('day'));
}
},
watch: {
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: {
},
mounted() {
this.$emit('update:range', this.range);
},
template: `
<div>
<table-view ref="view" v-bind="$attrs" :day="focusDate" :end="focusEnd">
<template v-slot="slot"><slot v-bind="slot" mode="week" /></template>
</table-view>
</div>
`
}
@@ -0,0 +1,158 @@
import {CoreFilterCmpt} from "../../../../components/filter/Filter.js";
import BsModal from '../../../Bootstrap/Modal.js';
import FormInput from "../../../Form/Input.js";
export default {
name: "TableView",
inject: {
events: "events",
timezone: "timezone",
tableActions: "tableActions",
},
components: {
CoreFilterCmpt,
BsModal,
FormInput
},
props: {
day: {
type: luxon.DateTime,
required: true
},
end: {
type: luxon.DateTime,
required: true
}
},
data()
{
return {
raumtyp_array: []
}
},
computed: {
start() {
return this.day.startOf('day');
},
tableEnd() {
return this.end.endOf('day');
},
preparedEvents() {
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)
.map(event => ({
...event.orig,
row_index: event.id,
}));
},
tabulatorOptions() {
return {
index: "row_index",
layout: 'fitDataStretch',
placeholder: "Keine Daten verfügbar",
persistenceID: "2026_03_09_table_view_v1",
data: this.preparedEvents,
columns: [
{
formatter: 'rowSelection',
titleFormatter: 'rowSelection',
titleFormatterParams: {
rowRange: "active"
},
headerSort: false,
width: 40
},
{title: 'Datum', field: 'datum', headerFilter: "input", formatter: (cell) => {
let val = cell.getValue();
if (!val)
return '&nbsp;';
return luxon.DateTime.fromISO(val).toFormat('dd.MM.yyyy')
}
},
{title: 'Von', field: 'beginn', headerFilter: "input"},
{title: 'Bis', field: 'ende', headerFilter: "input"},
{title: 'Lehrfach', field: 'lehrfach', headerFilter: "input"},
{title: 'Bezeichnung', field: 'lehrfach_bez', headerFilter: "input"},
{title: 'Lehrform', field: 'lehrform', headerFilter: "input"},
{title: 'Raum', field: 'ort_kurzbz', headerFilter: "input"},
{
title: 'Lektor',
field: 'lektor',
headerFilter: "input",
mutator: (value) => {
if (!value)
return '';
return value.map(l => l.kurzbz).join(', ') ?? '–'
}
},
{title: 'OE', field: 'organisationseinheit', headerFilter: "input"},
{title: 'Status', field: 'status_kurzbz', headerFilter: "input"},
]
}
}
},
methods:
{
openModal() {
this.$refs.raumModal.show();
},
async deleteSelected() {
let selected = this.$refs.tableViewTable?.tabulator?.getSelectedData() ?? [];
if (!selected.length) return;
let isConfirmed = await this.$fhcAlert.confirmDelete();
if (!isConfirmed) return;
await this.tableActions?.deleteEntries(selected)
this.$refs.tableViewTable.tabulator.deselectRow();
}
},
watch: {
preparedEvents(newData) {
this.$refs.tableViewTable?.tabulator?.setData(newData);
}
},
template: /* html */`
<div class="fhc-calendar-mode-table-view h-100 overflow-auto">
<core-filter-cmpt
ref="tableViewTable"
:tabulator-options="tabulatorOptions"
:table-only="true"
:side-menu="false"
:download="true"
>
<template #actions>
<button @click="deleteSelected" class="btn btn-outline-danger btn-sm">Löschen</button>
<button class="btn btn-outline-secondary btn-sm">Verschieben</button>
<button @click="openModal" class="btn btn-outline-secondary btn-sm">Raum wechsel</button>
</template>
</core-filter-cmpt>
<!--<bs-modal ref="raumModal" class="bootstrap-prompt" dialogClass="modal-lg">
<template #title>Raum verschiebung</template>
<form-input
:label="$p.t('lehre', 'raumtyp')"
type="select"
container-class="col-3"
name="raumtyp"
>
<option
v-for="raumtyp in raumtyp_array"
:value="raumtyp.raumtyp_kurzbz"
:key="raumtyp.raumtyp_kurzbz"
>
{{ raumtyp.raumtyp_kurzbz }} {{ raumtyp.beschreibung }}
</option>
</form-input>
<template #footer>
<button type="button" class="btn btn-primary">{{ $p.t('ui', 'speichern') }}</button>
</template>
</bs-modal>-->
</div>
`
}
@@ -2,6 +2,8 @@ import CalendarGrid from '../../Base/Grid.js';
import LabelDay from '../../Base/Label/Day.js';
import LabelDow from '../../Base/Label/Dow.js';
import LabelTime from '../../Base/Label/Time.js';
import FormInput from "../../../Form/Input.js";
export default {
name: "WeekView",
@@ -9,11 +11,13 @@ export default {
CalendarGrid,
LabelDay,
LabelDow,
LabelTime
LabelTime,
FormInput
},
inject: {
timeGrid: "timeGrid",
timezone: "timezone"
timezone: "timezone",
hoursPlan: { from: "hoursPlan", default: null },
},
props: {
day: {
@@ -22,6 +26,11 @@ export default {
},
collapseEmptyDays: Boolean
},
data() {
return {
visibleDays: JSON.parse(localStorage.getItem('fhc-calender-visible-days')) ?? [true, true, true, true, true, true, true]
}
},
computed: {
start() {
return this.day.startOf('week', { useLocaleWeeks: true });
@@ -39,14 +48,23 @@ export default {
};
});
} else {
// create 07:00-23:00
return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 }));
const start = this.hoursPlan?.start ?? 7;
const end = this.hoursPlan?.end ?? 23;
return Array.from({ length: end - start + 1 }, (e, i) => luxon.Duration.fromObject({ hours: i + start }));
}
},
dayVisibility() {
return this.axisMain.map(date => this.visibleDays[date.weekday -1])
}
},
methods: {
isToday(date) {
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
},
toggleDay(day)
{
this.visibleDays.splice(day, 1, !this.visibleDays[day])
localStorage.setItem('fhc-calender-visible-days', JSON.stringify(this.visibleDays))
}
},
template: /* html */`
@@ -58,9 +76,10 @@ export default {
:axis-main-collapsible="collapseEmptyDays"
:snap-to-grid="!!timeGrid"
all-day-events
:day-visibility="dayVisibility"
>
<template #main-header="{ date }">
<div :class="{ today: isToday(date) }">
<template #main-header="{ date, index }">
<div :class="{ today: isToday(date) }" class="d-flex flex-column align-items-center">
<label-dow
v-bind="{ date }"
@cal-click="evt => evt.detail.source = 'day'"
@@ -68,6 +87,13 @@ export default {
<label-day
v-bind="{ date }"
/>
<form-input
type="checkbox"
:checked="visibleDays[date.weekday - 1]"
@change="toggleDay(date.weekday - 1)"
@click.stop
>
</form-input>
</div>
</template>
<template #part-header="{ part }">
+305
View File
@@ -0,0 +1,305 @@
import FhcCalendar from "./Base.js";
import { useEventLoader } from '../../composables/EventLoader.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
import ModeTable from './Mode/Table.js';
import ApiKalender from '../../api/factory/tempus/kalender.js';
import draggable from '../../directives/draggable.js';
export default {
name: "CalendarTempus",
components: {
FhcCalendar
},
inject: {
renderers: {from: 'renderers'},
canToggleGrid: {
from: 'canToggleGrid',
default: false
},
appConfig: {
from: 'appConfig',
default: {
visible_status: 'all'
}
}
},
directives: {
draggable,
},
props: {
timezone: {
type: String,
required: true
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local()
},
mode: {
type: String,
default: 'Week'
},
getPromiseFunc: {
type: Function,
required: true
},
parkedEvents: {
type: Object,
default: () => new Set()
},
visibleLecturers: {
type: Array,
default: null
},
extraBackgrounds: {
type: Array,
default: () => []
},
visibleStatus: {
type: Array,
default: () => ['all']
},
showEvents: {
type: Boolean,
default: true
}
},
emits: [
"update:date",
"update:mode",
"update:range",
"update:date-range",
"drop",
"resize",
"event-hover",
"event-unhover",
"open-reservierung"
],
data() {
return {
modes: {
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth),
tableList: Vue.markRaw(ModeTable),
},
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound'))
},
week: {
collapseEmptyDays: false
}
},
currentMode: this.mode,
teachingunits: null,
hoursplan: null,
showRaster: true,
};
},
computed: {
backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone);
let past = [];
if (this.mode == 'Month')
{
past = [{
class: 'background-past',
end: now.startOf('day')
}];
}
else
{
past = [{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}];
}
return [
...past,
...(this.extraBackgrounds || [])
];
},
visibleEvents()
{
let list = this.events;
if (Array.isArray(this.visibleLecturers))
{
const visibleLectures = new Set(this.visibleLecturers);
list = list.filter(event => {
if (!event.lektor?.length)
return true;
return event.lektor.some(lektor => visibleLectures.has(lektor.mitarbeiter_uid));
});
}
if (!this.visibleStatus.length || this.visibleStatus.includes('all'))
return list;
return list.filter(event => this.visibleStatus.includes(event.status_kurzbz));
},
},
methods: {
eventStyle(event) {
if (!event.farbe)
return undefined;
return '--event-bg:#' + event.farbe;
},
updateRange(rangeInterval) {
if (this.currentMode === 'tableList')
return;
this.rangeInterval = rangeInterval;
this.$emit('update:range', rangeInterval);
},
handleDateRange({ start, end }) {
this.rangeInterval = luxon.Interval.fromDateTimes(start.startOf('day'), end.endOf('day'));
this.reset();
this.$emit('update:range', this.rangeInterval);
this.$emit('update:date-range', { start, end });
},
ondrop(payload){
this.$emit('drop', payload);
},
onresize(payload){
this.$emit('resize', payload);
},
resetEventLoader() {
this.reset();
},
navigatePrev() {
this.$refs.calendar.clickPrev();
},
navigateNext() {
this.$refs.calendar.clickNext();
},
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc);
Vue.watch(lv, newValue => {
context.emit('update:lv', newValue);
});
return {
rangeInterval,
events,
lv,
reset
};
},
created() {
this.$api
.call(ApiKalender.getStunden())
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
start: el.beginn,
end: el.ende
}));
});
this.$api
.call(ApiKalender.getCalendarHours())
.then(res => {
this.hoursplan = {
start: res.data.start,
end: res.data.end
};
});
},
template: /* html */`
<fhc-calendar
ref="calendar"
class="fhc-calendar-lvplan"
:date="date"
:modes="modes"
:mode-options="modeOptions"
:mode="mode"
:timezone="timezone"
:locale="$p.user_locale.value"
:events="visibleEvents || []"
:backgrounds="backgrounds"
:time-grid="showRaster ? teachingunits : null"
:hours-plan="hoursplan"
show-btns
:draggable-events="true"
:resizable-events="true"
:on-drop="currentMode === 'week' ? ondrop : null"
:on-resize="onresize"
@update:date="(newDate, newMode) => $emit('update:date', newDate, newMode)"
@update:mode="(newMode, newDate) => { currentMode = newMode; $emit('update:mode', newMode, newDate) }"
@update:range="updateRange"
@update:date-range="handleDateRange"
>
<template v-slot="{ event, mode }">
<div
:class="['event-type-' + event.type + ' ' + mode + 'PageContainer', { 'event--parked': parkedEvents.has(String(event.kalender_id)) }, {'event--opacity': !showEvents}]"
:type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)"
@mouseenter="$emit('event-hover', event)"
@mouseleave="$emit('event-unhover', event)"
>
<component
v-if="mode == 'event'"
:is="renderers[event.type]?.modalContent"
:event="event"
></component>
<component
v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle"
:event="event"
></component>
<component
v-else
:is="renderers[event.type]?.calendarEvent"
:event="event"
></component>
</div>
</template>
<template #actions>
<div class="d-flex align-items-center gap-2">
<div
class="d-flex align-items-center gap-2"
style="cursor:pointer"
@click="showRaster = !showRaster"
v-if="canToggleGrid"
>
<i :class="showRaster ? 'fa-solid fa-toggle-on text-primary' : 'fa-solid fa-toggle-off text-muted'"></i>
<span class="form-check-label">Stundenraster</span>
</div>
<div
class="d-flex align-items-center gap-2 "
v-draggable:move.noimage="{ type: 'reservierung', id: null, orig: {} }"
data-cy="reservationDragHandle"
>
<i
class="fa-solid fa-calendar-plus text-primary"
style="cursor:pointer"
@click.stop="$emit('open-reservierung')"
></i>
<span>Reservierung</span>
</div>
<div class="d-flex align-items-center gap-2">
<i :class="appConfig.ignore_kollision ? 'fa-solid fa-triangle-exclamation text-danger' : 'fa-solid fa-circle-check text-success'"></i>
<span class="form-check-label">
{{ appConfig.ignore_kollision ? 'Kollisionscheck aus' : 'Kollisionscheck an' }}
</span>
</div>
</div>
</template>
</fhc-calendar>`
}
+10 -6
View File
@@ -15,6 +15,10 @@ export default {
'deleted',
],
props: {
isListItemShown: {
type: Boolean,
default: true
},
endpoint: {
type: Object,
required: true
@@ -79,7 +83,7 @@ export default {
'id': tag_id
};
this.$api.call(this.endpoint.getTag(getData))
await this.$api.call(this.endpoint.getTag(getData))
.then(result => result.data)
.then(result => this.openModal(result))
},
@@ -132,7 +136,7 @@ export default {
{
postData.id = this.selectedTagId;
this.tagData.id = this.selectedTagId;
this.$api.call(this.endpoint.updateTag(postData));
await this.$api.call(this.endpoint.updateTag(postData));
this.$emit("updated", this.tagData);
this.$refs.tagModal.hide();
}
@@ -144,7 +148,7 @@ export default {
return;
}
this.$api.call(this.endpoint.addTag(postData))
await this.$api.call(this.endpoint.addTag(postData))
.then(response => response.data)
.then(response => {
if (typeof response === 'number') {
@@ -171,7 +175,7 @@ export default {
done: !this.tagData.done,
notiz: this.tagData.notiz,
}
this.$api.call(this.endpoint.doneTag(postData))
await this.$api.call(this.endpoint.doneTag(postData))
this.$emit("updated", this.tagData);
this.$refs.tagModal.hide();
},
@@ -180,7 +184,7 @@ export default {
let postData = {
id: this.selectedTagId
}
this.$api.call(this.endpoint.deleteTag(postData))
await this.$api.call(this.endpoint.deleteTag(postData))
this.$emit("deleted", this.selectedTagId)
this.$refs.tagModal.hide();
},
@@ -228,7 +232,7 @@ export default {
}
},
template: `
<div class="plus_button_container" @mouseleave="hideList">
<div v-if="isListItemShown" class="plus_button_container" @mouseleave="hideList">
<span :title="values.length === 0 ? 'Bitte Zeilen markieren' : ''">
<button @mouseover="showList = true"
:disabled="!values || values.length === 0"
+24
View File
@@ -0,0 +1,24 @@
import AppMenu from "../AppMenu.js";
export default {
name: "TempusAppMenu",
components: {
AppMenu,
},
template: /* html */ `
<aside id="appMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100">
<div class="offcanvas-header">
Tempus
<button
type="button"
class="btn-close text-reset"
data-bs-dismiss="offcanvas"
:aria-label="$p.t('ui/schliessen')"
></button>
</div>
<div class="offcanvas-body">
<app-menu app-identifier="tempus" />
</div>
</aside>
`,
};
+386
View File
@@ -0,0 +1,386 @@
import FormInput from '../Form/Input.js';
import draggable from "../../directives/draggable.js";
import ApiCoursePicker from '../../api/factory/tempus/coursepicker.js';
export default {
components: {
FormInput
},
directives: {
draggable
},
props: {
studiengaenge: {
type: Array,
default: [],
},
lecturers: {
type: Array,
default: [],
},
studiensemester: {
type: String,
default: null
},
previewLehreinheitId: {
type: [String, Number],
default: null
},
previewLoading: {
type: Boolean,
default: false
},
},
emits: ['select-lecturer', 'select-kw', 'preview-raumvorschlag'],
data() {
return {
searchparam: '',
allCourses: [],
sortBy: null,
multiWeekIds: new Set(),
favorites: new Set(),
filter: {}
}
},
computed: {
courses() {
const query = (this.searchparam ?? '').trim().toLowerCase();
let result;
if (query)
{
result = this.allCourses.filter(course =>
course.showname.toLowerCase().includes(query) ||
course.start_kw.includes(query) ||
course.lektoren?.some(lektor =>
lektor.name.toLowerCase().includes(query) ||
lektor.kurzbz.toLowerCase().includes(query)
)
);
}
else
result = this.allCourses;
if (this.sortBy === 'lektor-asc' || this.sortBy === 'lektor-desc')
{
let dir = this.sortBy === 'lektor-asc' ? 1 : -1;
result = result.sort((a, b) => {
let an = a.lektoren?.[0]?.kurzbz ?? '';
let bn = b.lektoren?.[0]?.kurzbz ?? '';
return an.localeCompare(bn) * dir;
});
}
else if (this.sortBy === 'kw-asc')
{
result = result.sort((a, b) => (a.start_kw ?? 0) - (b.start_kw ?? 0));
}
else if (this.sortBy === 'kw-desc')
{
result = result.sort((a, b) => (b.start_kw ?? 0) - (a.start_kw ?? 0));
}
else if (this.sortBy === 'stunden-asc')
{
result = result.sort((a, b) => (a.offenestunden ?? 0) - (b.offenestunden ?? 0));
}
else if (this.sortBy === 'stunden-desc')
{
result = result.sort((a, b) => (b.offenestunden ?? 0) - (a.offenestunden ?? 0));
}
return [...result].sort((a, b) => {
let ap = this.isPinned(a) ? 1 : 0;
let bp = this.isPinned(b) ? 1 : 0;
return bp - ap;
});
}
},
watch: {
studiengaenge: {
deep: true,
handler(val) {
this.loadCourses();
}
},
lecturers: {
deep: true,
handler() {
this.loadCourses();
}
},
studiensemester() {
if (this.studiengaenge)
this.loadCourses();
},
},
methods: {
async loadCourses()
{
const hasLektoren = this.lecturers.length > 0;
const hasStg = this.studiengaenge.length > 0;
this.filter = {};
if (hasLektoren)
this.filter.uid = this.lecturers;
if (hasStg) {
this.filter.stg = this.studiengaenge.map(
({ stg_kz, semester, orgform_kurzbz }) => ({
stg_kz,
semester,
orgform_kurzbz,
}),
);
}
if (Object.keys(this.filter).length === 0)
return this.allCourses = [];
this.$api.call(ApiCoursePicker.getCourses(this.filter, this.studiensemester))
.then(result => {
this.allCourses = result?.data?.map(e => ({
lehreinheit_id: e.lehreinheit_id,
lektoren: e.lektoren,
raumtyp: e.raumtyp,
raumtypalternativ: e.raumtypalternativ,
semesterstunden: e.planstunden,
stundenblockung: e.stundenblockung,
wochenrythmus: e.wochenrythmus,
offenestunden: e.offenestunden,
start_kw: e.start_kw,
anmerkung: e.anmerkung,
lehrfach: e.lehrfach,
lehrform: e.lehrform,
lehrfach_bez: e.lehrfach_bez,
lehrfach_farbe: e.lehrfach_farbe,
lehrverband: e.lehrverband,
showname: `${e.lehrfach} ${e.lehrform}`,
orig: {
type: 'lehreinheit',
lehreinheit_id: e.lehreinheit_id,
blockung: e.stundenblockung,
entry: e,
}
}));
})
.catch(this.$fhcAlert.handleSystemError);
},
dragLehreinheitCollection(course) {
const orig = course.orig;
return {
type: 'lehreinheit',
id: orig.lehreinheit_id,
orig: orig,
stundenblockung: course.stundenblockung,
multiweek: this.isMultiWeek(course),
};
},
courseStyle(course) {
if (!course.lehrfach_farbe)
return {};
return '--event-bg:#' + course.lehrfach_farbe;
},
selectLecturer(lektor) {
this.$emit('select-lecturer', lektor);
},
setSort(field) {
if (this.sortBy === `${field}-asc`)
this.sortBy = `${field}-desc`;
else if (this.sortBy === `${field}-desc`)
this.sortBy = null;
else
this.sortBy = `${field}-asc`;
},
sortIcon(field) {
if (this.sortBy === `${field}-asc`)
return 'fa-arrow-up';
if (this.sortBy === `${field}-desc`)
return 'fa-arrow-down';
return 'fa-sort';
},
isSortActive(field) {
return this.sortBy === `${field}-asc` || this.sortBy === `${field}-desc`;
},
reload()
{
this.loadCourses();
},
toggleMultiWeek(course)
{
let id = course.lehreinheit_id;
let weekIds = new Set(this.multiWeekIds);
if (weekIds.has(id))
weekIds.delete(id);
else
weekIds.add(id);
this.multiWeekIds = weekIds;
},
isMultiWeek(course)
{
return this.multiWeekIds.has(course.lehreinheit_id);
},
togglePin(course)
{
let id = course.lehreinheit_id;
let favs = new Set(this.favorites);
if (favs.has(id))
favs.delete(id);
else
favs.add(id);
this.favorites = favs;
localStorage.setItem('tempus_coursepicker_favs', JSON.stringify([...favs]));
},
isPinned(course)
{
return this.favorites.has(course.lehreinheit_id);
},
selectRaumvorschlag(course)
{
if (this.isRaumvorschlagActive(course))
this.$emit('preview-raumvorschlag', null);
else
this.$emit('preview-raumvorschlag', course.orig);
},
isRaumvorschlagActive(course)
{
return this.previewLehreinheitId != null && this.previewLehreinheitId === course.lehreinheit_id;
},
},
mounted() {
this.favorites = new Set(JSON.parse(localStorage.getItem('tempus_coursepicker_favs') || '[]'));
},
template: `
<div class="course-picker d-flex flex-column h-100">
<div class="p-2">
<form-input
:label="$p.t('ui', 'suche')"
type="text"
v-model="searchparam"
/>
<div class="d-flex gap-1 mt-2">
<button
type="button"
class="btn btn-sm btn-outline-secondary"
:disabled="studiengaenge.length <= 0"
@click="reload">
<i class="fa fa-rotate-right"></i>
</button>
<button
type="button"
class="btn btn-sm"
:class="isSortActive('lektor') ? 'btn-primary' : 'btn-outline-secondary'"
@click="setSort('lektor')">
Lkt <i class="fa" :class="sortIcon('lektor')"></i>
</button>
<button
type="button"
class="btn btn-sm"
:class="isSortActive('kw') ? 'btn-primary' : 'btn-outline-secondary'"
@click="setSort('kw')">
KW <i class="fa" :class="sortIcon('kw')"></i>
</button>
<button
type="button"
class="btn btn-sm"
:class="isSortActive('stunden') ? 'btn-primary' : 'btn-outline-secondary'"
@click="setSort('stunden')">
Std <i class="fa" :class="sortIcon('stunden')"></i>
</button>
</div>
</div>
<div v-if="Object.keys(filter).length <= 0" class="d-flex flex-column align-items-center justify-content-center text-center text-muted py-5 px-3 h-100">
<span class="small fw-semibold mb-1">{{$p.t('lehre', 'cptitleempty')}}</span>
<span class="small">{{$p.t('lehre', 'cpempty')}}</span>
</div>
<div v-else class="overflow-auto px-2 pb-2 flex-grow-1">
<div
v-for="course in courses"
:key="course.lehreinheit_id"
style="cursor:grab"
:style="courseStyle(course)"
class="course-picker-row rounded-3 mb-2 p-2"
:class="{ 'course-picker-row-pinned': isPinned(course), 'course-picker-row-preview': isRaumvorschlagActive(course) }"
v-draggable:move.noimage="dragLehreinheitCollection(course)"
tabindex="0"
>
<div class="d-flex gap-1 align-items-start">
<span class="fw-semibold small w-50" v-tooltip="course.lehrfach_bez">{{ course.lehrfach }} {{ course.lehrform }}</span>
<span class="fw-semibold small w-50" v-tooltip="course.raumtypalternativ">{{ course.raumtyp }}</span>
<i
class="fa fa-thumbtack"
:class="isPinned(course) ? 'text-primary' : 'text-muted'"
style="cursor:pointer"
@click.stop="togglePin(course)"
></i>
<i
class="fa fa-calendar-week"
:class="isMultiWeek(course) ? 'text-primary' : 'text-muted'"
style="cursor:pointer"
v-tooltip="isMultiWeek(course) ? 'MultiWeek aktiv' : 'MultiWeek Verplanung aktivieren'"
@click.stop="toggleMultiWeek(course)"
></i>
<i
v-if="!(previewLoading && isRaumvorschlagActive(course))"
class="fa fa-door-open"
:class="isRaumvorschlagActive(course) ? 'text-primary' : 'text-muted'"
style="cursor:pointer"
v-tooltip="isRaumvorschlagActive(course) ? 'Raumvorschlag ausblenden' : 'Raumvorschlag einblenden'"
@click.stop="selectRaumvorschlag(course)"
></i>
<i
v-else
class="fa fa-spinner fa-spin text-primary"
v-tooltip="'Räume werden geprüft...'"
></i>
</div>
<!--TODO(david) entfernen, dient nur für das mappen mit der lvverwaltung-->
<div class="d-flex gap-1">
<span class="small w-50" v-tooltip="course.lehreinheit_id">{{ course.lehreinheit_id }} </span>
</div>
<div class="d-flex gap-1 text-muted">
<div class="w-50 d-flex flex-column text-truncate" v-tooltip="course.anmerkung">
<span
v-for="verband in course.lehrverband"
:key="verband"
class="text-truncate">
{{ verband }}
</span>
</div>
<span
style="cursor:pointer"
class="text-decoration-underline w-50"
@click.stop="$emit('select-kw', course.start_kw)">KW: {{ course.start_kw }}
</span>
</div>
<div class="d-flex gap-1 text-muted">
<div class="w-50 d-flex flex-column"
v-tooltip="course.lektoren.length > 3 ? course.lektoren.map(l => l.kurzbz).join(', ') : null">
<span
v-for="lektor in course.lektoren.slice(0, 3)"
:key="lektor.uid"
style="cursor:pointer"
class="text-decoration-underline"
@click.stop="selectLecturer(lektor)">
{{ lektor.kurzbz }}
</span>
<span v-if="course.lektoren.length > 3" class="text-muted fst-italic">
+{{ course.lektoren.length - 3 }} {{$p.t('lehre', 'cpmore')}}
</span>
</div>
<span class="w-50 align-self-start">WR: {{ course.wochenrythmus }} Bl: {{ course.stundenblockung }}</span>
</div>
<div class="d-flex gap-1 text-muted">
<span class="w-50">Offen: {{ course.offenestunden }}</span>
<span class="w-50">{{ course.semesterstunden }}</span>
</div>
</div>
</div>
</div>
`
}
@@ -0,0 +1,59 @@
export default {
name: "LectureSelection",
props: {
lecturers: {
type: Array,
required: true
}
},
emits: ['remove'],
template: `
<div class="lecture-selection">
<div class="room-selection px-2" >
<div class="d-flex align-items-center justify-content-between">
<span class="fw-semibold"><i class="fa-solid fa-user me-2"></i>Lektoren</span>
<button
type="button"
class="btn btn-sm btn-link text-danger p-0"
@click="$emit('remove')"
title="Alle Lektoren entfernen"
>
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div v-for="l in lecturers" :key="l.uid">
<div class="fw-semibold pt-2 d-flex align-items-center justify-content-between">
{{ l.label }}
<button
type="button"
class="btn btn-sm btn-link text-danger p-0"
@click="$emit('remove', l.uid)"
title="Lektor entfernen"
>
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="overflow-auto flex-grow-1 pb-2">
<div class="d-flex align-items-center gap-2" @click="l.showEvents = !l.showEvents" style="cursor:pointer">
<i :class="l.showEvents ? 'fa-solid fa-toggle-on text-primary' : 'fa-solid fa-toggle-off text-muted'"></i>
<span class="form-check-label">Plan</span>
</div>
<div class="d-flex align-items-center gap-2" @click="l.overlays.blocks = !l.overlays.blocks" style="cursor:pointer">
<i :class="l.overlays.blocks ? 'fa-solid fa-toggle-on text-primary' : 'fa-solid fa-toggle-off text-muted'"></i>
<span class="form-check-label">Zeitsperren</span>
</div>
<div class="d-flex align-items-center gap-2" @click="l.overlays.wishes = !l.overlays.wishes" style="cursor:pointer">
<i :class="l.overlays.wishes ? 'fa-solid fa-toggle-on text-primary' : 'fa-solid fa-toggle-off text-muted'"></i>
<span class="form-check-label">Zeitwünsche</span>
</div>
<div class="d-flex align-items-center gap-2" @click="l.showCoursePicker = !l.showCoursePicker" style="cursor:pointer">
<i :class="l.showCoursePicker ? 'fa-solid fa-toggle-on text-primary' : 'fa-solid fa-toggle-off text-muted'"></i>
<span class="form-check-label">Course Picker</span>
</div>
</div>
</div>
</div>
</div>
`
}
@@ -0,0 +1,55 @@
export default {
name: "RoomSelection",
props: {
rooms: {
type: Array,
required: true
}
},
emits: ['update:rooms'],
methods: {
removeRoom(ort_kurzbz)
{
if (ort_kurzbz == null)
{
this.$emit('update:rooms', []);
}
else
{
this.$emit('update:rooms', this.rooms.filter(room => room.ort_kurzbz !== ort_kurzbz));
}
}
},
template: `
<div class="room-selection px-2">
<div class="d-flex align-items-center justify-content-between">
<span class="fw-semibold"><i class="fa-solid fa-door-open me-2"></i>Räume</span>
<button
type="button"
class="btn btn-sm btn-link text-danger p-0"
@click="removeRoom(null)"
title="Alle Räume entfernen"
>
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<ul class="list-unstyled mb-0">
<li
v-for="room in rooms"
:key="room.ort_kurzbz"
class="d-flex align-items-center justify-content-between"
>
<span>{{ room.ort_kurzbz }}</span>
<button
type="button"
class="btn btn-sm btn-link text-danger p-0"
@click="removeRoom(room.ort_kurzbz)"
title="Raum entfernen"
>
<i class="fa-solid fa-xmark"></i>
</button>
</li>
</ul>
</div>
`
}
@@ -0,0 +1,69 @@
export default {
name: "VerbandSelection",
props: {
studiengaenge: {
type: Array,
required: true
},
studiengaengeAll: {
type: Array,
default: () => []
}
},
emits: ['update:studiengaenge'],
methods: {
removeStg(stg_kz, semester, orgform_kurzbz)
{
if (stg_kz == null)
{
this.$emit('update:studiengaenge', []);
}
else
{
this.$emit('update:studiengaenge', this.studiengaenge.filter(studiengang => !(studiengang.stg_kz === stg_kz && studiengang.semester === semester && (!orgform_kurzbz || studiengang.orgform_kurzbz === orgform_kurzbz))));
}
},
stgLabel(stg)
{
let found = this.studiengaengeAll.find(s => s.studiengang_kz == stg.stg_kz);
let name = found?.name ?? stg.stg_kz;
if (stg.orgform_kurzbz)
return `${name} - ${stg.orgform_kurzbz}`;
return stg.semester ? `${name} - ${stg.semester}. Semester` : name;
}
},
template: `
<div class="room-selection px-2">
<div class="d-flex align-items-center justify-content-between">
<span class="fw-semibold"><i class="fa-solid fa-door-open me-2"></i>Studiengänge</span>
<button
type="button"
class="btn btn-sm btn-link text-danger p-0"
@click="removeStg(null)"
title="Alle Studiengänge entfernen"
>
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<ul class="list-unstyled mb-0">
<li
v-for="studiengang in studiengaenge"
class="d-flex align-items-center justify-content-between"
>
<span>{{ stgLabel(studiengang) }}</span>
<button
type="button"
class="btn btn-sm btn-link text-danger p-0"
@click="removeStg(studiengang.stg_kz, studiengang.semester, studiengang.orgform_kurzbz)"
title="Studiengang entfernen"
>
<i class="fa-solid fa-xmark"></i>
</button>
</li>
</ul>
</div>
`
}
@@ -0,0 +1,69 @@
export function getTempusSearchbarOptions(self)
{
return {
origin: 'tempus',
cssclass: "position-relative",
calcheightonly: true,
types: [
//"student",
"raum",
"mitarbeiter",
"mitarbeiter_ohne_zuordnung"
],
actions: {
raum: {
defaultaction: {
type: "function",
action: self.setOrt
},
childactions: [
{
label: "zum Filter hinzufügen",
icon: "fas fa-plus",
type: "function",
action: (data) => {
self.addToFilter(data, 'ort');
}
},
]
},
employee: {
defaultaction: {
type: "function",
action: (data) => {
self.setEmp(data);
}
},
childactions: [
{
label: "zum Filter hinzufügen",
icon: "fas fa-plus",
type: "function",
action: (data) => {
self.addToFilter(data, 'mitarbeiter');
}
},
{
label: "zum Course Picker hinzufügen",
icon: "fas fa-plus",
type: "function",
action: (data) => {
self.addToCoursePicker(data);
}
},
{
label: "LVVerwaltung",
icon: "fas fa-up-right-from-square",
type: "function",
action: (data) => {
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/LVVerwaltung/stdsem/" + self.selectedStudiensemester + "/emp/" + data.uid;
window.open(link, "_blank");
}
},
]
},
}
};
}
+108
View File
@@ -0,0 +1,108 @@
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: ["language-changed"],
methods: {
focusSearchbar() {
this.$refs.searchbar?.$refs?.input?.focus();
},
},
template: /* html */ `
<header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow">
<div class="col-md-4 col-lg-3 col-xl-2 d-flex align-items-center">
<button
class="btn btn-outline-light border-0 m-1 collapsed"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#appMenu"
aria-controls="appMenu"
aria-expanded="false"
:aria-label="$p.t('ui/toggle_nav')"
>
<span class="svg-icon svg-icon-apps"></span>
</button>
<a class="navbar-brand me-0" :href="tempusRoot">Tempus</a>
</div>
<button
class="btn btn-outline-light border-0 d-md-none m-1 collapsed"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#sidebarMenu"
aria-controls="sidebarMenu"
aria-expanded="false"
:aria-label="$p.t('ui/toggle_nav')"
>
<span class="fa-solid fa-table-list"></span>
</button>
<core-searchbar
ref="searchbar"
:searchoptions="searchbaroptions"
:searchfunction="searchfunction"
class="searchbar position-relative w-100"
show-btn-submit
></core-searchbar>
<div id="nav-user" class="dropdown">
<button
id="nav-user-btn"
class="btn btn-link rounded-0 py-0"
type="button"
data-bs-toggle="dropdown"
data-bs-target="#nav-user-menu"
aria-expanded="false"
aria-controls="nav-user-menu"
>
<img
:src="avatarUrl"
:alt="$p.t('profilUpdate/profilBild')"
class="bg-light avatar rounded-circle border border-light"
/>
</button>
<ul
ref="navUserDropdown"
class="dropdown-menu dropdown-menu-dark dropdown-menu-end rounded-0 text-center m-0"
aria-labelledby="nav-user-btn"
>
<li>
<button
type="button"
class="dropdown-item"
data-bs-toggle="modal"
data-bs-target="#configModal"
>
{{ $p.t('ui/settings') }}
</button>
</li>
<li><hr class="dropdown-divider m-0"/></li>
<li>
<nav-language @changed="$emit('language-changed', $event)" item-class="dropdown-item border-left-dark" />
</li>
<li><hr class="dropdown-divider m-0"/></li>
<li>
<a class="dropdown-item" :href="logoutUrl">
{{ $p.t('ui/logout') }}
</a>
</li>
</ul>
</div>
</header>
`,
};
@@ -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 <https://www.gnu.org/licenses/>.
*/
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: `
<bs-modal
ref="modal"
class="bootstrap-prompt"
dialog-class="modal-lg"
data-cy="historyModal"
>
<template #title>History</template>
<template #default>
<table v-if="entries.length" class="table table-bordered table-hover">
<thead class="table-light">
<tr>
<th>Von</th>
<th>Bis</th>
<th>Status</th>
<th>Ort</th>
</tr>
</thead>
<tbody>
<tr v-for="entry in entries" :key="entry.id">
<td>{{ entry.von }}</td>
<td>{{ entry.bis }}</td>
<td>{{ entry.status_kurzbz }}</td>
<td>{{ entry.ort }}</td>
</tr>
</tbody>
</table>
</template>
</bs-modal>
`,
};
@@ -0,0 +1,56 @@
export default {
name: "KeyboardShortcuts",
props: {
shortcuts: {
type: Array,
default: () => []
},
},
methods: {
isInputFocused(target)
{
if (!target)
return false;
return (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target.isContentEditable)
},
shortcutMatches(shortcut, e)
{
let shortcutWantsCtrl = shortcut.ctrl === true;
let ctrlIsPressed = e.ctrlKey || e.metaKey;
if (shortcutWantsCtrl !== ctrlIsPressed)
return false;
if (e.key !== shortcut.key)
return false;
let shouldSkipWhenTyping = shortcut.skipWhenTyping !== false;
return !(shouldSkipWhenTyping && this.isInputFocused(e.target));
},
handleKeydown(e)
{
for (let shortcut of this.shortcuts)
{
if (!this.shortcutMatches(shortcut, e))
continue;
let shouldPreventDefault = shortcut.preventDefault !== false;
if (shouldPreventDefault)
e.preventDefault();
shortcut.handler(e);
return;
}
}
},
mounted() {
window.addEventListener('keydown', this.handleKeydown);
},
beforeUnmount() {
window.removeEventListener('keydown', this.handleKeydown);
},
render() {
return null;
}
};
@@ -0,0 +1,123 @@
import BsModal from "../Bootstrap/Modal.js";
import ApiKalender from "../../api/factory/tempus/kalender.js";
export default {
name: "LehreinheitModal",
components: {
BsModal,
},
emits: ["saved"],
data() {
return {
entries: [],
removedEntries: [],
event: null,
};
},
computed: {
remainingCount() {
return this.entries.length - this.removedEntries.length;
},
},
methods: {
async show(orig)
{
if (!orig?.kalender_id)
return;
this.event = orig;
await this.$api.call(ApiKalender.getLehreinheiten(orig.kalender_id))
.then(result => result.data)
.then(result => {
this.entries = result
this.removedEntries = [];
if (this.entries.length > 1)
{
this.$refs.modal.show();
}
})
.catch(error => {
this.$fhcAlert.handleSystemError(error);
this.close();
});
},
hide() {
this.removedEntries = [];
this.$refs.modal.hide();
},
async confirm() {
let lehreinheiten_ids = this.removedEntries.map((entry) => entry.lehreinheit_id);
await this.$api.call(ApiKalender.deleteFromKalenderEvent(this.event.kalender_id, lehreinheiten_ids));
this.hide();
this.$emit("saved");
},
isRemoved(entry) {
return this.removedEntries.includes(entry);
},
removeEntry(entry) {
if (this.remainingCount <= 1)
return;
this.removedEntries.push(entry);
},
restoreEntry(entry) {
let idx = this.removedEntries.indexOf(entry);
if (idx !== -1)
this.removedEntries.splice(idx, 1);
},
},
template: `
<bs-modal
ref="modal"
class="bootstrap-prompt"
data-cy="lehreinheitModal"
>
<template #title>{{ $p.t('lehre','lehreinheit') }}</template>
<template #default>
<div style="max-height: 300px; overflow-y: auto;">
<div v-if="entries.length === 0" class="text-muted text-center py-3">
{{ $p.t('ui', 'keineEintraegeGefunden') }}
</div>
<div
v-for="(option, index) in entries"
:key="option.lehreinheit_id"
class="d-flex align-items-center justify-content-between w-100 py-1 border-bottom"
>
<span :class="{ 'text-decoration-line-through text-muted': isRemoved(option) }">({{ option.lehreinheit_id }}) {{ option.title }} </span>
<button
v-if="isRemoved(option)"
type="button"
class="btn btn-sm btn-outline-secondary"
@click="restoreEntry(option)"
>
<i class="fa-solid fa-rotate-left"></i>
</button>
<button
v-else
type="button"
class="btn btn-sm btn-outline-danger"
:disabled="remainingCount <= 1"
@click="removeEntry(option)"
>
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-3">
<button type="button" class="btn btn-secondary" @click="hide">
{{ $p.t('ui','abbrechen') }}
</button>
<button
type="button"
class="btn btn-primary"
@click="confirm"
>
{{ $p.t('global','speichern') }}
</button>
</div>
</template>
</bs-modal>
`,
};
+353
View File
@@ -0,0 +1,353 @@
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: [],
loading: false
};
},
computed: {
semester() {
if (!this.max_semester)
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)
{
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;
},
studienplanTrigger() {
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 ?? null;
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.formData.studiensemester_kurzbz);
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.formData.studiensemester_kurzbz);
this.$refs.modal.hide();
})
.catch(this.$fhcAlert.handleSystemError);
},
start(data) {
this.loading = true;
return this.$refs.form
.call(ApiTempusSync.start(data ?? this.formData))
.then(() => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successStart'));
this.$refs.modal.hide();
this.loading = false;
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => {
this.loading = false;
});
},
hideModal()
{
this.formData = {}
}
},
created() {
this.$api
.call(ApiTempusSync.getSyncStatus())
.then(result => this.syncStati = result.data ?? [])
.catch(error => this.$fhcAlert.handleSystemError(error));
},
template: `
<bs-modal ref="modal" @hideBsModal="hideModal">
<template #title>
<template v-if="mode === 'new'">{{ $p.t('lehre', 'tempus_sync_new') }}</template>
<template v-else-if="mode === 'edit'">{{ $p.t('lehre', 'tempus_sync_edit') }}</template>
<template v-else>{{ $p.t('lehre', 'tempus_sync_start') }}</template>
</template>
<core-form ref="form">
<form-validation></form-validation>
<form-input
container-class="mb-3"
type="select"
v-model="formData.oe_kurzbz"
name="oe_kurzbz"
:label="$p.t('lehre/organisationseinheit')"
required
>
<option
v-for="organisation in config.organisationen"
:key="organisation.oe_kurzbz"
:value="organisation.oe_kurzbz"
>
[{{ organisation.organisationseinheittyp_kurzbz }}] {{ organisation.bezeichnung }} {{ ['b', 'm'].includes(organisation.typ) ? organisation.stgbezeichnung : '' }}
</option>
</form-input>
<form-input
container-class="mb-3"
type="select"
v-model="formData.studiensemester_kurzbz"
name="studiensemester_kurzbz"
:label="$p.t('lehre/studiensemester')"
required
>
<option
v-for="studiensemester in config.studiensemestern"
:key="studiensemester.studiensemester_kurzbz"
:value="studiensemester.studiensemester_kurzbz"
>
{{ studiensemester.studiensemester_kurzbz }}
</option>
</form-input>
<form-input
container-class="mb-3"
type="select"
v-model="formData.ausbildungssemester"
name="ausbildungssemester"
:label="$p.t('lehre/ausbildungssemester')"
:disabled="!max_semester"
>
<option :value="null">-- {{ $p.t('fehlermonitoring', 'keineAuswahl') }} --</option>
<option
v-for="number in semester"
:key="number"
:value="number"
>
{{ number }}
</option>
</form-input>
<form-input
container-class="mb-3"
type="select"
v-model="formData.studienplan_id"
name="studienplan_id"
:label="$p.t('lehre/studienplan')"
:disabled="!studienplaene.length"
>
<option :value="null">-- {{ $p.t('fehlermonitoring', 'keineAuswahl') }} --</option>
<option
v-for="plan in studienplaene"
:key="plan.studienplan_id"
:value="plan.studienplan_id"
>
{{ plan.bezeichnung }}-{{plan.orgform_kurzbz}}
</option>
</form-input>
<form-input
container-class="mb-3"
type="DatePicker"
v-model="formData.datum_bis"
name="datum_bis"
:label="$p.t('ui/dateTo')"
auto-apply
:enable-time-picker="false"
text-input
format="dd.MM.yyyy"
preview-format="dd.MM.yyyy"
:teleport="true"
>
</form-input>
<form-input
container-class="mb-3"
type="select"
v-model="formData.sync_status_kurzbz"
name="sync_status_kurzbz"
:label="$p.t('global/status')"
>
<option :value="null">-- {{ $p.t('fehlermonitoring', 'keineAuswahl') }} --</option>
<option
v-for="status in syncStati"
:key="status.status_kurzbz"
:value="status.status_kurzbz"
>
{{ status.bezeichnung }}
</option>
</form-input>
<form-input
container-class="mb-3"
type="checkbox"
v-model="formData.mail"
name="mail"
:label="$p.t('lehre/mail_benachrichtigung')"
>
</form-input>
</core-form>
<template #footer>
<button
type="button"
class="btn btn-primary"
@click="save"
:disabled="loading"
>
<template v-if="mode === 'start'">{{ $p.t('global', 'jetztStarten') }}</template>
<template v-else>{{ $p.t('ui', 'ok') }}</template>
</button>
</template>
</bs-modal>`
};
@@ -0,0 +1,199 @@
import ApiKalender from '../../api/factory/tempus/kalender.js';
import BsModal from '../../../js/components/Bootstrap/Modal.js';
export default {
components: {
BsModal,
},
props: {
show: { type: Boolean, default: false },
lehreinheitId: { type: [String, Number], default: null },
ortKurzbz: { type: String, default: null },
startTime: { type: String, default: null },
endTime: { type: String, default: null },
},
emits: ['close', 'confirmed'],
data() {
return {
loading: false,
plan: [],
skippedWeeks: [],
errors: [],
restOffen: 0,
};
},
watch: {
show(val) {
if (val) {
this.loadPlan();
this.$refs.bsModal.show();
}
else {
this.$refs.bsModal.hide();
}
},
},
computed: {
countTermin() {
return (this.plan.filter(termin => termin.planen).length);
},
formattedSkippedWeeks() {
return this.skippedWeeks.map(week => luxon.DateTime.fromISO(week).toFormat('dd.MM.yyyy')).join('\n');
}
},
methods: {
async loadPlan() {
this.loading = true;
this.plan = [];
this.errors = [];
this.skippedWeeks = [];
this.restOffen = 0;
await this.$api.call(
ApiKalender.calculateMultiWeekPlan(
this.lehreinheitId,
this.ortKurzbz,
this.startTime,
this.endTime
))
.then(result => result.data)
.then(result => {
this.plan = (result.plan || []).map(termin => ({
...termin,
selected_ort_kurzbz: (
termin.raum_vorschlaege?.find(termin => termin.ort_kurzbz === this.ortKurzbz)?.ort_kurzbz
?? termin.raum_vorschlaege?.[0]?.ort_kurzbz
?? termin.ort_kurzbz
),
planen: (termin.collisions && termin.collisions.length > 0) ? false : true,
}));
this.skippedWeeks = result.skipped_weeks || [];
this.errors = result.errors || [];
this.restOffen = result.rest_offen || 0;
this.loading = false;
})
.catch(error => {
this.$fhcAlert.handleSystemError(error);
this.close();
});
},
async confirm() {
this.loading = true;
await this.$api.call(
ApiKalender.confirmMultiWeekPlan(this.plan.filter(termin => termin.planen), this.lehreinheitId)
);
this.$emit('confirmed');
this.close();
},
close() {
this.loading = false;
this.$emit('close');
},
hasCollision(termin) {
return termin.collisions && termin.collisions.length > 0;
},
roomOptions(termin) {
return termin.raum_vorschlaege || [];
},
formatDate(date)
{
return luxon.DateTime.fromISO(date).toFormat('dd.MM.yyyy');
},
formatTime(time)
{
return luxon.DateTime.fromISO(time).toFormat('HH:mm');
}
},
template: `
<bs-modal
ref="bsModal"
class="bootstrap-prompt"
dialog-class="modal-xl"
@hidden-bs-modal="close"
>
<template v-slot:title>Multi-Week Verplanung Vorschau</template>
<div v-if="loading" class="text-center py-4">
<i class="fa fa-spinner fa-spin"></i> Wird berechnet...
</div>
<div v-else>
<div v-if="restOffen > 0" class="alert alert-warning small">
Achtung: {{ restOffen }} Stunden konnten nicht verplant werden (Semesterende erreicht).
</div>
<div v-if="skippedWeeks.length" class="alert alert-warning small">
Übergesprungene Ferienwochen: <div style="white-space: pre-line;">{{ formattedSkippedWeeks }}</div>
</div>
<div v-if="errors.length" class="alert alert-danger small">
<div v-for="(err, i) in errors" :key="i">{{ err.message }}</div>
</div>
<table class="table table-sm align-middle">
<thead>
<tr>
<th>Datum</th>
<th>Zeit</th>
<th>Collisions</th>
<th>Raum</th>
<th>Planen?</th>
</tr>
</thead>
<tbody>
<tr v-for="(termin, i) in plan" :key="i" :class="hasCollision(termin) ? 'table-danger' : 'table-success'">
<td>{{ formatDate(termin.datum) }}</td>
<td>{{ formatTime(termin.start.split(' ')[1]) }} - {{ formatTime(termin.ende.split(' ')[1]) }}</td>
<td>
<span v-if="hasCollision(termin)" class="text-danger small">
<div style="white-space: pre-line;">{{ termin.collisions.map(c => c.message).join('\\n') }}</div>
</span>
<span v-else class="text-success small">
---
</span>
</td>
<td>
<select
v-if="roomOptions(termin).length"
class="form-select form-select-sm"
v-model="termin.selected_ort_kurzbz"
>
<option
v-for="raum in roomOptions(termin)"
:key="raum.ort_kurzbz"
:value="raum.ort_kurzbz"
>
{{ raum.ort_kurzbz }} (Score: {{ raum.score }})
</option>
</select>
<span v-else class="text-muted small">Kein Raum verfügbar</span>
</td>
<td>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
v-model="termin.planen"
:id="'override-' + i"
>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<template #footer>
<button class="btn btn-outline-secondary" @click="close">Abbrechen</button>
<button class="btn btn-primary" :disabled="loading || !plan.length" @click="confirm">
{{ countTermin }} Termine anlegen
</button>
</template>
</bs-modal>
`
}
@@ -0,0 +1,79 @@
import EventCard from '../Calendar/Base/Grid/Line/EventCard.js';
import drop from '../../directives/drop.js';
export default {
name: "ParkingSlot",
components: {
EventCard
},
directives: {
drop
},
emits: ['update:parkedKeys', 'select-kw'],
data() {
return {
parked: [],
parkedKeys: new Set()
};
},
methods: {
park(evt, items) {
const list = Array.isArray(items) ? items : [items];
const stored = JSON.parse(localStorage.getItem('tempus_parking') || '[]');
list.forEach(item => {
const key = `${item.id}`;
if (this.parkedKeys.has(key))
return;
this.parkedKeys.add(key);
stored.push({
type: item.type,
id: item.id,
orig: item.orig
});
this.parked.push(item);
});
localStorage.setItem('tempus_parking', JSON.stringify(stored));
this.$emit('update:parkedKeys', this.parkedKeys);
},
unpark(event) {
const key = `${event.id}`;
this.parkedKeys.delete(key);
this.parked = this.parked.filter(parkedEvent => parkedEvent.id !== event.id);
const stored = JSON.parse(localStorage.getItem('tempus_parking') || '[]').filter(parkedEvent => parkedEvent.id !== event.id);
localStorage.setItem('tempus_parking', JSON.stringify(stored));
this.$emit('update:parkedKeys', this.parkedKeys);
},
isParked(id) {
return this.parkedKeys.has(`${id}`);
}
},
mounted() {
const stored = JSON.parse(localStorage.getItem('tempus_parking') || '[]');
this.parked = stored;
this.parkedKeys = new Set(stored.map(store => `${store.id}`));
this.$emit('update:parkedKeys', this.parkedKeys);
},
template: `
<div class="overflow-auto" tabindex="-1">
<div
id="parkingslot"
class="parkingslot"
v-drop:move.kalender-collection="(evt, item) => park(evt, item)"
>
<i v-if="!parked.length" class="fa-solid fa-square-parking"></i>
<event-card
v-for="parkedEvent in parked"
:key="parkedEvent.id"
:event="parkedEvent"
parked
@select-kw="$emit('select-kw', $event)"
@unpark="unpark"
/>
</div>
</div>
`
}
@@ -0,0 +1,216 @@
import BsModal from "../Bootstrap/Modal.js";
import ApiKalender from "../../api/factory/tempus/kalender.js";
import FormInput from "../../../js/components/Form/Input.js";
export default {
name: "RaumauswahlModal",
components: {
BsModal,
Listbox: primevue.listbox,
FormInput
},
emits: ["saved"],
data() {
return {
entries: [],
selectedRooms: [],
event: null,
loading: false,
mode: 'edit',
pendingEvent: null,
location: null,
};
},
methods: {
async show(orig)
{
if (!orig?.kalender_id)
return;
this.mode = 'edit';
this.event = orig;
this.loading = true;
this.entries = [];
this.selectedRooms = [];
this.location = this.event.location
this.$refs.modal.show();
let result = await this.$api.call(ApiKalender.getRaumvorschlag(orig.kalender_id));
let fetchedEntries = result.data ?? [];
let currentRooms = orig.ort_kurzbz ?? [];
for (let ort_kurzbz of currentRooms)
{
let exists = fetchedEntries.some((r) => r.ort_kurzbz === ort_kurzbz);
if (!exists)
fetchedEntries.push({ ort_kurzbz, score: null, details: [] });
}
this.entries = fetchedEntries;
this.selectedRooms = this.entries.filter((room) => {return currentRooms.includes(room.ort_kurzbz)});
this.entries = [
...this.entries.filter((room) => currentRooms.includes(room.ort_kurzbz)),
...this.entries.filter((room) => !currentRooms.includes(room.ort_kurzbz)),
];
this.loading = false;
},
hide() {
this.selectedRooms = [];
this.pendingEvent = null;
this.$refs.modal.hide();
},
showForNew(lehreinheit_id, start_time, end_time, vorschlaege)
{
this.mode = 'new'
this.pendingEvent = {lehreinheit_id, start_time, end_time}
this.event = null;
this.loading = false;
this.entries = vorschlaege ?? [];
this.selectedRooms = [];
this.$refs.modal.show();
},
async confirm() {
if (this.mode === 'new')
{
const result = await this.$api.call(
ApiKalender.addKalenderEvent(
this.pendingEvent.lehreinheit_id,
this.selectedRooms.map((room) => room.ort_kurzbz),
this.pendingEvent.start_time,
this.pendingEvent.end_time
)
).then((result) => result.data)
if (result.needs_room_selection)
{
this.showForNew(this.pendingEvent.lehreinheit_id, this.pendingEvent.start_time, this.pendingEvent.end_time, result.raum_vorschlaege);
return;
}
this.hide();
this.$emit('saved');
return;
}
let orte = {};
orte.ort_kurzbz = this.selectedRooms.map((room) => room.ort_kurzbz);
orte.location = this.location;
if (orte.ort_kurzbz.length < 1 && orte.location?.trim().length < 1)
{
await this.$api.call(
ApiKalender.deleteOrtEntry(this.event.kalender_id),
);
}
else
{
await this.$api.call(
ApiKalender.updateKalenderEvent(this.event.kalender_id, {orte}),
);
}
this.hide();
this.$emit("saved");
},
},
watch: {
selectedRooms(newVal) {
let sorted = this.entries.filter((room) => {
return newVal.some((sel) => sel.ort_kurzbz === room.ort_kurzbz);
});
let same = sorted.length === newVal.length;
if (same)
{
same = sorted.every((room, i) => {
return room.ort_kurzbz === newVal[i].ort_kurzbz;
});
}
if (!same)
this.selectedRooms = sorted;
},
},
template: `
<bs-modal
ref="modal"
class="bootstrap-prompt"
data-cy="raumauswahlModal"
>
<template #title>{{ $p.t('lehre','roomselection') }}</template>
<template #default>
<div v-if="loading" class="text-center text-muted py-4">
<i class="fa-solid fa-spinner fa-spin me-2"></i>{{ $p.t('ui','loading') }}
</div>
<Listbox
v-else
v-model="selectedRooms"
:options="entries"
option-label="ort_kurzbz"
multiple
filter
:checkmark="true"
:highlightOnSelect="false"
class="w-100"
listStyle="max-height: 300px"
:emptyMessage="$p.t('ui', 'keineEintraegeGefunden')"
>
<template #option="{ option }">
<div class="room-option">
<i class="fa-solid fa-door-open text-muted"></i>
<span v-if="option.raumtyp_kurzbz">
[{{ option.raumtyp_kurzbz }}]
</span>
<span v-else></span>
<span class="fw-semibold">
{{ option.ort_kurzbz }}
</span>
<span class="text-muted small text-nowrap">
{{ option.max_person }} Pers.
</span>
<span
class="text-muted text-end"
v-tooltip="{ value: (option.details || []).join('\\n'), class: 'custom-tooltip' }"
>
{{ option.score }}
</span>
</div>
</template>
</Listbox>
<form-input
v-if="mode === 'edit'"
label="Location"
type="textarea"
container-class="col-12"
name="location"
v-model="location"
></form-input>
<div class="d-flex justify-content-end gap-2 mt-3">
<button type="button" class="btn btn-secondary" @click="hide">
{{ $p.t('ui','abbrechen') }}
</button>
<button
type="button"
class="btn btn-primary"
@click="confirm"
>
{{ $p.t('global','speichern') }}
</button>
</div>
</template>
</bs-modal>
`,
};
@@ -0,0 +1,196 @@
export default {
props: {
event: {
type: Object,
required: true,
},
},
computed: {
classes() {
const classes = [
'cis-renderer-lehreinheit-calendar-event',
'calendar-event-default',
'h-100',
'w-100',
'p-1',
];
if (this.event.collisions) {
classes.push('calendar-event-collisions');
}
return classes;
},
topicString() {
return Array.isArray(this.event.topic)
? this.event.topic.join(', ')
: this.event.topic;
},
ortString() {
let orte = [
...(this.event.ort_kurzbz || []),
this.event.location?.trim(),
].filter(Boolean);
return orte.join(', ');
},
gruppeString() {
return Array.isArray(this.event.gruppe)
? this.event.gruppe.map((gruppe) => gruppe.bezeichnung).join(', ')
: this.event.gruppe;
},
statusIcon()
{
if (['planning', 'sync_preview'].includes(this.event.status_kurzbz))
return 'fa-solid fa-pen-ruler text-muted';
else if (['preview', 'sync_live', 'to_delete_preview'].includes(this.event.status_kurzbz))
return 'fa-solid fa-chalkboard-user text-muted';
else if (['live', 'to_delete_live'].includes(this.event.status_kurzbz))
return 'fa-solid fa-user-graduate text-muted'
},
tooltipString() {
const tooltipArray = [];
tooltipArray.push(
[this.$p.t('global/uhrzeit'), [this.start, this.end].join(' - ')].join(
': ',
),
);
tooltipArray.push(
[this.$p.t('profilUpdate/topic'), this.topicString].join(': '),
);
tooltipArray.push([this.$p.t('person/ort'), this.ortString].join(': '));
if (this.gruppeString) {
tooltipArray.push(
[this.$p.t('lehre/gruppe'), this.gruppeString].join(': '),
);
}
if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) {
if (this.event.lektor.length > 3) {
tooltipArray.push(
[
this.$p.t('lehre/lektor'),
this.event.lektor
.slice(0, 3)
.map((lektor) => lektor.kurzbz)
.join('\n') +
'\n' +
this.$p.t('lehre/weitereLektoren', [
this.event.lektor.length - 3,
]),
].join(': '),
);
} else {
tooltipArray.push(
[
this.$p.t('lehre/lektor'),
this.event.lektor.map((lektor) => lektor.kurzbz).join('\n'),
].join(': '),
);
}
}
return tooltipArray.join('\n');
},
start() {
return luxon.Duration.fromISOTime(this.event.beginn).toISOTime({
suppressSeconds: true,
});
},
end() {
return luxon.Duration.fromISOTime(this.event.ende).toISOTime({
suppressSeconds: true,
});
},
tags() {
if (typeof this.event.tags === 'string') {
try {
return JSON.parse(this.event.tags);
} catch (e) {
console.error('Failed to parse tags:', e);
return [];
}
}
return this.event.tags || [];
},
tagsTooltip() {
if (this.tags.length === 0) {
return '';
}
return this.tags.map((tag) => tag.beschreibung).join('\n');
},
resourcesTooltip() {
if (!this.event.resources || this.event.resources.length === 0) {
return '';
}
let resources = this.event.resources;
if (typeof this.event.resources === 'string') {
try {
resources = JSON.parse(this.event.resources);
} catch (e) {
console.error('Failed to parse resources:', e);
return '';
}
}
return resources.map((resource) => resource.beschreibung).join('\n');
},
},
template: /*html*/ `
<div
:class="classes"
class="position-relative"
@wheel.stop
>
<div class="position-absolute top-0 start-0 m-1 d-flex gap-1" >
<i :class="statusIcon"></i>
<i
v-tooltip="resourcesTooltip"
v-if="event.has_assigned_resources"
class="fa-solid fa-table-list text-muted"
></i>
<i
v-tooltip="tagsTooltip"
v-if="tags?.length"
class="fa-solid fa-tags"
></i>
</div>
<div class="position-absolute bottom-0 start-0 m-1">
{{event.verplante_stunden}}
</div>
<div
v-if="!event.allDayEvent && event?.beginn && event?.ende"
class="event-time d-none d-xl-grid h-100"
>
<span>{{ start }}</span>
<span>{{ end }}</span>
</div>
<div class="event-text" v-tooltip="tooltipString">
<span class="event-topic">{{ topicString }}</span>
<span
class="event-place"
data-cy="calendar-event-room"
>{{ ortString }}</span>
<span v-if="gruppeString" class="event-gruppe">{{ gruppeString }}</span>
<span
v-for="(lektor,index) in event.lektor.slice(0, 3)"
class="event-lectors"
>
{{ lektor.kurzbz }}
</span>
<span
v-if="event.lektor.length > 3"
class="event-lectors-plus"
>
... +{{ event.lektor.length - 3 }}
</span>
</div>
</div>
`,
};
@@ -0,0 +1,200 @@
import { numberPadding, formatDate } from "../../../../helpers/DateHelpers.js"
import LvMenu from "../../../../components/Cis/Mylv/LvMenu.js";
import ApiLvPlan from '../../../../api/factory/lvPlan.js';
import ApiAddons from '../../../../api/factory/addons.js';
export default {
components:{
LvMenu,
},
props:{
event: {
type: Object,
required: true,
}
},
data() {
return {
lvMenu: []
};
},
computed: {
lektorenLinks: function () {
if (!this.event || !Array.isArray(this.event.lektor) || !this.event.lektor.length) return "a";
let lektorenLinks = {};
this.event.lektor.forEach((lektor) => {
lektorenLinks[lektor.kurzbz] = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/Cis/Profil/View/${lektor.mitarbeiter_uid}`;
})
return lektorenLinks;
},
getOrtContentLink: function () {
if (!this.event || !this.event.ort_content_id) return "a";
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/CisVue/Cms/content/${this.event.ort_content_id}`
},
start_time: function () {
if (!this.event.beginn)
return 'N/A';
if (!(this.event.beginn instanceof Date)) {
return this.event.beginn;
}
return numberPadding(this.event.beginn.getHours()) + ":" + numberPadding(this.event.beginn.getMinutes());
},
end_time: function () {
if (!this.event.ende)
return 'N/A';
if (!(this.event.ende instanceof Date)) {
return this.event.ende;
}
return numberPadding(this.event.ende.getHours()) + ":" + numberPadding(this.event.ende.getMinutes());
},
ortString() {
return Array.isArray(this.event.ort_kurzbz) ? this.event.ort_kurzbz.join(', ') : this.event.ort_kurzbz;
},
tags() {
if (typeof this.event.tags === 'string') {
try {
return JSON.parse(this.event.tags);
} catch (e) {
console.error('Failed to parse tags:', e);
return [];
}
}
return this.event.tags || [];
},
resourcesValue() {
let resources = this.event.resources;
if (typeof this.event.resources === 'string') {
try {
resources = JSON.parse(this.event.resources);
} catch (e) {
console.error('Failed to parse resources:', e);
return null;
}
}
return resources.map(resource => resource.beschreibung).join(', ');
},
},
methods: {
methodFormatDate: function (d) {
return formatDate(d);
},
},
created() {
if (this.event.type == 'lehreinheit') {
this.$api
.call(ApiLvPlan.getLehreinheitStudiensemester(Array.isArray(this.event.lehreinheit_id) ? this.event.lehreinheit_id[0] : this.event.lehreinheit_id))
.then(res => res.data)
.then(studiensemester_kurzbz => this.$api.call(
ApiAddons.getLvMenu(
this.event.lehrveranstaltung_id,
studiensemester_kurzbz
)
))
.then(res => {
this.lvMenu = res.data;
});
}
},
template: /*html*/`
<div>
<h5>
{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}
</h5>
<table class="table table-hover mb-4">
<tbody>
<tr>
<th>{{
$p.t('global','datum')?
$p.t('global','datum')+':'
:''
}}</th>
<td>{{methodFormatDate(event.datum)}}</td>
</tr>
<tr>
<th>{{
$p.t('ui','zeitraum')?
$p.t('ui','zeitraum')+':'
:''
}}</th>
<td>{{start_time + ' - ' + end_time}}</td>
</tr>
<tr>
<th>{{
$p.t('global','raum')?
$p.t('global','raum')+':'
:''
}}</th>
<td>
<a v-if="event.ort_content_id" :aria-label="$p.t('global','raum')" :title="$p.t('global','raum')" :href="getOrtContentLink"><i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true" style="color:#00649C"></i></a>
{{ortString}}
</td>
</tr>
<tr>
<th>{{
$p.t('lehre','lehrveranstaltung')?
$p.t('lehre','lehrveranstaltung')+':'
:''
}}</th>
<td>{{'('+event.lehrform+') ' + event.lehrfach_bez}}</td>
</tr>
<tr>
<th>{{
$p.t('lehre','lektor')?
$p.t('lehre','lektor')+':'
:''
}}</th>
<td>
<div id="lektorenContainer">
<div v-for="lektor in event.lektor" class="d-block">
<a v-if="lektorenLinks[lektor.kurzbz]" :aria-label="$p.t('lehre','lektor')" :title="$p.t('lehre','lektor')" :href="lektorenLinks[lektor.kurzbz]"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C" aria-hidden="true"></i></a>
{{lektor.kurzbz}}
</div>
</div>
</td>
</tr>
<tr>
<th>{{
$p.t('lehre','organisationseinheit')?
$p.t('lehre','organisationseinheit')+':'
:''
}}</th>
<td>{{event.organisationseinheit}}</td>
</tr>
<tr v-if="resourcesValue">
<th>{{
$p.t('ui','betriebsmittel')?
$p.t('ui','betriebsmittel')+':'
:''
}}</th>
<td>
{{ resourcesValue }}
</td>
</tr>
<tr v-if="tags.length">
<th>{{
$p.t('ui','tags')?
$p.t('ui','tags')+':'
:''
}}</th>
<td>
<div>
<span
v-for="tag in tags"
:key="tag.tag_typ_kurzbz"
:class="[tag.style, { tag_done: tag.done }]"
class="tag disabled"
>{{ tag.beschreibung }}</span>
</div>
</td>
</tr>
</tbody>
</table>
<lv-menu :containerStyles="['p-0']" :rowStyles="['m-0']" v-if="lvMenu.length" :menu="lvMenu" />
</div>`,
}
@@ -0,0 +1,12 @@
export default {
props:{
event: {
type: Object,
required: true,
}
},
template:`
<div v-if="event.titel">{{ event.titel + ' - ' + event.lehrfach_bez + ' [' + event.ort_kurzbz+']'}}</div>
<div v-else>{{ event.lehrfach_bez + ' [' + event.ort_kurzbz+']'}}</div>
`
}
@@ -0,0 +1,143 @@
export default {
props: {
event: {
type: Object,
required: true
}
},
computed: {
classes() {
const classes = ['cis-renderer-reservierungen-calendar-event', 'calendar-event-default', 'h-100', 'w-100', 'p-1'];
if (this.event.collisions) {
classes.push('calendar-event-collisions');
}
return classes;
},
tooltipString() {
const tooltipArray = [];
tooltipArray.push([
this.$p.t('global/uhrzeit'),
[this.start, this.end].join(' - ')
].join(": "));
tooltipArray.push([
this.$p.t('profilUpdate/topic'),
this.topicString
].join(": "));
tooltipArray.push([
this.$p.t('person/ort'),
this.ortString
].join(": "));
if (this.gruppeString) {
tooltipArray.push([
this.$p.t('lehre/gruppe'),
this.gruppeString
].join(": "));
}
if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) {
if (this.event.lektor.length > 3) {
tooltipArray.push([
this.$p.t('lehre/lektor'),
this.event.lektor.slice(0, 3).map(lektor => lektor.kurzbz).join("\n")
+ "\n" + this.$p.t('lehre/weitereLektoren', [this.event.lektor.length - 3])
].join(": "));
} else {
tooltipArray.push([
this.$p.t('lehre/lektor'),
this.event.lektor.map(lektor => lektor.kurzbz).join("\n")
].join(": "));
}
}
if (Array.isArray(this.event.teilnehmer_person) && this.event.teilnehmer_person.length > 0) {
if (this.event.teilnehmer_person.length > 3) {
tooltipArray.push([
this.$p.t('lehre/teilnehmer'),
this.event.teilnehmer_person.slice(0, 3).map(person => `${person.vorname} ${person.nachname}`).join("\n")
+ "\n" + this.$p.t('lehre/weitereTeilnehmer', [this.event.teilnehmer_person.length - 3])
].join(": "));
} else {
tooltipArray.push([
this.$p.t('lehre/teilnehmer'),
this.event.teilnehmer_person.map(person => `${person.vorname} ${person.nachname}`).join("\n")
].join(": "));
}
}
return tooltipArray.join("\n");
},
start() {
return luxon.Duration
.fromISOTime(this.event.beginn)
.toISOTime({ suppressSeconds: true });
},
end() {
return luxon.Duration
.fromISOTime(this.event.ende)
.toISOTime({ suppressSeconds: true });
},
ortString() {
let orte = [...(this.event.ort_kurzbz || []), this.event.location?.trim()].filter(Boolean)
return orte.join(', ')
},
topicString() {
return Array.isArray(this.event.titel) ? this.event.titel.join(', ') : this.event.titel;
},
gruppeString() {
return Array.isArray(this.event.teilnehmer_gruppe)
? this.event.teilnehmer_gruppe.map(gruppe => gruppe.gruppe_kurzbz).join(', ')
: this.event.teilnehmer_gruppe;
},
},
template: /* html */`
<div
:class="classes"
>
<div
v-if="!event.allDayEvent && event?.beginn && event?.ende"
class="event-time d-grid h-100"
>
<span>{{ start }}</span>
<span>{{ end }}</span>
</div>
<div class="event-text" v-tooltip="tooltipString">
<span class="event-topic">{{ topicString }}</span>
<span
v-for="lektor in event.lektor.slice(0, 3)"
class="event-lectors"
>
{{ lektor.kurzbz }}
</span>
<span
v-if="event.lektor.length > 3"
class="event-lectors-plus"
>
... +{{ event.lektor.length - 3 }}
</span>
<span
v-for="person in (event.teilnehmer_person || []).slice(0, 3)"
class="event-teilnehmer"
>
{{ person.vorname }} {{ person.nachname }}
</span>
<span
v-if="event.teilnehmer_person && event.teilnehmer_person.length > 3"
class="event-teilnehmer-plus"
>
... +{{ event.teilnehmer_person.length - 3 }}
</span>
<span
class="event-place"
data-cy="calendar-event-room"
>{{ ortString }}</span>
<span v-if="gruppeString" class="event-gruppe">{{ gruppeString }}</span>
</div>
</div>
`,
}
@@ -0,0 +1,133 @@
import { numberPadding, formatDate } from "../../../../helpers/DateHelpers.js"
import LvMenu from "../../../Cis/Mylv/LvMenu.js";
export default {
props:{
event: {
type: Object,
required: true,
},
lvMenu:{
type: Object,
required: false,
default: null,
},
},
components:{
LvMenu,
},
computed: {
lektorenLinks: function () {
if (!this.event || !Array.isArray(this.event.lektor) || !this.event.lektor.length) return "a";
let lektorenLinks = {};
this.event.lektor.forEach((lektor) => {
lektorenLinks[lektor.kurzbz] = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/Cis/Profil/View/${lektor.mitarbeiter_uid}`;
})
return lektorenLinks;
},
getOrtContentLink: function () {
if (!this.event || !this.event.ort_content_id) return "a";
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/CisVue/Cms/content/${this.event.ort_content_id}`
},
start_time: function () {
if (!this.event.beginn)
return 'N/A';
if (!(this.event.beginn instanceof Date)) {
return this.event.beginn;
}
return numberPadding(this.event.beginn.getHours()) + ":" + numberPadding(this.event.beginn.getMinutes());
},
end_time: function () {
if (!this.event.ende)
return 'N/A';
if (!(this.event.ende instanceof Date)) {
return this.event.ende;
}
return numberPadding(this.event.ende.getHours()) + ":" + numberPadding(this.event.ende.getMinutes());
},
ortString() {
return Array.isArray(this.event.ort_kurzbz) ? this.event.ort_kurzbz.join(', ') : this.event.ort_kurzbz;
},
},
methods: {
methodFormatDate: function (d) {
return formatDate(d);
},
},
template: `
<div>
<h6>
{{ event.beschreibung }}
</h6>
<h5>
{{$p.t('ui','reservierungsinformationen')}}
</h5>
<table class="table table-hover mb-4">
<tbody>
<tr>
<th>{{
$p.t('global','datum')?
$p.t('global','datum')+':'
:''
}}</th>
<td>{{methodFormatDate(event.datum)}}</td>
</tr>
<tr>
<th>{{
$p.t('ui','zeitraum')?
$p.t('ui','zeitraum')+':'
:''
}}</th>
<td>{{start_time + ' - ' + end_time}}</td>
</tr>
<tr>
<th>{{
$p.t('global','raum')?
$p.t('global','raum')+':'
:''
}}</th>
<td>
<a v-if="event.ort_content_id" :aria-label="$p.t('global','raum')" :title="$p.t('global','raum')" :href="getOrtContentLink"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C" aria-hidden="true"></i></a>
{{ortString}}
</td>
</tr>
<tr>
<th>{{
$p.t('ui','organisierende')?
$p.t('ui','organisierende')+':'
:''
}}</th>
<td>
<div v-for="lektor in event.lektor" class="d-block">
<a v-if="lektorenLinks[lektor.kurzbz]" :aria-label="$p.t('lehre','lektor')" :tooltip="$p.t('lehre','lektor')" :href="lektorenLinks[lektor.kurzbz]"><i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true" style="color:#00649C"></i></a>
{{lektor.kurzbz}}
</div>
</td>
</tr>
<tr>
<th>{{
$p.t('ui','teilnehmende')?
$p.t('ui','teilnehmende')+':'
:''
}}</th>
<td>
<div v-for="teilnehmer in event.teilnehmer_person" class="d-block">
{{teilnehmer.kurzbz}}
</div>
<div v-for="gruppe in event.teilnehmer_gruppe" class="d-block">
{{gruppe.gruppe_kurzbz}}
</div>
</td>
</tr>
</tbody>
</table>
</div>`,
}

Some files were not shown because too many files have changed in this diff Show More