Compare commits

...
Author SHA1 Message Date
Ivymaster 1d21ff6ecc Add kalender column rename from eindeutige_gruppen_id to eindeutige_kalender_gruppen_id 2026-08-06 13:31:55 +02:00
Ivymaster 08e63ac180 Merge branch 'feature-78220/titel-tals-tag-migrieren' into feature-77460/ICAL-new-tempus-implementation 2026-08-04 11:36:45 +02:00
Ivymaster 47c1e935d5 Merge branch 'feature-78052/tempus-tag-events' into feature-78220/titel-tals-tag-migrieren 2026-08-04 11:35:48 +02:00
Ivymaster 45b0268105 BUG: Added reload of events on lang change, for tag display update 2026-08-04 11:35:06 +02:00
Ivymaster c19f0a7b00 Add note tag title from old tempus, in new html previes 2026-08-04 11:23:48 +02:00
Ivymaster ef3b6461de Conflicts resolved 2026-08-04 09:08:12 +02:00
Ivymaster fd74edbe16 Add old tempus stundenplan title as tag for new tempus kalendar event 2026-08-04 08:53:55 +02:00
Ivymaster 200c79edaa Added new event details endopint for html preview 2026-07-30 13:04:16 +02:00
Ivymaster abddb8e6f5 Minor mods after consultation 2026-07-30 09:54:38 +02:00
Ivymaster 98cf44de7f BUG: Fix exc tempus tag modal closing, and tag translations 2026-07-29 17:18:51 +02:00
Ivymaster 3076c3b2c8 Mods found during manual testing 2026-07-29 14:10:59 +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
Ivymaster 02a81fd52e BUG: fix for available tempus event resource suggestions 2026-07-22 08:58:58 +02:00
Ivymaster 8a3909f4d5 WIP 2026-07-22 08:52:58 +02:00
Ivymaster 3a1e0318fe BUG: Issue with resources 2026-07-21 15:54:56 +02:00
Ivymaster e39658bcf8 WIP: Streamlining the old and new html lvplan preview 2026-07-21 13:54:37 +02:00
Ivymaster 5288c93882 WIP: add first version of html preview endpoint for lvplans 2026-07-20 16:37:39 +02:00
Ivymaster 7867c57f4e WIP: add first version of excel fetching endpoint for lvplans 2026-07-20 15:36:42 +02:00
Ivymaster f7762523f3 WIP: add first version of csv fetching endpoint for lvplans 2026-07-20 15:18:49 +02:00
Ivymaster 0a9b1c26d3 Add removal of unused links in lv calendars 2026-07-20 14:41:07 +02:00
Ivymaster 7d2246b2e9 BUG: add fix for display of unloaded calendar renderer 2026-07-20 14:40:23 +02:00
Ivymaster 66c4488e8a WIP: add first version of ical fetching endpoint for lvplans 2026-07-20 13:44:55 +02:00
Ivymaster 9a5df8c003 WIP: add first version of ical fetching endpoint 2026-07-20 13:01:40 +02:00
Ivymaster bca9bf0b8d Conflicts resolved 2026-07-20 09:54:56 +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
Ivymaster a78fc177a5 add event teilnehmer extension for getting targeted lecturers events. add demo icons display in calDAV calendar exports 2026-07-07 14:30:23 +02:00
Ivymaster 4c1d88a054 add clearing of unused classes in lvplan caldav export lib 2026-07-07 13:42:54 +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
Ivymaster 280df46d6d WIP: Second version of cal dav with new tempus structure 2026-07-06 18:17:23 +02:00
Ivymaster 3e17e99ce9 First version of cal dav with new tempus structure 2026-07-06 16:22:33 +02:00
Ivymaster 790d0d99ec add custom sabredev browser view 2026-06-30 14:17:10 +02:00
Ivymaster 5499c522cb add version migration of sabredav from 1.8.1 to 4.0.3 and updated files where it is used 2026-06-30 13:59:36 +02:00
Ivymaster ac5478f176 add sabredav package extraction to global composer file 2026-06-30 12:49:18 +02:00
Ivymaster 1805fc643e add bug fixes and test issue fixes 2026-06-24 17:59:55 +02:00
Ivymaster af7ca6b524 WIP: fixing issues found with cypress tests 2026-06-24 16:35:07 +02:00
Ivymaster 2630a1190b add update of kalender event location before req refetch 2026-06-23 17:10:08 +02:00
Ivymaster 0be1bb95a5 add minor mods 2026-06-22 16:05:46 +02:00
Ivymaster 91a691c5f8 Minor mod 2026-06-22 15:49:16 +02:00
Ivymaster 4edec4df00 add changes for calendar flicker on event reload, and calendar scroll mods 2026-06-22 15:06:53 +02:00
Ivymaster f5cd0f1a5d BUG: add query for inserting new resources collison global variable 2026-06-22 15:04:53 +02:00
Ivymaster 688f815178 BUG: add resovement for multiple ort import from old tempus per one kalendar item 2026-06-19 12:48:57 +02:00
Ivymaster 273b1d3fe4 add resources conflict checker and extend tempus tests with it 2026-06-19 11:41:56 +02:00
Ivymaster de60d0d09e Conflicts resolved 2026-06-15 12:47:29 +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
Ivymaster 245b724314 Minor rfactor 2026-06-15 01:01:33 +02:00
Ivymaster 10a5aaf9b4 Minor mods 2026-06-12 18:49:15 +02:00
Ivymaster 544c6e7319 Added seeders for tempus tests 2026-06-12 16:51:24 +02:00
Ivymaster a7ddc0d348 Merge branch 'master' into feature-68298/Tempus_Grundstruktur-tests 2026-06-08 16:56:42 +02:00
Ivymaster 1e2cafef1d Add API tests for tempus event creation feature 2026-06-08 12:26:43 +02:00
Ivymaster 4205e490ef Add API tests for tempus event update feature 2026-06-01 15:27:38 +02:00
Ivymaster 55ba874bb0 Merge branch 'feature-68298/Tempus_Grundstruktur' into feature-68298/Tempus_Grundstruktur-tests 2026-06-01 15:09:53 +02:00
ma0048 72692812ff lecture collision check bug fix 2026-06-01 15:08:29 +02:00
Ivymaster 90eb25716d WIP: Tempus api events 2026-06-01 15:01:29 +02:00
ma0048 80dabf5ce6 added join to hours for zeitsperren collision check 2026-06-01 14:28:27 +02:00
Ivymaster bc7fc9611b Merge branch 'feature-68298/Tempus_Grundstruktur' into feature-68298/Tempus_Grundstruktur-tests 2026-06-01 14:09:50 +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
Ivymaster 1ecf011ba2 Merge branch 'feature-68298/Tempus_Grundstruktur' into feature-68298/Tempus_Grundstruktur-tests 2026-06-01 11:25:44 +02:00
ma0048 e62848c773 added collision checks on creation 2026-06-01 11:18:53 +02:00
Ivymaster bccaf72399 Merge branch 'feature-68298/Tempus_Grundstruktur' into feature-68298/Tempus_Grundstruktur-tests 2026-06-01 09:17:07 +02:00
Ivymaster 64b6e4a009 Add tempus test file split 2026-05-31 23:17:22 +02:00
Ivymaster 40c18d3603 Add basic tests for tempus feature 2026-05-31 23:03:16 +02:00
Ivymaster d948251736 WIP: Tempus e2e tests 2026-05-27 10:29:13 +02:00
ma0048 9ac9804563 implement first version of collision checks 2026-05-26 14:09:58 +02:00
Ivymaster af3d96abc7 Merge branch 'feature-68298/Tempus_Grundstruktur' into feature-68298/Tempus_Grundstruktur-tests 2026-05-21 13:15:42 +02:00
Ivymaster dc05d16545 WIP: Adding base tempus tests 2026-05-21 13:12:09 +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
187 changed files with 26927 additions and 483 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'
);
});
+12
View File
@@ -122,9 +122,21 @@ $route['api/frontend/v1/stv/[sS]tudents/([WS]S[0-9]{4})/prestudent/(:num)'] = 'a
// // (studiensemester_kurzbz)/person/(person_id) // // (studiensemester_kurzbz)/person/(person_id)
$route['api/frontend/v1/stv/[sS]tudents/([WS]S[0-9]{4})/person/(:num)'] = 'api/frontend/v1/stv/Students/getPerson/$1/$2'; $route['api/frontend/v1/stv/[sS]tudents/([WS]S[0-9]{4})/person/(:num)'] = 'api/frontend/v1/stv/Students/getPerson/$1/$2';
$route['lvplan/caldav'] = 'LvPlanCalDAV';
$route['lvplan/caldav/.*'] = 'LvPlanCalDAV';
$route['lvplan/ical/subscription/google'] = 'LvPlanICALSubscription';
$route['lvplan/ical/download'] = 'LvPlanICALDownload';
$route['lvplan/csv/download'] = 'LvPlanCSVDownload';
$route['lvplan/excel/download'] = 'LvPlanExcelDownload';
$route['lvplan/html/preview'] = 'LvPlanHTMLPreview';
$route['lvplan/html/detail'] = 'LvPlanHTMLDetail';
// load routes from extensions, also look for environment-specific configs // load routes from extensions, also look for environment-specific configs
$subdirs = ['application/config/extensions', 'application/config/' . ENVIRONMENT . '/extensions']; $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) foreach($subdirs as $subdir)
{ {
if(is_dir($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],
];
@@ -0,0 +1,311 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
require_once(FHCPATH.'include/functions.inc.php');
class LvPlanCSVDownload extends Auth_Controller
{
const MAX_DATE_RANGE = 34560000; // 400 days
/**
* Object initialization.
*/
public function __construct()
{
parent::__construct([
'index' => ['basis/cis:r']
]);
$this->load->model('person/Benutzer_model', 'BenutzerModel');
$this->load->model('ressource/Ort_model', 'OrtModel');
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$this->loadPhrases(array('global', 'lehre', 'ui'));
}
/**
* Serves an authenticated CSV file download.
*
* @return void
*/
public function index()
{
$type = $this->getType();
$uid = getAuthUID();
$ortKurzbz = null;
$verbandFilters = null;
if ($type === 'ort')
$ortKurzbz = $this->getRoom();
elseif ($type === 'verband')
$verbandFilters = $this->getVerbandFilters();
else
{
$uid = $this->getUid();
$this->authorizeUid($uid);
$this->validateUid($uid);
}
$target = $this->getTarget();
list($begin, $ende) = $this->getDateRange();
$this->load->library('LvPlanCSVLib');
$content = $this->lvplancsvlib->getContent(
$uid,
$begin,
$ende,
$type,
$target,
$ortKurzbz,
$verbandFilters
);
$filename = $this->getFilename($begin, $ende, $target);
$this->output
->set_content_type('text/csv', 'UTF-8')
->set_header('Content-Disposition: attachment; filename="'.$filename.'"')
->set_header('Pragma: public')
->set_header('Expires: 0')
->set_output($content);
}
/**
* @return string Requested timetable type.
*/
private function getType()
{
$type = $this->input->get('type', true);
if (!in_array($type, array('student', 'lektor', 'ort', 'verband'), true))
show_error('Ungueltiger Parameter: type', 400);
return $type;
}
/**
* @return string Requested user UID.
*/
private function getUid()
{
$uid = $this->input->get('pers_uid', true);
if (!is_string($uid) || $uid === '' || !check_utf8($uid))
show_error('Ungueltiger Parameter: pers_uid', 400);
return $uid;
}
/**
* @return string Requested room identifier.
*/
private function getRoom()
{
$ortKurzbz = $this->input->get('ort_kurzbz', true);
if (!is_string($ortKurzbz) || $ortKurzbz === '' || !check_utf8($ortKurzbz))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
$this->OrtModel->addSelect('ort_kurzbz');
$room = $this->OrtModel->load($ortKurzbz);
if (isError($room))
show_error(getError($room), 500);
if (!hasData($room))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
return $ortKurzbz;
}
/**
* @return array Requested Lehrverband filters.
*/
private function getVerbandFilters()
{
$stgKz = $this->getInteger('stg_kz', true);
$semester = $this->getInteger('sem', false, true);
$verband = $this->getOptionalFilter('ver');
$gruppe = $this->getOptionalFilter('grp');
$this->StudiengangModel->addSelect('studiengang_kz');
$studiengang = $this->StudiengangModel->load($stgKz);
if (isError($studiengang))
show_error(getError($studiengang), 500);
if (!hasData($studiengang))
show_error('Ungueltiger Parameter: stg_kz', 400);
return array(
'stg_kz' => $stgKz,
'sem' => $semester,
'ver' => $verband,
'grp' => $gruppe
);
}
private function getInteger($name, $signed, $optional = false)
{
$value = $this->input->get($name, true);
if ($optional && ($value === null || $value === ''))
return null;
$pattern = $signed ? '/^-?\d+$/' : '/^\d+$/';
if (!is_string($value) || !preg_match($pattern, $value))
show_error('Ungueltiger Parameter: '.$name, 400);
$integer = filter_var($value, FILTER_VALIDATE_INT);
if ($integer === false || (!$signed && $integer < 0))
show_error('Ungueltiger Parameter: '.$name, 400);
return $integer;
}
private function getOptionalFilter($name)
{
$value = $this->input->get($name, true);
if ($value === null || $value === '' || $value === '0')
return null;
if (!is_string($value)
|| !check_utf8($value)
|| mb_strlen($value) > 20
|| !preg_match('/^[[:alnum:]_-]+$/u', $value))
{
show_error('Ungueltiger Parameter: '.$name, 400);
}
return $value;
}
/**
* @return string|null Requested CSV target.
*/
private function getTarget()
{
$target = $this->input->get('target', true);
if ($target !== null && $target !== 'outlook')
show_error('Ungueltiger Parameter: target', 400);
return $target;
}
/**
* Validates and expands the requested range to complete weeks.
*
* @return array{0: int, 1: int} Begin and end timestamps.
*/
private function getDateRange()
{
$begin = $this->getTimestamp('begin');
$ende = $this->getTimestamp('ende');
if ($ende < $begin)
show_error('Ungueltiger Datumsbereich', 400);
if ($ende - $begin > self::MAX_DATE_RANGE)
show_error('Datumsbereich zu gross', 400);
$begin = strtotime('monday this week', $begin);
$ende = strtotime('sunday this week', $ende);
return array($begin, $ende);
}
/**
* Builds the CSV download filename.
*
* @param int $begin Range start timestamp.
* @param int $ende Range end timestamp.
* @param string|null $target Requested CSV target.
* @return string Filename.
*/
private function getFilename($begin, $ende, $target)
{
$studiensemester = $this->getStudiensemester($begin, $ende);
$filename = $this->p->t('global', 'termine');
if ($studiensemester !== null)
$filename .= '_'.$studiensemester->studiensemester_kurzbz;
$filename .= '-'.date('Ymd');
if ($target === 'outlook')
$filename .= '_outlook';
$filename .= '.csv';
return $filename;
}
/**
* Gets the study semester covered by the requested date range.
*
* @param int $begin Range start timestamp.
* @param int $ende Range end timestamp.
* @return object|null Study semester, if one overlaps the range.
*/
private function getStudiensemester($begin, $ende)
{
$semesterRange = $this->StudiensemesterModel->getByDateRange(
date('Y-m-d', $begin),
date('Y-m-d', $ende)
);
if (isError($semesterRange) || !hasData($semesterRange))
return null;
$studiensemester = current(getData($semesterRange));
return $studiensemester;
}
/**
* @param string $name GET parameter name.
* @return int Unix timestamp.
*/
private function getTimestamp($name)
{
$value = $this->input->get($name, true);
if (!is_string($value) || !ctype_digit($value))
show_error('Ungueltiger Parameter: '.$name, 400);
$timestamp = filter_var(
$value,
FILTER_VALIDATE_INT,
array('options' => array('min_range' => 0))
);
if ($timestamp === false)
show_error('Ungueltiger Parameter: '.$name, 400);
return $timestamp;
}
/**
* Ensures access to another user's timetable is explicitly permitted.
*
* @param string $uid Requested user UID.
* @return void
*/
private function authorizeUid($uid)
{
if ($uid !== getAuthUID()
&& !$this->permissionlib->isBerechtigt('basis/other_lv_plan'))
{
show_error('Forbidden', 403);
}
}
/**
* Ensures the requested user exists.
*
* @param string $uid Requested user UID.
* @return void
*/
private function validateUid($uid)
{
$this->BenutzerModel->addSelect('uid');
$user = $this->BenutzerModel->load(array($uid));
if (isError($user))
show_error(getError($user), 500);
if (!hasData($user))
show_error('Ungueltiger Parameter: pers_uid', 400);
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*/
class LvPlanCalDAV extends FHC_Controller
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->load->library('sabredav/SabreDAVAuthLib');
$this->load->library('sabredav/SabreDAVCalDAVLib');
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
public function index()
{
$authBackend = new SabreDAVAuthLib();
$principalBackend = new SabreDAVACLPrincipalBackend($authBackend);
$calendarBackend = new SabreDAVCalDAVBackend($authBackend);
$tree = array(
new \Sabre\CalDAV\Principal\Collection($principalBackend),
new \Sabre\CalDAV\CalendarRoot($principalBackend, $calendarBackend)
);
$server = new \Sabre\DAV\Server($tree);
$baseUri = '/' . implode('/', array_filter([
trim(BASE_LOCATION, '/'),
trim($this->config->item('index_page'), '/'),
'lvplan',
'caldav'
]));
$server->setBaseUri($baseUri);
$authBackend->setRealm('SabreDAV');
$authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend);
$server->addPlugin($authPlugin);
$caldavPlugin = new \Sabre\CalDAV\Plugin();
$server->addPlugin($caldavPlugin);
$aclPlugin = new \Sabre\DAVACL\Plugin();
$server->addPlugin($aclPlugin);
$browser = new \Sabre\DAV\Browser\Plugin();
$server->addPlugin($browser);
$server->exec();
}
}
@@ -0,0 +1,292 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
require_once(FHCPATH.'include/functions.inc.php');
class LvPlanExcelDownload extends Auth_Controller
{
const MAX_DATE_RANGE = 34560000; // 400 days
/**
* Object initialization.
*/
public function __construct()
{
parent::__construct([
'index' => ['basis/cis:r']
]);
$this->load->model('person/Benutzer_model', 'BenutzerModel');
$this->load->model('ressource/Ort_model', 'OrtModel');
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$this->loadPhrases(array('global'));
}
/**
* Serves an authenticated Excel file download.
*
* @return void
*/
public function index()
{
$type = $this->getType();
$uid = getAuthUID();
$ortKurzbz = null;
$verbandFilters = null;
if ($type === 'ort')
$ortKurzbz = $this->getRoom();
elseif ($type === 'verband')
$verbandFilters = $this->getVerbandFilters();
else
{
$uid = $this->getUid();
$this->authorizeUid($uid);
$this->validateUid($uid);
}
list($begin, $ende) = $this->getDateRange();
$this->load->library('LvPlanExcelLib');
$content = $this->lvplanexcellib->getContent(
$uid,
$begin,
$ende,
$type,
$ortKurzbz,
$verbandFilters
);
$filename = $this->getFilename($begin, $ende);
$this->output
->set_content_type('application/vnd.ms-excel')
->set_header('Content-Disposition: attachment; filename="'.$filename.'"')
->set_header('Pragma: public')
->set_header('Expires: 0')
->set_output($content);
}
/**
* @return string Requested timetable type.
*/
private function getType()
{
$type = $this->input->get('type', true);
if (!in_array($type, array('student', 'lektor', 'ort', 'verband'), true))
show_error('Ungueltiger Parameter: type', 400);
return $type;
}
/**
* @return string Requested user UID.
*/
private function getUid()
{
$uid = $this->input->get('pers_uid', true);
if (!is_string($uid) || $uid === '' || !check_utf8($uid))
show_error('Ungueltiger Parameter: pers_uid', 400);
return $uid;
}
/**
* @return string Requested room identifier.
*/
private function getRoom()
{
$ortKurzbz = $this->input->get('ort_kurzbz', true);
if (!is_string($ortKurzbz) || $ortKurzbz === '' || !check_utf8($ortKurzbz))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
$this->OrtModel->addSelect('ort_kurzbz');
$room = $this->OrtModel->load($ortKurzbz);
if (isError($room))
show_error(getError($room), 500);
if (!hasData($room))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
return $ortKurzbz;
}
/**
* @return array Requested Lehrverband filters.
*/
private function getVerbandFilters()
{
$stgKz = $this->getInteger('stg_kz', true);
$semester = $this->getInteger('sem', false, true);
$verband = $this->getOptionalFilter('ver');
$gruppe = $this->getOptionalFilter('grp');
$this->StudiengangModel->addSelect('studiengang_kz');
$studiengang = $this->StudiengangModel->load($stgKz);
if (isError($studiengang))
show_error(getError($studiengang), 500);
if (!hasData($studiengang))
show_error('Ungueltiger Parameter: stg_kz', 400);
return array(
'stg_kz' => $stgKz,
'sem' => $semester,
'ver' => $verband,
'grp' => $gruppe
);
}
private function getInteger($name, $signed, $optional = false)
{
$value = $this->input->get($name, true);
if ($optional && ($value === null || $value === ''))
return null;
$pattern = $signed ? '/^-?\d+$/' : '/^\d+$/';
if (!is_string($value) || !preg_match($pattern, $value))
show_error('Ungueltiger Parameter: '.$name, 400);
$integer = filter_var($value, FILTER_VALIDATE_INT);
if ($integer === false || (!$signed && $integer < 0))
show_error('Ungueltiger Parameter: '.$name, 400);
return $integer;
}
private function getOptionalFilter($name)
{
$value = $this->input->get($name, true);
if ($value === null || $value === '' || $value === '0')
return null;
if (!is_string($value)
|| !check_utf8($value)
|| mb_strlen($value) > 20
|| !preg_match('/^[[:alnum:]_-]+$/u', $value))
{
show_error('Ungueltiger Parameter: '.$name, 400);
}
return $value;
}
/**
* Validates and expands the requested range to complete weeks.
*
* @return array{0: int, 1: int} Begin and end timestamps.
*/
private function getDateRange()
{
$begin = $this->getTimestamp('begin');
$ende = $this->getTimestamp('ende');
if ($ende < $begin)
show_error('Ungueltiger Datumsbereich', 400);
if ($ende - $begin > self::MAX_DATE_RANGE)
show_error('Datumsbereich zu gross', 400);
$begin = strtotime('monday this week', $begin);
$ende = strtotime('sunday this week', $ende);
return array($begin, $ende);
}
/**
* Builds the Excel download filename.
*
* @param int $begin Range start timestamp.
* @param int $ende Range end timestamp.
* @return string Filename.
*/
private function getFilename($begin, $ende)
{
$studiensemester = $this->getStudiensemester($begin, $ende);
$filename = $this->p->t('global', 'termine');
if ($studiensemester !== null)
$filename .= '_'.$studiensemester->studiensemester_kurzbz;
$filename .= '-'.date('Ymd').'.xls';
return $filename;
}
/**
* Gets the study semester covered by the requested date range.
*
* @param int $begin Range start timestamp.
* @param int $ende Range end timestamp.
* @return object|null Study semester, if one overlaps the range.
*/
private function getStudiensemester($begin, $ende)
{
$semesterRange = $this->StudiensemesterModel->getByDateRange(
date('Y-m-d', $begin),
date('Y-m-d', $ende)
);
if (isError($semesterRange) || !hasData($semesterRange))
return null;
$studiensemester = current(getData($semesterRange));
return $studiensemester;
}
/**
* @param string $name GET parameter name.
* @return int Unix timestamp.
*/
private function getTimestamp($name)
{
$value = $this->input->get($name, true);
if (!is_string($value) || !ctype_digit($value))
show_error('Ungueltiger Parameter: '.$name, 400);
$timestamp = filter_var(
$value,
FILTER_VALIDATE_INT,
array('options' => array('min_range' => 0))
);
if ($timestamp === false)
show_error('Ungueltiger Parameter: '.$name, 400);
return $timestamp;
}
/**
* Ensures access to another user's timetable is explicitly permitted.
*
* @param string $uid Requested user UID.
* @return void
*/
private function authorizeUid($uid)
{
if ($uid !== getAuthUID()
&& !$this->permissionlib->isBerechtigt('basis/other_lv_plan'))
{
show_error('Forbidden', 403);
}
}
/**
* Ensures the requested user exists.
*
* @param string $uid Requested user UID.
* @return void
*/
private function validateUid($uid)
{
$this->BenutzerModel->addSelect('uid');
$user = $this->BenutzerModel->load(array($uid));
if (isError($user))
show_error(getError($user), 500);
if (!hasData($user))
show_error('Ungueltiger Parameter: pers_uid', 400);
}
}
@@ -0,0 +1,333 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
require_once(FHCPATH.'include/functions.inc.php');
class LvPlanHTMLDetail extends Auth_Controller
{
const TIMEZONE = 'Europe/Vienna';
/**
* Object initialization.
*/
public function __construct()
{
parent::__construct(array(
'index' => array('basis/cis:r')
));
$this->load->model('person/Benutzer_model', 'BenutzerModel');
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->load->model('ressource/Ort_model', 'OrtModel');
$this->load->model('ressource/Stunde_model', 'StundeModel');
$this->loadPhrases(array(
'global' => array('termine'),
'ui' => array('organisierende', 'teilnehmende')
));
}
/**
* Serves details for one hour in the HTML timetable preview.
*
* @return void
*/
public function index()
{
$type = $this->getType();
$datum = $this->getDate();
$hour = $this->getHour();
if ($type === 'verband')
$filters = $this->getVerbandFilters();
elseif ($type === 'ort')
$filters = $this->getRoomFilters();
else
$filters = $this->getPersonalFilters();
$this->load->library('LvPlanHTMLDetailLib');
$content = $this->lvplanhtmldetaillib->getContent(
$datum,
$hour,
$type,
$filters
);
$this->output
->set_content_type('text/html', 'UTF-8')
->set_header('X-Content-Type-Options: nosniff')
->set_output($content);
}
/**
* @return string Requested timetable type.
*/
private function getType()
{
$type = $this->input->get('type', true);
if (!in_array($type, array('student', 'lektor', 'verband', 'ort'), true))
show_error('Ungueltiger Parameter: type', 400);
return $type;
}
/**
* @return string Date in YYYY-MM-DD format.
*/
private function getDate()
{
$value = $this->input->get('datum', true);
if (!is_string($value) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $value))
show_error('Ungueltiger Parameter: datum', 400);
$date = DateTime::createFromFormat(
'!Y-m-d',
$value,
new DateTimeZone(self::TIMEZONE)
);
$errors = DateTime::getLastErrors();
if ($date === false
|| ($errors !== false
&& ($errors['warning_count'] > 0 || $errors['error_count'] > 0))
|| $date->format('Y-m-d') !== $value)
{
show_error('Ungueltiger Parameter: datum', 400);
}
return $value;
}
/**
* @return array Selected timetable hour.
*/
private function getHour()
{
$number = $this->getInteger('stunde', false);
$this->StundeModel->addSelect('stunde, beginn, ende');
$result = $this->StundeModel->load($number);
if (isError($result))
show_error(getError($result), 500);
if (!hasData($result))
show_error('Ungueltiger Parameter: stunde', 400);
$hours = getData($result);
$hour = reset($hours);
$begin = isset($hour->beginn) ? (string)$hour->beginn : '';
$end = isset($hour->ende) ? (string)$hour->ende : '';
$beginSeconds = $this->timeToSeconds($begin);
$endSeconds = $this->timeToSeconds($end);
if ($beginSeconds === null || $endSeconds === null || $endSeconds <= $beginSeconds)
show_error('Ungueltiges Stundenraster', 500);
return array(
'number' => (string)$hour->stunde,
'begin' => $begin,
'end' => $end
);
}
/**
* @return array Personal timetable filters.
*/
private function getPersonalFilters()
{
$uid = $this->input->get('pers_uid', true);
if (!is_string($uid) || $uid === '' || !check_utf8($uid))
show_error('Ungueltiger Parameter: pers_uid', 400);
if ($uid !== getAuthUID()
&& !$this->permissionlib->isBerechtigt('basis/other_lv_plan'))
{
show_error('Forbidden', 403);
}
$this->BenutzerModel->addSelect(
'tbl_benutzer.uid, tbl_person.titelpre, tbl_person.vorname, '
.'tbl_person.nachname, tbl_person.titelpost'
);
$this->BenutzerModel->addJoin('public.tbl_person', 'person_id', 'LEFT');
$result = $this->BenutzerModel->load(array($uid));
if (isError($result))
show_error(getError($result), 500);
if (!hasData($result))
show_error('Ungueltiger Parameter: pers_uid', 400);
$users = getData($result);
$user = reset($users);
$nameParts = array();
foreach (array('titelpre', 'vorname', 'nachname', 'titelpost') as $property)
{
if (isset($user->{$property}) && trim($user->{$property}) !== '')
$nameParts[] = trim($user->{$property});
}
return array(
'uid' => $uid,
'full_name' => implode(' ', $nameParts)
);
}
/**
* @return array Room timetable filters.
*/
private function getRoomFilters()
{
$ortKurzbz = $this->input->get('ort_kurzbz', true);
if (!is_string($ortKurzbz) || $ortKurzbz === '' || !check_utf8($ortKurzbz))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
$this->OrtModel->addSelect('ort_kurzbz, bezeichnung, content_id');
$result = $this->OrtModel->load($ortKurzbz);
if (isError($result))
show_error(getError($result), 500);
if (!hasData($result))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
$rooms = getData($result);
$room = reset($rooms);
return array(
'ort_kurzbz' => $ortKurzbz,
'bezeichnung' => isset($room->bezeichnung) ? $room->bezeichnung : '',
'content_id' => isset($room->content_id) ? $room->content_id : null
);
}
/**
* @return array Lehrverband timetable filters.
*/
private function getVerbandFilters()
{
$stgKz = $this->getInteger('stg_kz', true);
$semester = $this->getInteger('sem', false, true);
$verband = $this->getOptionalFilter('ver');
$gruppe = $this->getOptionalFilter('grp');
$ortKurzbz = $this->getOptionalRoomFilter();
$this->StudiengangModel->addSelect(
'studiengang_kz, UPPER(typ || kurzbz) AS stg_kurzbz'
);
$result = $this->StudiengangModel->load($stgKz);
if (isError($result))
show_error(getError($result), 500);
if (!hasData($result))
show_error('Ungueltiger Parameter: stg_kz', 400);
$programs = getData($result);
$program = reset($programs);
return array(
'stg_kz' => $stgKz,
'stg_kurzbz' => isset($program->stg_kurzbz)
? $program->stg_kurzbz
: '',
'sem' => $semester,
'ver' => $verband,
'grp' => $gruppe,
'ort_kurzbz' => $ortKurzbz
);
}
/**
* Returns the room selected in a Verband preview cell, if present.
*
* @return string|null
*/
private function getOptionalRoomFilter()
{
$ortKurzbz = $this->input->get('ort_kurzbz', true);
if ($ortKurzbz === null || $ortKurzbz === '')
return null;
if (!is_string($ortKurzbz) || !check_utf8($ortKurzbz))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
$this->OrtModel->addSelect('ort_kurzbz');
$result = $this->OrtModel->load($ortKurzbz);
if (isError($result))
show_error(getError($result), 500);
if (!hasData($result))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
return $ortKurzbz;
}
/**
* @param string $name GET parameter name.
* @param bool $signed Whether negative values are allowed.
* @param bool $optional Whether the parameter may be omitted or empty.
* @return int|null
*/
private function getInteger($name, $signed, $optional = false)
{
$value = $this->input->get($name, true);
if ($optional && ($value === null || $value === ''))
return null;
$pattern = $signed ? '/^-?\d+$/' : '/^\d+$/';
if (!is_string($value) || !preg_match($pattern, $value))
show_error('Ungueltiger Parameter: '.$name, 400);
$isNegative = $signed && substr($value, 0, 1) === '-';
$digits = $isNegative ? substr($value, 1) : $value;
$digits = ltrim($digits, '0');
if ($digits === '')
$digits = '0';
$normalizedValue = $isNegative && $digits !== '0' ? '-'.$digits : $digits;
$integer = filter_var($normalizedValue, FILTER_VALIDATE_INT);
if ($integer === false || (!$signed && $integer < 0))
show_error('Ungueltiger Parameter: '.$name, 400);
return $integer;
}
/**
* Returns null for the legacy wildcard values "" and "0".
*
* @param string $name GET parameter name.
* @return string|null
*/
private function getOptionalFilter($name)
{
$value = $this->input->get($name, true);
if ($value === null || $value === '' || $value === '0')
return null;
if (!is_string($value)
|| !check_utf8($value)
|| mb_strlen($value) > 20
|| !preg_match('/^[[:alnum:]_-]+$/u', $value))
{
show_error('Ungueltiger Parameter: '.$name, 400);
}
return $value;
}
/**
* @param string $time PostgreSQL time value.
* @return int|null
*/
private function timeToSeconds($time)
{
if (!preg_match('/^(\d{1,2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?$/', $time, $matches))
return null;
$hours = (int)$matches[1];
$minutes = (int)$matches[2];
$seconds = isset($matches[3]) ? (int)$matches[3] : 0;
if ($hours > 23 || $minutes > 59 || $seconds > 59)
return null;
return $hours * 3600 + $minutes * 60 + $seconds;
}
}
@@ -0,0 +1,356 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
require_once(FHCPATH.'include/functions.inc.php');
class LvPlanHTMLPreview extends Auth_Controller
{
const MAX_DATE_RANGE = 34560000; // 400 days
const DEFAULT_RESERVATION_MIN_DAYS = 1;
const DEFAULT_RESERVATION_END_DATE = '2099-01-01';
/**
* Object initialization.
*/
public function __construct()
{
parent::__construct([
'index' => ['basis/cis:r']
]);
$this->load->model('person/Benutzer_model', 'BenutzerModel');
$this->load->model('crm/Student_model', 'StudentModel');
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->load->model('ressource/Ort_model', 'OrtModel');
$this->loadPhrases(
array(
'lvplan' => array(
'room_not_reservable',
'room_reservation_restricted_to_period'
)
)
);
}
/**
* Serves an authenticated, printable timetable preview.
*
* @return void
*/
public function index()
{
$type = $this->getType();
list($begin, $ende) = $this->getDateRange();
if ($type === 'verband')
$filters = $this->getVerbandFilters();
elseif ($type === 'ort')
$filters = $this->getRoomFilters();
else
$filters = $this->getPersonalFilters($type);
$this->load->library('LvPlanHTMLLib');
$content = $this->lvplanhtmllib->getContent(
$begin,
$ende,
$type,
$filters
);
$this->output
->set_content_type('text/html', 'UTF-8')
->set_header('X-Content-Type-Options: nosniff')
->set_output($content);
}
/**
* @return string Requested timetable type.
*/
private function getType()
{
$type = $this->input->get('type', true);
if (!in_array($type, array('student', 'lektor', 'verband', 'ort'), true))
show_error('Ungueltiger Parameter: type', 400);
return $type;
}
/**
* @return array Personal preview filters.
*/
private function getPersonalFilters($type)
{
// Legacy wochenplan calendar links include empty Lehrverband parameters
// for personal plans. They are irrelevant when type is student or lektor.
if ($this->hasVerbandParameters())
show_error('Ungueltige Parameterkombination', 400);
$uid = $this->input->get('pers_uid', true);
if (!is_string($uid) || $uid === '' || !check_utf8($uid))
show_error('Ungueltiger Parameter: pers_uid', 400);
if ($uid !== getAuthUID()
&& !$this->permissionlib->isBerechtigt('basis/other_lv_plan'))
{
show_error('Forbidden', 403);
}
$this->BenutzerModel->addSelect(
'tbl_benutzer.uid, tbl_person.titelpre, tbl_person.vorname, '
.'tbl_person.nachname, tbl_person.titelpost'
);
$this->BenutzerModel->addJoin('public.tbl_person', 'person_id', 'LEFT');
$user = $this->BenutzerModel->load(array($uid));
if (isError($user))
show_error(getError($user), 500);
if (!hasData($user))
show_error('Ungueltiger Parameter: pers_uid', 400);
$users = getData($user);
$userData = reset($users);
$nameParts = array();
foreach (array('titelpre', 'vorname', 'nachname', 'titelpost') as $property)
{
if (isset($userData->{$property}) && trim($userData->{$property}) !== '')
$nameParts[] = trim($userData->{$property});
}
$filters = array(
'uid' => $uid,
'full_name' => implode(' ', $nameParts)
);
if ($type === 'student')
{
$this->StudentModel->addSelect(
'studiengang_kz, semester, verband, gruppe'
);
$student = $this->StudentModel->load(array($uid));
if (isError($student))
show_error(getError($student), 500);
if (!hasData($student))
show_error('Ungueltiger Parameter: pers_uid', 400);
$students = getData($student);
$studentData = reset($students);
$filters['stg_kz'] = $studentData->studiengang_kz;
$filters['sem'] = $studentData->semester;
$filters['ver'] = $studentData->verband;
$filters['grp'] = $studentData->gruppe;
}
return $filters;
}
/**
* Legacy room links also carry pers_uid, but the UID does not filter or
* authorize a room plan. Only ort_kurzbz identifies the requested plan.
*
* @return array Room preview filters.
*/
private function getRoomFilters()
{
$ortKurzbz = $this->input->get('ort_kurzbz', true);
if (!is_string($ortKurzbz) || $ortKurzbz === '' || !check_utf8($ortKurzbz))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
$this->OrtModel->addSelect('ort_kurzbz, bezeichnung, reservieren');
$room = $this->OrtModel->load($ortKurzbz);
if (isError($room))
show_error(getError($room), 500);
if (!hasData($room))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
$rooms = getData($room);
$roomData = reset($rooms);
$reservationMinDays = defined('RES_TAGE_LEKTOR_MIN')
? (int)constant('RES_TAGE_LEKTOR_MIN')
: self::DEFAULT_RESERVATION_MIN_DAYS;
$reservationEndDate = defined('RES_TAGE_LEKTOR_BIS')
? constant('RES_TAGE_LEKTOR_BIS')
: self::DEFAULT_RESERVATION_END_DATE;
$reservationStart = jump_day(
time(),
$reservationMinDays - 1
);
$reservationEnd = strtotime($reservationEndDate);
return array(
'ort_kurzbz' => $ortKurzbz,
'bezeichnung' => isset($roomData->bezeichnung) ? $roomData->bezeichnung : '',
'room_reservable' => isset($roomData->reservieren)
&& $roomData->reservieren === true,
'room_reservation_permitted' => $this->permissionlib->isBerechtigt(
'lehre/reservierung:begrenzt',
'sui'
),
'room_reservation_start' => $reservationStart,
'room_reservation_end' => $reservationEnd,
'room_not_reservable_message' => $this->p->t(
'lvplan',
'room_not_reservable'
),
'room_reservation_restricted_message' => $this->p->t(
'lvplan',
'room_reservation_restricted_to_period',
array(
'start_date' => date('d.m.Y', $reservationStart),
'end_date' => date('d.m.Y', $reservationEnd)
)
)
);
}
/**
* @return array Lehrverband preview filters.
*/
private function getVerbandFilters()
{
// Legacy wochenplan links always carry pers_uid. It does not filter or
// authorize a Lehrverband plan and is intentionally ignored here.
$stgKz = $this->getInteger('stg_kz', true);
$semester = $this->getInteger('sem', false, true);
$verband = $this->getOptionalFilter('ver');
$gruppe = $this->getOptionalFilter('grp');
$this->StudiengangModel->addSelect(
'studiengang_kz, UPPER(typ || kurzbz) AS stg_kurzbz'
);
$studiengang = $this->StudiengangModel->load($stgKz);
if (isError($studiengang))
show_error(getError($studiengang), 500);
if (!hasData($studiengang))
show_error('Ungueltiger Parameter: stg_kz', 400);
$studiengaenge = getData($studiengang);
$studiengangData = reset($studiengaenge);
return array(
'stg_kz' => $stgKz,
'stg_kurzbz' => isset($studiengangData->stg_kurzbz)
? $studiengangData->stg_kurzbz
: '',
'sem' => $semester,
'ver' => $verband,
'grp' => $gruppe
);
}
/**
* @return bool Whether any non-empty Lehrverband-only parameters were supplied.
*/
private function hasVerbandParameters()
{
foreach (array('stg_kz', 'sem', 'ver', 'grp') as $name)
{
$value = $this->input->get($name, true);
if ($value !== null && $value !== '')
return true;
}
return false;
}
/**
* @param string $name GET parameter name.
* @param bool $signed Whether negative values are allowed.
* @param bool $optional Whether the parameter may be omitted or empty.
* @return int|null
*/
private function getInteger($name, $signed, $optional = false)
{
$value = $this->input->get($name, true);
if ($optional && ($value === null || $value === ''))
return null;
$pattern = $signed ? '/^-?\d+$/' : '/^\d+$/';
if (!is_string($value) || !preg_match($pattern, $value))
show_error('Ungueltiger Parameter: '.$name, 400);
$isNegative = $signed && substr($value, 0, 1) === '-';
$digits = $isNegative ? substr($value, 1) : $value;
$digits = ltrim($digits, '0');
if ($digits === '')
$digits = '0';
$normalizedValue = $isNegative && $digits !== '0' ? '-'.$digits : $digits;
$integer = filter_var($normalizedValue, FILTER_VALIDATE_INT);
if ($integer === false || (!$signed && $integer < 0))
show_error('Ungueltiger Parameter: '.$name, 400);
return $integer;
}
/**
* Returns null for the legacy wildcard values "" and "0".
*
* @param string $name GET parameter name.
* @return string|null
*/
private function getOptionalFilter($name)
{
$value = $this->input->get($name, true);
if ($value === null || $value === '' || $value === '0')
return null;
if (!is_string($value)
|| !check_utf8($value)
|| mb_strlen($value) > 20
|| !preg_match('/^[[:alnum:]_-]+$/u', $value))
{
show_error('Ungueltiger Parameter: '.$name, 400);
}
return $value;
}
/**
* Validates and expands the requested range to complete weeks.
*
* @return array{0: int, 1: int} Begin and end timestamps.
*/
private function getDateRange()
{
$begin = $this->getTimestamp('begin');
$ende = $this->getTimestamp('ende');
if ($ende < $begin)
show_error('Ungueltiger Datumsbereich', 400);
if ($ende - $begin > self::MAX_DATE_RANGE)
show_error('Datumsbereich zu gross', 400);
$begin = strtotime('monday this week', $begin);
$ende = strtotime('sunday this week', $ende);
return array($begin, $ende);
}
/**
* @param string $name GET parameter name.
* @return int Unix timestamp.
*/
private function getTimestamp($name)
{
$value = $this->input->get($name, true);
if (!is_string($value) || !ctype_digit($value))
show_error('Ungueltiger Parameter: '.$name, 400);
$timestamp = filter_var(
$value,
FILTER_VALIDATE_INT,
array('options' => array('min_range' => 0))
);
if ($timestamp === false)
show_error('Ungueltiger Parameter: '.$name, 400);
return $timestamp;
}
}
@@ -0,0 +1,308 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
require_once(FHCPATH.'include/functions.inc.php');
class LvPlanICALDownload extends Auth_Controller
{
const MAX_DATE_RANGE = 34560000; // 400 days
/**
* Object initialization.
*/
public function __construct()
{
parent::__construct([
'index' => ['basis/cis:r']
]);
$this->load->model('person/Benutzer_model', 'BenutzerModel');
$this->load->model('ressource/Ort_model', 'OrtModel');
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$this->loadPhrases(array('global', 'lehre', 'ui'));
}
/**
* Serves an authenticated iCalendar file download.
*
* @return void
*/
public function index()
{
$type = $this->getType();
$uid = getAuthUID();
$ortKurzbz = null;
$verbandFilters = null;
if ($type === 'ort')
$ortKurzbz = $this->getRoom();
elseif ($type === 'verband')
$verbandFilters = $this->getVerbandFilters();
else
{
$uid = $this->getUid();
$this->authorizeUid($uid);
$this->validateUid($uid);
}
$version = $this->getVersion();
list($begin, $ende) = $this->getDateRange();
$this->load->library('LvPlanICALLib');
$content = $this->lvplanicallib->getContent(
$uid,
$begin,
$ende,
$type,
$version,
$ortKurzbz,
$verbandFilters
);
$filename = $this->getFilename($begin, $ende, $version);
$this->output
->set_content_type('text/calendar', 'UTF-8')
->set_header('Content-Disposition: attachment; filename="'.$filename.'"')
->set_header('Pragma: public')
->set_header('Expires: 0')
->set_output($content);
}
/**
* @return string Requested timetable type.
*/
private function getType()
{
$type = $this->input->get('type', true);
if (!in_array($type, array('student', 'lektor', 'ort', 'verband'), true))
show_error('Ungueltiger Parameter: type', 400);
return $type;
}
/**
* @return string Requested user UID.
*/
private function getUid()
{
$uid = $this->input->get('pers_uid', true);
if (!is_string($uid) || $uid === '' || !check_utf8($uid))
show_error('Ungueltiger Parameter: pers_uid', 400);
return $uid;
}
/**
* @return string Requested room identifier.
*/
private function getRoom()
{
$ortKurzbz = $this->input->get('ort_kurzbz', true);
if (!is_string($ortKurzbz) || $ortKurzbz === '' || !check_utf8($ortKurzbz))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
$this->OrtModel->addSelect('ort_kurzbz');
$room = $this->OrtModel->load($ortKurzbz);
if (isError($room))
show_error(getError($room), 500);
if (!hasData($room))
show_error('Ungueltiger Parameter: ort_kurzbz', 400);
return $ortKurzbz;
}
/**
* @return array Requested Lehrverband filters.
*/
private function getVerbandFilters()
{
$stgKz = $this->getInteger('stg_kz', true);
$semester = $this->getInteger('sem', false, true);
$verband = $this->getOptionalFilter('ver');
$gruppe = $this->getOptionalFilter('grp');
$this->StudiengangModel->addSelect('studiengang_kz');
$studiengang = $this->StudiengangModel->load($stgKz);
if (isError($studiengang))
show_error(getError($studiengang), 500);
if (!hasData($studiengang))
show_error('Ungueltiger Parameter: stg_kz', 400);
return array(
'stg_kz' => $stgKz,
'sem' => $semester,
'ver' => $verband,
'grp' => $gruppe
);
}
private function getInteger($name, $signed, $optional = false)
{
$value = $this->input->get($name, true);
if ($optional && ($value === null || $value === ''))
return null;
$pattern = $signed ? '/^-?\d+$/' : '/^\d+$/';
if (!is_string($value) || !preg_match($pattern, $value))
show_error('Ungueltiger Parameter: '.$name, 400);
$integer = filter_var($value, FILTER_VALIDATE_INT);
if ($integer === false || (!$signed && $integer < 0))
show_error('Ungueltiger Parameter: '.$name, 400);
return $integer;
}
private function getOptionalFilter($name)
{
$value = $this->input->get($name, true);
if ($value === null || $value === '' || $value === '0')
return null;
if (!is_string($value)
|| !check_utf8($value)
|| mb_strlen($value) > 20
|| !preg_match('/^[[:alnum:]_-]+$/u', $value))
{
show_error('Ungueltiger Parameter: '.$name, 400);
}
return $value;
}
/**
* @return int Requested iCalendar major version.
*/
private function getVersion()
{
$version = $this->input->get('version', true);
if (!in_array($version, array('1', '2'), true))
show_error('Ungueltiger Parameter: version', 400);
return (int)$version;
}
/**
* Validates and expands the requested range to complete weeks.
*
* @return array{0: int, 1: int} Begin and end timestamps.
*/
private function getDateRange()
{
$begin = $this->getTimestamp('begin');
$ende = $this->getTimestamp('ende');
if ($ende < $begin)
show_error('Ungueltiger Datumsbereich', 400);
if ($ende - $begin > self::MAX_DATE_RANGE)
show_error('Datumsbereich zu gross', 400);
$begin = strtotime('monday this week', $begin);
$ende = strtotime('sunday this week', $ende);
return array($begin, $ende);
}
/**
* Builds the iCalendar download filename.
*
* @param int $begin Range start timestamp.
* @param int $ende Range end timestamp.
* @param int $version iCalendar major version.
* @return string Filename.
*/
private function getFilename($begin, $ende, $version)
{
$studiensemester = $this->getStudiensemester($begin, $ende);
$filename = $this->p->t('global', 'termine');
if ($studiensemester !== null)
$filename .= '_'.$studiensemester->studiensemester_kurzbz;
$filename .= '-'.date('Ymd').'_ical'.$version.'.ics';
return $filename;
}
/**
* Gets the study semester covered by the requested date range.
*
* @param int $begin Range start timestamp.
* @param int $ende Range end timestamp.
* @return object|null Study semester, if one overlaps the range.
*/
private function getStudiensemester($begin, $ende)
{
$semesterRange = $this->StudiensemesterModel->getByDateRange(
date('Y-m-d', $begin),
date('Y-m-d', $ende)
);
if (isError($semesterRange) || !hasData($semesterRange))
return null;
$studiensemester = current(getData($semesterRange));
return $studiensemester;
}
/**
* @param string $name GET parameter name.
* @return int Unix timestamp.
*/
private function getTimestamp($name)
{
$value = $this->input->get($name, true);
if (!is_string($value) || !ctype_digit($value))
show_error('Ungueltiger Parameter: '.$name, 400);
$timestamp = filter_var(
$value,
FILTER_VALIDATE_INT,
array('options' => array('min_range' => 0))
);
if ($timestamp === false)
show_error('Ungueltiger Parameter: '.$name, 400);
return $timestamp;
}
/**
* Ensures access to another user's timetable is explicitly permitted.
*
* @param string $uid Requested user UID.
* @return void
*/
private function authorizeUid($uid)
{
if ($uid !== getAuthUID()
&& !$this->permissionlib->isBerechtigt('basis/other_lv_plan'))
{
show_error('Forbidden', 403);
}
}
/**
* Ensures the requested user exists.
*
* @param string $uid Requested user UID.
* @return void
*/
private function validateUid($uid)
{
$this->BenutzerModel->addSelect('uid');
$user = $this->BenutzerModel->load(array($uid));
if (isError($user))
show_error(getError($user), 500);
if (!hasData($user))
show_error('Ungueltiger Parameter: pers_uid', 400);
}
}
@@ -0,0 +1,167 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
require_once(FHCPATH.'include/functions.inc.php');
class LvPlanICALSubscription extends FHC_Controller
{
/**
* Encrypted calendar subscription token.
*
* @var string|null
*/
private $cal;
/**
* UID authenticated by the calendar subscription token.
*
* @var string
*/
private $uid;
/**
* First date included in the subscription feed.
*
* @var int Unix timestamp at local midnight
*/
private $begin;
/**
* Last date included in the subscription feed.
*
* @var int Unix timestamp at local midnight
*/
private $ende;
/**
* Object initialization.
*/
public function __construct()
{
parent::__construct();
$this->load->model('person/Benutzer_model', 'BenutzerModel');
$this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$this->cal = $this->input->get('cal', true);
$this->uid = $this->authenticateSubscription($this->cal);
list($this->begin, $this->ende) = $this->determineDateRange();
}
/**
* Serves the iCalendar subscription feed.
*
* @return void
*/
public function index()
{
$this->load->library('LvPlanICALLib');
$content = $this->lvplanicallib->getContent(
$this->uid,
$this->begin,
$this->ende,
null,
2,
null,
null,
'FH-Kalender'
);
$filename = 'FH-Kalender_'.date('m_Y', $this->begin).'_ical.ics';
$this->output
->set_content_type('text/calendar', 'UTF-8')
->set_header('Content-Disposition: inline; filename="'.$filename.'"')
->set_header('Pragma: public')
->set_header('Expires: 0')
->set_output($content);
}
/**
* Authenticates an encrypted calendar subscription token.
*
* @param string|null $cal Encrypted UID from the subscription URL.
* @return string Authenticated UID.
*/
private function authenticateSubscription($cal)
{
if (isEmptyString($cal))
show_error('Fehlerhafter Parameter', 400);
if (!defined('LVPLAN_CYPHER_KEY'))
show_error('LVPLAN_CYPHER_KEY is not configured', 500);
$uid = decryptData($cal, constant('LVPLAN_CYPHER_KEY'));
if (!is_string($uid) || $uid === '' || !check_utf8($uid))
show_error('Fehlerhafter Parameter', 400);
$this->BenutzerModel->addSelect('uid');
$user = $this->BenutzerModel->load([$uid]);
if (isError($user))
show_error(getError($user), 500);
if (!hasData($user))
show_error('Ungueltiger Benutzername', 401);
return $uid;
}
/**
* Determines the subscription range from the current or next semester.
*
* During the final 30 days of the selected semester, the range is
* extended through the following semester. The resulting range is
* expanded to complete Monday-to-Sunday weeks.
*
* @return array{0: int, 1: int} Begin and end timestamps.
*/
private function determineDateRange()
{
$semesterResult = $this->StudiensemesterModel->getAkt();
if (isError($semesterResult))
show_error(getError($semesterResult), 500);
if (!hasData($semesterResult))
{
$semesterResult = $this->StudiensemesterModel->getNext();
if (isError($semesterResult))
show_error(getError($semesterResult), 500);
}
if (!hasData($semesterResult))
show_error('Studiensemester konnte nicht gefunden werden', 500);
$semester = current(getData($semesterResult));
$begin = strtotime($semester->start);
$ende = strtotime($semester->ende);
$semesterEnd = new DateTime($semester->ende);
$daysFromSemesterEnd = (int)$semesterEnd
->diff(new DateTime())
->format('%R%a');
if ($daysFromSemesterEnd >= -30)
{
$nextSemesterResult = $this->StudiensemesterModel
->getNextFrom($semester->studiensemester_kurzbz);
if (isError($nextSemesterResult))
show_error(getError($nextSemesterResult), 500);
if (hasData($nextSemesterResult))
$ende = strtotime(current(getData($nextSemesterResult))->ende);
}
$begin = strtotime('monday this week', $begin);
$ende = strtotime('sunday this week', $ende);
return [$begin, $ende];
}
}
+40
View File
@@ -0,0 +1,40 @@
<?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');
}
/**
* @return void
*/
public function _remap()
{
$this->load->view('Tempus', [
'permissions' => [
'admin' => $this->permissionlib->isBerechtigt('admin')
],
'variables' => [
'semester_aktuell' => $this->variablelib->getVar('semester_aktuell'),
'timezone' => $this->config->item('timezone')
]
]);
}
}
@@ -31,7 +31,7 @@ class RendererLoader extends FHCAPI_Controller
parent::__construct([ parent::__construct([
'GetRenderers' => self::PERM_LOGGED, 'GetRenderers' => self::PERM_LOGGED,
'GetTempusRenderers' => self::PERM_LOGGED,
]); ]);
$this->load->library('LogLib'); $this->load->library('LogLib');
@@ -66,6 +66,26 @@ class RendererLoader extends FHCAPI_Controller
$this->terminateWithSuccess($renderer_paths); $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);
}
@@ -118,54 +118,17 @@ class Gruppe extends FHCAPI_Controller
$query_words = explode(' ', $query); $query_words = explode(' ', $query);
$this->_ci->GruppeModel->addSelect('gruppe_kurzbz, $gruppen_result = $this->_ci->GruppeModel->search($query_words);
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();
if (isError($gruppen_result)) if (isError($gruppen_result))
$this->terminateWithError(getError($gruppen_result), self::ERROR_TYPE_GENERAL); $this->terminateWithError(getError($gruppen_result), self::ERROR_TYPE_GENERAL);
$gruppen_array = array();
if (hasData($gruppen_result)) if (hasData($gruppen_result))
$gruppen_array = getData($gruppen_result); $gruppen_array = getData($gruppen_result);
$this->_ci->LehrverbandModel->addSelect('CONCAT(UPPER(CONCAT(typ, kurzbz)), \'\', semester, verband, COALESCE(gruppe,\'\')) as gruppe_kurzbz, $lehrverband_result = $this->_ci->LehrverbandModel->search($query_words);
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_array = array(); $lehrverband_array = array();
@@ -25,7 +25,25 @@ class StgTree extends FHCAPI_Controller
return $this->_outputAuthError([$method => ['admin:r', 'assistenz:r']]); return $this->_outputAuthError([$method => ['admin:r', 'assistenz:r']]);
} }
return $this->getStudiengang($method); $count = count($params);
if (!$count)
return $this->getStudiengang($method);
if ($count == 1) {
if (is_numeric($params[0]))
return $this->getSemester($method, $params[0]);
else
return $this->getStudiengang($method, $params[0]);
}
if ($count == 2) {
if (is_numeric($params[0]))
return $this->getVerband($method, $params[0], $params[1]);
else
return $this->getSemester($method, $params[1], $params[0]);
}
show_404(); show_404();
} }
@@ -64,16 +82,27 @@ class StgTree extends FHCAPI_Controller
$this->terminateWithSuccess($list); $this->terminateWithSuccess($list);
} }
protected function getStudiengang($studiengang_kz) protected function getStudiengang($studiengang_kz, $org_form = null)
{ {
$link = $studiengang_kz . '/'; $link = $studiengang_kz . '/';
if ($org_form !== null)
$link .= $org_form . '/';
$this->StudiengangModel->addJoin('public.tbl_lehrverband v', 'studiengang_kz'); $this->StudiengangModel->addJoin('public.tbl_lehrverband v', 'studiengang_kz');
$this->StudiengangModel->addDistinct(); $this->StudiengangModel->addDistinct();
$this->StudiengangModel->addSelect("CONCAT(" . $this->StudiengangModel->escape($link) . ", semester) AS link", false); $this->StudiengangModel->addSelect("CONCAT(" . $this->StudiengangModel->escape($link) . ", semester) AS link", false);
$this->StudiengangModel->addSelect("CONCAT(UPPER(CONCAT(typ, kurzbz)), '-', semester, (SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END FROM public.tbl_lehrverband WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester ORDER BY verband, gruppe LIMIT 1)) AS name", false); $this->StudiengangModel->addSelect("CONCAT(
$this->StudiengangModel->addSelect("TRUE AS leaf", false); UPPER(CONCAT(typ, kurzbz)),
'-',
semester,
(
SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END
FROM public.tbl_lehrverband
WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester
ORDER BY verband, gruppe LIMIT 1
)
) AS name", false);
$this->StudiengangModel->addSelect('semester'); $this->StudiengangModel->addSelect('semester');
$this->StudiengangModel->addSelect($this->StudiengangModel->escape($studiengang_kz) . '::integer AS stg_kz', false); $this->StudiengangModel->addSelect($this->StudiengangModel->escape($studiengang_kz) . '::integer AS stg_kz', false);
@@ -97,6 +126,8 @@ class StgTree extends FHCAPI_Controller
$this->StudienordnungModel->addDistinct(); $this->StudienordnungModel->addDistinct();
$this->StudienordnungModel->addSelect("CONCAT(studiengang_kz, '/', p.orgform_kurzbz) AS link"); $this->StudienordnungModel->addSelect("CONCAT(studiengang_kz, '/', p.orgform_kurzbz) AS link");
$this->StudienordnungModel->addSelect("p.orgform_kurzbz AS name"); $this->StudienordnungModel->addSelect("p.orgform_kurzbz AS name");
$this->StudienordnungModel->addSelect("p.orgform_kurzbz AS orgform_kurzbz");
$this->StudienordnungModel->addSelect("studiengang_kz AS stg_kz");
$this->StudienordnungModel->addSelect("TRUE as leaf", false); $this->StudienordnungModel->addSelect("TRUE as leaf", false);
$this->StudienordnungModel->addJoin('lehre.tbl_studienplan p', 'studienordnung_id'); $this->StudienordnungModel->addJoin('lehre.tbl_studienplan p', 'studienordnung_id');
@@ -111,6 +142,115 @@ class StgTree extends FHCAPI_Controller
$list = array_merge($list, $result); $list = array_merge($list, $result);
} }
} }
$this->terminateWithSuccess($list);
}
protected function getSemester($studiengang_kz, $semester, $org_form = null)
{
$link = $studiengang_kz . '/';
if ($org_form !== null)
$link .= $org_form . '/';
$link .= $semester . '/';
$this->load->model('organisation/Gruppe_model', 'GruppeModel');
$this->GruppeModel->addDistinct();
$this->GruppeModel->addSelect("CONCAT(" . $this->GruppeModel->escape($link . 'grp/') . ", gruppe_kurzbz) AS link", false);
$this->GruppeModel->addSelect("CONCAT(gruppe_kurzbz, ' (', bezeichnung, ')') AS name", false);
$this->GruppeModel->addSelect("TRUE AS leaf", false);
$this->GruppeModel->addSelect('sort');
$this->GruppeModel->addSelect('gruppe_kurzbz');
$this->GruppeModel->addSelect($this->GruppeModel->escape($studiengang_kz) . '::integer AS stg_kz', false);
$this->GruppeModel->addOrder('sort');
$this->GruppeModel->addOrder('gruppe_kurzbz');
$where = [
'studiengang_kz' => $studiengang_kz,
'semester' => $semester,
'lehre' => true,
'sichtbar' => true,
'aktiv' => true,
'direktinskription' => false
];
if ($org_form !== null)
$where['orgform_kurzbz'] = $org_form;
$result = $this->GruppeModel->loadWhere($where);
$list = $this->getDataOrTerminateWithError($result);
$this->StudiengangModel->addJoin('public.tbl_lehrverband v', 'studiengang_kz');
$this->StudiengangModel->addSelect("CONCAT(" . $this->StudiengangModel->escape($link) . ", verband) AS link", false);
$this->StudiengangModel->addSelect("CONCAT(UPPER(CONCAT(typ, kurzbz)), '-', semester, verband, (SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END FROM public.tbl_lehrverband WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester AND verband=v.verband ORDER BY gruppe LIMIT 1)) AS name", false);
$this->StudiengangModel->addSelect("CASE WHEN MAX(gruppe)='' OR MAX(gruppe)=' ' THEN TRUE ELSE FALSE END AS leaf");
$this->StudiengangModel->addSelect($this->StudiengangModel->escape($semester) . ' AS semester');
$this->StudiengangModel->addSelect('verband');
$this->StudiengangModel->addSelect($this->StudiengangModel->escape($studiengang_kz) . '::integer AS stg_kz', false);
$this->StudiengangModel->addOrder('verband');
$this->StudiengangModel->addGroupBy('link, name, verband');
$where = [
'v.studiengang_kz' => $studiengang_kz,
'v.semester' => $semester,
'v.verband !=' => '',
'v.aktiv' => true
];
if ($org_form !== null && $semester) // NOTE(chris): on semester 0 show all?
$where['v.orgform_kurzbz'] = $org_form;
$result = $this->StudiengangModel->loadWhere($where);
$result = $this->getDataOrTerminateWithError($result);
$list = array_merge($list, $result);
$this->terminateWithSuccess($list);
}
protected function getVerband($studiengang_kz, $semester, $verband, $org_form = null)
{
$link = $studiengang_kz . '/';
if ($org_form !== null)
$link .= $org_form . '/';
$link .= $semester . '/'. $verband . '/';
$this->StudiengangModel->addJoin('public.tbl_lehrverband v', 'studiengang_kz');
$this->StudiengangModel->addDistinct();
$this->StudiengangModel->addSelect("CONCAT(" . $this->StudiengangModel->escape($link) . ", gruppe) AS link", false);
$this->StudiengangModel->addSelect("CONCAT(UPPER(CONCAT(typ, kurzbz)), '-', semester, verband, gruppe, (SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END FROM public.tbl_lehrverband WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester AND verband=v.verband AND gruppe=v.gruppe ORDER BY gruppe LIMIT 1)) AS name", false);
$this->StudiengangModel->addSelect("TRUE AS leaf", false);
$this->StudiengangModel->addSelect('v.semester');
$this->StudiengangModel->addSelect('v.verband');
$this->StudiengangModel->addSelect('gruppe');
$this->StudiengangModel->addSelect($this->StudiengangModel->escape($studiengang_kz) . '::integer AS stg_kz', false);
$this->StudiengangModel->addOrder('gruppe');
$where = [
'v.studiengang_kz' => $studiengang_kz,
'v.semester' => $semester,
'v.verband' => $verband,
'v.gruppe !=' => '',
'v.aktiv' => true
];
if ($org_form !== null && $semester)
$where['v.orgform_kurzbz'] = $org_form;
$result = $this->StudiengangModel->loadWhere($where);
$list = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($list); $this->terminateWithSuccess($list);
} }
@@ -0,0 +1,133 @@
<?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:r'],
'getHeader' => ['admin:r', 'assistenz:r'],
'set' => ['admin:r', 'assistenz:r'],
'updateCollision' => ['admin:r', 'assistenz:r'],
]);
// 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 = [];
$result = $this->_ci->VariableModel->getVariables(getAuthUID(), ['ignore_kollision', 'kollision_student', 'ignore_reservierung', 'ignore_zeitsperre', 'ignore_resources_collisions']);
$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'
];
$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()
{
$this->_ci->VariableModel->setVariable(
getAuthUID(),
'ignore_kollision',
$this->input->post('ignore_kollision') === true ? 'true' : 'false'
);
$this->_ci->VariableModel->setVariable(
getAuthUID(),
'kollision_student',
$this->input->post('kollision_student') === true ? 'true' : 'false'
);
$this->_ci->VariableModel->setVariable(
getAuthUID(),
'ignore_reservierung',
$this->input->post('ignore_reservierung') === true ? 'true' : 'false'
);
$this->_ci->VariableModel->setVariable(
getAuthUID(),
'ignore_zeitsperre',
$this->input->post('ignore_zeitsperre') === true ? 'true' : 'false'
);
$this->_ci->VariableModel->setVariable(
getAuthUID(),
'ignore_resources_collisions',
$this->input->post('ignore_resources_collisions') === 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,300 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Coursepicker extends FHCAPI_Controller
{
private $_ci;
public function __construct()
{
parent::__construct([
'search' => self::PERM_LOGGED,
'getByStg' => self::PERM_LOGGED
]);
$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 search()
{
$query = $this->input->get('query');
if (is_null($query))
$this->terminateWithError($this->p->t('ui', 'ungueltigeParameter'), self::ERROR_TYPE_GENERAL);
$query_words = explode(' ', $query);
//TODO Where weiter anpassen z.B. Fachbereich
$this->_ci->LehreinheitModel->addSelect('tbl_lehreinheit.lehreinheit_id,
tbl_lehreinheit.lvnr,
tbl_lehreinheit.lehrfach_id,
lehrfach.kurzbz AS lehrfach,
lehrfach.bezeichnung AS lehrfach_bez,
lehrfach.farbe AS lehrfach_farbe,
tbl_lehreinheit.lehrform_kurzbz AS lehrform,
lema.mitarbeiter_uid AS lektor_uid,
tbl_mitarbeiter.kurzbz AS lektor,
tbl_studiengang.studiengang_kz,
upper(tbl_studiengang.typ::character varying::text || tbl_studiengang.kurzbz::text) AS studiengang,
lvb.semester,
lvb.verband,
lvb.gruppe,
lvb.gruppe_kurzbz,
tbl_lehreinheit.raumtyp,
tbl_lehreinheit.raumtypalternativ,
tbl_lehreinheit.stundenblockung,
tbl_lehreinheit.wochenrythmus,
lema.semesterstunden,
lema.planstunden,
tbl_lehreinheit.start_kw,
tbl_lehreinheit.anmerkung,
tbl_lehreinheit.studiensemester_kurzbz');
$this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehreinheitmitarbeiter lema', 'tbl_lehreinheit.lehreinheit_id = lema.lehreinheit_id');
$this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehreinheitgruppe lvb', 'tbl_lehreinheit.lehreinheit_id = lvb.lehreinheit_id');
$this->_ci->LehreinheitModel->addJoin('public.tbl_studiengang', 'lvb.studiengang_kz = tbl_studiengang.studiengang_kz');
$this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehrveranstaltung lehrfach', 'tbl_lehreinheit.lehrfach_id = lehrfach.lehrveranstaltung_id');
$this->_ci->LehreinheitModel->addJoin('public.tbl_mitarbeiter', 'lema.mitarbeiter_uid = tbl_mitarbeiter.mitarbeiter_uid');
$this->_ci->LehreinheitModel->addJoin('lehre.tbl_lehrform', 'tbl_lehrform.lehrform_kurzbz = tbl_lehreinheit.lehrform_kurzbz');
$this->_ci->MitarbeiterModel->db->group_start();
foreach ($query_words as $word)
{
$this->_ci->LehreinheitModel->db->group_start();
$this->_ci->LehreinheitModel->db->where('lema.mitarbeiter_uid ILIKE', "%" . $word . "%");
$this->_ci->LehreinheitModel->db->or_where('lvb.gruppe_kurzbz ILIKE', "%" . $word . "%");
$this->_ci->LehreinheitModel->db->or_where('tbl_studiengang.kurzbzlang ILIKE', "%" . $word . "%");
$this->_ci->LehreinheitModel->db->or_where('lvb.verband ILIKE', "%" . $word . "%");
$this->_ci->LehreinheitModel->db->or_where('lvb.gruppe ILIKE', "%" . $word . "%");
$this->_ci->LehreinheitModel->db->or_where('lehrfach.bezeichnung ILIKE', "%" . $word . "%");
if (is_numeric($word))
{
$this->_ci->LehreinheitModel->db->or_where('tbl_studiengang.studiengang_kz', $word);
$this->_ci->LehreinheitModel->db->or_where('lvb.semester', $word);
}
$this->_ci->LehreinheitModel->db->group_end();
}
$this->_ci->LehreinheitModel->db->group_end();
$this->_ci->LehreinheitModel->db->where('tbl_lehreinheit.studiensemester_kurzbz = \'SS2025\'');
$this->_ci->LehreinheitModel->db->where(array('tbl_lehrform.verplanen' => true));
$result = $this->_ci->LehreinheitModel->load();
$this->terminateWithSuccess(hasData($result) ? getData($result) : array());
}
public function getByStg()
{
$this->_ci->form_validation->set_data($_POST);
$this->_ci->form_validation->set_rules('studiengaenge[]',"studiengaenge","required");
$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());
$studiengaenge = $this->input->post('studiengaenge');
$studiensemester_kurzbz = $this->input->post('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();
$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['studiengang_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();
$result = $this->_ci->LehreinheitModel->loadWhere(array(
'tbl_lehrform.verplanen' => true,
'tbl_lehreinheit.studiensemester_kurzbz' => $studiensemester_kurzbz
));
$result = hasData($result) ? getData($result) : array();
$lehreinheit_ids = array_values(array_unique(array_column($result, 'lehreinheit_id')));
$offen_map = array();
if (!empty($lehreinheit_ids))
{
$offen_result = $this->_ci->LehreinheitModel->getOffeneStunden($lehreinheit_ids);
if (hasData($offen_result))
{
foreach (getData($offen_result) as $row)
{
$offen_map[$row->lehreinheit_id . '_' . $row->mitarbeiter_uid] = $row;
}
}
}
$grouped = array();
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);
}
$this->terminateWithSuccess(array_values($grouped));
}
private function _formatArr($arr)
{
$values = array_values(array_unique($arr));
$formatted = implode(' ', $values);
if (count($values) > 1)
$formatted .= ' ?';
return $formatted;
}
}
@@ -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' => self::PERM_LOGGED,
'set' => self::PERM_LOGGED
]);
// 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' => self::PERM_LOGGED,
]);
$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,437 @@
<?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', 'ort_kurzbz'];
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getStunden' => self::PERM_LOGGED,
'getCalendarHours' => self::PERM_LOGGED,
'getPlan' => self::PERM_LOGGED,
'getPlanByOrt' => self::PERM_LOGGED,
'getRaumvorschlag' => self::PERM_LOGGED,
'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' => self::PERM_LOGGED,
'getZeitsperren' => self::PERM_LOGGED,
'updateKalenderEvent' => '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', ["uid" => getAuthUID()]);
$this->_ci->load->library('RaumvorschlagLib', ["uid" => getAuthUID()]);
$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->ort_kurzbz ?? null, $updateFields->start_time ?? null, $updateFields->end_time ?? null);
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 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->kalenderlib->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));
}
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,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'],
'storeResourcesToCalendarRelationship' => ['admin:w', 'assistenz:w'],
'getSchedulableResourcesByCalendar' => ['admin:r', 'assistenz:r'],
]);
$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', ["uid" => getAuthUID()]);
$this->load->library('KalenderLib', ["uid" => getAuthUID()]);
}
//------------------------------------------------------------------------------------------------------------------
// 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,231 @@
<?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', ["uid" => getAuthUID()]);
$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_data($_POST);
$this->_ci->form_validation->set_rules('titel',"titel","required");
$this->_ci->form_validation->set_rules('beschreibung',"beschreibung","required");
$this->_ci->form_validation->set_rules('ort_kurzbz[]',"ort_kurzbz","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());
$titel = $this->_ci->input->post('titel', TRUE);
$beschreibung = $this->_ci->input->post('beschreibung', 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);
$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, $ort_kurzbz, $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', ["uid" => null]);
}
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,865 @@
<?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 refreshTablesAndFullMigrateOfStundenplanReservierung($von, $bis = null, $studiengang_kz = null, $ort_kurzbz = null)
{
echo "Refreshing calendar related tables...\n";
$this->resetBeforeNewStundenplanReservierungImport();
echo "Calendar related tables refreshed successfully.\n\n";
echo "Starting full migration of Stundenplan and Reservierung from $von to " . ($bis ?? '2100-12-31') . "...\n";
$this->fullMigrateOfStundenplanReservierung($von, $bis, $studiengang_kz, $ort_kurzbz);
echo "Full migration of Stundenplan and Reservierung completed successfully.\n";
}
public function fullMigrateOfStundenplanReservierung($von, $bis = null, $studiengang_kz = null, $ort_kurzbz = null)
{
echo "Starting migration of Stundenplan from $von to " . ($bis ?? '2100-12-31') . "...\n";
$this->migrateStundenplan($von, $bis, $studiengang_kz);
echo "Stundenplan migration completed successfully.\n\n";
echo "Starting migration of Reservierung from $von to " . ($bis ?? '2100-12-31') . "...\n";
$this->migrateReservierung($von, $bis, $ort_kurzbz);
echo "Reservierung migration completed successfully.\n\n";
echo "Starting migration of Stundenplan Betriebsmittel entries...\n";
$this->migrateStundenplanBetriebsmittelEntries();
echo "Stundenplan Betriebsmittel entries migration completed successfully.\n\n";
echo "Starting to add title values as tags to calendar entries...\n";
$this->addTitleValuesAsTagsToKalenderEntries();
echo "Adding title values as tags to calendar entries completed successfully.\n\n";
}
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->where_in('stundenplandev_id', $ids);
$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->where_in('reservierung_id', $ids);
$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);
}
}
}
}
}
/**
* Removes calendar entries created by the Stundenplan and Reservierung migrations,
* so that both imports can be started from scratch.
*/
public function resetBeforeNewStundenplanReservierungImport()
{
$db = new DB_Model();
$db->db->trans_start();
$db->db->query('DELETE FROM sync.tbl_stundenplandev_kalender');
$db->db->query('DELETE FROM sync.tbl_reservierung_kalender');
$db->db->query("DELETE FROM lehre.tbl_kalender_event_teilnehmer
WHERE kalender_id IN (
SELECT kalender_id
FROM lehre.tbl_kalender
WHERE typ = 'reservierung'
)");
$db->db->query("DELETE FROM lehre.tbl_kalender_event
WHERE kalender_id IN (
SELECT kalender_id
FROM lehre.tbl_kalender
WHERE typ = 'reservierung'
)");
$db->db->query("DELETE FROM lehre.tbl_kalender_lehreinheit
WHERE kalender_id IN (
SELECT kalender_id
FROM lehre.tbl_kalender
WHERE typ = 'lehreinheit' OR typ = 'reservierung'
)");
$db->db->query("DELETE FROM lehre.tbl_kalender_ort
WHERE kalender_id IN (
SELECT kalender_id
FROM lehre.tbl_kalender
WHERE typ IN ('lehreinheit', 'reservierung')
)");
$db->db->query("DELETE FROM public.tbl_notiz
WHERE notiz_id IN (
SELECT notiz_id
FROM public.tbl_notizzuordnung
WHERE eindeutige_kalender_gruppen_id IN (
SELECT eindeutige_kalender_gruppen_id
FROM lehre.tbl_kalender
)
)");
$db->db->query("DELETE FROM public.tbl_notizzuordnung
WHERE eindeutige_kalender_gruppen_id IN (
SELECT eindeutige_kalender_gruppen_id
FROM lehre.tbl_kalender
)");
$db->db->query("DELETE FROM public.tbl_betriebsmittel_kalender
WHERE eindeutige_kalender_gruppen_id IN (
SELECT eindeutige_kalender_gruppen_id
FROM lehre.tbl_kalender
)");
$db->db->query("DELETE FROM lehre.tbl_kalender
WHERE typ IN ('lehreinheit', 'reservierung')");
$db->db->trans_complete();
if ($db->db->trans_status() === false)
return error('Reset before new import failed');
return success('Migration data reset successfully');
}
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);
}
public function addTitleValuesAsTagsToKalenderEntries() {
$dbModel = new DB_Model();
$query = "SELECT
k.eindeutige_kalender_gruppen_id,
ARRAY_AGG(DISTINCT(spd.titel) ORDER BY spd.titel) AS titel,
MAX(spd.updateamum) AS updateamum
FROM lehre.tbl_stundenplandev spd
JOIN sync.tbl_stundenplandev_kalender spdk ON spd.stundenplandev_id = spdk.stundenplandev_id
JOIN lehre.tbl_kalender k ON k.kalender_id = spdk.kalender_id
WHERE k.typ = 'lehreinheit' AND k.eindeutige_kalender_gruppen_id IS NOT NULL AND spd.titel IS NOT NULL
GROUP BY k.eindeutige_kalender_gruppen_id";
$result = $dbModel->execReadOnlyQuery($query);
if (isError($result)) {
error("Error while fetching title values for adding as tags: " . getError($result));
return;
}
if (!hasData($result)) {
error("No title values found for adding as tags.");
return;
}
$data = getData($result);
foreach($data as $block) {
$eindeutige_kalender_gruppen_id = $block->eindeutige_kalender_gruppen_id;
$notizText = implode(' - ', array_filter($block->titel, function($titel) {
return !empty(trim($titel));
}));
if (!empty($notizText)) {
$this->addTag($eindeutige_kalender_gruppen_id, $notizText, $block->updateamum);
}
}
}
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')
)
);
}
}
private function addTag($eindeutige_kalender_gruppen_id, $notizText, $lastUpdatedInOldSystem): bool
{
$notizType = 'hinweis';
$insertvonMockUser = 'oldToNewTempusMigration';
if (is_null($eindeutige_kalender_gruppen_id) || empty($eindeutige_kalender_gruppen_id)) {
var_dump("Error: eindeutige_kalender_gruppen_id is null or empty.");
return false;
}
$this->load->model('person/Notiz_model', 'NotizModel');
$this->load->model('system/Notiztyp_model', 'NotiztypModel');
$this->load->model('person/Notizzuordnung_model', 'NotizzuordnungModel');
$checkTyp = $this->NotiztypModel->loadWhere(array('typ_kurzbz' => $notizType));
if (isError($checkTyp))
{
error("Error occurred while checking Notiztyp: " . $checkTyp->message);
return false;
}
if (!hasData($checkTyp))
{
error("Notiztyp 'hinweis' does not exist. Please create it before adding tags.");
return false;
}
$this->NotizzuordnungModel->addJoin('tbl_notiz', 'tbl_notiz.notiz_id = tbl_notizzuordnung.notiz_id', 'LEFT');
$this->NotizzuordnungModel->db->where('tbl_notiz.typ', $notizType);
$this->NotizzuordnungModel->db->where('tbl_notiz.insertvon', $insertvonMockUser);
$oldNotizZuordnung = $this->NotizzuordnungModel->loadWhere(array('eindeutige_kalender_gruppen_id' => $eindeutige_kalender_gruppen_id));
if (isError($oldNotizZuordnung))
{
error("Error occurred while checking Notizzuordnung: " . $oldNotizZuordnung->message);
return false;
}
if (hasData($oldNotizZuordnung))
{
return $this->updateTag($eindeutige_kalender_gruppen_id, $notizText, $lastUpdatedInOldSystem);
}
$insertResult = $this->NotizModel->insert(array(
'titel' => 'TAG',
'text' => $notizText,
'verfasser_uid' => null,
'erledigt' => false,
'insertamum' => date('Y-m-d H:i:s'),
'insertvon' => $insertvonMockUser,
'typ' => $notizType
));
if (isError($insertResult))
{
error("Error occurred while inserting Notiz: " . $insertResult->message);
return false;
}
$insertZuordnung = $this->NotizzuordnungModel->insert(array(
'notiz_id' => $insertResult->retval,
'eindeutige_kalender_gruppen_id' => $eindeutige_kalender_gruppen_id
));
if (isError($insertZuordnung))
{
error("Error occurred while inserting Notizzuordnung: " . $insertZuordnung->message);
return false;
}
return true;
}
private function updateTag($eindeutige_kalender_gruppen_id, $notizText, $lastUpdatedInOldSystem)
{
$notizType = 'hinweis';
$insertvonMockUser = 'oldToNewTempusMigration';
$this->NotizzuordnungModel->addJoin('tbl_notiz', 'tbl_notiz.notiz_id = tbl_notizzuordnung.notiz_id', 'LEFT');
$this->NotizzuordnungModel->db->where('tbl_notiz.typ', $notizType);
$this->NotizzuordnungModel->db->where('tbl_notiz.insertvon', $insertvonMockUser);
$notizZuordnungRes = $this->NotizzuordnungModel->loadWhere(array('eindeutige_kalender_gruppen_id' => $eindeutige_kalender_gruppen_id));
if (isError($notizZuordnungRes))
{
error("Error occurred while loading Notizzuordnung: " . $notizZuordnungRes->message);
return false;
}
$tag = $this->NotizModel->loadWhere(array('notiz_id' => $notizZuordnungRes->retval[0]->notiz_id));
if (isError($tag))
{
error("Error occurred while loading Notiz: " . $tag->message);
return false;
}
if (!hasData($tag))
{
error("Error occurred while loading Notiz: " . $tag->message);
return false;
}
$adjustedLastUpdatedInOldSystem = date('Y-m-d H:i:s', strtotime($lastUpdatedInOldSystem) + 5);
if ($tag->retval[0]->updateamum >= $adjustedLastUpdatedInOldSystem)
{
return true;
}
$updateData = $this->NotizModel->update(array('notiz_id' => $tag->retval[0]->notiz_id),
array('text' => $notizText,
'updateamum' => date('Y-m-d H:i:s'),
'updatevon' => $insertvonMockUser,
'bearbeiter_uid' => null,
)
);
if (isError($updateData))
{
error("Error occurred while updating Notiz: " . $updateData->message);
return false;
}
return true;
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
interface ICollisionCheck
{
public function getName();
public function check($data);
public function checkAll($kalender_ids);
}
+38 -4
View File
@@ -38,6 +38,8 @@ class Tag_Controller extends FHCAPI_Controller
public function getTag($readonly_tags = null) public function getTag($readonly_tags = null)
{ {
$language = $this->_getLanguageIndex(); $language = $this->_getLanguageIndex();
$index_bezeichnung_mehrsprachig = $language - 1;
$id = $this->input->get('id'); $id = $this->input->get('id');
if (is_array($readonly_tags) && !isEmptyArray($readonly_tags)) if (is_array($readonly_tags) && !isEmptyArray($readonly_tags))
@@ -62,7 +64,7 @@ class Tag_Controller extends FHCAPI_Controller
$this->NotizModel->addSelect( $this->NotizModel->addSelect(
"tbl_notiz.titel, "tbl_notiz.titel,
tbl_notiz.text, tbl_notiz.text,
array_to_json(bezeichnung_mehrsprachig::varchar[])->>". $language. " as bezeichnung, array_to_json(bezeichnung_mehrsprachig::varchar[])->>". $index_bezeichnung_mehrsprachig. " as bezeichnung,
tbl_notiz.notiz_id, tbl_notiz.notiz_id,
tbl_notiz_typ.style, tbl_notiz_typ.style,
tbl_notiz.erledigt as done, tbl_notiz.erledigt as done,
@@ -87,9 +89,12 @@ class Tag_Controller extends FHCAPI_Controller
public function getTags($tags = null) public function getTags($tags = null)
{ {
$language = $this->_getLanguageIndex();
$index_bezeichnung_mehrsprachig = $language - 1;
$this->NotiztypModel->addSelect( $this->NotiztypModel->addSelect(
'typ_kurzbz as tag_typ_kurzbz, 'typ_kurzbz as tag_typ_kurzbz,
array_to_json(bezeichnung_mehrsprachig::varchar[])->>0 as bezeichnung, array_to_json(bezeichnung_mehrsprachig::varchar[])->>'. $index_bezeichnung_mehrsprachig. ' as bezeichnung,
style, style,
beschreibung, beschreibung,
tag tag
@@ -106,7 +111,36 @@ class Tag_Controller extends FHCAPI_Controller
$notiztypen = $this->NotiztypModel->loadWhere(array('aktiv' => true)); $notiztypen = $this->NotiztypModel->loadWhere(array('aktiv' => true));
$this->terminateWithSuccess(hasData($notiztypen) ? getData($notiztypen) : array()); $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) public function addTag($withZuordnung = true, $updatable_tags = null)
{ {
$postData = $this->getPostJson(); $postData = $this->getPostJson();
@@ -126,7 +160,7 @@ class Tag_Controller extends FHCAPI_Controller
if (!in_array($postData->tag_typ_kurzbz, $tags)) if (!in_array($postData->tag_typ_kurzbz, $tags))
$this->terminateWithError($this->p->t('ui', 'keineBerechtigung')); $this->terminateWithError($this->p->t('ui', 'keineBerechtigung'));
} }
if ($withZuordnung) if ($withZuordnung)
{ {
$return = array(); $return = array();
@@ -135,7 +169,7 @@ class Tag_Controller extends FHCAPI_Controller
$this->terminateWithError('Error occurred', self::ERROR_TYPE_GENERAL); $this->terminateWithError('Error occurred', self::ERROR_TYPE_GENERAL);
$values = array_unique($postData->values); $values = array_unique($postData->values);
foreach ($values as $value) foreach ($values as $value)
{ {
$insertResult = $this->addNotiz($postData); $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,74 @@
<?php
defined('BASEPATH') || exit('No direct script access allowed');
use CI3_Events as Events;
class CollisionChecker
{
private $_checks = [];
private $_ci;
private $_uid;
public function __construct($params)
{
$this->_ci =& get_instance();
if (isset($params['uid']) && !isEmptyString($params['uid'])){
$this->_uid = $params['uid'];
} else {
show_error('uid of logged user not passed!');
}
$this->_ci->load->library('collision/checks/RoomCollisionCheck', ['uid' => $this->_uid]);
$this->_ci->load->library('collision/checks/LectureCollisionCheck', ['uid' => $this->_uid]);
$this->_ci->load->library('collision/checks/VerbandCollisionCheck', ['uid' => $this->_uid]);
$this->_ci->load->library('collision/checks/StudentCollisionCheck', ['uid' => $this->_uid]);
$this->_ci->load->library('collision/checks/ResourcesCollisionCheck', ['uid' => $this->_uid]);
$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,170 @@
<?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');
}
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/>';
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>';
}
}
+420
View File
@@ -0,0 +1,420 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class LvPlanCSVLib
{
const TARGET_OUTLOOK = 'outlook';
const GENERIC_HEADER = array(
'title',
'category',
'location',
'description',
'keywords',
'start_date',
'start_time',
'end_date',
'end_time',
'alarm',
'recur_type',
'recur_end_date',
'recur_interval',
'recur_data'
);
const OUTLOOK_HEADER = array(
'Betreff',
'Beginnt am',
'Beginnt um',
'Endet am',
'Endet um',
'Ganztaegiges Ereignis',
'Erinnerung Ein/Aus',
'Erinnerung am',
'Erinnerung um',
'Besprechungsplanung',
'Erforderliche Teilnehmer',
'Optionale Teilnehmer',
'Besprechungsressourcen',
'Abrechnungsinformationen',
'Beschreibung',
'Kategorien',
'Ort',
'Priorität',
'Privat',
'Reisekilometer',
'Vertraulichkeit',
'Zeitspanne zeigen als'
);
private $ci;
public function __construct()
{
$this->ci =& get_instance();
}
/**
* Builds CSV content for a user's timetable.
*
* @param string $uid User UID.
* @param int $begin First included date as Unix timestamp.
* @param int $ende Last included date as Unix timestamp.
* @param string $type Timetable type (student, lektor, ort or verband).
* @param string|null $target Optional CSV target (outlook).
* @param string|null $ortKurzbz Room identifier for room plans.
* @param array|null $verbandFilters Lehrverband filters.
* @return string
*/
public function getContent(
$uid,
$begin,
$ende,
$type,
$target = null,
$ortKurzbz = null,
$verbandFilters = null
) {
if (!in_array($type, array('student', 'lektor', 'ort', 'verband'), true))
throw new InvalidArgumentException('Unsupported timetable type');
if ($target !== null && $target !== self::TARGET_OUTLOOK)
throw new InvalidArgumentException('Unsupported CSV target');
if ($type === 'ort' && (!is_string($ortKurzbz) || $ortKurzbz === ''))
throw new InvalidArgumentException('Missing room timetable filter');
if ($type === 'verband'
&& (!is_array($verbandFilters)
|| !array_key_exists('stg_kz', $verbandFilters)
|| !array_key_exists('sem', $verbandFilters)))
{
throw new InvalidArgumentException('Missing Lehrverband timetable filters');
}
$isLecturer = $type === 'lektor';
$this->ci->load->library('KalenderLib', array('uid' => $uid));
$startDate = date('Y-m-d', $begin);
$endDate = date('Y-m-d', $ende);
if ($type === 'verband')
{
$events = $this->ci->kalenderlib->getPlanForVerband(
$startDate,
$endDate,
$verbandFilters['stg_kz'],
$verbandFilters['sem'],
isset($verbandFilters['ver']) ? $verbandFilters['ver'] : null,
isset($verbandFilters['grp']) ? $verbandFilters['grp'] : null
);
}
elseif ($type === 'ort')
{
$events = $this->ci->kalenderlib
->getPlanForRoom($startDate, $endDate, $ortKurzbz);
}
elseif ($isLecturer)
{
$events = $this->ci->kalenderlib
->getPlanForLecturerByLecturer($startDate, $endDate, $uid);
}
else
{
$events = $this->ci->kalenderlib
->getPlanForStudentByStudent($startDate, $endDate, $uid);
}
if (!is_array($events))
$events = array();
$content = $this->buildCSVLine(
$target === self::TARGET_OUTLOOK ? self::OUTLOOK_HEADER : self::GENERIC_HEADER
);
foreach ($events as $event)
{
$eventData = $this->getEventData($event, $type);
if ($eventData === null)
continue;
$content .= $this->buildCSVLine(
$target === self::TARGET_OUTLOOK
? $this->buildOutlookRow($eventData)
: $this->buildGenericRow($eventData)
);
}
return $content;
}
/**
* @param object $event Calendar event.
* @return array|null Normalized CSV values or null for invalid dates.
*/
private function getEventData($event, $type = null)
{
$start = $this->createDateTime(isset($event->isostart) ? $event->isostart : null);
$end = $this->createDateTime(isset($event->isoend) ? $event->isoend : null);
if ($start === null || $end === null)
return null;
$timezone = new DateTimeZone('Europe/Vienna');
$start->setTimezone($timezone);
$end->setTimezone($timezone);
$isReservation = isset($event->type) && $event->type === 'reservierung';
$title = $isReservation
? (isset($event->titel) ? $event->titel : '')
: $this->joinValues(isset($event->topic) ? $event->topic : array());
if ($type === 'verband') {
$title = '';
}
$location = $this->joinValues(
isset($event->ort_kurzbz) ? $event->ort_kurzbz : array(),
' / '
);
$description = $this->getDescription(
$event,
$isReservation
);
return array(
'title' => $title,
'location' => $location,
'description' => $description,
'start_date' => $start->format('d.m.Y'),
'start_time' => $start->format('H:i:s'),
'end_date' => $end->format('d.m.Y'),
'end_time' => $end->format('H:i:s')
);
}
/**
* Builds a localized event description from populated event details.
*
* @param object $event Calendar event.
* @param bool $isReservation Whether the event is a reservation.
* @param string $participants Participant abbreviations.
* @param string $lecturers Lecturer abbreviations.
* @param string $groups Participant group labels.
* @return string Description lines separated by CRLF.
*/
private function getDescription($event, $isReservation)
{
$groups = $this->getGroups($event);
$lecturers = $this->getLecturers($event);
$participants = $this->getParticipants($event);
$parts = array();
if ($isReservation)
{
if ($participants !== '')
$parts[] = $this->ci->p->t('ui', 'teilnehmende').': '.$participants;
}
else
{
$subject = isset($event->lehrfach_bez) ? $event->lehrfach_bez : '';
if ($subject !== '')
$parts[] = $this->ci->p->t('global', 'lehrfach').': '.$subject;
if ($lecturers !== '')
$parts[] = $this->ci->p->t('lehre', 'lektorInnen').': '.$lecturers;
}
if ($groups !== '')
$parts[] = $this->ci->p->t('global', 'gruppen').': '.$groups;
$description = isset($event->beschreibung) ? $event->beschreibung : '';
if ($description !== '')
$parts[] = $this->ci->p->t('global', 'beschreibung').': '.$description;
return implode("\r\n", $parts);
}
/**
* @param array $eventData Normalized event values.
* @return array Generic CSV row.
*/
private function buildGenericRow(array $eventData)
{
$category = defined('LVPLAN_KATEGORIE') ? LVPLAN_KATEGORIE : 'StundenplanTW';
return array(
$eventData['title'],
$category,
$eventData['location'],
$eventData['description'],
'Stundenplan',
$eventData['start_date'],
$eventData['start_time'],
$eventData['end_date'],
$eventData['end_time'],
'',
'',
'',
'',
''
);
}
/**
* @param array $eventData Normalized event values.
* @return array Outlook CSV row.
*/
private function buildOutlookRow(array $eventData)
{
$subject = $eventData['title'];
return array(
$subject,
$eventData['start_date'],
$eventData['start_time'],
$eventData['end_date'],
$eventData['end_time'],
'Aus',
'Aus',
'',
'',
'',
'',
'',
'',
'',
$eventData['description'],
'StundenplanFH',
$eventData['location'],
'Normal',
'Aus',
'',
'Normal',
'2'
);
}
/**
* @param object $event Calendar event.
* @return string Lecturer abbreviations.
*/
private function getLecturers($event)
{
$lecturers = array();
if (isset($event->lektor) && is_array($event->lektor))
{
foreach ($event->lektor as $lecturer)
{
if (isset($lecturer['kurzbz']) && $lecturer['kurzbz'] !== '')
$lecturers[] = $lecturer['kurzbz'];
}
}
sort($lecturers, SORT_NATURAL | SORT_FLAG_CASE);
return implode(' / ', array_unique($lecturers));
}
/**
* @param object $event Calendar event.
* @return string Participant group labels.
*/
private function getGroups($event)
{
$groups = array();
if (isset($event->gruppe) && is_array($event->gruppe))
{
foreach ($event->gruppe as $group)
{
if (isset($group['bezeichnung']) && $group['bezeichnung'] !== '')
$groups[] = $group['bezeichnung'];
}
}
if (isset($event->teilnehmer_gruppe) && is_array($event->teilnehmer_gruppe))
{
foreach ($event->teilnehmer_gruppe as $group)
{
if (isset($group['gruppe_kurzbz']) && $group['gruppe_kurzbz'] !== '')
$groups[] = $group['gruppe_kurzbz'];
}
}
sort($groups, SORT_NATURAL | SORT_FLAG_CASE);
return implode(' / ', array_unique($groups));
}
/**
* @param mixed $value String or list of strings.
* @param string $separator Join separator.
* @return string
*/
private function joinValues($value, $separator = ' / ')
{
return is_array($value) ? implode($separator, $value) : (string)$value;
}
/**
* Builds one fully quoted RFC-style CSV row.
*
* @param array $values Row values.
* @return string
*/
private function buildCSVLine(array $values)
{
$escapedValues = array_map(function ($value) {
$value = preg_replace('/\r\n|\r|\n/', "\r\n", (string)$value);
return '"'.str_replace('"', '""', $value).'"';
}, $values);
return implode(',', $escapedValues)."\r\n";
}
/**
* @param mixed $dateTime Date/time input.
* @return DateTime|null
*/
private function createDateTime($dateTime)
{
if ($dateTime instanceof DateTime)
return clone $dateTime;
if ($dateTime === null || $dateTime === '')
return null;
try
{
if (is_numeric($dateTime))
return new DateTime('@'.(int)$dateTime);
return new DateTime($dateTime);
}
catch (Exception $e)
{
return null;
}
}
private function getParticipants($event)
{
$values = array();
if (!isset($event->teilnehmer_person) || !is_array($event->teilnehmer_person))
return '';
foreach ($event->teilnehmer_person as $participant)
{
$value = $participant['kurzbz'];
if ($value !== '')
$values[] = $value;
}
sort($values, SORT_NATURAL | SORT_FLAG_CASE);
return implode(', ', array_unique($values));
}
}
+473
View File
@@ -0,0 +1,473 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
require_once(FHCPATH.'include/Excel/excel.php');
class LvPlanExcelLib
{
const WORKSHEET_NAME = 'Termine';
const HEADER = array(
'Datum',
'Von',
'Bis',
'Ort',
'Lektoren',
'Gruppen',
'Lehrfach',
'Anmerkung',
'StundeVon',
'StundeBis'
);
private $ci;
public function __construct()
{
$this->ci =& get_instance();
}
/**
* Builds a BIFF8 Excel workbook for a user's timetable.
*
* @param string $uid User UID.
* @param int $begin First included date as Unix timestamp.
* @param int $ende Last included date as Unix timestamp.
* @param string $type Timetable type (student, lektor, ort or verband).
* @param string|null $ortKurzbz Room identifier for room plans.
* @param array|null $verbandFilters Lehrverband filters.
* @return string Binary workbook content.
*/
public function getContent(
$uid,
$begin,
$ende,
$type,
$ortKurzbz = null,
$verbandFilters = null
) {
if (!in_array($type, array('student', 'lektor', 'ort', 'verband'), true))
throw new InvalidArgumentException('Unsupported timetable type');
if ($type === 'ort' && (!is_string($ortKurzbz) || $ortKurzbz === ''))
throw new InvalidArgumentException('Missing room timetable filter');
if ($type === 'verband'
&& (!is_array($verbandFilters)
|| !array_key_exists('stg_kz', $verbandFilters)
|| !array_key_exists('sem', $verbandFilters)))
{
throw new InvalidArgumentException('Missing Lehrverband timetable filters');
}
$isLecturer = $type === 'lektor';
$this->ci->load->library('KalenderLib', array('uid' => $uid));
$this->ci->load->model('ressource/Stunde_model', 'StundeModel');
$startDate = date('Y-m-d', $begin);
$endDate = date('Y-m-d', $ende);
if ($type === 'verband')
{
$events = $this->ci->kalenderlib->getPlanForVerband(
$startDate,
$endDate,
$verbandFilters['stg_kz'],
$verbandFilters['sem'],
isset($verbandFilters['ver']) ? $verbandFilters['ver'] : null,
isset($verbandFilters['grp']) ? $verbandFilters['grp'] : null
);
}
elseif ($type === 'ort')
{
$events = $this->ci->kalenderlib
->getPlanForRoom($startDate, $endDate, $ortKurzbz);
}
elseif ($isLecturer)
{
$events = $this->ci->kalenderlib
->getPlanForLecturerByLecturer($startDate, $endDate, $uid);
}
else
{
$events = $this->ci->kalenderlib
->getPlanForStudentByStudent($startDate, $endDate, $uid);
}
if (!is_array($events))
$events = array();
usort($events, array($this, 'compareEventsByStart'));
$hourRaster = $this->loadHourRaster();
$rows = array();
foreach ($events as $event)
{
$row = $this->buildEventRow($event, $hourRaster);
if ($row !== null)
$rows[] = $row;
}
return $this->buildWorkbook($rows);
}
/**
* @return array Timetable-hour raster ordered by hour number.
*/
private function loadHourRaster()
{
$this->ci->StundeModel->addSelect('stunde, beginn, ende');
$this->ci->StundeModel->addOrder('stunde', 'ASC');
$result = $this->ci->StundeModel->load();
if (isError($result))
throw new RuntimeException(getError($result));
if (!hasData($result))
return array();
$raster = array();
foreach (getData($result) as $hour)
{
$begin = $this->timeToSeconds($hour->beginn);
$end = $this->timeToSeconds($hour->ende);
if ($begin === null || $end === null)
continue;
$raster[] = array(
'stunde' => $hour->stunde,
'begin' => $begin,
'end' => $end
);
}
return $raster;
}
/**
* @param object $event Calendar event.
* @param array $hourRaster Timetable-hour raster.
* @return array|null Excel row or null for invalid dates.
*/
private function buildEventRow($event, array $hourRaster)
{
$start = $this->createDateTime(isset($event->isostart) ? $event->isostart : null);
$end = $this->createDateTime(isset($event->isoend) ? $event->isoend : null);
if ($start === null || $end === null)
return null;
$timezone = new DateTimeZone('Europe/Vienna');
$start->setTimezone($timezone);
$end->setTimezone($timezone);
$isReservation = isset($event->type) && $event->type === 'reservierung';
$subject = $this->getSubject($event);
$note = isset($event->beschreibung) ? $event->beschreibung : '';
if ($note === '' && isset($event->titel) && !$isReservation)
$note = $event->titel;
return array(
$start->format('d.m.Y'),
$start->format('H:i:s'),
$end->format('H:i:s'),
$this->joinValues(isset($event->ort_kurzbz) ? $event->ort_kurzbz : array()),
!$isReservation ? $this->getParticipants($event) : $this->getLecturers($event),
$this->getGroups($event),
$subject,
$note,
$this->findHour($start->format('H:i:s'), $hourRaster, "start"),
$this->findHour($end->format('H:i:s'), $hourRaster, "end")
);
}
/**
* @param object $event Calendar event.
* @return string Event subject.
*/
private function getSubject($event)
{
if (isset($event->type) && $event->type === 'reservierung')
{
return isset($event->titel) ? $event->titel : '';
}
$subject = isset($event->lehrfach_bez) ? $event->lehrfach_bez : '';
if ($subject === '')
$subject = $this->joinValues(isset($event->topic) ? $event->topic : array());
return $subject;
}
/**
* @param array $rows Workbook rows.
* @return string Binary BIFF8 workbook content.
*/
private function buildWorkbook(array $rows)
{
$tempFile = tempnam(sys_get_temp_dir(), 'lvplan_excel_');
if ($tempFile === false)
throw new RuntimeException('Could not create temporary Excel file');
try
{
$workbook = new Spreadsheet_Excel_Writer($tempFile);
$workbook->setVersion(8);
$workbook->setTempDir(sys_get_temp_dir());
$worksheet =& $workbook->addWorksheet(self::WORKSHEET_NAME);
$worksheet->setInputEncoding('utf-8');
$boldFormat =& $workbook->addFormat();
$boldFormat->setBold();
$maxLengths = array();
foreach (self::HEADER as $column => $title)
{
$worksheet->writeString(0, $column, $title, $boldFormat);
$maxLengths[$column] = $this->getMaxLineLength($title);
}
foreach ($rows as $rowIndex => $row)
{
foreach ($row as $column => $value)
{
$value = (string)$value;
$worksheet->writeString($rowIndex + 1, $column, $value);
$maxLengths[$column] = max(
$maxLengths[$column],
$this->getMaxLineLength($value)
);
}
}
foreach ($maxLengths as $column => $length)
$worksheet->setColumn($column, $column, min($length + 2, 255));
$result = $workbook->close();
if ($result !== true)
throw new RuntimeException('Could not generate Excel workbook');
$content = file_get_contents($tempFile);
if ($content === false)
throw new RuntimeException('Could not read generated Excel workbook');
return $content;
}
finally
{
if (is_file($tempFile))
unlink($tempFile);
}
}
/**
* @param object $event Calendar event.
* @return string Lecturer names.
*/
private function getLecturers($event)
{
$lecturers = array();
if (isset($event->lektor) && is_array($event->lektor))
{
foreach ($event->lektor as $lecturer)
{
$name = trim(
(isset($lecturer['vorname']) ? $lecturer['vorname'] : '').' '
.(isset($lecturer['nachname']) ? $lecturer['nachname'] : '')
);
if ($name === '' && isset($lecturer['kurzbz']))
$name = $lecturer['kurzbz'];
if ($name !== '')
$lecturers[] = $name;
}
}
sort($lecturers, SORT_NATURAL | SORT_FLAG_CASE);
return implode(', ', array_unique($lecturers));
}
/**
* @param object $event Calendar event.
* @return string Participant group labels.
*/
private function getGroups($event)
{
$groups = array();
if (isset($event->gruppe) && is_array($event->gruppe))
{
foreach ($event->gruppe as $group)
{
if (isset($group['bezeichnung']) && $group['bezeichnung'] !== '')
$groups[] = $group['bezeichnung'];
}
}
if (isset($event->teilnehmer_gruppe) && is_array($event->teilnehmer_gruppe))
{
foreach ($event->teilnehmer_gruppe as $group)
{
if (isset($group['gruppe_kurzbz']) && $group['gruppe_kurzbz'] !== '')
$groups[] = $group['gruppe_kurzbz'];
}
}
sort($groups, SORT_NATURAL | SORT_FLAG_CASE);
return implode(',', array_unique($groups));
}
/**
* @param string $time Local time.
* @param array $hourRaster Timetable-hour raster.
* @return string|int Timetable-hour number or an empty string.
*/
private function findHour($time, array $hourRaster, string $context = '')
{
$seconds = $this->timeToSeconds($time);
if ($seconds === null || count($hourRaster) === 0)
return '';
$firstHour = current($hourRaster);
$lastHour = end($hourRaster);
if ($seconds < $firstHour['begin'] || $seconds > $lastHour['end'])
return '';
foreach ($hourRaster as $hour)
{
if (
($seconds < $hour['end'] && $context === "start") ||
($seconds <= $hour['end'] && $context === "end")
)
return $hour['stunde'];
}
return '';
}
/**
* @param string $time Time in HH:MM[:SS] format.
* @return int|null Seconds since midnight.
*/
private function timeToSeconds($time)
{
if (!is_string($time) || !preg_match('/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/', $time, $matches))
return null;
$hour = (int)$matches[1];
$minute = (int)$matches[2];
$second = isset($matches[3]) ? (int)$matches[3] : 0;
if ($hour > 23 || $minute > 59 || $second > 59)
return null;
return $hour * 3600 + $minute * 60 + $second;
}
/**
* @param mixed $value String or list of strings.
* @return string
*/
private function joinValues($value)
{
return is_array($value) ? implode(' / ', $value) : (string)$value;
}
/**
* @param string $value Cell content.
* @return int Maximum visual line length.
*/
private function getMaxLineLength($value)
{
$lines = preg_split('/\r\n|\r|\n/', (string)$value);
$maxLength = 0;
foreach ($lines as $line)
$maxLength = max($maxLength, mb_strlen($line));
return $maxLength;
}
/**
* @param object $first First event.
* @param object $second Second event.
* @return int Comparison result.
*/
private function compareEventsByStart($first, $second)
{
$firstTimestamp = isset($first->isostart) ? strtotime($first->isostart) : false;
$secondTimestamp = isset($second->isostart) ? strtotime($second->isostart) : false;
$firstTimestamp = $firstTimestamp === false ? PHP_INT_MAX : $firstTimestamp;
$secondTimestamp = $secondTimestamp === false ? PHP_INT_MAX : $secondTimestamp;
if ($firstTimestamp === $secondTimestamp)
return 0;
return $firstTimestamp < $secondTimestamp ? -1 : 1;
}
/**
* @param mixed $dateTime Date/time input.
* @return DateTime|null
*/
private function createDateTime($dateTime)
{
if ($dateTime instanceof DateTime)
return clone $dateTime;
if ($dateTime === null || $dateTime === '')
return null;
try
{
if (is_numeric($dateTime))
return new DateTime('@'.(int)$dateTime);
return new DateTime($dateTime);
}
catch (Exception $e)
{
return null;
}
}
private function getParticipants($event)
{
$values = array();
if (!isset($event->teilnehmer_person) || !is_array($event->teilnehmer_person))
return '';
foreach ($event->teilnehmer_person as $participant)
{
$value = $participant['vorname'] . ' ' . $participant['nachname'];
if ($value !== '')
$values[] = $value;
}
sort($values, SORT_NATURAL | SORT_FLAG_CASE);
return implode(', ', array_unique($values));
}
private function getNestedValue($value, array $properties)
{
foreach ($properties as $property)
{
if (is_array($value) && isset($value[$property]))
return (string)$value[$property];
if (is_object($value) && isset($value->{$property}))
return (string)$value->{$property};
}
return is_scalar($value) ? (string)$value : '';
}
}
@@ -0,0 +1,780 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
require_once(FHCPATH.'include/functions.inc.php');
require_once(FHCPATH.'include/phrasen.class.php');
class LvPlanHTMLDetailLib
{
const TIMEZONE = 'Europe/Vienna';
private $ci;
private $legacyPhrases;
public function __construct()
{
$this->ci =& get_instance();
$this->legacyPhrases = new phrasen(getSprache());
}
/**
* Builds a timetable detail document for one date and hour.
*
* @param string $datum Date in YYYY-MM-DD format.
* @param array $hour Selected timetable hour.
* @param string $type Timetable type.
* @param array $filters Type-specific filters.
* @return string
*/
public function getContent($datum, array $hour, $type, array $filters)
{
if (!in_array($type, array('student', 'lektor', 'verband', 'ort'), true))
throw new InvalidArgumentException('Unsupported timetable type');
foreach (array('number', 'begin', 'end') as $property)
{
if (!isset($hour[$property]) || !is_scalar($hour[$property]))
throw new InvalidArgumentException('Invalid timetable hour');
}
$events = $this->loadEvents($datum, $type, $filters);
$events = $this->filterEventsByHour($events, $datum, $hour);
if ($type === 'verband'
&& isset($filters['ort_kurzbz'])
&& $filters['ort_kurzbz'] !== '')
{
$events = $this->filterEventsByRoom(
$events,
$filters['ort_kurzbz']
);
}
$sections = array(
'teaching' => array(),
'reservations' => array(),
'events' => array()
);
foreach ($events as $event)
{
if ($event->type === 'lehreinheit')
$sections['teaching'][] = $event;
elseif ($event->type === 'reservierung')
$sections['reservations'][] = $event;
else
$sections['events'][] = $event;
}
return $this->renderDocument($datum, $hour, $sections);
}
/**
* Loads the same published calendar data used by the HTML preview.
*
* @return array
*/
private function loadEvents($datum, $type, array $filters)
{
$uid = isset($filters['uid']) && is_string($filters['uid'])
? $filters['uid']
: getAuthUID();
$this->ci->load->library('KalenderLib', array('uid' => $uid));
if ($type === 'verband')
{
$events = $this->ci->kalenderlib->getPlanForVerband(
$datum,
$datum,
$filters['stg_kz'],
$filters['sem'],
isset($filters['ver']) ? $filters['ver'] : null,
isset($filters['grp']) ? $filters['grp'] : null
);
}
elseif ($type === 'ort')
{
$events = $this->ci->kalenderlib->getPlanForRoom(
$datum,
$datum,
$filters['ort_kurzbz']
);
}
elseif ($type === 'lektor')
{
$events = $this->ci->kalenderlib->getPlanForLecturerByLecturer(
$datum,
$datum,
$filters['uid']
);
}
else
{
$events = $this->ci->kalenderlib->getPlanForStudentByStudent(
$datum,
$datum,
$filters['uid']
);
}
return is_array($events) ? $events : array();
}
/**
* Keeps events that overlap the selected timetable hour.
*
* @return array
*/
private function filterEventsByHour(array $events, $datum, array $hour)
{
$timezone = new DateTimeZone(self::TIMEZONE);
$hourStart = DateTime::createFromFormat(
'!Y-m-d H:i:s',
$datum.' '.$this->normalizeTime($hour['begin']),
$timezone
);
$hourEnd = DateTime::createFromFormat(
'!Y-m-d H:i:s',
$datum.' '.$this->normalizeTime($hour['end']),
$timezone
);
if ($hourStart === false || $hourEnd === false || $hourEnd <= $hourStart)
throw new InvalidArgumentException('Invalid timetable hour range');
$matches = array();
foreach ($events as $event)
{
$start = $this->getEventDateTime($event, 'isostart', 'datum', 'beginn');
$end = $this->getEventDateTime($event, 'isoend', 'datum', 'ende');
if ($start === null || $end === null || $end <= $start)
continue;
if ($start < $hourEnd && $end > $hourStart)
{
$matches[] = array(
'event' => $event,
'start' => $start,
'end' => $end
);
}
}
usort($matches, function ($first, $second) {
$comparison = $first['start']->getTimestamp()
<=> $second['start']->getTimestamp();
if ($comparison !== 0)
return $comparison;
$firstId = isset($first['event']->kalender_id)
? (int)$first['event']->kalender_id
: 0;
$secondId = isset($second['event']->kalender_id)
? (int)$second['event']->kalender_id
: 0;
return $firstId <=> $secondId;
});
return array_map(function ($match) {
return $match['event'];
}, $matches);
}
/**
* Keeps Verband events assigned to the room selected in the preview cell.
*
* @return array
*/
private function filterEventsByRoom(array $events, $ortKurzbz)
{
$matches = array();
foreach ($events as $event)
{
$rooms = isset($event->ort_kurzbz)
? $event->ort_kurzbz
: array();
if (!is_array($rooms))
$rooms = array($rooms);
foreach ($rooms as $room)
{
if (is_scalar($room) && (string)$room === (string)$ortKurzbz)
{
$matches[] = $event;
continue 2;
}
}
if (!isset($event->ort_details) || !is_array($event->ort_details))
continue;
foreach ($event->ort_details as $room)
{
if ($this->getNestedValue($room, 'ort_kurzbz') === (string)$ortKurzbz)
{
$matches[] = $event;
continue 2;
}
}
}
return $matches;
}
/**
* Renders the complete HTML document.
*/
private function renderDocument($datum, array $hour, array $sections)
{
$title = $this->phrase('lvplan', 'lehrveranstaltungsplanDetails');
$html = '<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8">';
$html .= '<meta name="viewport" content="width=device-width, initial-scale=1">';
$html .= '<title>'.$this->escape($title).'</title>';
$html .= '<link rel="stylesheet" href="'
.$this->escape($this->getAppRoot().'skin/style.css.php')
.'" type="text/css">';
$html .= '<style>'
.'.stdplan td{vertical-align:top}.detail-lines{white-space:normal}'
.'.detail-empty{color:#666}.stdplan{margin-bottom:1em}'
.'</style></head><body id="inhalt">';
$html .= '<h2>'.$this->escape($this->phrase('lvplan', 'lehrveranstaltungsplan'))
.' &rArr; '.$this->escape($this->phrase('abgabetool', 'details')).'</h2>';
$html .= $this->escape($this->phrase('abgabetool', 'datum')).': '
.$this->escape($this->formatDate($datum)).'<br>';
$html .= $this->escape($this->phrase('global', 'stunde')).': '
.$this->escape($hour['number']).' ('
.$this->escape(substr((string)$hour['begin'], 0, 5)).'–'
.$this->escape(substr((string)$hour['end'], 0, 5)).')<br><br>';
if (!empty($sections['teaching']))
$html .= $this->renderTeachingTable($sections['teaching']);
if (!empty($sections['reservations']))
$html .= $this->renderReservationTable($sections['reservations']);
if (!empty($sections['events']))
$html .= $this->renderEventTable($sections['events']);
if (empty($sections['teaching'])
&& empty($sections['reservations'])
&& empty($sections['events']))
{
$html .= '<p class="detail-empty">0 '
.$this->escape($this->phrase('global', 'termine')).'</p>';
}
if (defined('MAIL_LVPLAN'))
{
$html .= '<p>'.$this->phrase(
'lvplan',
'FragenZuLvPlan',
array(constant('MAIL_LVPLAN'))
).'</p>';
}
return $html.'</body></html>';
}
/**
* Renders teaching-unit details.
*/
private function renderTeachingTable(array $events)
{
$html = '<table class="stdplan"><thead><tr>';
foreach (array(
array('lvplan', 'unr'),
array('lvaliste', 'lektor'),
array('lvplan', 'ort'),
array('lvaliste', 'lehrfach'),
array('global', 'bezeichnung'),
array('global', 'verband'),
array('lvplan', 'einheit'),
array('lvplan', 'info')
) as $heading)
{
$html .= '<th>'.$this->escape($this->phrase($heading[0], $heading[1])).'</th>';
}
$html .= '</tr></thead><tbody>';
foreach ($events as $index => $event)
{
$html .= '<tr class="liste'.(($index + 1) % 2).'">';
$html .= $this->cell($this->renderScalarLines(
isset($event->unr) ? $event->unr : array()
));
$html .= $this->cell($this->renderPeople(
isset($event->lektor) ? $event->lektor : array(),
'profile'
));
$html .= $this->cell($this->renderRooms($event));
$html .= $this->cell($this->renderSubjectValues($event, 'kurzbz'));
$html .= $this->cell($this->renderSubjectValues($event, 'bezeichnung'));
$html .= $this->cell($this->renderTeachingGroups($event, false));
$html .= $this->cell($this->renderTeachingGroups($event, true));
$html .= $this->cell($this->renderOldTempusNoticeTagNote($event));
$html .= '</tr>';
}
return $html.'</tbody></table>';
}
/**
* Renders reservation details.
*/
private function renderReservationTable(array $events)
{
$html = '<h2>'.$this->escape($this->phrase('lvplan', 'reservierungen')).'</h2>';
$html .= '<table class="stdplan"><thead><tr>';
foreach (array(
array('global', 'titel'),
array('lvplan', 'ort'),
array('global', 'person'),
array('global', 'beschreibung'),
array('lvplan', 'reserviertVon')
) as $heading)
{
$html .= '<th>'.$this->escape($this->phrase($heading[0], $heading[1])).'</th>';
}
$html .= '</tr></thead><tbody>';
foreach ($events as $index => $event)
{
$html .= '<tr class="liste'.(($index + 1) % 2).'">';
$html .= $this->cell($this->escape($this->getScalar($event, 'titel')));
$html .= $this->cell($this->renderRooms($event));
$html .= $this->cell($this->renderParticipants($event));
$html .= $this->cell($this->renderText(
$this->getScalar($event, 'beschreibung')
));
$html .= $this->cell($this->renderCreator($event));
$html .= '</tr>';
}
return $html.'</tbody></table>';
}
/**
* Renders non-teaching, non-reservation calendar events.
*/
private function renderEventTable(array $events)
{
$html = '<h2>'.$this->escape($this->phrase('global', 'termine')).'</h2>';
$html .= '<table class="stdplan"><thead><tr>';
foreach (array(
array('global', 'titel'),
array('lvplan', 'ort'),
array('ui', 'organisierende'),
array('ui', 'teilnehmende'),
array('global', 'beschreibung')
) as $heading)
{
$html .= '<th>'.$this->escape($this->phrase($heading[0], $heading[1])).'</th>';
}
$html .= '</tr></thead><tbody>';
foreach ($events as $index => $event)
{
$html .= '<tr class="liste'.(($index + 1) % 2).'">';
$html .= $this->cell($this->escape($this->getEventTitle($event)));
$html .= $this->cell($this->renderRooms($event));
$html .= $this->cell($this->renderPeople(
isset($event->lektor) ? $event->lektor : array(),
'mail'
));
$html .= $this->cell($this->renderParticipants($event));
$html .= $this->cell($this->renderText(
$this->getScalar($event, 'beschreibung')
));
$html .= '</tr>';
}
return $html.'</tbody></table>';
}
private function renderSubjectValues($event, $property)
{
$values = array();
if (isset($event->lehrfach_details) && is_array($event->lehrfach_details))
{
foreach ($event->lehrfach_details as $subject)
{
$value = $this->getNestedValue($subject, $property);
if ($value !== '')
$values[] = $value;
}
}
if (empty($values))
{
$fallback = $property === 'kurzbz'
? $this->getScalar($event, 'lehrfach')
: $this->getScalar($event, 'lehrfach_bez');
if ($fallback !== '')
$values[] = $fallback;
}
return $this->renderScalarLines($values);
}
private function renderTeachingGroups($event, $specialGroups)
{
$values = array();
if (isset($event->gruppe) && is_array($event->gruppe))
{
foreach ($event->gruppe as $group)
{
$groupCode = $this->getNestedValue($group, 'gruppe_kurzbz');
if ($specialGroups && $groupCode !== '')
$values[] = $groupCode;
elseif (!$specialGroups && $groupCode === '')
{
$designation = $this->getNestedValue($group, 'bezeichnung');
if ($designation !== '')
$values[] = $designation;
}
}
}
return $this->renderGroupLinks($values);
}
private function renderParticipants($event)
{
$parts = array();
$people = $this->renderPeople(
isset($event->teilnehmer_person) ? $event->teilnehmer_person : array(),
'mail'
);
if ($people !== '')
$parts[] = $people;
$groups = array();
if (isset($event->teilnehmer_gruppe) && is_array($event->teilnehmer_gruppe))
{
foreach ($event->teilnehmer_gruppe as $group)
{
$value = $this->getNestedValue($group, 'bezeichnung');
if ($value === '')
$value = $this->getNestedValue($group, 'gruppe_kurzbz');
if ($value !== '')
$groups[] = $value;
}
}
$groupLinks = $this->renderGroupLinks($groups);
if ($groupLinks !== '')
$parts[] = $groupLinks;
return implode('<br>', $parts);
}
private function renderCreator($event)
{
if (isset($event->created_by) && (is_array($event->created_by) || is_object($event->created_by)))
return $this->renderPeople(array($event->created_by), 'plain');
return $this->renderPeople(
isset($event->lektor) ? $event->lektor : array(),
'plain'
);
}
private function renderPeople(array $people, $linkType)
{
$values = array();
$seen = array();
foreach ($people as $person)
{
$uid = $this->getNestedValue($person, 'mitarbeiter_uid');
if ($uid === '')
$uid = $this->getNestedValue($person, 'uid');
$nameParts = array();
foreach (array('titelpre', 'vorname', 'nachname', 'titelpost') as $property)
{
$value = trim($this->getNestedValue($person, $property));
if ($value !== '')
$nameParts[] = $value;
}
$name = implode(' ', $nameParts);
if ($name === '')
$name = $uid !== '' ? $uid : $this->getNestedValue($person, 'kurzbz');
if ($name === '')
continue;
$key = $uid !== '' ? $uid : $name;
if (isset($seen[$key]))
continue;
$seen[$key] = true;
$label = $this->escape($name);
if ($linkType === 'profile' && $uid !== '')
{
$personalNumber = $this->getNestedValue($person, 'personalnummer');
$profileAllowed = $personalNumber === ''
|| !is_numeric($personalNumber)
|| (int)$personalNumber >= 0;
if ($profileAllowed)
$label = $this->link($this->getProfileUrl($uid), $label);
}
elseif ($linkType === 'mail' && $uid !== '' && defined('DOMAIN'))
{
$label = $this->link(
'mailto:'.$uid.'@'.constant('DOMAIN'),
$label
);
}
$values[] = $label;
}
return implode('<br>', $values);
}
private function renderRooms($event)
{
$values = array();
$seen = array();
if (isset($event->ort_details) && is_array($event->ort_details))
{
foreach ($event->ort_details as $room)
{
$code = $this->getNestedValue($room, 'ort_kurzbz');
if ($code === '' || isset($seen[$code]))
continue;
$seen[$code] = true;
$label = $this->escape($code);
$contentId = $this->getNestedValue($room, 'content_id');
$title = $this->getNestedValue($room, 'bezeichnung');
if ($contentId !== '')
$label = $this->link($this->getRoomUrl($contentId), $label, $title);
elseif ($title !== '')
$label = '<span title="'.$this->escape($title).'">'.$label.'</span>';
$values[] = $label;
}
}
if (isset($event->ort_kurzbz) && is_array($event->ort_kurzbz))
{
foreach ($event->ort_kurzbz as $code)
{
$code = is_scalar($code) ? (string)$code : '';
if ($code === '' || isset($seen[$code]))
continue;
$seen[$code] = true;
$values[] = $this->escape($code);
}
}
if (isset($event->locations) && is_array($event->locations))
{
foreach ($event->locations as $location)
{
$location = is_scalar($location) ? trim((string)$location) : '';
if ($location !== '' && !isset($seen['location:'.$location]))
{
$seen['location:'.$location] = true;
$values[] = $this->escape($location);
}
}
}
return implode('<br>', $values);
}
private function renderGroupLinks(array $groups)
{
$values = array();
foreach ($this->uniqueScalarValues($groups) as $group)
{
$label = $this->escape($group);
if (defined('DOMAIN'))
{
$label = $this->link(
'mailto:'.mb_strtolower($group).'@'.constant('DOMAIN'),
$label
);
}
$values[] = $label;
}
return implode('<br>', $values);
}
private function renderScalarLines($values)
{
if (!is_array($values))
$values = array($values);
$escaped = array_map(array($this, 'escape'), $this->uniqueScalarValues($values));
return implode('<br>', $escaped);
}
private function uniqueScalarValues(array $values)
{
$result = array();
$seen = array();
foreach ($values as $value)
{
if (!is_scalar($value))
continue;
$value = trim((string)$value);
if ($value === '' || isset($seen[$value]))
continue;
$seen[$value] = true;
$result[] = $value;
}
sort($result, SORT_STRING | SORT_FLAG_CASE);
return $result;
}
private function renderOldTempusNoticeTagNote($event)
{
if (empty($event->tags)) return null;
$parsedTags = $event->tags;
if (!is_array($event->tags))
$parsedTags = json_decode($event->tags, true);
foreach ($parsedTags as $tag)
{
if (isset($tag['typ_kurzbz']) && $tag['typ_kurzbz'] === 'hinweis' && isset($tag['insertvon']) && $tag['insertvon'] === 'oldToNewTempusMigration')
return $tag['notiz'] ?? null;
}
return null;
}
private function renderText($value)
{
return nl2br($this->escape($value), false);
}
private function getEventTitle($event)
{
foreach (array('titel', 'lehrfach_bez', 'lehrfach') as $property)
{
$value = trim($this->getScalar($event, $property));
if ($value !== '')
return $value;
}
return '';
}
private function getEventDateTime($event, $isoProperty, $dateProperty, $timeProperty)
{
$value = isset($event->{$isoProperty}) ? $event->{$isoProperty} : null;
if (($value === null || $value === '')
&& isset($event->{$dateProperty})
&& isset($event->{$timeProperty}))
{
$value = $event->{$dateProperty}.' '.$event->{$timeProperty};
}
if ($value === null || $value === '')
return null;
try
{
$date = is_numeric($value)
? new DateTime('@'.(int)$value)
: new DateTime($value, new DateTimeZone(self::TIMEZONE));
$date->setTimezone(new DateTimeZone(self::TIMEZONE));
return $date;
}
catch (Exception $exception)
{
return null;
}
}
private function normalizeTime($time)
{
$time = (string)$time;
if (preg_match('/^(\d{1,2}:\d{2}:\d{2})(?:\.\d+)?$/', $time, $matches))
return $matches[1];
if (preg_match('/^\d{1,2}:\d{2}$/', $time))
return $time.':00';
return $time;
}
private function formatDate($datum)
{
$date = DateTime::createFromFormat(
'!Y-m-d',
$datum,
new DateTimeZone(self::TIMEZONE)
);
return $date === false ? $datum : $date->format('d.m.Y');
}
private function getScalar($object, $property)
{
return isset($object->{$property}) && is_scalar($object->{$property})
? (string)$object->{$property}
: '';
}
private function getNestedValue($value, $property)
{
if (is_array($value) && isset($value[$property]) && is_scalar($value[$property]))
return (string)$value[$property];
if (is_object($value) && isset($value->{$property}) && is_scalar($value->{$property}))
return (string)$value->{$property};
return '';
}
private function cell($content)
{
return '<td class="detail-lines">'.$content.'</td>';
}
private function link($url, $escapedLabel, $title = '')
{
$html = '<a href="'.$this->escape($url).'"';
if ($title !== '')
$html .= ' title="'.$this->escape($title).'"';
return $html.'>'.$escapedLabel.'</a>';
}
private function getProfileUrl($uid)
{
return $this->getAppRoot().'cis/private/profile/index.php?'
.http_build_query(array('uid' => $uid), '', '&', PHP_QUERY_RFC3986);
}
private function getRoomUrl($contentId)
{
return $this->getAppRoot().'cms/content.php?'
.http_build_query(
array('content_id' => $contentId),
'',
'&',
PHP_QUERY_RFC3986
);
}
private function phrase($category, $phrase, array $parameters = array())
{
if (($category === 'global' && $phrase === 'termine')
|| ($category === 'ui'
&& in_array($phrase, array('organisierende', 'teilnehmende'), true)))
{
return $this->ci->p->t($category, $phrase, $parameters);
}
return $this->legacyPhrases->t($category.'/'.$phrase, $parameters);
}
private function escape($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
private function getAppRoot()
{
return defined('APP_ROOT') ? rtrim(APP_ROOT, '/').'/' : '/';
}
}
+864
View File
@@ -0,0 +1,864 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class LvPlanHTMLLib
{
const TIMEZONE = 'Europe/Vienna';
const DETAIL_WINDOW_NAME = 'Details';
const DETAIL_WINDOW_OPTIONS = 'height=320,width=550,left=0,top=0,hotkeys=0,resizable=yes,status=no,scrollbars=no,toolbar=no,location=no,menubar=no,dependent=yes';
CONST NOTICE_TAG_ICON = '📝';
private $ci;
public function __construct()
{
$this->ci =& get_instance();
}
/**
* Builds a complete printable timetable document.
*
* @param int $begin First included date as Unix timestamp.
* @param int $ende Last included date as Unix timestamp.
* @param string $type Timetable type.
* @param array $filters Type-specific filters.
* @return string
*/
public function getContent($begin, $ende, $type, array $filters)
{
if (!in_array($type, array('student', 'lektor', 'verband', 'ort'), true))
throw new InvalidArgumentException('Unsupported timetable type');
if ($type === 'verband')
$events = $this->getVerbandEvents($begin, $ende, $filters);
elseif ($type === 'ort')
$events = $this->getRoomEvents($begin, $ende, $filters);
else
$events = $this->getPersonalEvents($begin, $ende, $type, $filters);
$hourRaster = $this->loadHourRaster();
$normalizedEvents = $this->normalizeEvents($events);
return $this->renderDocument(
$begin,
$ende,
$type,
$filters,
$hourRaster,
$normalizedEvents
);
}
/**
* @return array Personal plan events.
*/
private function getPersonalEvents($begin, $ende, $type, array $filters)
{
if (!isset($filters['uid']) || !is_string($filters['uid']) || $filters['uid'] === '')
throw new InvalidArgumentException('Missing personal timetable UID');
$uid = $filters['uid'];
$isLecturer = $type === 'lektor';
$this->ci->load->library('KalenderLib', array('uid' => $uid));
$startDate = $this->formatLocalDate($begin);
$endDate = $this->formatLocalDate($ende);
if ($isLecturer)
{
$events = $this->ci->kalenderlib
->getPlanForLecturerByLecturer($startDate, $endDate, $uid);
}
else
{
$events = $this->ci->kalenderlib
->getPlanForStudentByStudent($startDate, $endDate, $uid);
}
return is_array($events) ? $events : array();
}
/**
* @return array Room plan events.
*/
private function getRoomEvents($begin, $ende, array $filters)
{
if (!isset($filters['ort_kurzbz'])
|| !is_string($filters['ort_kurzbz'])
|| $filters['ort_kurzbz'] === '')
{
throw new InvalidArgumentException('Missing room timetable filter');
}
$this->ci->load->library('KalenderLib', array('uid' => getAuthUID()));
$events = $this->ci->kalenderlib->getPlanForRoom(
$this->formatLocalDate($begin),
$this->formatLocalDate($ende),
$filters['ort_kurzbz']
);
return is_array($events) ? $events : array();
}
/**
* @return array Lehrverband plan events.
*/
private function getVerbandEvents($begin, $ende, array $filters)
{
foreach (array('stg_kz', 'sem') as $name)
{
if (!array_key_exists($name, $filters))
throw new InvalidArgumentException('Missing Lehrverband filter: '.$name);
}
$this->ci->load->library('KalenderLib', array('uid' => getAuthUID()));
$events = $this->ci->kalenderlib->getPlanForVerband(
$this->formatLocalDate($begin),
$this->formatLocalDate($ende),
$filters['stg_kz'],
$filters['sem'],
isset($filters['ver']) ? $filters['ver'] : null,
isset($filters['grp']) ? $filters['grp'] : null
);
return is_array($events) ? $events : array();
}
/**
* @return array Timetable-hour raster.
*/
private function loadHourRaster()
{
$this->ci->load->model('ressource/Stunde_model', 'StundeModel');
$result = $this->ci->StundeModel->execReadOnlyQuery(
'SELECT stunde, beginn, ende
FROM lehre.tbl_stunde
ORDER BY stunde'
);
if (isError($result))
throw new RuntimeException(getError($result));
if (!hasData($result))
return array();
$raster = array();
foreach (getData($result) as $hour)
{
$start = $this->timeToSeconds($hour->beginn);
$end = $this->timeToSeconds($hour->ende);
if ($start === null || $end === null || $end <= $start)
continue;
$raster[] = array(
'number' => (string)$hour->stunde,
'begin' => $start,
'end' => $end,
'begin_label' => substr((string)$hour->beginn, 0, 5),
'end_label' => substr((string)$hour->ende, 0, 5)
);
}
return $raster;
}
/**
* @param array $events Events from KalenderLib.
* @return array
*/
private function normalizeEvents(array $events)
{
$normalized = array();
foreach ($events as $event)
{
$start = $this->getEventDateTime($event, 'isostart', 'datum', 'beginn');
$end = $this->getEventDateTime($event, 'isoend', 'datum', 'ende');
if ($start === null || $end === null || $end <= $start)
continue;
$isReservation = isset($event->type) && $event->type === 'reservierung';
$title = '';
if ($isReservation) {
$title = $this->getScalar($event, 'titel');
} else {
$title = $this->joinValue(isset($event->topic) ? $event->topic : '');
}
if ($title === '')
$title = $this->getScalar($event, 'lehrfach_bez');
$infoTag = $this->getOldTempusNoticeTag($event);
if (!empty($infoTag))
{
$title .= ' ' . self::NOTICE_TAG_ICON;
}
$normalized[] = array(
'start' => $start,
'end' => $end,
'date' => $start->format('Y-m-d'),
'title' => $title,
'location' => $this->joinValue(
isset($event->ort_kurzbz) ? $event->ort_kurzbz : '',
' / '
),
'detail_ort_kurzbz' => $this->getFirstScalarValue(
isset($event->ort_kurzbz) ? $event->ort_kurzbz : ''
),
'lecturers' => $this->getLecturers($event),
'participants' => $this->getParticipants($event),
'groups' => $this->getGroups($event),
'note' => $this->getEventNote($event, $isReservation),
'color' => $this->getColor($event),
'infoTagTitle' => !empty($infoTag) && isset($infoTag['notiz']) ? $infoTag['notiz'] : '',
);
}
usort($normalized, function ($first, $second) {
return $first['start']->getTimestamp() <=> $second['start']->getTimestamp();
});
return $normalized;
}
/**
* Renders the complete HTML document.
*/
private function renderDocument(
$begin,
$ende,
$type,
array $filters,
array $hourRaster,
array $events
) {
$timezone = new DateTimeZone(self::TIMEZONE);
$rangeStart = (new DateTime('@'.$begin))->setTimezone($timezone)->setTime(0, 0, 0);
$rangeEnd = (new DateTime('@'.$ende))->setTimezone($timezone)->setTime(23, 59, 59);
$weeks = $this->buildWeeks($rangeStart, $rangeEnd, $hourRaster, $events);
$title = $this->getDocumentTitle($type, $filters);
$appRoot = $this->getAppRoot();
$html = '<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8">';
$html .= '<meta name="viewport" content="width=device-width, initial-scale=1">';
$html .= '<title>'.$this->escape('Kalender').'</title>';
$html .= '<link rel="stylesheet" href="'.$this->escape($appRoot.'skin/style.css.php').'" type="text/css">';
$html .= '<link rel="stylesheet" media="print" href="'.$this->escape($appRoot.'skin/cis.css').'" type="text/css">';
$html .= '<link rel="stylesheet" media="print" href="'.$this->escape($appRoot.'skin/print.css').'" type="text/css">';
$html .= '<style>'.$this->getStyles().'</style></head>';
$html .= '<body id="inhalt"><h1>'.$this->escape($title).'</h1>';
foreach ($weeks as $week)
$html .= $this->renderWeek($week, $hourRaster, $type, $filters);
if (empty($weeks))
$html .= '<p>Keine Termine im gewählten Zeitraum.</p>';
$html .= '</body></html>';
return $html;
}
/**
* Builds week/day/cell collections for rendering.
*/
private function buildWeeks(DateTime $rangeStart, DateTime $rangeEnd, array $raster, array $events)
{
$weeks = array();
$weekStart = clone $rangeStart;
while ($weekStart <= $rangeEnd)
{
$weekKey = $weekStart->format('Y-m-d');
$weekEnd = (clone $weekStart)->modify('+6 days');
$days = array();
for ($dayOffset = 0; $dayOffset < 7; $dayOffset++)
{
$date = (clone $weekStart)->modify('+'.$dayOffset.' days');
$days[$date->format('Y-m-d')] = array(
'date' => $date,
'cells' => array_fill(0, count($raster), array())
);
}
$weeks[$weekKey] = array(
'start' => clone $weekStart,
'end' => $weekEnd,
'days' => $days,
'outside' => array()
);
$weekStart->modify('+7 days');
}
foreach ($events as $event)
{
$eventWeek = clone $event['start'];
$eventWeek->modify('monday this week')->setTime(0, 0, 0);
$weekKey = $eventWeek->format('Y-m-d');
$dateKey = $event['date'];
if (!isset($weeks[$weekKey]['days'][$dateKey]))
continue;
$matchingSlots = $this->getMatchingSlots($event, $raster);
if (empty($matchingSlots))
{
$weeks[$weekKey]['outside'][] = $event;
continue;
}
foreach ($matchingSlots as $slot)
$weeks[$weekKey]['days'][$dateKey]['cells'][$slot][] = $event;
}
return array_values($weeks);
}
/**
* @return array Raster indexes overlapping an event.
*/
private function getMatchingSlots(array $event, array $raster)
{
if ($event['start']->format('Y-m-d') !== $event['end']->format('Y-m-d'))
return array();
$start = $this->timeToSeconds($event['start']->format('H:i:s'));
$end = $this->timeToSeconds($event['end']->format('H:i:s'));
$matches = array();
foreach ($raster as $index => $hour)
{
if ($start < $hour['end'] && $end > $hour['begin'])
$matches[] = $index;
}
return $matches;
}
/**
* Renders one printable week.
*/
private function renderWeek(array $week, array $raster, $type, array $filters)
{
$weekdayNames = array(
1 => 'Montag',
2 => 'Dienstag',
3 => 'Mittwoch',
4 => 'Donnerstag',
5 => 'Freitag',
6 => 'Samstag',
7 => 'Sonntag'
);
$html = '<div style="padding-top: 10px;" class="page-break-after">';
$html .= '<table class="stdplan" width="100%" border="0" cellpadding="1" cellspacing="1" name="Stundenplantabelle" align="center">';
$html .= '<thead><tr>';
$html .= '<th align="right">Stunde&nbsp;<br>Beginn&nbsp;<br>Ende&nbsp;</th>';
foreach ($raster as $hour)
{
$html .= '<th><div align="center">'.$this->escape($hour['number']).'<br>';
$html .= '&nbsp;'.$this->escape($hour['begin_label']).'&nbsp;<br>';
$html .= '&nbsp;'.$this->escape($hour['end_label']).'&nbsp;</div></th>';
}
$html .= '</tr></thead><tbody>';
foreach ($week['days'] as $day)
{
$weekday = (int)$day['date']->format('N');
$html .= '<tr><td>'.$weekdayNames[$weekday].'<br>';
$html .= $day['date']->format('j.M Y').'<br></td>';
foreach ($day['cells'] as $slotIndex => $events)
{
if (empty($events))
{
$html .= '<td valign="center" align="center"></td>';
continue;
}
$html .= '<td nowrap valign="top" align="center">';
$hourNumber = isset($raster[$slotIndex]['number'])
? $raster[$slotIndex]['number']
: null;
foreach ($events as $event)
$html .= $this->renderEvent(
$event,
$type,
false,
$filters,
$hourNumber
);
$html .= '</td>';
}
$html .= '</tr>';
}
$html .= '</tbody></table>';
if (!empty($week['outside']))
{
$html .= '<div class="outside"><h3>Termine außerhalb des Stundenrasters</h3>';
foreach ($week['outside'] as $event)
{
$html .= $this->renderEvent(
$event,
$type,
true,
$filters,
$this->getNearestHourNumber($event, $raster)
);
}
$html .= '</div>';
}
if ($type === 'ort')
$html .= $this->renderRoomReservationNotice($week, $filters);
return $html.'</div>';
}
/**
* Renders the room reservation warning used by the legacy weekly plan.
*/
private function renderRoomReservationNotice(array $week, array $filters)
{
$requiredFilters = array(
'room_reservable',
'room_reservation_permitted',
'room_reservation_start',
'room_reservation_end',
'room_not_reservable_message',
'room_reservation_restricted_message'
);
foreach ($requiredFilters as $filter)
{
if (!array_key_exists($filter, $filters))
return '';
}
$weekEnd = (clone $week['end'])->modify('+1 day')->getTimestamp();
$canReserveInWeek = $filters['room_reservation_permitted']
&& $filters['room_reservable']
&& $weekEnd >= time()
&& $weekEnd >= $filters['room_reservation_start']
&& $week['start']->getTimestamp() <= $filters['room_reservation_end'];
if ($canReserveInWeek)
return '';
$message = $filters['room_reservation_permitted']
&& !$filters['room_reservable']
? $filters['room_not_reservable_message']
: $filters['room_reservation_restricted_message'];
return '<table><tr><td><br><span style="color: orange">'
.$this->escape($message)
.'</span></td></tr></table>';
}
/**
* Renders an escaped event card.
*/
private function renderEvent(
array $event,
$type,
$showDate,
array $filters,
$hourNumber
) {
$style = $event['color'] !== ''
? ' style="background-color: #'.$event['color'].'; margin-bottom: 3px;"'
: '';
$html = '<div align="center"'.$style.'>';
$detailUrl = $this->buildDetailUrl($event, $type, $filters, $hourNumber);
$linkTitle = $this->escape($event['note']);
$infoTagTitle = isset($event['infoTagTitle']) && $event['infoTagTitle'] !== ''
? $event['infoTagTitle']
: null;
if ($infoTagTitle !== null)
{
$linkTitle = $this->escape($infoTagTitle);
}
$html .= '<a class="stpl_detail" title="'.$linkTitle.'"';
if ($detailUrl !== null)
{
$onclick = 'window.open('
.$this->encodeJavaScriptString($detailUrl).','
.$this->encodeJavaScriptString(self::DETAIL_WINDOW_NAME).','
.$this->encodeJavaScriptString(self::DETAIL_WINDOW_OPTIONS)
.');return false;';
$html .= ' href="'.$this->escape($detailUrl).'"';
$html .= ' target="'.self::DETAIL_WINDOW_NAME.'"';
$html .= ' onclick="'.$this->escape($onclick).'"';
}
$html .= '>';
if ($showDate)
$html .= $event['start']->format('d.m.Y H:i').'–'.$event['end']->format('H:i').'<br>';
$html .= $this->escape($event['title']);
if (($type === 'lektor' || $type === 'verband' || $type === 'ort')
&& $event['groups'] !== '')
$html .= '<br>'.$this->renderLines($event['groups']);
if ($type !== 'lektor' && $type !== 'ort' && $event['lecturers'] !== '')
$html .= '<br>'.$this->renderLines($event['lecturers']);
if ($type === 'ort' && $event['participants'] !== '')
$html .= '<br>'.$this->renderLines($event['participants']);
if ($event['location'] !== '' && $type !== 'ort')
$html .= '<br>'.$this->escape($event['location']);
if ($event['note'] !== '' && $this->shouldShowNotes())
$html .= '<br>'.$this->escape($event['note']);
return $html.'</a></div>';
}
/**
* Builds the URL consumed by the legacy timetable detail popup.
*/
private function buildDetailUrl(array $event, $type, array $filters, $hourNumber)
{
if ($hourNumber === null || !preg_match('/^\d+$/', (string)$hourNumber))
return null;
$uid = isset($filters['uid']) && $filters['uid'] !== ''
? $filters['uid']
: getAuthUID();
$room = $type === 'ort' && isset($filters['ort_kurzbz'])
? $filters['ort_kurzbz']
: $event['detail_ort_kurzbz'];
$parameters = array(
'type' => $type,
'datum' => $event['date'],
'stunde' => (string)$hourNumber,
'pers_uid' => $uid,
'stg_kz' => isset($filters['stg_kz']) ? $filters['stg_kz'] : '',
'sem' => isset($filters['sem']) ? $filters['sem'] : '',
'ver' => isset($filters['ver']) && $filters['ver'] !== null
? $filters['ver']
: '',
'grp' => isset($filters['grp']) && $filters['grp'] !== null
? $filters['grp']
: '',
'ort_kurzbz' => $room
);
return $this->getAppRoot().'index.ci.php/lvplan/html/detail?'
.http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
}
/**
* Returns the hour whose configured start is closest to the event start.
*/
private function getNearestHourNumber(array $event, array $raster)
{
if (empty($raster))
return null;
$eventStart = $this->timeToSeconds($event['start']->format('H:i:s'));
if ($eventStart === null)
return null;
$nearestNumber = null;
$nearestDistance = null;
foreach ($raster as $hour)
{
$distance = abs($eventStart - $hour['begin']);
if ($nearestDistance === null || $distance < $nearestDistance)
{
$nearestDistance = $distance;
$nearestNumber = $hour['number'];
}
}
return $nearestNumber;
}
private function getDocumentTitle($type, array $filters)
{
//return 'Kalender';
if ($type === 'ort')
{
$title = 'Ort: '.$filters['ort_kurzbz'];
if (isset($filters['bezeichnung']) && $filters['bezeichnung'] !== '')
$title .= ' - '.$filters['bezeichnung'];
return $title;
}
if ($type === 'verband')
{
$studiengang = isset($filters['stg_kurzbz'])
&& $filters['stg_kurzbz'] !== ''
? $filters['stg_kurzbz']
: $filters['stg_kz'];
$title = 'Lehrverband: '.$studiengang.'-'.$filters['sem'];
if (isset($filters['ver']) && $filters['ver'] !== null)
$title .= $filters['ver'];
else
$title .= 0;
if (isset($filters['grp']) && $filters['grp'] !== null)
$title .= $filters['grp'];
else
$title .= 0;
return $title;
}
if ($type === 'lektor')
{
$name = isset($filters['full_name']) && $filters['full_name'] !== ''
? $filters['full_name']
: $filters['uid'];
return 'Lektor: '.$name;
}
return 'Student: '.$filters['uid'];
}
private function getEventDateTime($event, $isoProperty, $dateProperty, $timeProperty)
{
$value = isset($event->{$isoProperty}) ? $event->{$isoProperty} : null;
if (($value === null || $value === '')
&& isset($event->{$dateProperty})
&& isset($event->{$timeProperty}))
{
$value = $event->{$dateProperty}.' '.$event->{$timeProperty};
}
if ($value === null || $value === '')
return null;
try
{
$date = is_numeric($value) ? new DateTime('@'.(int)$value) : new DateTime($value);
$date->setTimezone(new DateTimeZone(self::TIMEZONE));
return $date;
}
catch (Exception $e)
{
return null;
}
}
private function getLecturers($event)
{
$values = array();
if (!isset($event->lektor) || !is_array($event->lektor))
return '';
foreach ($event->lektor as $lecturer)
{
$value = $this->getNestedValue($lecturer, array('kurzbz', 'mitarbeiter_uid'));
if ($value !== '')
$values[] = $value;
}
sort($values, SORT_STRING | SORT_FLAG_CASE);
return implode(', ', array_unique($values));
}
private function getParticipants($event)
{
$values = array();
if (!isset($event->teilnehmer_person) || !is_array($event->teilnehmer_person))
return '';
foreach ($event->teilnehmer_person as $participant)
{
$value = $this->getNestedValue($participant, array('kurzbz', 'student_uid'));
if ($value !== '')
$values[] = $value;
}
sort($values, SORT_STRING | SORT_FLAG_CASE);
return implode(', ', array_unique($values));
}
private function getGroups($event)
{
$values = array();
foreach (array('teilnehmer_gruppe', 'gruppe') as $property)
{
if (!isset($event->{$property}) || !is_array($event->{$property}))
continue;
foreach ($event->{$property} as $group)
{
$value = $this->getNestedValue(
$group,
array('bezeichnung', 'kuerzel', 'gruppe_kurzbz')
);
if ($value !== '')
$values[] = $value;
}
}
sort($values, SORT_STRING | SORT_FLAG_CASE);
return implode(', ', array_unique($values));
}
private function getEventNote($event, $isReservation)
{
$note = $this->getScalar($event, 'beschreibung');
if ($note === '')
$note = $this->getScalar($event, 'anmerkung');
if ($note === '' && !$isReservation)
$note = $this->getScalar($event, 'titel');
return $note;
}
private function getColor($event)
{
$color = ltrim($this->getScalar($event, 'farbe'), '#');
return preg_match('/^[0-9a-fA-F]{6}$/', $color) ? strtoupper($color) : '';
}
private function getNestedValue($value, array $properties)
{
foreach ($properties as $property)
{
if (is_array($value) && isset($value[$property]))
return (string)$value[$property];
if (is_object($value) && isset($value->{$property}))
return (string)$value->{$property};
}
return is_scalar($value) ? (string)$value : '';
}
private function getScalar($event, $property)
{
return isset($event->{$property}) && is_scalar($event->{$property})
? (string)$event->{$property}
: '';
}
private function getFirstScalarValue($value)
{
if (is_scalar($value))
return (string)$value;
if (!is_array($value))
return '';
foreach ($value as $item)
{
if (is_scalar($item) && (string)$item !== '')
return (string)$item;
}
return '';
}
private function joinValue($value, $separator = ', ')
{
if (!is_array($value))
return is_scalar($value) ? (string)$value : '';
$values = array();
foreach ($value as $item)
{
if (is_scalar($item) && (string)$item !== '')
$values[] = (string)$item;
}
return implode($separator, array_unique($values));
}
private function timeToSeconds($time)
{
if (!preg_match('/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/', (string)$time, $matches))
return null;
$hours = (int)$matches[1];
$minutes = (int)$matches[2];
$seconds = isset($matches[3]) ? (int)$matches[3] : 0;
if ($hours > 23 || $minutes > 59 || $seconds > 59)
return null;
return $hours * 3600 + $minutes * 60 + $seconds;
}
private function formatLocalDate($timestamp)
{
$date = new DateTime('@'.(int)$timestamp);
$date->setTimezone(new DateTimeZone(self::TIMEZONE));
return $date->format('Y-m-d');
}
private function escape($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
private function encodeJavaScriptString($value)
{
return json_encode(
(string)$value,
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
);
}
private function renderLines($value)
{
$lines = array_map('trim', explode(', ', (string)$value));
$lines = array_filter($lines, function ($line) {
return $line !== '';
});
$lines = array_map(array($this, 'escape'), $lines);
return implode('<br>', $lines);
}
private function getAppRoot()
{
return defined('APP_ROOT') ? rtrim(APP_ROOT, '/').'/' : '/';
}
private function shouldShowNotes()
{
return !defined('LVPLAN_ANMERKUNG_ANZEIGEN') || LVPLAN_ANMERKUNG_ANZEIGEN;
}
private function getStyles()
{
return '@page{size:landscape;margin:10mm}'
.'.outside{margin-top:10px}.outside>div{display:inline-block;vertical-align:top;min-width:180px;margin:0 3px 3px 0;padding:3px}'
.'@media print{.outside>div{break-inside:avoid}}';
}
private function getOldTempusNoticeTag($event)
{
if (empty($event->tags)) return null;
$parsedTags = $event->tags;
if (!is_array($event->tags))
$parsedTags = json_decode($event->tags, true);
foreach ($parsedTags as $tag)
{
if (isset($tag['typ_kurzbz']) && $tag['typ_kurzbz'] === 'hinweis' && isset($tag['insertvon']) && $tag['insertvon'] === 'oldToNewTempusMigration')
return $tag;
}
return null;
}
}
+449
View File
@@ -0,0 +1,449 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class LvPlanICALLib
{
const CATEGORY_TIMETABLE = 'Stundenplan';
const CATEGORY_EXAM = 'StundenplanExam';
const CATEGORY_REMOTE = 'StundenplanRemote';
const ICON_EXAM = '📝';
const ICON_REMOTE = '📍';
const ICON_RESERVATION = '📌';
private $ci;
public function __construct()
{
$this->ci =& get_instance();
}
/**
* Builds complete iCalendar content for a user.
*
* If no type is given, the user's role is inferred for subscription feeds.
* Downloads pass the type explicitly so dual-role users receive the selected
* timetable.
*
* @param string $uid User UID.
* @param int $begin First included date as Unix timestamp.
* @param int $ende Last included date as Unix timestamp.
* @param string|null $type Timetable type (student, lektor, ort or verband).
* @param int $version iCalendar major version (1 or 2).
* @param string|null $ortKurzbz Room identifier for room plans.
* @param array|null $verbandFilters Lehrverband filters.
* @param string|null $calendarName Display name for calendar subscriptions.
* @return string
*/
public function getContent(
$uid,
$begin,
$ende,
$type = null,
$version = 2,
$ortKurzbz = null,
$verbandFilters = null,
$calendarName = null
) {
if (!in_array($version, array(1, 2), true))
throw new InvalidArgumentException('Unsupported iCalendar version');
if ($type !== null && !in_array($type, array('student', 'lektor', 'ort', 'verband'), true))
throw new InvalidArgumentException('Unsupported timetable type');
if ($type === 'ort' && (!is_string($ortKurzbz) || $ortKurzbz === ''))
throw new InvalidArgumentException('Missing room timetable filter');
if ($type === 'verband'
&& (!is_array($verbandFilters)
|| !array_key_exists('stg_kz', $verbandFilters)
|| !array_key_exists('sem', $verbandFilters)))
{
throw new InvalidArgumentException('Missing Lehrverband timetable filters');
}
$this->ci->load->library('KalenderLib', array('uid' => $uid));
if ($type === null)
{
$this->ci->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
$isLecturerResult = $this->ci->MitarbeiterModel->isMitarbeiter($uid);
$isLecturer = isSuccess($isLecturerResult) && getData($isLecturerResult) === true;
}
else
{
$isLecturer = $type === 'lektor';
}
$startDate = date('Y-m-d', $begin);
// KalenderLib uses an exclusive end date; $ende is inclusive here.
$endDate = date('Y-m-d', strtotime('+1 day', $ende));
if ($type === 'verband')
{
$events = $this->ci->kalenderlib->getPlanForVerband(
$startDate,
$endDate,
$verbandFilters['stg_kz'],
$verbandFilters['sem'],
isset($verbandFilters['ver']) ? $verbandFilters['ver'] : null,
isset($verbandFilters['grp']) ? $verbandFilters['grp'] : null
);
}
elseif ($type === 'ort')
{
$events = $this->ci->kalenderlib
->getPlanForRoom($startDate, $endDate, $ortKurzbz);
}
elseif ($isLecturer)
{
$events = $this->ci->kalenderlib
->getPlanForLecturerByLecturer($startDate, $endDate, $uid);
}
else
{
$events = $this->ci->kalenderlib
->getPlanForStudentByStudent($startDate, $endDate, $uid);
}
if (!is_array($events))
$events = array();
$content = $this->buildCalendarHeader($version, $calendarName);
foreach ($events as $event)
$content .= $this->buildEvent($event);
return $content.$this->buildICalLine('END', 'VCALENDAR');
}
private function buildCalendarHeader($version, $calendarName = null)
{
$productId = defined('CAMPUS_NAME') && CAMPUS_NAME !== ''
? CAMPUS_NAME
: 'FHComplete';
$header = $this->buildICalLine('BEGIN', 'VCALENDAR')
.$this->buildICalLine('VERSION', $version.'.0')
.$this->buildICalTextLine('PRODID', $productId);
if (is_string($calendarName) && $calendarName !== '')
{
$header .= $this->buildICalTextLine('NAME', $calendarName);
$header .= $this->buildICalTextLine('X-WR-CALNAME', $calendarName);
}
return $header
.$this->buildICalLine('BEGIN', 'VTIMEZONE')
.$this->buildICalLine('TZID', 'Europe/Vienna')
.$this->buildICalLine('BEGIN', 'DAYLIGHT')
.$this->buildICalLine('TZOFFSETFROM', '+0100')
.$this->buildICalLine('RRULE', 'FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU')
.$this->buildICalLine('DTSTART', '19810329T020000')
.$this->buildICalLine('TZNAME', 'GMT+02:00')
.$this->buildICalLine('TZOFFSETTO', '+0200')
.$this->buildICalLine('END', 'DAYLIGHT')
.$this->buildICalLine('BEGIN', 'STANDARD')
.$this->buildICalLine('TZOFFSETFROM', '+0200')
.$this->buildICalLine('RRULE', 'FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU')
.$this->buildICalLine('DTSTART', '19961027T030000')
.$this->buildICalLine('TZNAME', 'GMT+01:00')
.$this->buildICalLine('TZOFFSETTO', '+0100')
.$this->buildICalLine('END', 'STANDARD')
.$this->buildICalLine('END', 'VTIMEZONE');
}
private function buildEvent($event)
{
$isReservation = $event->type === 'reservierung';
$summaryIcon = '';
$summary = $this->getSummary($event);
$description = $this->getDescription($event);
$category = self::CATEGORY_TIMETABLE;
if ($event->type === 'lehreinheit')
{
if ($event->lehrform === 'EXAM')
{
$summaryIcon = self::ICON_EXAM;
$category = self::CATEGORY_EXAM;
}
elseif (!isset($event->ko_ort_kurzbz) || $event->ko_ort_kurzbz === '')
{
$summaryIcon = self::ICON_REMOTE;
$category = self::CATEGORY_REMOTE;
}
}
elseif ($event->type === 'reservierung'
&& (!isset($event->ko_ort_kurzbz) || $event->ko_ort_kurzbz === ''))
{
$summaryIcon = self::ICON_RESERVATION;
$category = self::CATEGORY_REMOTE;
}
if ($summaryIcon !== '')
$summary = $summaryIcon.' '.$summary;
$startDate = $this->formatICalLocalDateTime($event->isostart);
$endDate = $this->formatICalLocalDateTime($event->isoend);
$lastModified = $this->formatICalUtcDateTime(
isset($event->updateamum) ? $event->updateamum : null
);
$dtStamp = $lastModified ?: $this->formatICalUtcDateTime($event->isostart);
if ($dtStamp === null)
$dtStamp = '19700101T000000Z';
$fragment = $this->buildICalLine('BEGIN', 'VEVENT')
.$this->buildICalTextLine('UID', $event->eindeutige_kalender_gruppen_id)
.$this->buildICalLine('SEQUENCE', (int)$event->kalender_id)
.$this->buildICalTextLine('SUMMARY', $summary)
.$this->buildICalTextLine('DESCRIPTION', $description)
.$this->buildICalTextLine('LOCATION', $this->joinValues($event->ort_kurzbz))
.$this->buildICalTextLine('CATEGORIES', $category)
.$this->buildICalLine('DTSTART', $startDate, array('TZID' => 'Europe/Vienna'))
.$this->buildICalLine('DTEND', $endDate, array('TZID' => 'Europe/Vienna'));
if ($lastModified !== null)
$fragment .= $this->buildICalLine('LAST-MODIFIED', $lastModified);
return $fragment
.$this->buildICalLine('DTSTAMP', $dtStamp)
.$this->buildICalLine('END', 'VEVENT');
}
/**
* @param object $event Calendar event.
* @return string Event summary.
*/
private function getSummary($event)
{
if (isset($event->type) && $event->type === 'reservierung')
{
return isset($event->titel) ? $event->titel : '';
}
$subject = isset($event->lehrfach_bez) ? $event->lehrfach_bez : '';
if ($subject === '')
$subject = $this->joinValues(isset($event->topic) ? $event->topic : array());
return $subject;
}
/**
* @param object $event Calendar event.
* @return string Event description.
*/
private function getDescription($event)
{
$isReservation = isset($event->type) && $event->type === 'reservierung';
$parts = array();
$subject = $isReservation && isset($event->lehrfach_bez)
? $event->lehrfach_bez
: '';
if ($subject !== '')
$parts[] = $this->ci->p->t('global', 'lehrfach').': '.$subject;
if ($isReservation)
{
$participants = $this->getParticipants($event);
if ($participants !== '')
$parts[] = $this->ci->p->t('ui', 'teilnehmende').': '.$participants;
}
else
{
$lecturers = $this->getLecturers($event);
if ($lecturers !== '')
$parts[] = $this->ci->p->t('lehre', 'lektorInnen').': '.$lecturers;
}
$groups = $this->getGroups($event);
if ($groups !== '')
$parts[] = $this->ci->p->t('global', 'gruppen').': '.$groups;
$description = isset($event->beschreibung) ? $event->beschreibung : '';
if ($description !== '')
$parts[] = $this->ci->p->t('global', 'beschreibung').': '.$description;
return implode("\r\n", $parts);
}
private function joinValues($value)
{
return is_array($value) ? implode(', ', $value) : (string)$value;
}
private function buildICalTextLine($name, $value, array $parameters = array())
{
return $this->buildICalLine($name, $this->escapeICalText($value), $parameters);
}
private function buildICalLine($name, $value, array $parameters = array())
{
$line = strtoupper($name);
foreach ($parameters as $parameterName => $parameterValue)
{
$parameterValues = is_array($parameterValue) ? $parameterValue : array($parameterValue);
$escapedValues = array();
foreach ($parameterValues as $singleParameterValue)
$escapedValues[] = $this->escapeICalParameterValue($singleParameterValue);
$line .= ';'.strtoupper($parameterName).'='.implode(',', $escapedValues);
}
return $this->foldICalLine($line.':'.(string)$value);
}
private function foldICalLine($line)
{
$line = (string)$line;
$folded = '';
while (strlen($line) > 75)
{
$part = mb_strcut($line, 0, 75, 'UTF-8');
$folded .= $part."\r\n";
$line = ' '.mb_strcut($line, strlen($part), strlen($line) - strlen($part), 'UTF-8');
}
return $folded.$line."\r\n";
}
private function escapeICalText($value)
{
$value = (string)$value;
$value = str_replace('\\', '\\\\', $value);
$value = str_replace(array("\r\n", "\r", "\n"), '\\n', $value);
return str_replace(array(';', ','), array('\\;', '\\,'), $value);
}
private function escapeICalParameterValue($value)
{
$value = (string)$value;
$value = str_replace(array('\\', '"', "\r", "\n"), array('\\\\', '\\"', '', ''), $value);
if (preg_match('/[;:,]/', $value))
return '"'.$value.'"';
return $value;
}
private function formatICalLocalDateTime($dateTime)
{
$dateTime = $this->createDateTime($dateTime);
if ($dateTime === null)
return null;
$dateTime->setTimezone(new DateTimeZone('Europe/Vienna'));
return $dateTime->format('Ymd\THis');
}
private function formatICalUtcDateTime($dateTime)
{
$dateTime = $this->createDateTime($dateTime);
if ($dateTime === null)
return null;
$dateTime->setTimezone(new DateTimeZone('UTC'));
return $dateTime->format('Ymd\THis\Z');
}
private function createDateTime($dateTime)
{
if ($dateTime instanceof DateTime)
return clone $dateTime;
if ($dateTime === null || $dateTime === '')
return null;
try
{
if (is_numeric($dateTime))
return new DateTime('@'.(int)$dateTime);
return new DateTime($dateTime);
}
catch (Exception $e)
{
return null;
}
}
/**
* @param object $event Calendar event.
* @return string Participant group labels.
*/
private function getGroups($event)
{
$groups = array();
if (isset($event->gruppe) && is_array($event->gruppe))
{
foreach ($event->gruppe as $group)
{
if (isset($group['bezeichnung']) && $group['bezeichnung'] !== '')
$groups[] = $group['bezeichnung'];
}
}
if (isset($event->teilnehmer_gruppe) && is_array($event->teilnehmer_gruppe))
{
foreach ($event->teilnehmer_gruppe as $group)
{
if (isset($group['gruppe_kurzbz']) && $group['gruppe_kurzbz'] !== '')
$groups[] = $group['gruppe_kurzbz'];
}
}
sort($groups, SORT_NATURAL | SORT_FLAG_CASE);
return implode(',', array_unique($groups));
}
/**
* @param object $event Calendar event.
* @return string Lecturer abbreviations.
*/
private function getLecturers($event)
{
$lecturers = array();
if (isset($event->lektor) && is_array($event->lektor))
{
foreach ($event->lektor as $lecturer)
{
if (isset($lecturer['kurzbz']) && $lecturer['kurzbz'] !== '')
$lecturers[] = $lecturer['kurzbz'];
}
}
sort($lecturers, SORT_NATURAL | SORT_FLAG_CASE);
return implode(' / ', array_unique($lecturers));
}
private function getParticipants($event)
{
$values = array();
if (!isset($event->teilnehmer_person) || !is_array($event->teilnehmer_person))
return '';
foreach ($event->teilnehmer_person as $participant)
{
$value = $participant['vorname'] . ' ' . $participant['nachname'];
if ($value !== '')
$values[] = $value;
}
sort($values, SORT_NATURAL | SORT_FLAG_CASE);
return implode(', ', array_unique($values));
}
}
+283
View File
@@ -0,0 +1,283 @@
<?php
if (! defined("BASEPATH")) exit("No direct script access allowed");
use CI3_Events as Events;
class RaumvorschlagLib
{
private $_ci;
private $_uid;
public function __construct($params)
{
$this->_ci =& get_instance();
if (isset($params['uid']) && !isEmptyString($params['uid'])){
$this->_uid = $params['uid'];
} else {
show_error('uid of logged user not passed!');
}
$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('education/Lehreinheitgruppe_model', 'LehreinheitgruppeModel');
$this->_ci->load->model('education/Lehreinheitgruppe_model', 'LehreinheitgruppeModel');
$this->_ci->load->library('CollisionChecker', ["uid" => getAuthUID()]);
$this->_ci->load->library('KalenderLib', ["uid" => getAuthUID()]);
$this->_ci->load->library('PhrasesLib', array('ui'));
}
public function getVorschlaege($kalender_id)
{
$event = $this->_ci->kalenderlib->getByKalenderId($kalender_id);
$event = $event[0];
$lektor_uids = array_column($event->lektor, 'mitarbeiter_uid');
$gruppen_kurzbz = array_values(array_filter(array_column($event->gruppe, 'gruppe_kurzbz')));
$lehrverband_gruppen = array_values(array_filter($event->gruppe, function($gruppe)
{
return empty($gruppe['gruppe_kurzbz']);
}));
$tages_events = $this->_ci->kalenderlib->getForRaumvorschlag(
$event->datum,
$event->datum,
$lektor_uids,
$gruppen_kurzbz,
$lehrverband_gruppen
);
$lektor_davor = $this->_getEventDavor($tages_events, $event->isostart, $lektor_uids, 'lektor');
$gruppen_davor = $this->_getEventDavor($tages_events, $event->isostart, $gruppen_kurzbz, 'gruppe');
$lektor_davor_ort = $lektor_davor ? $this->_getOrtDetails($lektor_davor->ort_kurzbz) : null;
$gruppen_davor_ort = $gruppen_davor ? $this->_getOrtDetails($gruppen_davor->ort_kurzbz) : null;
$kandidaten = $this->_getRaumkandidaten($event);
if (empty($kandidaten)) return [];
$ratings = [];
foreach ($kandidaten as $raum)
{
$rating = ['ort_kurzbz' => $raum->ort_kurzbz, 'score' => 100, 'details' => []];
$this->_rateLektor($rating, $raum, $lektor_davor_ort);
$this->_rateGruppen($rating, $raum, $gruppen_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;
if ($type === 'lektor')
$event_uids = array_column($event->lektor, 'mitarbeiter_uid');
else
$event_uids = array_column($event->gruppe, 'gruppe_kurzbz');
if (empty(array_intersect($event_uids, $uids)))
continue;
if ($kandidat === null || $event->isoend > $kandidat->isoend)
$kandidat = $event;
}
return $kandidat;
}
private function _getRaumkandidaten($event)
{
$lehreinheit = $this->_ci->LehreinheitModel->load($event->lehreinheit_id[0]);
if (!hasData($lehreinheit)) return [];
$lehreinheit = getData($lehreinheit)[0];
$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);
$this->_ci->KalenderModel->db->where_not_in('tbl_kalender.status_kurzbz', ['deleted']);
$this->_ci->KalenderModel->db->where('tbl_kalender_ort.ort_kurzbz IS NOT NULL', null, false);
$belegte = $this->_ci->KalenderModel->load();
$belegte_orte = hasData($belegte) ? array_column(getData($belegte), 'ort_kurzbz') : [];
if (empty($lehreinheit->raumtyp))
{
$raeume = $this->_getFreieRaeume(null, $belegte_orte);
return hasData($raeume) ? getData($raeume) : [];
}
$vorschlaege = [];
$raeume = $this->_getFreieRaeume($lehreinheit->raumtyp, $belegte_orte);
if (hasData($raeume))
$vorschlaege = getData($raeume);
if (count($vorschlaege) < 5 && !empty($lehreinheit->raumtypalternativ))
{
$bereits_gefunden = array_merge($belegte_orte, array_column($vorschlaege, 'ort_kurzbz'));
$alternativ = $this->_getFreieRaeume($lehreinheit->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');
$this->_ci->OrtModel->addJoin('public.tbl_ortraumtyp', 'ort_kurzbz');
$this->_ci->OrtModel->db->where('raumtyp_kurzbz', $raumtyp);
$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();
}
}
+4 -4
View File
@@ -14,9 +14,9 @@ class VariableLib
/** /**
* VariableLib constructor. * VariableLib constructor.
* Loads variable of logged in user. * Loads variable of logged in user.
* @param $loggeduid * @param $params
*/ */
public function __construct($loggeduid) public function __construct($params)
{ {
$this->_ci =& get_instance(); $this->_ci =& get_instance();
@@ -25,8 +25,8 @@ class VariableLib
$this->_ci->load->model('system/Variable_model', 'VariableModel'); $this->_ci->load->model('system/Variable_model', 'VariableModel');
$this->_ci->load->model('organisation/studiensemester_model', 'StudiensemesterModel'); $this->_ci->load->model('organisation/studiensemester_model', 'StudiensemesterModel');
if (isset($loggeduid['uid']) && !isEmptyString($loggeduid['uid'])) if (isset($params['uid']) && !isEmptyString($params['uid']))
$this->_setVariables($loggeduid['uid']); $this->_setVariables($params['uid']);
else else
{ {
show_error('uid of logged user not passed!'); show_error('uid of logged user not passed!');
@@ -0,0 +1,266 @@
<?php
class LectureCollisionCheck implements ICollisionCheck
{
private $_ci;
private $_uid;
public function __construct($params)
{
$this->_ci =& get_instance();
if (isset($params['uid']) && !isEmptyString($params['uid'])){
$this->_uid = $params['uid'];
} else {
show_error('uid of logged user not passed!');
}
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->model('ressource/zeitsperre_model', 'ZeitsperreModel');
$this->_ci->load->library('VariableLib', ['uid' => $this->_uid]);
$this->_ci->load->library('PhrasesLib', array('ui'));
}
public function getName()
{
return 'lecture';
}
public function check($data)
{
if (!isset($data->von, $data->bis, $data->kalender_id)) return [];
if ($this->_ci->variablelib->getVar('ignore_kollision') === 'true') return [];
$uids = $this->_getUids($data->kalender_id);
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', ['archived', 'deleted', 'to_delete']);
$this->_ci->KalenderModel->db->where_not_in('current_lehreinheit_ma.mitarbeiter_uid', $kollisionsfreie_user);
$this->_ci->KalenderModel->db->where_in('tbl_kalender.kalender_id', $kalender_ids);
$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']);
$this->_ci->KalenderModel->db->where_not_in('current_lehreinheit_ma.mitarbeiter_uid', $kollisionsfreie_user);
$this->_ci->KalenderModel->db->where_in('tbl_kalender.kalender_id', $kalender_ids);
$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->where_in('tbl_kalender.kalender_id', $kalender_ids);
$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 _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);
$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'));
$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);
$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'));
$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,93 @@
<?php
class ResourcesCollisionCheck implements ICollisionCheck
{
private $_ci;
private $_uid;
public function __construct($params)
{
$this->_ci =& get_instance();
if (isset($params['uid']) && !isEmptyString($params['uid'])){
$this->_uid = $params['uid'];
} else {
show_error('uid of logged user not passed!');
}
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->library('VariableLib', array('uid' => $this->_uid));
$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, $data->kalender_id)) 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');
$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']);
$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']);
$this->_ci->KalenderModel->db->where_in('tbl_kalender.kalender_id', $kalender_ids);
$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,89 @@
<?php
class RoomCollisionCheck implements ICollisionCheck
{
private $_ci;
private $_uid;
public function __construct($params)
{
$this->_ci =& get_instance();
if (isset($params['uid']) && !isEmptyString($params['uid'])){
$this->_uid = $params['uid'];
} else {
show_error('uid of logged user not passed!');
}
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->library('VariableLib', array('uid' => $this->_uid));
$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, $data->kalender_id) || 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');
$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']);
$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', ['archived', 'deleted', 'to_delete']);
$this->_ci->KalenderModel->db->where_in('tbl_kalender.kalender_id', $kalender_ids);
$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,255 @@
<?php
class StudentCollisionCheck implements ICollisionCheck
{
private $_ci;
private $_uid;
public function __construct($params)
{
$this->_ci =& get_instance();
if (isset($params['uid']) && !isEmptyString($params['uid'])){
$this->_uid = $params['uid'];
} else {
show_error('uid of logged user not passed!');
}
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->library('VariableLib', array('uid' => $this->_uid));
$this->_ci->load->library('PhrasesLib', array('ui'));
}
public function getName()
{
return 'student';
}
public function check($data)
{
if (!isset($data->von, $data->bis, $data->kalender_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();
$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)
";
$result1 = $dbModel->execReadOnlyQuery($qry1, array_merge(
[$data->kalender_id],
$kollisionsfreie_user,
[$data->kalender_id],
$kollisionsfreie_user
));
if (isError($result1) || !hasData($result1)) return [];
$curUids = array_flip(array_column(getData($result1), 'uid'));
$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 > ?
AND tbl_kalender.kalender_id != ?
AND tbl_kalender.status_kurzbz NOT IN ('archived', 'deleted', 'to_delete')
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 > ?
AND tbl_kalender.kalender_id != ?
AND tbl_kalender.status_kurzbz NOT IN ('archived', 'deleted', 'to_delete')
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, $data->kalender_id],
$kollisionsfreie_user,
[$data->bis, $data->von, $data->kalender_id],
$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')
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')
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,370 @@
<?php
class VerbandCollisionCheck implements ICollisionCheck
{
private $_ci;
private $_uid;
public function __construct($params)
{
$this->_ci =& get_instance();
if (isset($params['uid']) && !isEmptyString($params['uid'])){
$this->_uid = $params['uid'];
} else {
show_error('uid of logged user not passed!');
}
$this->_ci->load->model('ressource/Kalender_model', 'KalenderModel');
$this->_ci->load->library('VariableLib', array('uid' => $this->_uid));
$this->_ci->load->library('PhrasesLib', array('ui'));
}
public function getName()
{
return 'verband';
}
public function check($data)
{
if (!isset($data->von, $data->bis, $data->kalender_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 (!$kollision_student && !$kollision_reservierung) return [];
$dbModel = new DB_Model();
$collisions = [];
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'
";
}
$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 (
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 ."
) 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 (
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 ."
) 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')
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_gruppen, [
$data->bis,
$data->von,
$data->kalender_id,
]);
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')
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')
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,38 @@
<?php
class SabreDAVAuthLib extends \Sabre\DAV\Auth\Backend\AbstractBasic
{
private $_ci;
protected $currentUser = null;
public function __construct()
{
$this->_ci =& get_instance();
$this->_ci->load->library('AuthLib', array(false));
$this->_ci->load->helper('hlp_authentication');
}
public function getCurrentUser()
{
if ($this->currentUser)
return $this->currentUser;
if (isLogged())
return getAuthUID();
return null;
}
protected function validateUserPass($username, $password)
{
if (isSuccess($this->_ci->authlib->checkUserAuthByUsernamePassword($username, $password)))
{
$this->currentUser = $username;
return true;
}
$this->currentUser = null;
return false;
}
}
@@ -0,0 +1,11 @@
<?php
require_once __DIR__.'/caldav/SabreDAVACLPrincipalBackend.php';
require_once __DIR__.'/caldav/SabreDAVCalDAVBackend.php';
/**
* Loads the SabreDAV CalDAV classes for the LVPlan calendar endpoint.
*/
class SabreDAVCalDAVLib
{
}
@@ -0,0 +1,179 @@
<?php
/**
* PDO principal backend
*
* This is a simple principal backend that maps exactly to the users table, as
* used by Sabre_DAV_Auth_Backend_PDO.
*
* It assumes all principals are in a single collection. The default collection
* is 'principals/', but this can be overriden.
*
* @package Sabre
* @subpackage DAVACL
* @copyright Copyright (C) 2007-2011 Rooftop Solutions. All rights reserved.
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class SabreDAVACLPrincipalBackend implements \Sabre\DAVACL\PrincipalBackend\BackendInterface
{
/**
* PDO table name for 'principals'
*
* @var string
*/
protected $tableName;
/**
* PDO table name for 'group members'
*
* @var string
*/
protected $groupMembersTableName;
protected $result_ma;
protected $auth;
/**
* Sets up the backend.
*
* @param PDO $pdo
* @param string $tableName
*/
public function __construct($auth)
{
$this->auth = $auth;
/*
$ma = new mitarbeiter();
$this->result_ma = $ma->getMitarbeiter(null,null,null);
*/
}
/**
* Liefert den eingeloggten User
*/
function getUser()
{
return $this->auth->getCurrentUser();
}
/**
* Returns a list of principals based on a prefix.
*
* This prefix will often contain something like 'principals'. You are only
* expected to return principals that are in this base path.
*
* You are expected to return at least a 'uri' for every user, you can
* return any additional properties if you wish so. Common properties are:
* {DAV:}displayname
* {http://sabredav.org/ns}email-address - This is a custom SabreDAV
* field that's actualy injected in a number of other properties. If
* you have an email address, use this property.
*
* @param string $prefixPath
* @return array
*/
public function getPrincipalsByPrefix($prefixPath)
{
//$prefixPath = principals
//error_log('Principal.php/getPrincipalsByPrefix('.$prefixPath.')');
$principals = array();
$user = $this->getUser();
if($prefixPath=='principals')
{
$principals[] = array(
'id' => $user,
'uri' => 'principals/'.$user,
'{DAV:}displayname' => $user,
'{http://sabredav.org/ns}email-address' => $user.'@example.com',
);
}
return $principals;
}
/**
* Returns a specific principal, specified by it's path.
* The returned structure should be the exact same as from
* getPrincipalsByPrefix.
*
* @param string $path
* @return array
*/
public function getPrincipalByPath($path)
{
$user = mb_substr($path,11);
$result = array(
'id' => $user,
'uri' => 'principals/'.$user,
'{DAV:}displayname' => $user,
'{http://sabredav.org/ns}email-address' => $user.'@example.com',
);
return $result;
}
/**
* Returns the list of members for a group-principal
*
* @param string $principal
* @return array
*/
public function getGroupMemberSet($principal)
{
$result = array();
return $result;
}
/**
* Returns the list of groups a principal is a member of
*
* @param string $principal
* @return array
*/
public function getGroupMembership($principal)
{
$result = array();
if(preg_match('/^principals\/[0-9A-Za-z\-]*$/',$principal))
{
$user = mb_substr($principal,11);
}
return $result;
}
/**
* Updates the list of group members for a group principal.
*
* The principals should be passed as a list of uri's.
*
* @param string $principal
* @param array $members
* @return void
*/
public function setGroupMemberSet($principal, array $members)
{
throw new \Sabre\DAV\Exception('Not implemented');
}
public function updatePrincipal($path, \Sabre\DAV\PropPatch $propPatch)
{
$propPatch->setRemainingResultCode(403);
}
public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
{
return array();
}
public function findByUri($uri, $principalPrefix)
{
return null;
}
}
@@ -0,0 +1,574 @@
<?php
/**
* CalDAV backend
*/
class SabreDAVCalDAVBackend extends \Sabre\CalDAV\Backend\AbstractBackend
{
protected $auth;
protected $CI;
CONST CALENDAR_NAME = 'LVPlan';
CONST CAL_CATEGORY_STUNDENPLAN = 'Stundenplan';
CONST CAL_CATEGORY_STUNDENPLAN_EXAM = 'StundenplanExam';
CONST CAL_CATEGORY_STUNDENPLAN_REMOTE = 'StundenplanRemote';
CONST CAL_CATEGORY_STUNDENPLAN_EXAM_ICON = '📝';
CONST CAL_CATEGORY_STUNDENPLAN_REMOTE_ICON = '📍';
CONST CAL_CATEGORY_RESERVATION_ICON = '📌';
/**
* Creates the backend
*
* @param AuthBackend $auth
*/
public function __construct($auth)
{
$this->auth = $auth;
$this->CI =& get_instance();
}
/**
* Liefert den eingeloggten User
*/
function getUser()
{
return $this->auth->getCurrentUser();
}
/**
* Returns a list of calendars for a principal.
*
* Every project is an array with the following keys:
* * id, a unique id that will be used by other functions to modify the
* calendar. This can be the same as the uri or a database key.
* * uri, which the basename of the uri with which the calendar is
* accessed.
* * principalUri. The owner of the calendar. Almost always the same as
* principalUri passed to this method.
*
* Furthermore it can contain webdav properties in clark notation. A very
* common one is '{DAV:}displayname'.
*
* @param string $principalUri
* @return array
*/
public function getCalendarsForUser($principalUri)
{
$user = mb_substr($principalUri,11);
$calendars = array();
$calendar = array(
'id' => $user,
'uri' => self::CALENDAR_NAME.'-'.$user,
'principaluri' => 'principals/'.$user,
'{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => $this->buildCalendarCtag($user),
'{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet(array('VEVENT')),
'{DAV:}displayname' => self::CALENDAR_NAME,
'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description comes here',
'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'Europe/Vienna',
'{http://apple.com/ns/ical/}calendar-order' => '1',
'{http://apple.com/ns/ical/}calendar-color' => '#FF0000',
'{http://sabredav.org/ns}read-only' => 1
);
$calendars[] = $calendar;
return $calendars;
}
/**
* Creates a new calendar for a principal.
*
* If the creation was a success, an id must be returned that can be used to reference
* this calendar in other methods, such as updateCalendar
*
* @param string $principalUri
* @param string $calendarUri
* @param array $properties
*/
public function createCalendar($principalUri,$calendarUri, array $properties)
{
return $calendarUri;
}
/**
* Updates properties for a calendar.
*
* The mutations array uses the propertyName in clark-notation as key,
* and the array value for the property value. In the case a property
* should be deleted, the property value will be null.
*
* This method must be atomic. If one property cannot be changed, the
* entire operation must fail.
*
* If the operation was successful, true can be returned.
* If the operation failed, false can be returned.
*
* Deletion of a non-existant property is always succesful.
*
* Lastly, it is optional to return detailed information about any
* failures. In this case an array should be returned with the following
* structure:
*
* array(
* 403 => array(
* '{DAV:}displayname' => null,
* ),
* 424 => array(
* '{DAV:}owner' => null,
* )
* )
*
* In this example it was forbidden to update {DAV:}displayname.
* (403 Forbidden), which in turn also caused {DAV:}owner to fail
* (424 Failed Dependency) because the request needs to be atomic.
*
* @param string $calendarId
* @param array $mutations
* @return bool|array
*/
public function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch)
{
$propPatch->setRemainingResultCode(200);
}
/**
* Delete a calendar and all it's objects
*
* @param string $calendarId
* @return void
*/
public function deleteCalendar($calendarId)
{
return null;
}
public function getCalendarData($userUID, $objectUri=null)
{
$this->CI->load->model("ressource/Mitarbeiter_model", "MitarbeiterModel");
$this->CI->load->model("person/Benutzer_model", "BenutzerModel");
$user = $this->CI->BenutzerModel->loadWhere(array("uid" => $userUID));
if(!$user)
die('User invalid');
$this->CI->load->library('KalenderLib', ['uid' => $userUID]);
$isUserEmployeeResult = $this->CI->MitarbeiterModel->isMitarbeiter($userUID);
if (isError($isUserEmployeeResult)) {
return;
}
$isUserEmployee = getData($isUserEmployeeResult);
$startDate = date('Y-m-d', mktime(0,0,0,date('m'),date('d')-14,date('Y')));
$endDate = date('Y-m-d', mktime(0,0,0,date('m')+6,date('d'),date('Y')));
$data = [];
if ($isUserEmployee) {
$data = $this->CI->kalenderlib->getPlanForLecturerByLecturer($startDate, $endDate, $userUID);
} else {
$data = $this->CI->kalenderlib->getPlanForStudentByStudent($startDate, $endDate, $userUID);
}
if(!is_array($data))
$data = array();
foreach($data as $item)
{
$item->calendarDataFragment = $this->buildCalendarDataFragment($item);
}
if (is_null($objectUri))
{
return $data;
}
$normalizedObjectUri = $this->normalizeObjectUri($objectUri);
foreach($data as $row)
{
if(md5($row->eindeutige_kalender_gruppen_id) == $normalizedObjectUri)
{
return $row;
}
}
return null;
}
public function makeCal($event)
{
return $this->buildICalLine('BEGIN', 'VCALENDAR')
.$this->buildICalLine('VERSION', '2.0')
.$this->buildICalTextLine('PRODID', 'FH Technikum Wien')
.$this->buildICalLine('BEGIN', 'VTIMEZONE')
.$this->buildICalLine('TZID', 'Europe/Vienna')
.$this->buildICalLine('BEGIN', 'DAYLIGHT')
.$this->buildICalLine('TZOFFSETFROM', '+0100')
.$this->buildICalLine('RRULE', 'FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU')
.$this->buildICalLine('DTSTART', '19810329T020000')
.$this->buildICalLine('TZNAME', 'GMT+02:00')
.$this->buildICalLine('TZOFFSETTO', '+0200')
.$this->buildICalLine('END', 'DAYLIGHT')
.$this->buildICalLine('BEGIN', 'STANDARD')
.$this->buildICalLine('TZOFFSETFROM', '+0200')
.$this->buildICalLine('RRULE', 'FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU')
.$this->buildICalLine('DTSTART', '19961027T030000')
.$this->buildICalLine('TZNAME', 'GMT+01:00')
.$this->buildICalLine('TZOFFSETTO', '+0100')
.$this->buildICalLine('END', 'STANDARD')
.$this->buildICalLine('END', 'VTIMEZONE')
.$event
.$this->buildICalLine('END', 'VCALENDAR');
}
protected function buildCalendarDataFragment($item)
{
$summaryIcon = '';
$summary = $item->type == 'reservierung' ? $item->titel : $item->topic;
$description = "";
if ($item->type == 'reservierung') {
$description = $item->beschreibung;
} else {
$description = $item->lehrfach_bez . "\r\n";
if (isset($item->lektor) && is_array($item->lektor) && count($item->lektor) > 0) {
$description .= "Lektor*in: " . join(", ", array_map(function($teacher) { return $teacher["kurzbz"]; }, $item->lektor)) . "\r\n";
}
}
$category = self::CAL_CATEGORY_STUNDENPLAN;
if ($item->type === 'lehreinheit') {
if ($item->lehrform === 'EXAM') {
$summaryIcon = self::CAL_CATEGORY_STUNDENPLAN_EXAM_ICON;
$category = self::CAL_CATEGORY_STUNDENPLAN_EXAM;
} else if (!isset($item->ko_ort_kurzbz) || $item->ko_ort_kurzbz === '') {
$summaryIcon = self::CAL_CATEGORY_STUNDENPLAN_REMOTE_ICON;
$category = self::CAL_CATEGORY_STUNDENPLAN_REMOTE;
}
} else if ($item->type === 'reservierung' && (!isset($item->ko_ort_kurzbz) || $item->ko_ort_kurzbz === '')) {
$summaryIcon = self::CAL_CATEGORY_RESERVATION_ICON;
$category = self::CAL_CATEGORY_STUNDENPLAN_REMOTE;
}
if ($summaryIcon !== '') {
$summary = $summaryIcon . ' ' . $summary;
}
$parsedStartDate = $this->formatICalLocalDateTime($item->isostart);
$parsedEndDate = $this->formatICalLocalDateTime($item->isoend);
$lastModified = $this->formatICalUtcDateTime(isset($item->updateamum) ? $item->updateamum : null);
$dtStamp = $lastModified ?: $this->formatICalUtcDateTime($item->isostart);
if($dtStamp === null)
$dtStamp = '19700101T000000Z';
$fragment = $this->buildICalLine('BEGIN', 'VEVENT')
.$this->buildICalTextLine('UID', $item->eindeutige_kalender_gruppen_id)
.$this->buildICalLine('SEQUENCE', (int)$item->kalender_id)
.$this->buildICalTextLine('SUMMARY', $summary)
.$this->buildICalTextLine('DESCRIPTION', $description)
.$this->buildICalTextLine('LOCATION', isset($item->ort_kurzbz) ? $item->ort_kurzbz : '')
.$this->buildICalTextLine('CATEGORIES', $category)
.$this->buildICalLine('DTSTART', $parsedStartDate, array('TZID' => 'Europe/Vienna'))
.$this->buildICalLine('DTEND', $parsedEndDate, array('TZID' => 'Europe/Vienna'));
if($lastModified !== null)
$fragment .= $this->buildICalLine('LAST-MODIFIED', $lastModified);
return $fragment
.$this->buildICalLine('DTSTAMP', $dtStamp)
.$this->buildICalLine('END', 'VEVENT');
}
protected function buildCalendarCtag($userUID)
{
$data = $this->getCalendarData($userUID);
if(!is_array($data))
$data = array();
$fragments = array();
foreach($data as $row)
{
$lastModified = isset($row->updateamum) ? $this->getLastModifiedTimestamp($row->updateamum) : null;
$calendarData = isset($row->calendarDataFragment) ? $this->makeCal($row->calendarDataFragment) : '';
$fragments[] = $this->getObjectUri($row).':'.$lastModified.':'.md5($calendarData);
}
sort($fragments, SORT_STRING);
return self::CALENDAR_NAME.'-'.$userUID.'-'.md5(implode('|', $fragments));
}
protected function buildICalTextLine($name, $value, array $parameters = array())
{
return $this->buildICalLine($name, $this->escapeICalText($value), $parameters);
}
protected function buildICalLine($name, $value, array $parameters = array())
{
$line = strtoupper($name);
foreach($parameters as $parameterName => $parameterValue)
{
$parameterValues = is_array($parameterValue) ? $parameterValue : array($parameterValue);
$escapedValues = array();
foreach($parameterValues as $singleParameterValue)
$escapedValues[] = $this->escapeICalParameterValue($singleParameterValue);
$line .= ';'.strtoupper($parameterName).'='.implode(',', $escapedValues);
}
return $this->foldICalLine($line.':'.(string)$value);
}
protected function foldICalLine($line)
{
$line = (string)$line;
$folded = '';
while(strlen($line) > 75)
{
$part = mb_strcut($line, 0, 75, 'UTF-8');
$folded .= $part."\r\n";
$line = ' '.mb_strcut($line, strlen($part), strlen($line) - strlen($part), 'UTF-8');
}
return $folded.$line."\r\n";
}
protected function escapeICalText($value)
{
$value = (string)$value;
$value = str_replace('\\', '\\\\', $value);
$value = str_replace(array("\r\n", "\r", "\n"), '\n', $value);
return str_replace(array(';', ','), array('\;', '\,'), $value);
}
protected function escapeICalParameterValue($value)
{
$value = (string)$value;
$value = str_replace(array('\\', '"', "\r", "\n"), array('\\\\', '\"', '', ''), $value);
if(preg_match('/[;:,]/', $value))
return '"'.$value.'"';
return $value;
}
protected function formatICalLocalDateTime($dateTime)
{
$dateTime = $this->createDateTime($dateTime);
if($dateTime === null)
return null;
$dateTime->setTimezone(new DateTimeZone('Europe/Vienna'));
return $dateTime->format('Ymd\THis');
}
protected function formatICalUtcDateTime($dateTime)
{
$dateTime = $this->createDateTime($dateTime);
if($dateTime === null)
return null;
$dateTime->setTimezone(new DateTimeZone('UTC'));
return $dateTime->format('Ymd\THis\Z');
}
protected function createDateTime($dateTime)
{
if($dateTime instanceof DateTime)
return clone $dateTime;
if($dateTime === null || $dateTime === '')
return null;
try
{
if(is_numeric($dateTime))
return new DateTime('@'.(int)$dateTime);
return new DateTime($dateTime);
}
catch(Exception $e)
{
return null;
}
}
protected function getObjectUri($row)
{
return md5($row->eindeutige_kalender_gruppen_id).'.ics';
}
protected function normalizeObjectUri($objectUri)
{
$normalizedObjectUri = (string)$objectUri;
if(substr($normalizedObjectUri, -4) === '.ics')
$normalizedObjectUri = substr($normalizedObjectUri, 0, -4);
if(mb_strpos($normalizedObjectUri, '@') !== false)
$normalizedObjectUri = mb_substr($normalizedObjectUri, mb_strpos($normalizedObjectUri, '@') + 1);
if($this->hasObjectUriDatePrefix($normalizedObjectUri))
return mb_substr($normalizedObjectUri, mb_strpos($normalizedObjectUri, '-') + 1);
return $normalizedObjectUri;
}
protected function hasObjectUriDatePrefix($objectUri)
{
$hyphenPosition = mb_strpos($objectUri, '-');
if($hyphenPosition !== 15)
return false;
return preg_match('/^\d{8}T\d{6}$/', mb_substr($objectUri, 0, 15)) === 1;
}
protected function buildCalendarObject($row, $calendarId)
{
$calendarData = $this->makeCal($row->calendarDataFragment);
return array(
"id"=>$row->eindeutige_kalender_gruppen_id,
"calendardata"=>$calendarData,
"uri"=>$this->getObjectUri($row),
"lastmodified"=>$this->getLastModifiedTimestamp($row->updateamum),
"etag"=>'"'.md5($calendarData).'"',
"calendarid"=>$calendarId,
"size"=>strlen($calendarData),
"component"=>'vevent'
);
}
protected function getLastModifiedTimestamp($lastModified)
{
if($lastModified === null || $lastModified === '')
return null;
if(is_numeric($lastModified))
return (int)$lastModified;
$timestamp = strtotime($lastModified);
if($timestamp === false)
return null;
return $timestamp;
}
/**
* Returns all calendar objects within a calendar.
*
* Every item contains an array with the following keys:
* * id - unique identifier which will be used for subsequent updates
* * calendardata - The iCalendar-compatible calnedar data
* * uri - a unique key which will be used to construct the uri. This can be any arbitrary string.
* * lastmodified - a timestamp of the last modification time
* * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
* ' "abcdef"')
* * calendarid - The calendarid as it was passed to this function.
*
* Note that the etag is optional, but it's highly encouraged to return for
* speed reasons.
*
* The calendardata is also optional. If it's not returned
* 'getCalendarObject' will be called later, which *is* expected to return
* calendardata.
*
* @param string $calendarId
* @return array
*/
public function getCalendarObjects($calendarId)
{
//$user = $this->getUser();
$user = $calendarId;
$data = $this->getCalendarData($user);
//error_log("Caldav_Backend.php/getCalendarObjects($calendarId) ");
$return = array();
if(!is_array($data))
return $return;
foreach($data as $row)
{
$return[] = $this->buildCalendarObject($row, $calendarId);
}
return $return;
}
/**
* Returns information from a single calendar object, based on it's object
* uri.
*
* The returned array must have the same keys as getCalendarObjects. The
* 'calendardata' object is required here though, while it's not required
* for getCalendarObjects.
*
* @param string $calendarId
* @param string $objectUri
* @return array
*/
public function getCalendarObject($calendarId,$objectUri)
{
$user = $calendarId;
$data = $this->getCalendarData($user,$objectUri);
if(empty($data))
{
return null;
}
elseif(is_object($data))
{
$ret = $this->buildCalendarObject($data, $calendarId);
}
return isset($ret) ? $ret : null;
}
/**
* Creates a new calendar object.
*
* @param string $calendarId
* @param string $objectUri
* @param string $calendarData
* @return void
*/
public function createCalendarObject($calendarId,$objectUri,$calendarData)
{
return null;
}
/**
* Updates an existing calendarobject, based on it's uri.
*
* @param string $calendarId
* @param string $objectUri
* @param string $calendarData
* @return void
*/
public function updateCalendarObject($calendarId,$objectUri,$calendarData)
{
if(is_resource($calendarData))
$calendarData = stream_get_contents($calendarData);
return '"'.md5((string)$calendarData).'"';
}
/**
* Deletes an existing calendar object.
*
* @param string $calendarId
* @param string $objectUri
* @return void
*/
public function deleteCalendarObject($calendarId,$objectUri)
{
return null;
}
}
@@ -0,0 +1,36 @@
<?php
class SabreDAVReadOnlyACL
{
public static function getAcl($principalUri, $includeFreeBusy = false)
{
$acl = array(
array(
'privilege' => '{DAV:}read',
'principal' => $principalUri,
'protected' => true,
),
array(
'privilege' => '{DAV:}read',
'principal' => $principalUri.'/calendar-proxy-read',
'protected' => true,
),
);
if($includeFreeBusy)
{
$acl[] = array(
'privilege' => '{'.\Sabre\CalDAV\Plugin::NS_CALDAV.'}read-free-busy',
'principal' => '{DAV:}authenticated',
'protected' => true,
);
}
return $acl;
}
public static function ignoreWrite()
{
return null;
}
}
@@ -0,0 +1,80 @@
<?php
class SabreDAVReadOnlyCalendar extends \Sabre\CalDAV\Calendar
{
protected $readOnlyCaldavBackend;
protected $readOnlyCalendarInfo;
public function __construct(\Sabre\CalDAV\Backend\BackendInterface $caldavBackend, $calendarInfo)
{
parent::__construct($caldavBackend, $calendarInfo);
$this->readOnlyCaldavBackend = $caldavBackend;
$this->readOnlyCalendarInfo = $calendarInfo;
}
public function getChild($name)
{
$obj = $this->readOnlyCaldavBackend->getCalendarObject($this->readOnlyCalendarInfo['id'], $name);
if(!$obj)
throw new \Sabre\DAV\Exception\NotFound('Calendar object not found');
$obj['acl'] = $this->getCalendarObjectAcl();
return new SabreDAVReadOnlyCalendarObject($this->readOnlyCaldavBackend, $this->readOnlyCalendarInfo, $obj);
}
public function getChildren()
{
$objs = $this->readOnlyCaldavBackend->getCalendarObjects($this->readOnlyCalendarInfo['id']);
$children = array();
foreach($objs as $obj)
{
$obj['acl'] = $this->getCalendarObjectAcl();
$children[] = new SabreDAVReadOnlyCalendarObject($this->readOnlyCaldavBackend, $this->readOnlyCalendarInfo, $obj);
}
return $children;
}
public function createFile($name, $calendarData = null)
{
return SabreDAVReadOnlyACL::ignoreWrite();
}
public function createDirectory($name)
{
return SabreDAVReadOnlyACL::ignoreWrite();
}
public function delete()
{
return SabreDAVReadOnlyACL::ignoreWrite();
}
public function setName($newName)
{
return SabreDAVReadOnlyACL::ignoreWrite();
}
public function updateProperties($mutations)
{
return true;
}
public function propPatch(\Sabre\DAV\PropPatch $propPatch)
{
$propPatch->setRemainingResultCode(200);
}
public function getACL()
{
return SabreDAVReadOnlyACL::getAcl($this->getOwner(), true);
}
protected function getCalendarObjectAcl()
{
return SabreDAVReadOnlyACL::getAcl($this->getOwner());
}
}
@@ -0,0 +1,57 @@
<?php
class SabreDAVReadOnlyCalendarHome extends \Sabre\CalDAV\CalendarHome
{
protected $readOnlyCaldavBackend;
protected $readOnlyPrincipalInfo;
public function __construct(\Sabre\CalDAV\Backend\BackendInterface $caldavBackend, $principalInfo)
{
parent::__construct($caldavBackend, $principalInfo);
$this->readOnlyCaldavBackend = $caldavBackend;
$this->readOnlyPrincipalInfo = $principalInfo;
}
public function getChildren()
{
$calendars = $this->readOnlyCaldavBackend->getCalendarsForUser($this->readOnlyPrincipalInfo['uri']);
$objs = array();
foreach($calendars as $calendar)
{
$objs[] = new SabreDAVReadOnlyCalendar($this->readOnlyCaldavBackend, $calendar);
}
return $objs;
}
public function createExtendedCollection($name, \Sabre\DAV\MkCol $mkCol)
{
return SabreDAVReadOnlyACL::ignoreWrite();
}
public function createFile($filename, $data = null)
{
return SabreDAVReadOnlyACL::ignoreWrite();
}
public function createDirectory($filename)
{
return SabreDAVReadOnlyACL::ignoreWrite();
}
public function delete()
{
return SabreDAVReadOnlyACL::ignoreWrite();
}
public function setName($name)
{
return SabreDAVReadOnlyACL::ignoreWrite();
}
public function getACL()
{
return SabreDAVReadOnlyACL::getAcl($this->readOnlyPrincipalInfo['uri']);
}
}
@@ -0,0 +1,84 @@
<?php
class SabreDAVReadOnlyCalendarObject extends \Sabre\CalDAV\CalendarObject
{
protected $readOnlyCalendarInfo;
public function __construct(\Sabre\CalDAV\Backend\BackendInterface $caldavBackend, array $calendarInfo, array $objectData)
{
parent::__construct($caldavBackend, $calendarInfo, $objectData);
$this->readOnlyCalendarInfo = $calendarInfo;
}
public function put($calendarData)
{
return $this->getCurrentETag();
}
public function getETag()
{
if($this->isWriteRequest())
{
$ifMatch = isset($_SERVER['HTTP_IF_MATCH']) ? trim($_SERVER['HTTP_IF_MATCH']) : '';
if($ifMatch !== '' && $ifMatch !== '*')
{
$requestedEtags = explode(',', $ifMatch);
foreach($requestedEtags as $requestedEtag)
{
$requestedEtag = trim($requestedEtag);
if($requestedEtag !== '')
return $requestedEtag;
}
}
}
return parent::getETag();
}
public function getLastModified()
{
if($this->isWriteRequest())
{
$ifUnmodifiedSince = isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE']) ? trim($_SERVER['HTTP_IF_UNMODIFIED_SINCE']) : '';
if($ifUnmodifiedSince !== '')
{
$timestamp = strtotime($ifUnmodifiedSince);
if($timestamp !== false)
return max(0, $timestamp - 1);
}
}
return parent::getLastModified();
}
public function delete()
{
return SabreDAVReadOnlyACL::ignoreWrite();
}
public function setName($name)
{
return SabreDAVReadOnlyACL::ignoreWrite();
}
public function getACL()
{
return SabreDAVReadOnlyACL::getAcl($this->readOnlyCalendarInfo['principaluri']);
}
protected function isWriteRequest()
{
if(!isset($_SERVER['REQUEST_METHOD']))
return false;
return in_array(strtoupper($_SERVER['REQUEST_METHOD']), array('PUT', 'DELETE', 'PROPPATCH', 'MOVE', 'COPY', 'PATCH'), true);
}
protected function getCurrentETag()
{
if(isset($this->objectData['etag']) && $this->objectData['etag'] !== '')
return $this->objectData['etag'];
return parent::getETag();
}
}
@@ -0,0 +1,17 @@
<?php
class SabreDAVReadOnlyCalendarRoot extends \Sabre\CalDAV\CalendarRoot
{
protected $readOnlyCaldavBackend;
public function __construct(\Sabre\DAVACL\PrincipalBackend\BackendInterface $principalBackend, \Sabre\CalDAV\Backend\BackendInterface $caldavBackend, $principalPrefix = 'principals')
{
parent::__construct($principalBackend, $caldavBackend, $principalPrefix);
$this->readOnlyCaldavBackend = $caldavBackend;
}
public function getChildForPrincipal(array $principal)
{
return new SabreDAVReadOnlyCalendarHome($this->readOnlyCaldavBackend, $principal);
}
}
@@ -395,6 +395,77 @@ EOSQL;
return $this->execReadOnlyQuery($qry, $params); return $this->execReadOnlyQuery($qry, $params);
} }
public function getOffeneStunden($lehreinheit_ids)
{
if (!is_array($lehreinheit_ids))
$lehreinheit_ids = [$lehreinheit_ids];
if (empty($lehreinheit_ids))
return success([]);
$db = new DB_Model();
$placeholders = implode(',', array_fill(0, count($lehreinheit_ids), '?'));
$qry = "WITH eventdauer AS (
SELECT
tbl_kalender_lehreinheit.lehreinheit_id,
tbl_kalender.von,
tbl_kalender.bis,
tbl_kalender.kalender_id
FROM lehre.tbl_lehreinheit
JOIN lehre.tbl_kalender_lehreinheit USING(lehreinheit_id)
JOIN lehre.tbl_kalender USING(kalender_id)
WHERE tbl_lehreinheit.lehreinheit_id IN ($placeholders)
GROUP BY tbl_kalender_lehreinheit.lehreinheit_id, tbl_kalender.von, tbl_kalender.bis, tbl_kalender.kalender_id
),
raster AS (
SELECT
eventdauer.lehreinheit_id,
eventdauer.kalender_id,
eventdauer.von,
eventdauer.bis,
vonstunde.stunde AS vonstunde_nr,
bisstunde.stunde AS bisstunde_nr
FROM eventdauer
LEFT JOIN lehre.tbl_stunde vonstunde ON eventdauer.von::time = vonstunde.beginn
LEFT JOIN lehre.tbl_stunde bisstunde ON eventdauer.bis::time = bisstunde.ende
),
unterrichtsdauer AS (
SELECT
raster.lehreinheit_id,
raster.kalender_id,
sum(EXTRACT(EPOCH FROM (LEAST(stunde.ende, raster.bis::time) - GREATEST(stunde.beginn, raster.von::time))) / 60) AS minuten
FROM raster JOIN lehre.tbl_stunde stunde ON stunde.beginn < raster.bis::time AND stunde.ende > raster.von::time
WHERE raster.vonstunde_nr IS NULL OR raster.bisstunde_nr IS NULL
GROUP BY raster.lehreinheit_id, raster.kalender_id
),
verplantestunden AS (
SELECT raster.lehreinheit_id, raster.kalender_id,
CASE
WHEN raster.vonstunde_nr IS NOT NULL AND raster.bisstunde_nr IS NOT NULL
THEN (raster.bisstunde_nr - raster.vonstunde_nr + 1)
ELSE coalesce(unterrichtsdauer.minuten, 0) / 45
END AS verplantestunde
FROM raster LEFT JOIN unterrichtsdauer ON unterrichtsdauer.lehreinheit_id = raster.lehreinheit_id AND unterrichtsdauer.kalender_id = raster.kalender_id
),
verplant_gesamt AS (
SELECT lehreinheit_id, SUM(verplantestunde) AS verplant
FROM verplantestunden
GROUP BY lehreinheit_id
)
SELECT
lema.lehreinheit_id,
lema.mitarbeiter_uid,
lema.planstunden,
ROUND(coalesce(verplant_gesamt.verplant, 0), 2) AS verplant,
ROUND((lema.planstunden - coalesce(verplant_gesamt.verplant, 0)), 2) AS offenestunden
FROM lehre.tbl_lehreinheitmitarbeiter lema
LEFT JOIN verplant_gesamt ON verplant_gesamt.lehreinheit_id = lema.lehreinheit_id
WHERE lema.lehreinheit_id IN ($placeholders)";
return $db->execReadOnlyQuery($qry, array_merge($lehreinheit_ids, $lehreinheit_ids));
}
private function getLVTmp($stg_kz = null) private function getLVTmp($stg_kz = null)
{ {
$qry = "SELECT DISTINCT ON(lehrveranstaltung_id) *, $qry = "SELECT DISTINCT ON(lehrveranstaltung_id) *,
@@ -11,4 +11,26 @@ class Gruppe_model extends DB_Model
$this->dbTable = 'public.tbl_gruppe'; $this->dbTable = 'public.tbl_gruppe';
$this->pk = 'gruppe_kurzbz'; $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; 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); 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,27 @@
<?php
class Kalender_model extends DB_Model
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->dbTable = 'lehre.tbl_kalender';
$this->pk = 'kalender_id';
$this->load->helper('hlp_common');
}
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 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,
'vuedatepicker11' => true,
'momentjs2' => true,
'tags' => true,
'phrases' => array(
'global',
'ui',
'notiz',
),
'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/npm-asset/primevue/tree/tree.min.js',
#'vendor/npm-asset/primevue/toast/toast.min.js'
'vendor/moment/luxonjs/luxon.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); ?>
+32
View File
@@ -136,6 +136,36 @@ function jumpKalender(){
+'&ver='+document.getElementById('ver_semplan').value+'&grp='+document.getElementById('grp_semplan').value+'&begin='+document.getElementById('studiensemester').value+'&format=html', '_blank'); +'&ver='+document.getElementById('ver_semplan').value+'&grp='+document.getElementById('grp_semplan').value+'&begin='+document.getElementById('studiensemester').value+'&format=html', '_blank');
} }
} }
function jumpKalenderNew(){
var stgKz = document.getElementById('stg_kz_semplan').value;
var semesterRange = document.getElementById('studiensemester').value;
if (stgKz == '') {
alert("<?php echo $p->t('lvplan/bitteEinenStudiengangAuswaehlen');?>");
return;
}
if (semesterRange == '') {
alert("<?php echo $p->t('lvplan/bitteEinStudiensemesterAuswaehlen');?>");
return;
}
var rangeMatch = semesterRange.match(/^(\d+)&(?:amp;)?ende=(\d+)$/);
if (!rangeMatch) {
alert("Fehlerhafter Datumsbereich");
return;
}
var url = <?php echo json_encode(APP_ROOT.'index.ci.php/lvplan/html/preview'); ?>
+ '?type=verband'
+ '&stg_kz=' + encodeURIComponent(stgKz)
+ '&sem=' + encodeURIComponent(document.getElementById('sem_semplan').value)
+ '&ver=' + encodeURIComponent(document.getElementById('ver_semplan').value)
+ '&grp=' + encodeURIComponent(document.getElementById('grp_semplan').value)
+ '&begin=' + encodeURIComponent(rangeMatch[1])
+ '&ende=' + encodeURIComponent(rangeMatch[2]);
window.open(url, '_blank');
}
function checkSetStudiengang(){ function checkSetStudiengang(){
if (document.getElementById('stg_kz').value == '') { if (document.getElementById('stg_kz').value == '') {
alert("<?php echo $p->t('lvplan/bitteEinenStudiengangAuswaehlen');?>"); alert("<?php echo $p->t('lvplan/bitteEinenStudiengangAuswaehlen');?>");
@@ -307,6 +337,7 @@ function LoadGruppe(type)
echo '<li><a class="Item" href="'.$caldavurl.'">'.$p->t('lvplan/caldavURL').'</a></li>'; echo '<li><a class="Item" href="'.$caldavurl.'">'.$p->t('lvplan/caldavURL').'</a></li>';
echo '<li><a class="Item" href="'.APP_ROOT.'webdav/lvplan.php/principals/'.$uid.'">'.$p->t('lvplan/caldavURLMac').'</a></li>'; echo '<li><a class="Item" href="'.APP_ROOT.'webdav/lvplan.php/principals/'.$uid.'">'.$p->t('lvplan/caldavURLMac').'</a></li>';
echo '<li><a class="Item" href="'.APP_ROOT.'webdav/google.php?cal='.encryptData($uid,LVPLAN_CYPHER_KEY).'&'.microtime(true).'">'.$p->t('lvplan/googleURL').'</a></li>'; echo '<li><a class="Item" href="'.APP_ROOT.'webdav/google.php?cal='.encryptData($uid,LVPLAN_CYPHER_KEY).'&'.microtime(true).'">'.$p->t('lvplan/googleURL').'</a></li>';
echo '<li><a class="Item" href="'.APP_ROOT.'index.ci.php/lvplan/ical/subscription/google?cal='.encryptData($uid,LVPLAN_CYPHER_KEY).'&'.microtime(true).'">NEW '.$p->t('lvplan/googleURL').'</a></li>';
echo '</ul>'; echo '</ul>';
echo ' </div>'; echo ' </div>';
} }
@@ -519,6 +550,7 @@ if(!defined('CIS_LVPLAN_ARCHIVAUSWAHL_ANZEIGEN') || CIS_LVPLAN_ARCHIVAUSWAHL_ANZ
echo '</td> echo '</td>
<td colspan="3" valign="bottom"> <td colspan="3" valign="bottom">
<input type="button" name="Abschicken" value="'.$p->t('lvplan/semesterplanLaden').'" onClick="jumpKalender()"> <input type="button" name="Abschicken" value="'.$p->t('lvplan/semesterplanLaden').'" onClick="jumpKalender()">
&nbsp;<a class="Item" href="#" onClick="jumpKalenderNew(); return false;">HTML-new</a>
</td> </td>
</tr> </tr>
</table> </table>
+1 -1
View File
@@ -268,7 +268,7 @@ if(!$stdplan->load_data($type,$pers_uid,$ort_kurzbz,$stg_kz,$sem,$ver,$grp,$grup
} }
//Ueberschriften in HTML //Ueberschriften in HTML
if($format=='HTML') if($format=='html')
{ {
if($type=='verband' || $type=='einheit') if($type=='verband' || $type=='einheit')
{ {
+3 -1
View File
@@ -443,7 +443,7 @@
} }
], ],
"require": { "require": {
"php": ">=5.6.40", "php": ">=7.0.0",
"afarkas/html5shiv": "3.7.*", "afarkas/html5shiv": "3.7.*",
"alvaro-prieto/colresizable": "1.6", "alvaro-prieto/colresizable": "1.6",
@@ -506,6 +506,8 @@
"rmariuzzo/jquery-checkboxes": "1.0.7", "rmariuzzo/jquery-checkboxes": "1.0.7",
"sabre/dav": "4.0.3",
"sabre/event": "5.0.3",
"scottjehl/respond": "1.4.2", "scottjehl/respond": "1.4.2",
"tapmodo/jcrop": "2.0.4", "tapmodo/jcrop": "2.0.4",
-3
View File
@@ -152,9 +152,6 @@ define('TAGE_PRO_WOCHE','7');
// Kalenderkategorie beim Export des LVPlans // Kalenderkategorie beim Export des LVPlans
define('LVPLAN_KATEGORIE', 'StundenplanTW'); define('LVPLAN_KATEGORIE', 'StundenplanTW');
// Key zum Verschluesseln des LV-Plan Google Links
define('LVPLAN_CYPHER_KEY',pack('H*', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'));
// Gibt an ob Termine aus dem Vorsemester nach der Semesterhaelfte des Folgesemesters // Gibt an ob Termine aus dem Vorsemester nach der Semesterhaelfte des Folgesemesters
// noch im pers. LVPan aufscheinen. // noch im pers. LVPan aufscheinen.
// true | false // true | false
+3
View File
@@ -185,6 +185,9 @@ define('LVPLAN_ANMERKUNG_ANZEIGEN',true);
//Gruppieren zeitgleicher Lehreinheiten im LV-Plan //Gruppieren zeitgleicher Lehreinheiten im LV-Plan
define('LVPLAN_LEHREINHEITEN_GRUPPIEREN',true); define('LVPLAN_LEHREINHEITEN_GRUPPIEREN',true);
// Key zum Verschluesseln des LV-Plan Kalender-Links
define('LVPLAN_CYPHER_KEY', pack('H*', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'));
// Ende-Datum des LVPlan Syncs Format: 2014-02-01 // Ende-Datum des LVPlan Syncs Format: 2014-02-01
// Wenn leer wird jeweils bis Semesterende gesynct // Wenn leer wird jeweils bis Semesterende gesynct
define('LVPLAN_SYNC_ENDE',''); define('LVPLAN_SYNC_ENDE','');
+102 -3
View File
@@ -512,6 +512,69 @@ class wochenplan extends basis_db
$this->kal_link.='&pers_uid='.$this->pers_uid.'&ort_kurzbz='.$this->ort_kurzbz.'&stg_kz='.$this->stg_kz.'&sem='.$this->sem.'&ver='.$this->ver.'&grp='.$this->grp.'&gruppe_kurzbz='.$this->gruppe_kurzbz.'&lva='.$this->lva; $this->kal_link.='&pers_uid='.$this->pers_uid.'&ort_kurzbz='.$this->ort_kurzbz.'&stg_kz='.$this->stg_kz.'&sem='.$this->sem.'&ver='.$this->ver.'&grp='.$this->grp.'&gruppe_kurzbz='.$this->gruppe_kurzbz.'&lva='.$this->lva;
$kal_link_ws=$this->kal_link.'&begin='.$this->studiensemester_now->start.'&ende='.$this->studiensemester_now->ende; $kal_link_ws=$this->kal_link.'&begin='.$this->studiensemester_now->start.'&ende='.$this->studiensemester_now->ende;
$kal_link_ss=$this->kal_link.'&begin='.$this->studiensemester_next->start.'&ende='.$this->studiensemester_next->ende; $kal_link_ss=$this->kal_link.'&begin='.$this->studiensemester_next->start.'&ende='.$this->studiensemester_next->ende;
$kal_link_new_ws = null;
$kal_link_new_ss = null;
$kal_link_new_downloads = false;
// Build only the type-specific parameters accepted by the new endpoints.
if ($this->type == 'student' || $this->type == 'lektor')
{
$kal_link_new_downloads = true;
$kal_link_new_ws = http_build_query(array(
'type' => $this->type,
'pers_uid' => $this->pers_uid,
'begin' => $this->studiensemester_now->start,
'ende' => $this->studiensemester_now->ende
), '', '&');
$kal_link_new_ss = http_build_query(array(
'type' => $this->type,
'pers_uid' => $this->pers_uid,
'begin' => $this->studiensemester_next->start,
'ende' => $this->studiensemester_next->ende
), '', '&');
}
elseif ($this->type == 'ort' && $this->ort_kurzbz != '' && $this->ort_kurzbz != 'all')
{
$kal_link_new_downloads = true;
$kal_link_new_ws = http_build_query(array(
'type' => $this->type,
'pers_uid' => $this->pers_uid,
'ort_kurzbz' => $this->ort_kurzbz,
'begin' => $this->studiensemester_now->start,
'ende' => $this->studiensemester_now->ende
), '', '&');
$kal_link_new_ss = http_build_query(array(
'type' => $this->type,
'pers_uid' => $this->pers_uid,
'ort_kurzbz' => $this->ort_kurzbz,
'begin' => $this->studiensemester_next->start,
'ende' => $this->studiensemester_next->ende
), '', '&');
}
elseif ($this->type == 'verband')
{
$kal_link_new_downloads = true;
$kal_link_new_ws = http_build_query(array(
'type' => $this->type,
'pers_uid' => $this->pers_uid,
'stg_kz' => $this->stg_kz,
'sem' => $this->sem,
'ver' => $this->ver,
'grp' => $this->grp,
'begin' => $this->studiensemester_now->start,
'ende' => $this->studiensemester_now->ende
), '', '&');
$kal_link_new_ss = http_build_query(array(
'type' => $this->type,
'pers_uid' => $this->pers_uid,
'stg_kz' => $this->stg_kz,
'sem' => $this->sem,
'ver' => $this->ver,
'grp' => $this->grp,
'begin' => $this->studiensemester_next->start,
'ende' => $this->studiensemester_next->ende
), '', '&');
}
echo $this->crlf; echo $this->crlf;
echo $this->studiensemester_now->name.'<br>'.$this->crlf; echo $this->studiensemester_now->name.'<br>'.$this->crlf;
@@ -521,6 +584,18 @@ class wochenplan extends basis_db
echo ' <A href="'.$kal_link_ws.'&format=csv&target=outlook" title="CSV-Outlook"><IMG src="../../../skin/images/outlook.png" height="30" alt="CSV-Outlook" border="0"></A>'.$this->crlf; echo ' <A href="'.$kal_link_ws.'&format=csv&target=outlook" title="CSV-Outlook"><IMG src="../../../skin/images/outlook.png" height="30" alt="CSV-Outlook" border="0"></A>'.$this->crlf;
echo ' <A href="'.$kal_link_ws.'&format=ical&version=1&target=ical" title="iCal Version 1.0"><IMG src="../../../skin/images/ical1.0.png" height="30" alt="vCal Version 1.0" border="0"></A>'.$this->crlf; echo ' <A href="'.$kal_link_ws.'&format=ical&version=1&target=ical" title="iCal Version 1.0"><IMG src="../../../skin/images/ical1.0.png" height="30" alt="vCal Version 1.0" border="0"></A>'.$this->crlf;
echo ' <A href="'.$kal_link_ws.'&format=ical&version=2&target=ical" title="iCal Version 2.0"><IMG src="../../../skin/images/ical2.0.png" height="30" alt="vCal Version 2.0" border="0"></A>'.$this->crlf; echo ' <A href="'.$kal_link_ws.'&format=ical&version=2&target=ical" title="iCal Version 2.0"><IMG src="../../../skin/images/ical2.0.png" height="30" alt="vCal Version 2.0" border="0"></A>'.$this->crlf;
if ($kal_link_new_ws !== null)
{
echo ' <br>'.$this->crlf;
echo ' <A href="'.APP_ROOT.'index.ci.php/lvplan/html/preview?'.htmlspecialchars($kal_link_new_ws, ENT_QUOTES, 'UTF-8').'" target="_blank" title="HTML-new">HTML-new</A> '.$this->crlf;
if ($kal_link_new_downloads)
{
echo ' <A href="'.APP_ROOT.'index.ci.php/lvplan/excel/download?'.htmlspecialchars($kal_link_new_ws, ENT_QUOTES, 'UTF-8').'" title="excel-new">excel-new</A> '.$this->crlf;
echo ' <A href="'.APP_ROOT.'index.ci.php/lvplan/csv/download?'.htmlspecialchars($kal_link_new_ws, ENT_QUOTES, 'UTF-8').'" title="CSV-new">CSV-new</A> '.$this->crlf;
echo ' <A href="'.APP_ROOT.'index.ci.php/lvplan/csv/download?'.htmlspecialchars($kal_link_new_ws, ENT_QUOTES, 'UTF-8').'&amp;target=outlook" title="CSV-Outlook-new">CSV-Outlook-new</A> '.$this->crlf;
echo ' <A href="'.APP_ROOT.'index.ci.php/lvplan/ical/download?'.htmlspecialchars($kal_link_new_ws, ENT_QUOTES, 'UTF-8').'&amp;version=2" title="iCal Version 2.0-new">iCal2-new</A>'.$this->crlf;
}
}
echo ' </td><td class="stdplan" style="padding:8px;" align="center">'.$p->t('lvplan/semesterplaene').'</td><td style="padding:3px 15px 0px 15px;" align="center">'; echo ' </td><td class="stdplan" style="padding:8px;" align="center">'.$p->t('lvplan/semesterplaene').'</td><td style="padding:3px 15px 0px 15px;" align="center">';
echo ' <span style="color:#999">'.$this->studiensemester_next->name.'</span><br>'.$this->crlf; echo ' <span style="color:#999">'.$this->studiensemester_next->name.'</span><br>'.$this->crlf;
echo ' <A href="'.$kal_link_ss.'&format=html" target="_blank" title="HTML"><IMG src="../../../skin/images/html_light.png" height="30" alt="HTML" border="0"></A>'.$this->crlf; echo ' <A href="'.$kal_link_ss.'&format=html" target="_blank" title="HTML"><IMG src="../../../skin/images/html_light.png" height="30" alt="HTML" border="0"></A>'.$this->crlf;
@@ -529,6 +604,18 @@ class wochenplan extends basis_db
echo ' <A href="'.$kal_link_ss.'&format=csv&target=outlook" title="CSV-Outlook"><IMG src="../../../skin/images/outlook_light.png" height="30" alt="CSV-Outlook" border="0"></A>'.$this->crlf; echo ' <A href="'.$kal_link_ss.'&format=csv&target=outlook" title="CSV-Outlook"><IMG src="../../../skin/images/outlook_light.png" height="30" alt="CSV-Outlook" border="0"></A>'.$this->crlf;
echo ' <A href="'.$kal_link_ss.'&format=ical&version=1&target=ical" title="iCal Version 1.0"><IMG src="../../../skin/images/ical1.0_light.png" height="30" alt="iCal Version 1.0" border="0"></A>'.$this->crlf; echo ' <A href="'.$kal_link_ss.'&format=ical&version=1&target=ical" title="iCal Version 1.0"><IMG src="../../../skin/images/ical1.0_light.png" height="30" alt="iCal Version 1.0" border="0"></A>'.$this->crlf;
echo ' <A href="'.$kal_link_ss.'&format=ical&version=2&target=ical" title="iCal Version 2.0"><IMG src="../../../skin/images/ical2.0_light.png" height="30" alt="iCal Version 2.0" border="0"></A>'.$this->crlf; echo ' <A href="'.$kal_link_ss.'&format=ical&version=2&target=ical" title="iCal Version 2.0"><IMG src="../../../skin/images/ical2.0_light.png" height="30" alt="iCal Version 2.0" border="0"></A>'.$this->crlf;
if ($kal_link_new_ss !== null)
{
echo ' <br>'.$this->crlf;
echo ' <A href="'.APP_ROOT.'index.ci.php/lvplan/html/preview?'.htmlspecialchars($kal_link_new_ss, ENT_QUOTES, 'UTF-8').'" target="_blank" title="HTML-new">HTML-new</A> '.$this->crlf;
if ($kal_link_new_downloads)
{
echo ' <A href="'.APP_ROOT.'index.ci.php/lvplan/excel/download?'.htmlspecialchars($kal_link_new_ss, ENT_QUOTES, 'UTF-8').'" title="excel-new">excel-new</A> '.$this->crlf;
echo ' <A href="'.APP_ROOT.'index.ci.php/lvplan/csv/download?'.htmlspecialchars($kal_link_new_ss, ENT_QUOTES, 'UTF-8').'" title="CSV-new">CSV-new</A> '.$this->crlf;
echo ' <A href="'.APP_ROOT.'index.ci.php/lvplan/csv/download?'.htmlspecialchars($kal_link_new_ss, ENT_QUOTES, 'UTF-8').'&amp;target=outlook" title="CSV-Outlook-new">CSV-Outlook-new</A> '.$this->crlf;
echo ' <A href="'.APP_ROOT.'index.ci.php/lvplan/ical/download?'.htmlspecialchars($kal_link_new_ss, ENT_QUOTES, 'UTF-8').'&amp;version=2" title="iCal Version 2.0-new">iCal2-new</A>'.$this->crlf;
}
}
echo ' </td></tr></table>'.$this->crlf; echo ' </td></tr></table>'.$this->crlf;
echo ' </TD>'.$this->crlf; echo ' </TD>'.$this->crlf;
@@ -2656,9 +2743,21 @@ class wochenplan extends basis_db
} }
else else
{ {
echo $this->crlf.'"'.$lehrfach[$idx].'","'.$lvplan_kategorie.'","'.$this->std_plan[$i][$j][$idx]->ort.'","Stundenplan'.$this->crlf.$this->std_plan[$i][$j][$idx]->lehrfach.$this->crlf;
echo (($this->std_plan[$i][$j][$idx]->mitarbeiter_kurzbz === null) ? $this->std_plan[$i][$j][$idx]->lektor : $this->std_plan[$i][$j][$idx]->mitarbeiter_kurzbz).$this->crlf.$lvb.$this->crlf.$this->std_plan[$i][$j][$idx]->ort.(LVPLAN_ANMERKUNG_ANZEIGEN?$this->crlf.$this->std_plan[$i][$j][$idx]->anmerkung:'').'","Stundenplan",'; $description = '","Stundenplan'.$this->crlf
echo '"'.$start_date.'","'.$start_time.'","'.$end_date.'","'.$end_time.'",,,,,'; .$this->std_plan[$i][$j][$idx]->lehrfach.$this->crlf // somehow the title
.(($this->std_plan[$i][$j][$idx]->mitarbeiter_kurzbz === null) ? $this->std_plan[$i][$j][$idx]->lektor : $this->std_plan[$i][$j][$idx]->mitarbeiter_kurzbz).$this->crlf
.$lvb.$this->crlf
.$this->std_plan[$i][$j][$idx]->ort
.(LVPLAN_ANMERKUNG_ANZEIGEN?$this->crlf.$this->std_plan[$i][$j][$idx]->anmerkung:'');
echo $this->crlf.
'"'.$lehrfach[$idx].'","' // title
.$lvplan_kategorie.'","' // category
.$this->std_plan[$i][$j][$idx]->ort
.$description
.'","Stundenplan",' // keywords
.'"'.$start_date.'","'.$start_time.'","'.$end_date.'","'.$end_time.'",,,,,';
} }
} }
} }
+12
View File
@@ -97,3 +97,15 @@
display: none; 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
);
}
+285
View File
@@ -0,0 +1,285 @@
@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);
}
.parkingevent {
border: 1px dashed !important;
border-color: #AAAAAA;
padding: 0.5rem;
margin-bottom: 0.5rem;
margin-left: 0.5rem;
margin-right: 0.5rem;
}
: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);
}
.wish-w--2 {
background-color: #FF2200;
}
.wish-w--1 {
background-color: #FF9922;
}
.wish-w-1 {
background-color: #CCFFCC;
}
.wish-w-2 {
background-color: #48FA66;
}
.fhc-calendar-base-grid-line-event {
position: relative !important;
}
.updated-event {
position: relative !important;
z-index: 10;
animation: modernFocus 1s ease-out;
border-radius: 0.25rem;
}
.updated-event-long {
position: relative !important;
z-index: 10;
animation: modernFocus 1.8s ease-out;
border-radius: 0.25rem;
}
@keyframes modernFocus {
0% {
background-color: #cfd4d8;
box-shadow:
0 0 0 0 rgba(120, 120, 120, 0.8),
0 0 0 0 rgba(255, 255, 255, 0.6);
}
30% {
background-color: #eef1f3;
box-shadow:
0 0 0 8px rgba(120, 120, 120, 0.25),
0 0 30px 8px rgba(255, 255, 255, 0.7);
}
60% {
background-color: #f7f8f9;
box-shadow:
0 0 0 14px rgba(120, 120, 120, 0.15),
0 0 45px 12px rgba(255, 255, 255, 0.5);
}
100% {
background-color: inherit;
box-shadow:
0 0 0 24px rgba(120, 120, 120, 0),
0 0 60px 20px rgba(255, 255, 255, 0);
}
}
.deemphasized-event {
animation: deEmphasize 1s ease-out;
}
.deemphasized-event-long {
animation: deEmphasize 1.8s ease-out;
}
@keyframes deEmphasize {
0% {
opacity: 1;
filter: saturate(1) brightness(1);
}
30% {
opacity: 0.55;
filter: saturate(0.4) brightness(0.4);
}
100% {
opacity: 1;
filter: saturate(1) brightness(1);
}
}
.spinner-overlay {
position: absolute;
inset: 0; /* top:0 right:0 bottom:0 left:0 */
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
background: rgba(255, 255, 255, 1);
opacity: 0.4;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #ddd;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
+40
View File
@@ -2,6 +2,10 @@
cursor: pointer; cursor: pointer;
font-size: var(--fhc-calendar-fontsize-event, .875rem); 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-day .fhc-calendar-base-grid-line,
.fhc-calendar-mode-week .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)); padding: 0 var(--fhc-calendar-gap-events, var(--fhc-calendar-gap, 1px));
@@ -69,3 +73,39 @@
.fhc-calendar-base .event > *:hover { .fhc-calendar-base .event > *:hover {
filter: brightness(120%); 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
@@ -17,6 +17,13 @@
transform: scale(1.15); transform: scale(1.15);
} }
/* add prop that disasbles hover and cursor*/
.tag.disabled:hover
{
transform: none;
cursor: default;
}
.tag_rot { .tag_rot {
background-color: #ff0000ff; 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,17 @@
export default {
search(query) {
return {
method: 'get',
url: 'api/frontend/v1/tempus/coursepicker/search',
params: { query }
};
},
getByStg(studiengaenge, studiensemester_kurzbz) {
return {
method: 'post',
url: 'api/frontend/v1/tempus/coursepicker/getByStg',
params: { studiengaenge, studiensemester_kurzbz }
};
},
};
+8
View File
@@ -0,0 +1,8 @@
export default {
getStudiengaenge() {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Info/getStudiengaenge',
};
},
};
+136
View File
@@ -0,0 +1,136 @@
export default {
getPlan(filter, start_date, end_date)
{
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/getPlan',
params: { ...filter, 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}
};
},
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 }
};
},
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 }
};
},
};
@@ -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, ort_kurzbz, start_date, end_date, teilnehmer, specialFinalGroups, specialGroups, groups) {
return {
method: 'post',
url: '/api/frontend/v1/tempus/Reservierung/addReservierung',
params: { titel, beschreibung, ort_kurzbz, 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 }
};
}
}
};
+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
};
},
};
+46
View File
@@ -0,0 +1,46 @@
/**
* 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 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 },
]
});
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')); this.$fhcAlert.alertSuccess(this.$p.t('ui/settings_saved'));
}) })
.catch(this.$fhcAlert.handleSystemErrors); .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() { created() {
this.$api this.reload();
.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);
}, },
template: /* html */` template: /* html */`
<fhc-form class="stv-config" ref="form" @submit.prevent="update"> <fhc-form class="stv-config" ref="form" @submit.prevent="update">
+46 -19
View File
@@ -1,26 +1,35 @@
import BaseDraganddrop from './Base/DragAndDrop.js';
import BaseHeader from './Base/Header.js'; import BaseHeader from './Base/Header.js';
import BaseSlider from './Base/Slider.js'; import BaseSlider from './Base/Slider.js';
import BsModal from '../Bootstrap/Modal.js'; import BsModal from '../Bootstrap/Modal.js';
import CalClick from '../../directives/Calendar/Click.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 { export default {
name: "CalendarBase", name: "CalendarBase",
components: { components: {
BaseDraganddrop,
BaseHeader, BaseHeader,
BaseSlider, BaseSlider,
BsModal BsModal
}, },
directives: { directives: {
CalClick CalClick,
DragClick,
Draggable,
Drop
}, },
provide() { provide() {
return { 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), locale: Vue.computed(() => this.locale),
timezone: Vue.computed(() => this.timezone), timezone: Vue.computed(() => this.timezone),
timeGrid: Vue.computed(() => this.timeGrid), timeGrid: Vue.computed(() => this.timeGrid),
hoursPlan: Vue.computed(() => this.hoursPlan),
draggableEvents: Vue.computed(() => { draggableEvents: Vue.computed(() => {
if (!this.draggableEvents) if (!this.draggableEvents)
return () => false; return () => false;
@@ -43,8 +52,20 @@ export default {
return () => true; 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), hasDragoverFunc: Vue.computed(() => this.onDragover),
mode: Vue.computed(() => this.mode) mode: Vue.computed(() => this.mode),
onResize: Vue.computed(() => this.onResize || null),
}; };
}, },
props: { props: {
@@ -93,11 +114,18 @@ export default {
type: Boolean, type: Boolean,
default: undefined default: undefined
}, },
btnTableList: {
type: Boolean,
default: undefined
},
timeGrid: Array, timeGrid: Array,
hoursPlan: Object,
draggableEvents: [Boolean, Array, Function], draggableEvents: [Boolean, Array, Function],
dropableEvents: [Boolean, Array, Function], dropableEvents: [Boolean, Array, Function],
resizableEvents: [Boolean, Array, Function],
onDragover: Function, onDragover: Function,
onDrop: Function onDrop: Function,
onResize: Function
}, },
emits: [ emits: [
"click:next", "click:next",
@@ -136,7 +164,7 @@ export default {
res.start = luxon.DateTime.fromMillis(res.start, { zone: this.timezone, locale: this.locale }); res.start = luxon.DateTime.fromMillis(res.start, { zone: this.timezone, locale: this.locale });
else if (res.start instanceof Date) else if (res.start instanceof Date)
res.start = luxon.DateTime.fromJSDate(res.start, { zone: this.timezone, locale: this.locale }); 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) 'string' || res.start instanceof String)
res.start = luxon.DateTime.fromISO(res.start, { zone: this.timezone, locale: this.locale }); res.start = luxon.DateTime.fromISO(res.start, { zone: this.timezone, locale: this.locale });
} }
@@ -145,7 +173,7 @@ export default {
res.end = luxon.DateTime.fromMillis(res.end, { zone: this.timezone, locale: this.locale }); res.end = luxon.DateTime.fromMillis(res.end, { zone: this.timezone, locale: this.locale });
else if (res.end instanceof Date) else if (res.end instanceof Date)
res.end = luxon.DateTime.fromJSDate(res.end, { zone: this.timezone, locale: this.locale }); 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) 'string' || res.end instanceof String)
res.end = luxon.DateTime.fromISO(res.end, { zone: this.timezone, locale: this.locale }); res.end = luxon.DateTime.fromISO(res.end, { zone: this.timezone, locale: this.locale });
} }
@@ -171,7 +199,10 @@ export default {
// choose default mode // choose default mode
let mode = this.mode; let mode = this.mode;
if (mode) if (mode)
mode = mode.toLowerCase(); {
const lower = mode.toLowerCase();
mode = Object.keys(this.modes).find(key => key.toLowerCase() === lower) || null;
}
if (!mode || !this.modes[mode]) if (!mode || !this.modes[mode])
mode = Object.keys(this.modes).find(Boolean); // start with first entry as active mode mode = Object.keys(this.modes).find(Boolean); // start with first entry as active mode
return mode || ''; return mode || '';
@@ -188,7 +219,7 @@ export default {
}, },
watch: { watch: {
sDate(n, o) { 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; this.internalDate = this.sDate;
}, },
sMode() { sMode() {
@@ -240,9 +271,7 @@ export default {
break; break;
} }
}, },
onDropItem(evt, start, end) {
this.$emit('drop', evt, start, end);
},
showEventModal(eventObj) { showEventModal(eventObj) {
this.modalEvent = eventObj; this.modalEvent = eventObj;
this.$refs.modal.show(); this.$refs.modal.show();
@@ -263,11 +292,7 @@ export default {
}, },
template: /* html */` template: /* html */`
<div class="fhc-calendar-base h-100"> <div class="fhc-calendar-base h-100">
<base-draganddrop <div class="card h-100"
class="card h-100"
:events="convertedEvents"
:backgrounds="convertedBackgrounds"
@drop="onDropItem"
v-cal-click:container v-cal-click:container
@cal-click-default.capture="handleClickDefaults" @cal-click-default.capture="handleClickDefaults"
> >
@@ -282,6 +307,7 @@ export default {
:btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))" :btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))"
:btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))" :btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))"
:btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))" :btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))"
:btn-table-list="!!modes['tableList'] && (btnTableList || (showBtns && btnTableList !== false))"
:mode-options="modeOptions ? modeOptions[cMode] : undefined" :mode-options="modeOptions ? modeOptions[cMode] : undefined"
> >
<slot name="actions" /> <slot name="actions" />
@@ -293,11 +319,12 @@ export default {
@update:range="$emit('update:range', $event)" @update:range="$emit('update:range', $event)"
@request-modal-open="showEventModal" @request-modal-open="showEventModal"
@request-modal-close="hideEventModal" @request-modal-close="hideEventModal"
@drop="$emit('drop', $event)"
v-bind="modeOptions ? modeOptions[cMode] : null || {}" v-bind="modeOptions ? modeOptions[cMode] : null || {}"
> >
<template v-slot="slot"><slot v-bind="slot" /></template> <template v-slot="slot"><slot v-bind="slot" /></template>
</component> </component>
</base-draganddrop> </div>
<bs-modal ref="modal" dialog-class="modal-lg" body-class="" @hidden-bs-modal="onModalHidden"> <bs-modal ref="modal" dialog-class="modal-lg" body-class="" @hidden-bs-modal="onModalHidden">
<template #title> <template #title>
<slot v-if="modalEvent" v-bind="{mode: 'eventheader', event: modalEvent.event}" /> <slot v-if="modalEvent" v-bind="{mode: 'eventheader', event: modalEvent.event}" />
@@ -308,4 +335,4 @@ export default {
</bs-modal> </bs-modal>
</div> </div>
` `
} }
+193 -23
View File
@@ -1,7 +1,8 @@
import GridLine from './Grid/Line.js'; import GridLine from './Grid/Line.js';
import GridLineEvent from './Grid/Line/Event.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 { export default {
name: "CalendarGrid", name: "CalendarGrid",
@@ -10,12 +11,18 @@ export default {
GridLineEvent GridLineEvent
}, },
directives: { directives: {
CalDnd drop
}, },
inject: { inject: {
originalEvents: "events", originalEvents: "events",
originalBackgrounds: "backgrounds", originalBackgrounds: "backgrounds",
dropAllowed: "dropAllowed" dropAllowed: "dropAllowed",
onDrop: "onDrop",
onResize: "onResize",
timeGrid: {
from: "timeGrid",
default: () => []
}
}, },
provide() { provide() {
return { return {
@@ -57,10 +64,10 @@ export default {
}, },
data() { data() {
return { return {
dragging: false,
resizeObserver: null, resizeObserver: null,
mutationObserver: null, mutationObserver: null,
userScroll: true userScroll: true,
isDragging: false
}; };
}, },
computed: { computed: {
@@ -252,17 +259,27 @@ export default {
pageLeft += this.getPageLeft(el.offsetParent); pageLeft += this.getPageLeft(el.offsetParent);
return pageLeft; return pageLeft;
}, },
getTimestampFromMouse(evt, dayTimestamp) { getTimestampFromMouse(evt, dayTimestamp)
{
let mouse, mouseFrac; 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; 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; 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 */ /* SCROLLING */
@@ -308,7 +325,153 @@ export default {
} else { } else {
this.$refs.scroller.scrollTo(0, 0); 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);
},
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);
const snappedPart = blocks.find(b => grabOffset >= b.start && grabOffset < b.end) || part;
const dropStart = dropDay.plus(snappedPart.start);
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
});
}
});
},
},
setup()
{
const resizeHandler = useResizeHandler();
return { resizeHandler };
}, },
beforeUnmount() { beforeUnmount() {
this.disableAutoScroll(); this.disableAutoScroll();
@@ -318,6 +481,7 @@ export default {
class="fhc-calendar-base-grid" class="fhc-calendar-base-grid"
style="display:grid;width:100%;height:100%;overflow:auto" style="display:grid;width:100%;height:100%;overflow:auto"
:style="'grid-template-' + axisRow + 's:auto' + (allDayEvents ? ' auto ' : ' ') + '1fr;grid-template-' + axisCol + 's:auto ' + styleGridCols" :style="'grid-template-' + axisRow + 's:auto' + (allDayEvents ? ' auto ' : ' ') + '1fr;grid-template-' + axisCol + 's:auto ' + styleGridCols"
data-cy="calendar-base-grid"
> >
<div <div
class="grid-header" class="grid-header"
@@ -382,11 +546,11 @@ export default {
ref="body" ref="body"
class="grid-body" class="grid-body"
style="display:grid;grid-template-rows:subgrid;grid-template-columns:subgrid" 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'" :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 <template
v-for="(date, index) in axisMain" v-for="(date, index) in axisMain"
@@ -398,12 +562,16 @@ export default {
class="part-body" class="part-body"
style="position:relative" style="position:relative"
:style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i" :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 }" /> <slot name="part-body" v-bind="{ index, part }" />
<div <div
v-if="snapToGrid && dragging" v-if="snapToGrid"
style="position:absolute;inset:0;z-index:1" style="position:absolute;inset:0"
v-cal-dnd:dropzone.once="{date: date.plus(part.start || part), ends: ends.slice(ends.findIndex(end => end > date))}" :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>
</div> </div>
<grid-line <grid-line
@@ -413,6 +581,7 @@ export default {
:events="eventsNormal[index]" :events="eventsNormal[index]"
:backgrounds="backgrounds[index]" :backgrounds="backgrounds[index]"
style="position:relative" style="position:relative"
@resize-start="handleResizeStart"
:style="'grid-' + axisRow + ':1/-1;grid-' + axisCol + ':' + (1+index)" :style="'grid-' + axisRow + ':1/-1;grid-' + axisCol + ':' + (1+index)"
> >
<template #event="slot"> <template #event="slot">
@@ -420,9 +589,10 @@ export default {
</template> </template>
<template #dropzone> <template #dropzone>
<div <div
v-if="!snapToGrid && dragging" v-if="!snapToGrid"
style="position:absolute;inset:0;z-index:1" style="position:absolute;inset:0"
v-cal-dnd:dropzone="evt => getTimestampFromMouse(evt, date)" :style="{ zIndex: isDragging ? 10 : 1 }"
v-drop:move.lehreinheit.kalender.reservierung="(evt, item) => onDropFree(evt, item, date)"
></div> ></div>
</template> </template>
</grid-line> </grid-line>
@@ -432,4 +602,4 @@ export default {
</div> </div>
</div> </div>
` `
} }
@@ -12,7 +12,17 @@ export default {
LineEvent, LineEvent,
LineBackground, LineBackground,
}, },
inject: ["axisRow", "shouldCompactEvents", "compactibleEventTypes"], inject: {
axisRow: "axisRow",
shouldCompactEvents: {
from: "shouldCompactEvents",
default: undefined,
},
compactibleEventTypes: {
from: "compactibleEventTypes",
default: undefined,
},
},
props: { props: {
date: { date: {
type: luxon.DateTime, type: luxon.DateTime,
@@ -123,6 +133,7 @@ export default {
template: /* html */ ` template: /* html */ `
<div <div
class="fhc-calendar-base-grid-line" class="fhc-calendar-base-grid-line"
:data-day="date.toISODate()"
style="position:relative;display:grid;grid-auto-flow:dense" style="position:relative;display:grid;grid-auto-flow:dense"
:style="'grid-template-' + axisRow + 's:subgrid'" :style="'grid-template-' + axisRow + 's:subgrid'"
> >
@@ -137,6 +148,7 @@ export default {
v-if="!event.display || event.display === 'default'" v-if="!event.display || event.display === 'default'"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')" :style="'grid-' + axisRow + ': ' + event.rows.join('/')"
:event="event" :event="event"
@resize-start="$emit('resize-start', $event)"
> >
<template v-slot="slot"> <template v-slot="slot">
<slot name="event" v-bind="slot" /> <slot name="event" v-bind="slot" />
@@ -1,15 +1,41 @@
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'; import CalClick from '../../../../../directives/Calendar/Click.js';
export default { export default {
name: "GridLineEvent", name: "GridLineEvent",
directives: { directives: {
CalDnd, draggable,
drop,
CalClick CalClick
}, },
emits: [
'resize-start'
],
data() {
return {
contextMenu: {
show: false,
x: 0,
y: 0
}
};
},
inject: { inject: {
draggableEvents: "draggableEvents", draggableEvents: "draggableEvents",
mode: "mode" resizableEvents: {
from: "resizableEvents",
default: () => () => false
},
mode: "mode",
contextMenuActions: {
from: "contextMenuActions",
default: () => ({})
},
onDrop: {
from: "onDrop",
default: () => null
}
}, },
props: { props: {
event: { event: {
@@ -27,6 +53,9 @@ export default {
draggable() { draggable() {
return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode); return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode);
}, },
resizable() {
return !this.isHeaderOrFooter && this.resizableEvents(this.event.orig, this.mode);
},
classes() { classes() {
const classes = []; const classes = [];
if (this.isHeaderOrFooter) { if (this.isHeaderOrFooter) {
@@ -37,21 +66,135 @@ export default {
if (this.event.endsHere) if (this.event.endsHere)
classes.push('event-end'); 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';
return this.contextMenuActions[type] ?? this.contextMenuActions['default'] ?? [];
} }
}, },
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.onDrop({
item: [obj],
start: this.event.start.toISO(),
end: this.event.end.toISO(),
ctrlKey: false,
targetKalenderId: null
});
},
},
template:`
<div <div
class="fhc-calendar-base-grid-line-event event" class="fhc-calendar-base-grid-line-event event"
:class="classes" :class="classes"
style="z-index: 1" style="z-index: 11"
:draggable="draggable" :draggable="draggable"
v-cal-dnd:draggable="event" :data-id="'event-' + event.orig.kalender_id"
:data-group-id="'event-group-' + event.orig.eindeutige_kalender_gruppen_id"
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" v-cal-click:event="isHeaderOrFooter ? event : event.orig"
@contextmenu.prevent="onRightClick"
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"> <slot :event="isHeaderOrFooter ? event : event.orig">
{{ event.orig }} {{ event.orig }}
</slot> </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> </div>
` `
} }
@@ -0,0 +1,36 @@
import draggable from '../../../../../directives/draggable.js';
export default {
name: 'EventCard',
directives: {
draggable,
},
props: {
event: { type: Object, required: true },
parked: Boolean
},
computed: {
dragKalenderCollection() {
return this.event
},
topicString() {
return Array.isArray(this.event.orig.topic) ? this.event.orig.topic.join(', ') : this.event.orig.topic;
},
},
template: `
<div
class="fhc-calendar-base-grid-line-event event"
v-draggable:move.noimage="dragKalenderCollection"
style="border:1px"
data-cy="calendar-event"
>
<div class="title">
{{ topicString || event.orig.titel || event.orig.lehrfach }}
</div>
<div>
{{ event.orig.datum }} {{ event.orig.beginn }}–{{ event.orig.ende }}
<span v-if="event.ort_kurzbz">· {{ event.orig.ort_kurzbz }}</span>
</div>
</div>
`
};
+12 -1
View File
@@ -24,7 +24,8 @@ export default {
btnMonth: Boolean, btnMonth: Boolean,
btnWeek: Boolean, btnWeek: Boolean,
btnDay: Boolean, btnDay: Boolean,
btnList: Boolean btnList: Boolean,
btnTableList: Boolean
}, },
emits: [ emits: [
"next", "next",
@@ -89,6 +90,16 @@ export default {
> >
<i class="fa fa-table-list"></i> <i class="fa fa-table-list"></i>
</button> </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> </div>
</div> </div>
@@ -39,6 +39,7 @@ export default {
case "list": case "list":
return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.listLength }).ts - 1]; return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.listLength }).ts - 1];
case "week": case "week":
case "tableList":
return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts]; return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts];
case "day": case "day":
return this.convertedDate; return this.convertedDate;
@@ -51,6 +52,7 @@ export default {
case "month": case "month":
return this.date.toLocaleString({ month: 'long', year: 'numeric' }); return this.date.toLocaleString({ month: 'long', year: 'numeric' });
case "week": case "week":
case "tableList":
var year = this.date.localWeekYear; var year = this.date.localWeekYear;
var week = this.date.toFormat('nn'); var week = this.date.toFormat('nn');
return this.$p.t('calendar/year_kw', { year, week }); return this.$p.t('calendar/year_kw', { year, week });
@@ -76,6 +78,7 @@ export default {
break; break;
case "list": case "list":
case "week": case "week":
case "tableList":
date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale); date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break; break;
case "day": case "day":
@@ -96,7 +99,7 @@ export default {
@update:model-value="update" @update:model-value="update"
:format="() => title" :format="() => title"
:month-picker="mode == 'month'" :month-picker="mode == 'month'"
:week-picker="mode == 'week'" :week-picker="mode == 'week' || mode == 'tableList'"
:range="mode == 'list' ? { autoRange: listLength - 1 } : false" :range="mode == 'list' ? { autoRange: listLength - 1 } : false"
:text-input="mode == 'day'" :text-input="mode == 'day'"
:week-start="weekStart" :week-start="weekStart"
+3 -3
View File
@@ -182,17 +182,17 @@ export default {
> >
<component <component
v-if="mode == 'event'" v-if="mode == 'event'"
:is="renderers[event.type]?.modalContent" :is="renderers?.[event.type]?.modalContent"
:event="event" :event="event"
></component> ></component>
<component <component
v-else-if="mode == 'eventheader'" v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle" :is="renderers?.[event.type]?.modalTitle"
:event="event" :event="event"
></component> ></component>
<component <component
v-else v-else
:is="renderers[event.type]?.calendarEvent" :is="renderers?.[event.type]?.calendarEvent"
:event="event" :event="event"
:timeSlotDisplayBehavior=" :timeSlotDisplayBehavior="
$props.mode.toLowerCase() === 'list' ? 'always' : 'default' $props.mode.toLowerCase() === 'list' ? 'always' : 'default'
@@ -1,7 +1,8 @@
import LabelDay from '../../Base/Label/Day.js'; import LabelDay from '../../Base/Label/Day.js';
import LabelDow from '../../Base/Label/Dow.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'; import CalClick from '../../../../directives/Calendar/Click.js';
// TODO(chris): drag and drop // TODO(chris): drag and drop
@@ -13,7 +14,7 @@ export default {
LabelDow LabelDow
}, },
directives: { directives: {
CalDnd, draggable,
CalClick CalClick
}, },
inject: { inject: {
@@ -86,7 +87,7 @@ export default {
v-else v-else
class="event" class="event"
:draggable="draggable(event)" :draggable="draggable(event)"
v-cal-dnd:draggable="event" v-draggable="event.orig"
v-cal-click:event="event.orig" v-cal-click:event="event.orig"
> >
<slot :event="event.orig" mode="list" /> <slot :event="event.orig" mode="list" />
@@ -36,7 +36,9 @@ export default {
} }
} }
return events; return events;
}) }),
draggableEvents: () => false,
resizableEvents: () => false
}; };
}, },
inject: { inject: {
+109
View File
@@ -0,0 +1,109 @@
import BaseSlider from '../Base/Slider.js';
import TableView from './Table/View.js';
export default {
name: "ModeTable",
components: {
BaseSlider,
TableView
},
props: {
currentDate: {
type: luxon.DateTime,
required: true
}
},
emits: [
"update:currentDate",
"update:range",
"click",
"requestModalOpen"
],
data() {
return {
focusDate: this.currentDate,
rangeOffset: 0
};
},
computed: {
range() {
let first = this.focusDate.startOf('week', { useLocaleWeeks: true });
let last = this.focusDate.endOf('week', { useLocaleWeeks: true });
if (this.rangeOffset != 0) {
if (this.rangeOffset < 0) {
first = first.plus({ weeks: this.rangeOffset });
} else {
last = last.plus({ weeks: this.rangeOffset });
}
}
return luxon.Interval.fromDateTimes(first, last);
}
},
watch: {
currentDate() {
if (this.currentDate.locale != this.focusDate.locale) {
this.focusDate = this.currentDate;
this.$emit('update:range', this.range);
} else {
this.rangeOffset = this.currentDate.startOf('week', { useLocaleWeeks: true }).diff(this.focusDate.startOf('week', { useLocaleWeeks: true }), 'weeks').weeks;
if (this.rangeOffset) {
this.$emit('update:range', this.range);
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
}
}
}
},
methods: {
prevPage() {
this.rangeOffset = this.$refs.slider.target - 1;
this.$emit('update:range', this.range);
this.$refs.slider.prevPage().then(this.updatePage);
},
nextPage() {
this.rangeOffset = this.$refs.slider.target + 1;
this.$emit('update:range', this.range);
this.$refs.slider.nextPage().then(this.updatePage);
},
updatePage(weeks) {
const newFocusDate = this.focusDate.plus({ weeks });
this.focusDate = newFocusDate;
this.rangeOffset = 0;
this.$emit('update:currentDate', this.focusDate);
this.$emit('update:range', this.range);
},
viewAttrs(weeks) {
const day = this.focusDate.plus({ weeks });
return { ...this.$attrs, day };
},
handleClickDefaults(evt) {
switch (evt.detail.source) {
case 'day':
// default: Set current-date
this.$emit('update:currentDate', evt.detail.value);
break;
case 'event':
// default: Request Modal
this.$emit('requestModalOpen', { event: evt.detail.value });
break;
}
}
},
mounted() {
this.$emit('update:range', this.range);
},
template: `
<div
class="fhc-calendar-mode-week flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
>
<base-slider ref="slider" v-slot="slot">
<table-view ref="view" v-bind="viewAttrs(slot.offset)">
<template v-slot="slot"><slot v-bind="slot" mode="week" /></template>
</table-view>
</base-slider>
</div>
`
}
@@ -0,0 +1,162 @@
import {CoreFilterCmpt} from "../../../../components/filter/Filter.js";
import BsModal from '../../../Bootstrap/Modal.js';
import FormInput from "../../../Form/Input.js";
import ApiDetails from "../../../../api/lehrveranstaltung/details.js";
export default {
name: "TableView",
inject: {
events: "events",
timezone: "timezone",
tableActions: "tableActions",
},
components: {
CoreFilterCmpt,
BsModal,
FormInput
},
props: {
day: {
type: luxon.DateTime,
required: true
}
},
data()
{
return {
raumtyp_array: []
}
},
computed: {
start() {
return this.day.startOf('week', { useLocaleWeeks: true });
},
preparedEvents() {
const end = this.start.plus({ days: 7 });
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);
}
},
/* mounted() {
this.$api.call(ApiDetails.getRaumtyp())
.then(result => {
this.raumtyp_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},*/
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>
`
}
@@ -13,7 +13,8 @@ export default {
}, },
inject: { inject: {
timeGrid: "timeGrid", timeGrid: "timeGrid",
timezone: "timezone" timezone: "timezone",
hoursPlan: { from: "hoursPlan", default: null },
}, },
props: { props: {
day: { day: {
@@ -39,8 +40,9 @@ export default {
}; };
}); });
} else { } else {
// create 07:00-23:00 const start = this.hoursPlan?.start ?? 7;
return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 })); const end = this.hoursPlan?.end ?? 23;
return Array.from({ length: end - start + 1 }, (e, i) => luxon.Duration.fromObject({ hours: i + start }));
} }
} }
}, },

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