From c535ce5e2f78f5223662c97543f718334abf2a35 Mon Sep 17 00:00:00 2001 From: Christian Hofmaier Date: Tue, 8 Sep 2020 20:56:19 +0200 Subject: [locations] Migrate openingtimes from infoscreen to locations module - move openingtimes from infoscreen db to locations db - read-only openingtimes in infoscreen --- modules-available/locations/clientscript.js | 153 ++++++++++++++++++ modules-available/locations/config.json | 8 +- modules-available/locations/install.inc.php | 22 +++ .../locations/lang/de/template-tags.json | 14 ++ .../locations/lang/en/template-tags.json | 14 ++ modules-available/locations/pages/details.inc.php | 177 +++++++++++++++++++++ .../locations/templates/ajax-opening-location.html | 173 ++++++++++++++++++++ .../locations/templates/location-subnets.html | 44 ++++- .../locations/templates/locations.html | 5 + 9 files changed, 605 insertions(+), 5 deletions(-) create mode 100644 modules-available/locations/clientscript.js create mode 100644 modules-available/locations/templates/ajax-opening-location.html (limited to 'modules-available/locations') diff --git a/modules-available/locations/clientscript.js b/modules-available/locations/clientscript.js new file mode 100644 index 00000000..25c255fb --- /dev/null +++ b/modules-available/locations/clientscript.js @@ -0,0 +1,153 @@ +/* + * Generic helpers. + */ + +/** + * Initialize timepicker on given element. + */ +function setTimepicker($e) { + $e.timepicker({ + minuteStep: 15, + appendWidgetTo: 'body', + showSeconds: false, + showMeridian: false, + defaultTime: false + }); +} + +function getTime(str) { + if (!str) return false; + str = str.split(':'); + if (str.length !== 2) return false; + var h = parseInt(str[0].replace(/^0/, '')); + var m = parseInt(str[1].replace(/^0/, '')); + if (h < 0 || h > 23) return false; + if (m < 0 || m > 59) return false; + return h * 60 + m; +} + +const allDays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + +/* + * Opening times related... + */ + +var slxIdCounter = 0; + +/** + * Adds a new opening time to the table in expert mode. + */ +function newOpeningTime(vals) { + var $row = $('#expert-template').find('div.row').clone(); + if (vals['days'] && Array.isArray(vals['days'])) { + for (var i = 0; i < allDays.length; ++i) { + $row.find('.i-' + allDays[i]).prop('checked', vals['days'].indexOf(allDays[i]) !== -1); + } + } + $row.find('input').each(function() { + var $inp = $(this); + if ($inp.length === 0) return; + slxIdCounter++; + $inp.prop('id', 'id-inp-' + slxIdCounter); + $inp.siblings('label').prop('for', 'id-inp-' + slxIdCounter); + }); + $row.find('.i-openingtime').val(vals['openingtime']); + $row.find('.i-closingtime').val(vals['closingtime']); + $('#expert-table').append($row); + return $row; +} + +/** + * Convert fields from simple mode view to entries in expert mode. + * @returns {Array} + */ +function simpleToExpert() { + var retval = []; + if ($('#week-open').val() || $('#week-close').val()) { + retval.push({ + 'days': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], + 'openingtime': $('#week-open').val(), + 'closingtime': $('#week-close').val(), + 'tag': '#week' + }); + } + if ($('#saturday-open').val() || $('#saturday-close').val()) { + retval.push({ + 'days': ['Saturday'], + 'openingtime': $('#saturday-open').val(), + 'closingtime': $('#saturday-close').val(), + 'tag': '#saturday' + }); + } + if ($('#sunday-open').val() || $('#sunday-close').val()) { + retval.push({ + 'days': ['Sunday'], + 'openingtime': $('#sunday-open').val(), + 'closingtime': $('#sunday-close').val(), + 'tag': '#sunday' + }); + } + return retval; +} + +/** + * Triggered when the form is submitted + */ +function submitLocationSettings(event) { + var schedule, s, e; + var badFormat = false; + $('#settings-outer').find('.red-bg').removeClass('red-bg'); + if ($('#week-open').length > 0) { + schedule = simpleToExpert(); + for (var i = 0; i < schedule.length; ++i) { + s = getTime(schedule[i].openingtime); + e = getTime(schedule[i].closingtime); + if (s === false) { + $(schedule[i].tag + '-open').addClass('red-bg'); + badFormat = true; + } + if (e === false || e <= s) { + $(schedule[i].tag + '-close').addClass('red-bg'); + badFormat = true; + } + } + } else { + // Serialize + schedule = []; + $('#expert-table').find('.expert-row').each(function () { + var $t = $(this); + if ($t.find('.i-delete').is(':checked')) return; // Skip marked as delete + var entry = { + 'days': [], + 'openingtime': $t.find('.i-openingtime').val(), + 'closingtime': $t.find('.i-closingtime').val() + }; + for (var i = 0; i < allDays.length; ++i) { + if ($t.find('.i-' + allDays[i]).is(':checked')) { + entry['days'].push(allDays[i]); + } + } + if (entry.openingtime.length === 0 && entry.closingtime.length === 0 && entry.days.length === 0) return; // Also ignore empty lines + s = getTime(entry.openingtime); + e = getTime(entry.closingtime); + if (s === false) { + $t.find('.i-openingtime').addClass('red-bg'); + badFormat = true; + } + if (e === false || e <= s) { + $t.find('.i-closingtime').addClass('red-bg'); + badFormat = true; + } + if (entry.days.length === 0) { + $t.find('.days-box').addClass('red-bg'); + badFormat = true; + } + if (badFormat) return; + schedule.push(entry); + }); + } + if (badFormat) { + event.preventDefault(); + } + $('#json-openingtimes').val(JSON.stringify(schedule)); +} \ No newline at end of file diff --git a/modules-available/locations/config.json b/modules-available/locations/config.json index 110f8b67..ee2600f2 100644 --- a/modules-available/locations/config.json +++ b/modules-available/locations/config.json @@ -1,3 +1,9 @@ { - "category": "main.content" + "category": "main.content", + "dependencies": [ + "bootstrap_timepicker" + ], + "scripts": [ + "clientscript.js" + ] } \ No newline at end of file diff --git a/modules-available/locations/install.inc.php b/modules-available/locations/install.inc.php index d4e1b67b..31d53560 100644 --- a/modules-available/locations/install.inc.php +++ b/modules-available/locations/install.inc.php @@ -35,5 +35,27 @@ $res[] = tableAddConstraint('subnet', 'locationid', 'location', 'locationid', $res[] = tableAddConstraint('setting_location', 'locationid', 'location', 'locationid', 'ON UPDATE CASCADE ON DELETE CASCADE'); +// Update + +// 2020-07-14 Add openingtime column to location table, then migrate data and delete the column from locationinfo + +if (!tableHasColumn('location', 'openingtime')) { + if (Database::exec("ALTER TABLE location ADD openingtime BLOB") === false) { + finalResponse(UPDATE_FAILED, 'Could not create openingtime column'); + } else { + if (Module::get('locationinfo') !== false) { + if (Database::exec( + "UPDATE location, locationinfo_locationconfig + SET location.openingtime = locationinfo_locationconfig.openingtime + WHERE location.locationid = locationinfo_locationconfig.locationid") === false) { + finalResponse(UPDATE_FAILED, 'Could not migrate openingtime data from table to table'); + } + if (Database::exec("ALTER TABLE locationinfo_locationconfig DROP COLUMN openingtime") === false) { + finalResponse(UPDATE_FAILED, 'Could not delete openingtime column'); + } + } + } +} + // Create response for browser responseFromArray($res); diff --git a/modules-available/locations/lang/de/template-tags.json b/modules-available/locations/lang/de/template-tags.json index 2a03aa85..18176278 100644 --- a/modules-available/locations/lang/de/template-tags.json +++ b/modules-available/locations/lang/de/template-tags.json @@ -4,6 +4,8 @@ "lang_assignSubnetExplanation": "Rechner, die in einen der hier aufgef\u00fchrten Adressbereiche fallen, werden diesem Ort zugeschrieben und erhalten damit z.B. f\u00fcr diesen Raum angepasste Veranstaltungslisten.", "lang_assignedSubnets": "Zugeordnete Subnetze bzw. IP-Bereiche", "lang_bootMenu": "Bootmen\u00fc", + "lang_closingTime": "Schlie\u00dfungszeit", + "lang_day": "Tag", "lang_deleteChildLocations": "Untergeordnete Orte ebenfalls l\u00f6schen", "lang_deleteLocation": "Ort l\u00f6schen", "lang_deleteSubnet": "Bereich l\u00f6schen", @@ -11,6 +13,7 @@ "lang_editConfigVariables": "Konfig.-Variablen", "lang_editRoomplan": "Raumplan bearbeiten", "lang_endAddress": "Endadresse", + "lang_expertMode": "Experten Modus", "lang_fixMachineAssign": "Zuweisungen anzeigen\/aufheben", "lang_ip": "IP-Adresse", "lang_listOfSubnets": "Liste der Subnetze", @@ -29,17 +32,28 @@ "lang_matchingMachines": "Enthaltene Rechner", "lang_mismatchHeading": "Rechner mit widerspr\u00fcchlicher Raumzuweisung", "lang_mismatchIntroText": "Die hier aufgelisteten Rechner wurden mittels des Raumplaners im oben genannten Raum platziert. Ihrer IP-Adresse nach fallen diese jedoch in einen anderen Raum (durch die f\u00fcr diesen definierten IP-Ranges). Wenn Sie die entsprechenden Rechner hier markieren und auf \"Zur\u00fccksetzen\" klicken, werden die Rechner aus dem oben genannten Raumplan entfernt. Wenn Sie stattdessen auf \"Verschieben\" klicken, werden die Rechner mit ihrer aktuellen Position aus dem jetzigen Raum in den eigentlichen Raum (siehe letzte Spalte der Tabelle) verschoben.", + "lang_monTilFr": "Montag - Freitag", "lang_moveMachines": "In durch Subnet zugeordneten Raum verschieben", "lang_moveable": "Verschiebbar", "lang_name": "Name", "lang_numMachinesWithOverrides": "Anzahl Rechner, bei denen mindestens eine Konfigurationsvariable \u00fcberschrieben wird", + "lang_openingTime": "\u00d6ffnungszeit", "lang_overridenVarsForLocation": "Anzahl Variablen, die an diesem Ort \u00fcberschrieben werden", "lang_parentLocation": "\u00dcbergeordneter Ort", "lang_referencingLectures": "Veranstaltungen", "lang_resetMachines": "Raumzuweisung zur\u00fccksetzen", + "lang_saturday": "Samstag", + "lang_shortFriday": "Fr", + "lang_shortMonday": "Mo", + "lang_shortSaturday": "Sa", + "lang_shortSunday": "Su", + "lang_shortThursday": "Do", + "lang_shortTuesday": "Di", + "lang_shortWednesday": "Mi", "lang_showRoomplan": "Raumplan anzeigen", "lang_startAddress": "Startadresse", "lang_subnet": "IP-Bereich", + "lang_sunday": "Sonntag", "lang_sysConfig": "Lokalisierung", "lang_thisListByLocation": "Orte", "lang_thisListBySubnet": "Subnetze", diff --git a/modules-available/locations/lang/en/template-tags.json b/modules-available/locations/lang/en/template-tags.json index 3a79494c..c4fabdb0 100644 --- a/modules-available/locations/lang/en/template-tags.json +++ b/modules-available/locations/lang/en/template-tags.json @@ -4,6 +4,8 @@ "lang_assignSubnetExplanation": "Client machines which fall into an IP range listed below will be assigned to this location and will see an according lecture list (e.g. they will see lectures that are exclusively assigned to this location).", "lang_assignedSubnets": "Assigned subnets \/ IP ranges", "lang_bootMenu": "Boot menu", + "lang_closingTime": "Closing time", + "lang_day": "Day", "lang_deleteChildLocations": "Delete child locations as well", "lang_deleteLocation": "Delete location", "lang_deleteSubnet": "Delete range", @@ -11,6 +13,7 @@ "lang_editConfigVariables": "Config vars", "lang_editRoomplan": "Edit roomplan", "lang_endAddress": "End address", + "lang_expertMode": "Expert mode", "lang_fixMachineAssign": "Fix or remove assignment", "lang_ip": "IP address", "lang_listOfSubnets": "List of subnets", @@ -29,17 +32,28 @@ "lang_matchingMachines": "Matching clients", "lang_mismatchHeading": "Machines with mismatching room plan assignment", "lang_mismatchIntroText": "Machines listed here are assigned to the room above, but judging from their IP address, should actually be in another room (because of the IP range(s) assigned to that room). By selecting machines below and clicking \"reset\", they will be removed from their current room plan. If you choose \"move\", they will be transferred to the plan of the room they should actually belong to (see last column of table).", + "lang_monTilFr": "Monday - Friday", "lang_moveMachines": "Move to room designated by IP address", "lang_moveable": "Moveable", "lang_name": "Name", "lang_numMachinesWithOverrides": "Number of clients where at least one variable is overridden", + "lang_openingTime": "Opening Time", "lang_overridenVarsForLocation": "Number of variables that get overridden for this location", "lang_parentLocation": "Parent location", "lang_referencingLectures": "Assigned Lectures", "lang_resetMachines": "Reset room assignment", + "lang_saturday": "Saturday", + "lang_shortFriday": "Fri", + "lang_shortMonday": "Mon", + "lang_shortSaturday": "Sat", + "lang_shortSunday": "Sun", + "lang_shortThursday": "Thu", + "lang_shortTuesday": "Tue", + "lang_shortWednesday": "Wed", "lang_showRoomplan": "Show room plan", "lang_startAddress": "Start address", "lang_subnet": "IP range", + "lang_sunday": "Sunday", "lang_sysConfig": "Localization\/Integration", "lang_thisListByLocation": "Locations", "lang_thisListBySubnet": "Subnets", diff --git a/modules-available/locations/pages/details.inc.php b/modules-available/locations/pages/details.inc.php index 81b58456..a55460cf 100644 --- a/modules-available/locations/pages/details.inc.php +++ b/modules-available/locations/pages/details.inc.php @@ -8,6 +8,9 @@ class SubPage if ($action === 'updatelocation') { self::updateLocation(); return true; + } else if ($action === 'updateOpeningtimes') { + self::updateOpeningTimes(); + return true; } return false; } @@ -22,10 +25,75 @@ class SubPage if ($action === 'showlocation') { self::ajaxShowLocation(); return true; + } elseif ($action === 'getOpeningtimes') { + $id = Request::any('locid', 0, 'int'); + self::ajaxOpeningTimes($id); + return true; } return false; } + private static function updateOpeningTimes() { + $openingTimes = Request::post('openingtimes', '', 'string'); + $locationid = Request::post('locationid', false, 'int'); + + User::assertPermission('location.edit', $locationid); + + // Construct opening-times for database + if ($openingTimes !== '') { + $openingTimes = json_decode($openingTimes, true); + if (!is_array($openingTimes)) { + $openingTimes = ''; + } else { + $mangled = array(); + foreach (array_keys($openingTimes) as $key) { + $entry = $openingTimes[$key]; + if (!isset($entry['days']) || !is_array($entry['days']) || empty($entry['days'])) { + Message::addError('ignored-line-no-days'); + continue; + } + $start = self::getTime($entry['openingtime']); + $end = self::getTime($entry['closingtime']); + if ($start === false) { + Message::addError('ignored-invalid-start', $entry['openingtime']); + continue; + } + if ($end === false) { + Message::addError('ignored-invalid-end', $entry['closingtime']); + continue; + } + if ($end <= $start) { + Message::addError('ignored-invalid-range', $entry['openingtime'], $entry['closingtime']); + continue; + } + unset($entry['tag']); + $mangled[] = $entry; + } + if (empty($mangled)) { + $openingTimes = null; + } else { + $openingTimes = json_encode($mangled); + } + } + } + // Check if opening-times changed + // $res = Database::queryFirst('SELECT openingtime FROM location WHERE locationid = :locationid', compact('locationid')); + // $otChanged = $res === false || $res['openingtime'] !== $openingTimes; + + Database::exec('UPDATE location SET openingtime = :openingtime WHERE locationid = :locationid', + array('locationid' => $locationid, 'openingtime' => $openingTimes)); + + return true; + } + + private static function getTime($str) + { + $str = explode(':', $str); + if (count($str) !== 2) return false; + if ($str[0] < 0 || $str[0] > 23 || $str[1] < 0 || $str[1] > 59) return false; + return $str[0] * 60 + $str[1]; + } + private static function updateLocation() { $locationId = Request::post('locationid', false, 'integer'); @@ -313,4 +381,113 @@ class SubPage echo Render::parse('location-subnets', $data); } + private static function ajaxOpeningTimes($id) { + User::assertPermission('location.edit', $id); + $openTimes = Database::queryFirst("SELECT openingtime FROM `location` WHERE locationid = :id", array('id' => $id)); + if ($openTimes !== false) { + $openingTimes = json_decode($openTimes['openingtime'], true); + } + if (!isset($openingTimes) || !is_array($openingTimes)) { + $openingTimes = array(); + } + $data = array('id' => $id); + $data['expertMode'] = !self::isSimpleMode($openingTimes); + $data['schedule_data'] = json_encode($openingTimes); + + echo Render::parse('ajax-opening-location', $data); + } + + private static function isSimpleMode(&$array) { + if (empty($array)) + return true; + // Decompose by day + $new = array(); + foreach ($array as $row) { + $s = self::getTime($row['openingtime']); + $e = self::getTime($row['closingtime']); + if ($s === false || $e === false || $e <= $s) + continue; + foreach ($row['days'] as $day) { + self::addDay($new, $day, $s, $e); + } + } + // Merge by timespan, but always keep saturday and sunday separate + $merged = array(); + foreach ($new as $day => $ranges) { + foreach ($ranges as $range) { + if ($day === 'Saturday' || $day === 'Sunday') { + $add = $day; + } else { + $add = ''; + } + $key = '#' . $range[0] . '#' . $range[1] . '#' . $add; + if (!isset($merged[$key])) { + $merged[$key] = array(); + } + $merged[$key][$day] = true; + } + } + // Check if it passes as simple mode + if (count($merged) > 3) + return false; + foreach ($merged as $days) { + if (count($days) === 5) { + $res = array_keys($days); + $res = array_intersect($res, array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday")); + if (count($res) !== 5) + return false; + } elseif (count($days) === 1) { + if (!isset($days['Saturday']) && !isset($days['Sunday'])) { + return false; + } + } else { + return false; + } + } + // Valid simple mode, finally transform back to what we know + $new = array(); + foreach ($merged as $span => $days) { + preg_match('/^#(\d+)#(\d+)#/', $span, $out); + $new[] = array( + 'days' => array_keys($days), + 'openingtime' => floor($out[1] / 60) . ':' . ($out[1] % 60), + 'closingtime' => floor($out[2] / 60) . ':' . ($out[2] % 60), + ); + } + $array = $new; + return true; + } + + private static function addDay(&$array, $day, $s, $e) + { + if (!isset($array[$day])) { + $array[$day] = array(array($s, $e)); + return; + } + foreach (array_keys($array[$day]) as $key) { + $current = $array[$day][$key]; + if ($s <= $current[0] && $e >= $current[1]) { + // Fully dominated + unset($array[$day][$key]); + continue; // Might partially overlap with additional ranges, keep going + } + if ($current[0] <= $s && $current[1] >= $s) { + // $start lies within existing range + if ($current[0] <= $e && $current[1] >= $e) + return; // Fully in existing range, do nothing + // $end seems to extend range we're checking against but $start lies within this range, update and keep going + $s = $current[0]; + unset($array[$day][$key]); + continue; + } + // Last possibility: $start is before range, $end within range + if ($current[0] <= $e && $current[1] >= $e) { + // $start must lie before range start, otherwise we'd have hit the case above + $e = $current[1]; + unset($array[$day][$key]); + continue; + } + } + $array[$day][] = array($s, $e); + } } \ No newline at end of file diff --git a/modules-available/locations/templates/ajax-opening-location.html b/modules-available/locations/templates/ajax-opening-location.html new file mode 100644 index 00000000..09fe7869 --- /dev/null +++ b/modules-available/locations/templates/ajax-opening-location.html @@ -0,0 +1,173 @@ + +
+

{{lang_openingTime}}

+ {{^expertMode}} +
+ +
+ {{lang_expertMode}} +
+
+ + + + + + + + + + + + + + + + + + + + + + +
{{lang_day}}{{lang_openingTime}}{{lang_closingTime}}
{{lang_monTilFr}} +
+ + + + +
+
+
+ + + + +
+
{{lang_saturday}} +
+ + + + +
+
+
+ + + + +
+
{{lang_sunday}} +
+ + + + +
+
+
+ + + + +
+
+
+ {{/expertMode}} + +
+
+
+
+
{{lang_openingTime}}
+
{{lang_closingTime}}
+
{{lang_delete}}
+
+
+
+
+ + + {{lang_openingTime}} + +
+
+
+
+ + + + diff --git a/modules-available/locations/templates/location-subnets.html b/modules-available/locations/templates/location-subnets.html index 6062b559..b85ddbec 100644 --- a/modules-available/locations/templates/location-subnets.html +++ b/modules-available/locations/templates/location-subnets.html @@ -62,7 +62,7 @@
{{lang_locationInfo}}
-
+
{{#haveDozmod}}
{{lang_referencingLectures}}: {{lectures}} @@ -81,7 +81,13 @@
{{/haveStatistics}}
-
+
+ +
+
{{#roomplanner}} @@ -91,7 +97,7 @@ {{/roomplanner}}
-
+
@@ -130,4 +136,34 @@
-
\ No newline at end of file +
+ + + + \ No newline at end of file diff --git a/modules-available/locations/templates/locations.html b/modules-available/locations/templates/locations.html index 7adfe2fc..12d401c1 100644 --- a/modules-available/locations/templates/locations.html +++ b/modules-available/locations/templates/locations.html @@ -250,5 +250,10 @@ function deleteSubnetWarning(locid) { form.submit(); } } + +function loadOpeningTimes(locid) { + $("#openingTimesModal" + locid).find('.modal-body').load("?do=Locations&page=details&action=getOpeningtimes&locid=" + locid) +} + // --> -- cgit v1.2.3-55-g7522