summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.idea/php.xml9
-rw-r--r--doc/locationinfo114
-rw-r--r--modules-available/locationinfo/api.inc.php397
-rw-r--r--modules-available/locationinfo/config.json3
-rwxr-xr-xmodules-available/locationinfo/frontend/doorsign.html1804
-rwxr-xr-xmodules-available/locationinfo/frontend/img/overlay/rollstuhl.svg104
-rw-r--r--modules-available/locationinfo/frontend/img/pc_defect.svg58
-rw-r--r--modules-available/locationinfo/frontend/img/pc_defect_eink.svg58
-rw-r--r--modules-available/locationinfo/frontend/img/pc_free.svg50
-rw-r--r--modules-available/locationinfo/frontend/img/pc_free_eink.svg57
-rw-r--r--modules-available/locationinfo/frontend/img/pc_off.svg50
-rw-r--r--modules-available/locationinfo/frontend/img/pc_off_eink.svg50
-rw-r--r--modules-available/locationinfo/frontend/img/pc_used.svg50
-rw-r--r--modules-available/locationinfo/frontend/img/pc_used_eink.svg58
-rwxr-xr-xmodules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.css284
-rwxr-xr-xmodules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.iml10
-rwxr-xr-xmodules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.js2961
-rw-r--r--modules-available/locationinfo/frontend/panel.html858
-rw-r--r--modules-available/locationinfo/hooks/translation.inc.php24
-rw-r--r--modules-available/locationinfo/inc/coursebackend.inc.php365
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php133
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php113
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php352
-rw-r--r--modules-available/locationinfo/inc/locationinfo.inc.php63
-rw-r--r--modules-available/locationinfo/install.inc.php34
-rw-r--r--modules-available/locationinfo/lang/de/backend-davinci.json8
-rw-r--r--modules-available/locationinfo/lang/de/backend-dummy.json14
-rw-r--r--modules-available/locationinfo/lang/de/backend-hisinone.json16
-rw-r--r--modules-available/locationinfo/lang/de/messages.json8
-rw-r--r--modules-available/locationinfo/lang/de/module.json5
-rw-r--r--modules-available/locationinfo/lang/de/template-tags.json73
-rw-r--r--modules-available/locationinfo/lang/en/backend-davinci.json8
-rw-r--r--modules-available/locationinfo/lang/en/backend-dummy.json14
-rw-r--r--modules-available/locationinfo/lang/en/backend-hisinone.json16
-rw-r--r--modules-available/locationinfo/lang/en/messages.json8
-rw-r--r--modules-available/locationinfo/lang/en/module.json5
-rw-r--r--modules-available/locationinfo/lang/en/template-tags.json73
-rw-r--r--modules-available/locationinfo/lang/pt/template-tags.json3
-rw-r--r--modules-available/locationinfo/page.inc.php569
-rw-r--r--modules-available/locationinfo/templates/_page.html4
-rw-r--r--modules-available/locationinfo/templates/config.html395
-rw-r--r--modules-available/locationinfo/templates/location-info.html219
-rw-r--r--modules-available/locationinfo/templates/server-prop-bool.html16
-rw-r--r--modules-available/locationinfo/templates/server-prop-dropdown.html19
-rw-r--r--modules-available/locationinfo/templates/server-prop-generic.html16
-rw-r--r--modules-available/locationinfo/templates/server-settings.html91
-rw-r--r--modules-available/locationinfo/templates/timetable.html216
-rw-r--r--modules-available/locations/inc/location.inc.php12
-rw-r--r--modules-available/rebootcontrol/inc/rebootqueries.inc.php4
49 files changed, 9832 insertions, 39 deletions
diff --git a/.idea/php.xml b/.idea/php.xml
deleted file mode 100644
index 4052c39a..00000000
--- a/.idea/php.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
- <component name="PhpProjectSharedConfiguration" php_language_level="5.4.0" />
- <component name="PhpUnit">
- <phpunit_settings>
- <PhpUnitSettings />
- </phpunit_settings>
- </component>
-</project> \ No newline at end of file
diff --git a/doc/locationinfo b/doc/locationinfo
new file mode 100644
index 00000000..e8f4ea50
--- /dev/null
+++ b/doc/locationinfo
@@ -0,0 +1,114 @@
+########## API ##########
+
+/slx-admin/api.php?do=locationinfo
+
+&action=roominfo
+&id= (1 or 1,2,3…)
+[optional] &coords= (true/false)
+Returns an array with the information of the room(s). (JSON) pcState => (IDLE, OCCUPIED, OFF, BROKEN))
+e.g.:
+[{"id":"11","computer":[{"id":"6BF41E7F-C663-E211-9BAC-C5625F50F9E8","pcState":"OFF"}]}]
+
+&action=openingtime
+&id= (1 or 1,2,3...)
+Return an array with the openingtime of the room(s). (JSON)
+e.g.:
+[{"id":"11","openingtime":{"Monday":[{"HourOpen":"7","MinutesOpen":"00","HourClose":"20","MinutesClose":"00"}],"Tuesday":[{"HourOpen":"7","MinutesOpen":"00","HourClose":"20","MinutesClose":"00"}],"Wednesday":[{"HourOpen":"7","MinutesOpen":"00","HourClose":"20","MinutesClose":"00"}],"Thursday":[{"HourOpen":"7","MinutesOpen":"00","HourClose":"20","MinutesClose":"00"}],"Friday":[{"HourOpen":"7","MinutesOpen":"00","HourClose":"20","MinutesClose":"00"}],"Saturday":[{"HourOpen":"9","MinutesOpen":"00","HourClose":"13","MinutesClose":"00"}]}}]
+
+&action=roomtree
+&id= (1 or 1,2,3...)
+Returns the roomtree. (JSON)
+e.g.:
+[{"id":"8","name":"4er-Netz","childs":[{"id":"1","name":"4er-Netz(1-10)","childs":[{"id":"5","name":"bwLehrstuhl 4.4","childs":[]}]},{"id":"11","name":"Raum -113","childs":[]}]}]
+
+&action=config
+&id= (1)
+Return the config of a room. (JSON)
+e.g.:
+{"language":"en","mode":1,"vertical":false,"eco":false,"scaledaysauto":false,"daystoshow":7,"rotation":0,"scale":50,"switchtime":20,"calupdate":30,"roomupdate":30,"configupdate":180,"room":"Raum -113","time":"2017-3-27 2:36:40"}
+
+&action=pcstates
+&id= (1 or 1,2,3...)
+Returns an array of the state stats of the room(s). (JSON)
+e.g.:
+[{"id":"11","idle":0,"occupied":0,"off":1,"broken":0}]
+
+&action=calendar
+&id= (1 or 1,2,3...)
+Returns an array with the calendar of the room(s). (JSON)
+e.g.:
+[{"id":7,"calendar":[{"title":"test exam","start":"2017-3-08 13:00:00","end":"2017-3-08 16:00:00"}]}]
+
+##############################
+########## Frontend ##########
+##############################
+
+Doorsign
+========
+
+1.Usage
+=======
+parameter
+
+required:
+ id: [integer] room id, see in admin panel. For e.g.: id=5 or multiple, up to 4 e.g.: id=5,6,7,8
+
+optional:
+
+ lang:[en,de] set the language
+ mode:[1,2,3,4] sets the displaying
+ 1: Calendar & Room
+ 2: only Calendar
+ 3: only Room
+ 4: Calendar & Room alternately
+ daystoshow:[1,2,3,4,5,6,7] sets how many days the calendar shows
+ scale:[10-90] scales the calendar and Roomplan in mode 1
+ switchtime:[1-120] sets the time between switchen in mode 4 (in seconds)
+ calupdate: Time the calender querys for updates,in minutes.
+ roomupdate: Time the PCs in the room gets updated,in seconds.
+ rotation:[0-3] rotation of the roomplan
+ vertical:[true] only mode 1, sets the calendar above the roomplan
+ configupdate: Time interval the config gets updated (in minutes)
+ scaledaysauto: [true] if true it finds automatically the daystoshow parameter depending on display size
+
+
+All the optional parameters will overwrite the config settings in the admin panel. If more then one room is shown,then it will
+overwrite it for all rooms.
+
+2. Add an overlay
+=================
+First you need an Image(svg,png,jpg), add it to ./locationinfo/frontend/img/overlay.
+You can add your own css class if you want. To do so create an css calss named .overlay-YOUR_IMAGE_NAME in the doorsign.html.
+You can find an example in the doorsign.html called ".overlay-rollstuhl".
+The backend functionaltiy is right now not implemented since it relays on the roominfo module.
+But you can add it manually.
+You need to add the image name (without ending) in the machine database on the position column with the key overlays in an array.
+
+For example (the name of the images could be overlay1.jpg, overlay1.svg):
+
+{"gridRow":"41","gridCol":"48","itemlook":"pc-south", "overlays":["overlay1","overlay2"]}
+
+
+3.Tipps & Tricks
+==================
+
+-if you show 2-3 rooms in mode 1, it's useful to use vertical mode.
+-if calendar items don't fit, show less days or if in mode 1 give the calendar more space(calendar width);
+-it is possible to use different modes if you show more then one room
+
+4.CourseBackend
+===============
+fetchSchedule returns an array containing an array as value and the local room ID as key.
+The contained array contains arrays that have this form ["start"=>'JJJJ-MM-DD HH:MM:SS',"end"=>'JJJJ-MM-DD HH:MM:SS',"title"=>string].
+getError returns the last errormessage.
+checkConection uses a hardcoded room for test purposes.
+If you want to write a new Class you can look at the Dummy Class to learn the structure.
+You also should write the language files for your options into the lang directory.
+
+Panel
+=====
+
+parameter
+
+required:
+ id: [integer] room id, see in admin panel. For e.g.: id=5 or multiple, up to 4 e.g.: id=5,6,7,8
diff --git a/modules-available/locationinfo/api.inc.php b/modules-available/locationinfo/api.inc.php
index 0d84ebce..9b698d65 100644
--- a/modules-available/locationinfo/api.inc.php
+++ b/modules-available/locationinfo/api.inc.php
@@ -1,7 +1,394 @@
<?php
-echo json_encode(array(
- 'key' => 'value',
- 'number' => 123,
- 'list' => array(1,2,3,4,5,6,'foo')
-));
+HandleParameters();
+
+/**
+ * Handles the API paramenters.
+ */
+function HandleParameters()
+{
+
+ $getAction = Request::get('action', 0, 'string');
+ $output = false;
+ if ($getAction == "locationinfo") {
+ $locationIds = Request::get('id', 0, 'string');
+ $array = filterIdList($locationIds);
+ $getCoords = Request::get('coords', false, 'bool');
+ $output = getLocationInfo($array, $getCoords);
+ } elseif ($getAction == "openingtime") {
+ $locationIds = Request::get('id', 0, 'string');
+ $array = filterIdList($locationIds);
+ $output = getOpeningTime($array);
+ } elseif ($getAction == "config") {
+ $locationId = Request::get('id', 0, 'int');
+ $output = getConfig($locationId);
+ } elseif ($getAction == "pcstates") {
+ $locationIds = Request::get('id', 0, 'string');
+ $array = filterIdList($locationIds);
+ $output = getPcStates($array);
+ } elseif ($getAction == "locationtree") {
+ $locationIds = Request::get('id', 0, 'string');
+ $array = filterIdList($locationIds);
+ $output = getLocationTree($array);
+ } elseif ($getAction == "calendar") {
+ $locationIds = Request::get('id', 0, 'string');
+ $array = filterIdList($locationIds);
+ $output = getCalendar($array);
+ }
+ if ($output !== false) {
+ echo json_encode($output);
+ }
+}
+
+/**
+ * Filters the id list. Removes Double / non-int / hidden locations.
+ *
+ * @param string $locationIds comma separated list of location ids
+ * @return array The filtered array of the location ids.
+ */
+function filterIdList($locationIds)
+{
+ $idList = explode(',', $locationIds);
+ $filteredIdList = array_filter($idList, 'is_numeric');
+ $filteredIdList = array_unique($filteredIdList);
+ $filteredIdList = filterHiddenLocations($filteredIdList);
+
+ return $filteredIdList;
+}
+
+/**
+ * Filters the hidden locations from an array.
+ *
+ * @param int[] $idArray Id list
+ * @return array Filtered id list
+ */
+function filterHiddenLocations($idArray)
+{
+ $idArray = array_flip($idArray);
+ if (!empty($idArray)) {
+ $query = "SELECT locationid FROM `locationinfo_locationconfig` WHERE hidden <> 0 AND locationid IN (:idlist)";
+ $dbquery = Database::simpleQuery($query, array('idlist' => $idArray));
+ while ($dbresult = $dbquery->fetch(PDO::FETCH_ASSOC)) {
+ unset($idArray[$dbresult['locationid']]);
+ }
+ }
+
+ return array_flip($idArray);
+}
+
+// ########## <Locationinfo> ##########
+/**
+ * Gets the location info of the given locations.
+ *
+ * @param int[] $idList list of ids.
+ * @param bool $coords Defines if coords should be included or not.
+ * @return array location info struct
+ */
+function getLocationInfo($idList, $coords = false)
+{
+ if (empty($idList))
+ return [];
+
+ $positionCol = $coords ? 'm.position,' : '';
+ $query = "SELECT m.locationid, m.machineuuid, $positionCol m.logintime, m.lastseen, m.lastboot FROM machine m
+ WHERE m.locationid IN (:idlist)";
+ $dbquery = Database::simpleQuery($query, array('idlist' => $idList));
+
+ // Iterate over matching machines
+ $dbresult = array();
+ while ($dbdata = $dbquery->fetch(PDO::FETCH_ASSOC)) {
+
+ // Set the id if the locationid changed.
+ if (!isset($dbresult[$dbdata['locationid']])) {
+ $dbresult[$dbdata['locationid']] = array('id' => $dbdata['locationid'], 'computer' => array());
+ }
+ // Compact the pc data in one array.
+ $pc = array('id' => $dbdata['machineuuid']);
+ if ($coords && !empty($dbdata['position'])) {
+ $position = json_decode($dbdata['position'], true);
+ if (isset($position['gridCol']) && isset($position['gridRow'])) {
+ $pc['x'] = $position['gridCol'];
+ $pc['y'] = $position['gridRow'];
+ if (isset($position['overlays']) && is_array($position['overlays'])) {
+ $pc['overlays'] = $position['overlays'];
+ } else {
+ $pc['overlays'] = array();
+ }
+ }
+ }
+ $pc['pcState'] = LocationInfo::getPcState($dbdata);
+
+ // Add the array to the computer list.
+ $dbresult[$dbdata['locationid']]['computer'][] = $pc;
+ }
+
+ // The array keys are only used for the isset -> Return only the values.
+ return array_values($dbresult);
+}
+
+// ########## </Locationinfo> ###########
+
+// ########## <Openingtime> ##########
+/**
+ * Gets the Opening time of the given locations.
+ *
+ * @param int[] $idList list of locations
+ * @return array Opening times struct
+ */
+function getOpeningTime($idList)
+{
+ $locations = Location::getLocationsAssoc();
+ $allIds = $idList;
+ foreach ($idList as $id) {
+ if (isset($locations[$id]) && isset($locations[$id]['parents'])) {
+ $allIds = array_merge($allIds, $locations[$id]['parents']);
+ }
+ }
+ if (empty($allIds))
+ return [];
+ $openingTimes = array();
+ $qs = '?' . str_repeat(',?', count($allIds) - 1);
+ $res = Database::simpleQuery("SELECT locationid, openingtime FROM locationinfo_locationconfig WHERE locationid IN ($qs)",
+ array_values($allIds));
+ while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
+ $openingTimes[(int)$row['locationid']] = $row['openingtime'];
+ }
+ $returnValue = array();
+ foreach ($idList as $locationid) {
+ $id = $locationid;
+ while ($id !== 0) {
+ if (!empty($openingTimes[$id])) {
+ $cal = json_decode($openingTimes[$id], true);
+ if (is_array($cal)) {
+ $cal = formatOpeningtime($cal);
+ }
+ if (!empty($cal)) {
+ $returnValue[] = array(
+ 'id' => $locationid,
+ 'openingtime' => $cal,
+ );
+ break;
+ }
+ }
+ $id = $locations[$id]['parentlocationid'];
+ }
+ }
+ return $returnValue;
+}
+
+/**
+ * Format the openingtime in the frontend needed format.
+ * One key per week day, wich contains an array of {
+ * 'HourOpen' => hh, 'MinutesOpen' => mm,
+ * 'HourClose' => hh, 'MinutesClose' => mm }
+ *
+ * @param array $openingtime The opening time in the db saved format.
+ * @return mixed The opening time in the frontend needed format.
+ */
+function formatOpeningtime($openingtime)
+{
+ $result = array();
+ $weekarray = array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday");
+ foreach ($weekarray as $d) {
+ $array = array();
+ foreach ($openingtime as $opt) {
+ foreach ($opt['days'] as $val) {
+ if ($val == $d) {
+ $arr = array();
+
+ $openTime = explode(':', $opt['openingtime']);
+ $arr['HourOpen'] = $openTime[0];
+ $arr['MinutesOpen'] = $openTime[1];
+
+ $closeTime = explode(':', $opt['closingtime']);
+ $arr['HourClose'] = $closeTime[0];
+ $arr['MinutesClose'] = $closeTime[1];
+
+ $array[] = $arr;
+ }
+ }
+ if (!empty($array)) {
+ $result[$d] = $array;
+ }
+ }
+ }
+ return $result;
+}
+
+// ########## </Openingtime> ##########
+/**
+ * Gets the config of the location.
+ *
+ * @param int $locationID ID of the location
+ * @return array configuration struct
+ */
+function getConfig($locationID)
+{
+ $dbresult = Database::queryFirst("SELECT l.locationname, li.config FROM `location` AS l
+ LEFT JOIN `locationinfo_locationconfig` AS li ON (l.locationid = li.locationid)
+ WHERE l.locationid = :locationID", array('locationID' => $locationID));
+
+ $config = defaultConfig();
+
+ if ($dbresult !== false) {
+ if (!empty($dbresult['config'])) {
+ $json = json_decode($dbresult['config'], true);
+ if (is_array($json)) {
+ $config = $json + $config;
+ }
+ }
+ $config['room'] = $dbresult['locationname'];
+ }
+
+ $config['time'] = date('Y-m-d H:i:s');
+
+ return $config;
+}
+
+/**
+ * Creates and returns a default config for room that didn't saved a config yet.
+ *
+ * @return array Return a default config.
+ */
+function defaultConfig()
+{
+ return array(
+ 'language' => 'en',
+ 'mode' => 1,
+ 'vertical' => false,
+ 'eco' => false,
+ 'scaledaysauto' => true,
+ 'daystoshow' => 7,
+ 'rotation' => 0,
+ 'scale' => 50,
+ 'switchtime' => 20,
+ 'calupdate' => 30,
+ 'roomupdate' => 5,
+ 'configupdate' => 180,
+ );
+}
+
+/**
+ * Gets the pc states of the given locations.
+ *
+ * @param int[] $idList list of the location ids.
+ * @return string PC state JSON
+ */
+function getPcStates($idList)
+{
+ $pcStates = array();
+
+ $locationInfoList = getLocationInfo($idList);
+ foreach ($locationInfoList as $locationInfo) {
+ $result = array(
+ 'id' => $locationInfo['id'],
+ 'idle' => 0,
+ 'occupied' => 0,
+ 'off' => 0,
+ 'broken' => 0,
+ );
+
+ foreach ($locationInfo['computer'] as $computer) {
+ $key = strtolower($computer['pcState']);
+ if (isset($result[$key])) {
+ $result[$key]++;
+ }
+ }
+
+ $pcStates[] = $result;
+ }
+ return $pcStates;
+}
+
+/**
+ * Gets the location tree of the given locations.
+ *
+ * @param int[] $idList Array list of the locations.
+ * @return array location tree data
+ */
+function getLocationTree($idList)
+{
+ $locations = Location::getTree();
+
+ $ret = findLocations($locations, $idList);
+ return $ret;
+}
+
+function findLocations($locations, $idList)
+{
+ $ret = array();
+ foreach ($locations as $location) {
+ if (in_array($location['locationid'], $idList)) {
+ $ret[] = $location;
+ } elseif (!empty($location['children'])) {
+ $ret = array_merge($ret, findLocations($location['children'], $idList));
+ }
+ }
+ return $ret;
+}
+
+// ########## <Calendar> ###########
+/**
+ * Gets the calendar of the given ids.
+ *
+ * @param int[] $idList list with the location ids.
+ * @return string Calendar JSON.
+ */
+function getCalendar($idList)
+{
+ if (empty($idList))
+ return [];
+
+ // Build SQL query for multiple ids.
+ $query = "SELECT l.locationid, l.serverid, l.serverlocationid, s.servertype, s.credentials
+ FROM `locationinfo_locationconfig` AS l
+ INNER JOIN locationinfo_coursebackend AS s ON (s.serverid = l.serverid)
+ WHERE l.hidden = 0 AND l.locationid IN (:idlist)
+ ORDER BY s.servertype ASC";
+ $dbquery = Database::simpleQuery($query, array('idlist' => array_values($idList)));
+
+ $serverList = array();
+ while ($dbresult = $dbquery->fetch(PDO::FETCH_ASSOC)) {
+ if (!isset($serverList[$dbresult['serverid']])) {
+ $serverList[$dbresult['serverid']] = array(
+ 'credentials' => json_decode($dbresult['credentials'], true),
+ 'type' => $dbresult['servertype'],
+ 'idlist' => array()
+ );
+ }
+ $serverList[$dbresult['serverid']]['idlist'][] = $dbresult['locationid'];
+ }
+
+ $resultArray = array();
+ foreach ($serverList as $serverid => $server) {
+ $serverInstance = CourseBackend::getInstance($server['type']);
+ if ($serverInstance === false) {
+ EventLog::warning('Cannot fetch schedule for locationid ' . $server['locationid']
+ . ': Backend type ' . $server['type'] . ' unknown. Disabling location.');
+ Database::exec("UPDATE locationinfo_locationconfig SET serverid = 0 WHERE locationid = :lid",
+ array('lid' => $server['locationid']));
+ continue;
+ }
+ $credentialsOk = $serverInstance->setCredentials($serverid, $server['credentials']);
+
+ if ($credentialsOk) {
+ $calendarFromBackend = $serverInstance->fetchSchedule($server['idlist']);
+ } else {
+ $calendarFromBackend = array();
+ }
+
+ LocationInfo::setServerError($serverid, $serverInstance->getError());
+
+ if (is_array($calendarFromBackend)) {
+ foreach ($calendarFromBackend as $key => $value) {
+ $resultArray[] = array(
+ 'id' => $key,
+ 'calendar' => $value,
+ );
+ }
+ }
+ }
+ return $resultArray;
+}
+
+// ########## </Calendar> ##########
diff --git a/modules-available/locationinfo/config.json b/modules-available/locationinfo/config.json
index 706412d0..d4cab3f0 100644
--- a/modules-available/locationinfo/config.json
+++ b/modules-available/locationinfo/config.json
@@ -1,3 +1,4 @@
{
- "category":"main.content"
+ "category":"main.content",
+ "dependencies": ["js_jqueryui", "bootstrap_timepicker", "locations", "bootstrap_switch"]
}
diff --git a/modules-available/locationinfo/frontend/doorsign.html b/modules-available/locationinfo/frontend/doorsign.html
new file mode 100755
index 00000000..6c943e03
--- /dev/null
+++ b/modules-available/locationinfo/frontend/doorsign.html
@@ -0,0 +1,1804 @@
+<!--
+
+parameter
+
+required:
+ id: [integer] room id, see in admin panel
+optional:
+
+ lang:[en,de] set the language
+ mode:[1,2,3,4] sets the displaying
+ 1: Calendar & Room
+ 2: only Calendar
+ 3: only Room
+ 4: Calendar & Room alternately
+ daystoshow:[1,2,3,4,5,6,7] sets how many days the calendar shows
+ scale:[10-90] scales the calendar and Roomplan in mode 1
+ switchtime:[1-120] sets the time between switchen in mode 4 (in seconds)
+ calupdate: Time the calender querys for updates,in minutes.
+ roomupdate: Time the PCs in the room gets updated,in seconds.
+ rotation:[0-3] rotation of the roomplan
+ vertical:[true] only mode 1, sets the calendar above the roomplan
+ configupdate: Time interval the config gets updated (in minutes)
+ scaledaysauto: [true] if true it finds automaticly the daystoshow parameter depending on display size
+
+-->
+<!DOCTYPE html>
+<html lang="de">
+<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8">
+<head>
+ <title>DoorSign</title>
+ <script type='text/javascript' src='../../../script/jquery.js'></script>
+ <script type='text/javascript' src='../../js_jqueryui/clientscript.js'></script>
+ <link rel='stylesheet' type='text/css' href='../../js_jqueryui/style.css'/>
+ <link rel='stylesheet' type='text/css' href='jquery-week-calendar/jquery.weekcalendar.css'/>
+
+ <script type='text/javascript' src="jquery-week-calendar/jquery.weekcalendar.js"></script>
+ <style type='text/css'>
+
+ body {
+ font-family: "Lucida Grande", Helvetica, Arial, Verdana, sans-serif;
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ float: left;
+ box-sizing: border-box;
+
+ background-color: #cacaca;
+ overflow: hidden;
+ position: absolute;
+ display: table;
+
+ }
+
+ .row {
+ background-color: #404040;
+ box-shadow: 0 0.1875rem 0.375rem rgba(0, 0, 0, 0.25);
+ margin-bottom: 4px;
+
+ width: 100%;
+
+ }
+
+ .header {
+ display: table;
+ color: white;
+ padding: 0;
+ height: 8vw;
+
+ }
+
+ .progressbar {
+ width: 0px;
+ height: 2px;
+ position: absolute;
+ background-color: red;
+ bottom: 2px;
+ z-index: 1;
+ }
+
+ .font {
+ display: table-cell;
+ vertical-align: middle;
+ font-size: 3vw;
+ font-weight: bold
+ }
+
+ .courseText {
+ text-align: center;
+ }
+
+ .row::after {
+ content: "";
+ clear: both;
+ display: block;
+ }
+
+ [class*="col-"] {
+ float: left;;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ .col-1 {
+ width: 33%;
+ }
+
+ .col-2 {
+ width: 33%;
+ }
+
+ .roomLayoutDesign {
+ position: relative;
+ float: left;
+ boxSizing: border-box;
+ }
+
+ .roompadding {
+ float: left;
+ position: relative;
+ }
+
+ .room {
+ position: relative;
+ background-color: white;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ border-width: 1px;
+ border-color: darkgrey;
+ border-style: solid;
+
+ }
+
+ .calendar {
+ float: left;
+ padding: 0;
+ dboxSizing: border-box;
+ background: linear-gradient(#cccccc, white);
+
+ }
+
+ .free-busy-busy {
+ background: grey;
+ }
+
+ .ui-widget-content {
+ color: white;
+ }
+
+ .wc-header {
+ background-color: #404040;
+ font-weight: bold;
+ }
+
+ .ui-state-default {
+ text-shadow: none;
+ }
+
+ .square {
+ display: table;
+ float: right;
+ padding: 0;
+ width: 8vw;
+ height: 8vw;
+ }
+
+ .FreeSeatsFont {
+ display: table-cell;
+ vertical-align: middle;
+ font-size: 6vw;
+ color: white;
+ top: 0;
+ margin: 0 auto;
+ position: relative;
+ text-align: center;
+ font-weight: bold;
+ overflow: visible;
+ }
+
+ .PCImgDiv {
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ display: inline-block;
+
+ }
+
+ .OverlayDiv {
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ display: table;
+ }
+
+ .pcImg {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+
+ }
+
+ .overlay {
+ position: relative;
+ width: 50%;
+ height: 50%;
+ opacity: 0.5;
+ float: left;
+ z-index: 5;
+ }
+
+ .overlay-rollstuhl {
+ width: 25%;
+ height: 50%;
+ background-color: white;
+ opacity: 0.5;
+ float: left;
+ }
+
+ .wc-scrollable-grid {
+
+ }
+
+ .ui-widget-content .ui-state-active {
+ font-weight: bold;
+ color: black;
+ }
+
+ .wc-container {
+ font-weight: bold;
+ }
+
+ .wc-today {
+ background-color: white;
+ }
+
+ .wc-time-header-cell {
+ background-color: #eeeeee;
+ border: none;
+ }
+
+ .ui-corner-all {
+ moz-border-radius-bottomright: 0;
+ webkit-border-bottom-right-radius: 0;
+ -khtml-border-bottom-right-radius: 0;
+ border-bottom-right-radius: 0;
+
+ moz-border-radius-topright: 0;
+ webkit-border-top-right-radius: 0;
+ -khtml-border-top-right-radius: 0;
+ border-top-right-radius: 0;
+
+ moz-border-radius-bottomleft: 0;
+ webkit-border-bottom-left-radius: 0;
+ -khtml-border-bottom-left-radius: 0;
+ border-bottom-left-radius: 0;
+
+ moz-border-radius-topleft: 0;
+ webkit-border-left-right-radius: 0;
+ -khtml-border-left-right-radius: 0;
+ border-top-left-radius: 0;
+ }
+
+ .wc-scrollable-grid .wc-day-column-first {
+ border-style: solid;
+
+ }
+
+ [class*="wc-day-"] {
+ border-color: grey;
+ }
+
+
+ </style>
+ <script type='text/javascript'>
+
+ var rooms = {};
+ var lastSwitchTime;
+ var roomsToshow = 0;
+ var roomIds;
+ //Todo change these
+ var date;
+ var supportSvg = typeof SVGRect != "undefined";
+ var calendarQueryUrl;
+ var translation = {
+ "en": {
+ "room": "Room",
+ "closed": "Closed",
+ "free": "Free",
+ "shortDays": ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ "longDays": ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+ "to": "to"
+
+ },
+ "de": {
+ "room": "Raum",
+ "closed": "Geschlossen",
+ "free": "Frei",
+ "shortDays": ["Son", "Mon", "Die", "Mit", "Don", "Frei", "Sam"],
+ "longDays": ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"],
+ "to": "bis"
+ },
+ "pt": {
+ "room": "Quarto",
+ "closed": "Fechado",
+ "free": "Livre",
+ "shortDays": ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Se', 'Sáb'],
+ "longDays": ['Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira',
+ 'Sexta-feira', 'Sábado'],
+ "to": "para"
+
+ }
+ };
+
+
+ $(document).ready(function () {
+ if (!getId()) {
+
+ }
+ });
+
+
+ /**
+ * Downloads the config of a room, also reloads the page if the config hase changed over time
+ * @param id ID of the room
+ * @param room Room Object, only needed if it already exists
+ */
+
+ function getConfig(id, room) {
+ $.ajax({
+ url: "../../../api.php?do=locationinfo&action=config&id=" + id,
+ dataType: 'json',
+ cache: false,
+ timeout: 30000,
+ success: function (result) {
+
+ if (room == null) {
+
+ if (result.room == null) {
+
+ for (var i = roomIds.length - 1; i >= 0; i--) {
+ if (roomIds[i] === id) {
+ roomIds.splice(i, 1);
+ }
+ }
+ if (roomsToshow == roomIds.length) {
+
+ initRooms();
+
+ }
+ return;
+ }
+
+ var time = new Date(result.time);
+ if (isNaN(time.getTime())) {
+ time = new Date();
+ }
+ SetUpDate(time);
+ delete result.time;
+
+ room = addRoom(id, result.room, result);
+ roomsToshow++;
+ if (roomsToshow == roomIds.length) {
+
+ initRooms();
+
+ }
+
+
+ } else {
+
+ delete result.time;
+ if (JSON.stringify(rooms[id].config) != JSON.stringify(result)) {
+ // reload Page if someone changed config
+ location.reload(true);
+ }
+ }
+ // check for config changes from time to time
+ setTimeout(function () {
+ getConfig(id, room);
+ }, room.config.configupdate);
+
+
+ }, error: function () {
+ //Todo Error handling:
+ }
+ })
+ }
+
+
+ /**
+ * gets the Parameter IDs from the Urls
+ */
+ function getId() {
+ roomIds = getUrlParameter("id");
+ if (roomIds == null) {
+
+ var text = "<h1>Error: No Room Id's given in parameter </h1>";
+ $("body").append(text);
+
+ return false;
+ }
+
+ roomIds = roomIds.split(',');
+ if (roomIds.length > 4) {
+ roomIds.length = 4;
+ }
+
+ for (var i = 0; i < roomIds.length; i++) {
+ getConfig(roomIds[i], null);
+
+ }
+
+ }
+
+
+ /**
+ * gets Additional Parameters from the URL, and from the
+ * downloaded json.
+ * also makes sure parameters are in a given range
+ * @param room Room Object
+ */
+
+ function getParamerter(room) {
+
+
+ if (room.config != null) {
+ room.config.switchtime = room.config.switchtime * 1000;
+ room.config.calupdate = room.config.calupdate * 60 * 1000;
+ room.config.roomupdate = room.config.roomupdate * 1000;
+ room.config.configupdate = room.config.configupdate * 60 * 1000;
+ }
+
+ if (getUrlParameter("mode") != null) {
+ room.config.mode = parseInt(getUrlParameter("mode"));
+ }
+ if (getUrlParameter("calupdate") != null) {
+ room.config.calupdate = (parseInt(getUrlParameter("calupdate")) * 60 * 1000);
+ }
+ if (getUrlParameter("roomupdate") != null) {
+ room.config.roomupdate = (parseInt(getUrlParameter("roomupdate")) * 1000);
+ }
+ if (getUrlParameter("daystoshow") != null) {
+ room.config.daystoshow = parseInt(getUrlParameter("daystoshow"));
+ }
+ if (getUrlParameter("scaledaysauto") == "true") {
+ room.config.scaledaysauto = true;
+ } else if (getUrlParameter("scaledaysauto") == "false") {
+ room.config.scaledaysauto = false;
+ }
+ if (getUrlParameter("vertical") == "true") {
+ room.config.vertical = true;
+ } else if (getUrlParameter("vertical") == "false") {
+ room.config.vertical = false;
+ }
+ if (getUrlParameter("einkmode") == "true") {
+ room.config.einkmode = true;
+ } else if (getUrlParameter("einkmode") == "false") {
+ room.config.einkmode = false;
+ }
+
+ if (getUrlParameter("scale") != null) {
+ room.config.scale = parseInt(getUrlParameter("scale"));
+ }
+ if (getUrlParameter("rotation") != null) {
+ room.config.rotation = parseInt(getUrlParameter("rotation"));
+ }
+ if (getUrlParameter("switchtime") != null) {
+ room.config.switchtime = (parseInt(getUrlParameter("switchtime")) * 1000);
+ }
+ if (getUrlParameter("configupdate") != null) {
+ room.config.configupdate = (parseInt(getUrlParameter("configupdate")) * 60 * 1000);
+ }
+
+ // parameter validation
+ if (room.config.switchtime == null || isNaN(room.config.switchtime) || room.config.switchtime > 120 * 1000
+ || room.config.switchtime < 1 * 1000) {
+ room.config.switchtime = 5 * 1000;
+ }
+ if (room.config.scale == null || isNaN(room.config.scale) || room.config.scale > 90 || room.config.scale < 10) {
+ room.config.scale = 50;
+ }
+ if (room.config.vertical == null) {
+ room.config.vertical = false;
+ }
+ if (room.config.daystoshow == null || isNaN(room.config.daystoshow) || room.config.daystoshow > 7
+ || room.config.daystoshow < 1) {
+ room.config.daystoshow = 7;
+ }
+
+ if (room.config.roomupdate == null || isNaN(room.config.roomupdate) || room.config.roomupdate < 1000) {
+ room.config.roomupdate = 20 * 1000;
+ }
+ if (room.config.configupdate == null || isNaN(room.config.configupdate) || (room.config.configupdate < 1)) {
+ room.config.configupdate = 30 * 60 * 1000;
+ }
+
+ if (room.config.calupdate == null || isNaN(room.config.calupdate) || room.config.calupdate < 60 * 1000) {
+ room.config.calupdate = 30 * 60 * 1000;
+ }
+ if (room.config.mode == null || isNaN(room.config.mode) || room.config.mode > 4 || room.config.mode < 1) {
+ room.config.mode = 1;
+ }
+ if (room.config.rotation == null || isNaN(room.config.rotation) || room.config.rotation < 0
+ || room.config.rotation > 4) {
+ room.config.rotation = 0;
+ }
+
+ if (getUrlParameter("lang") != null && getUrlParameter("lang") in translation) {
+ room.config.language = getUrlParameter("lang");
+ }
+ $('html').attr('lang', room.config.language);
+ }
+
+ /**
+ * generates the Room divs and calls the needed functions depending on the rooms mode
+ */
+ function initRooms() {
+
+ var width = "100%";
+ var height = "100%";
+ if (roomsToshow == 2 || roomsToshow == 4) {
+ width = "50%";
+ }
+ if (roomsToshow == 3) {
+ width = "33%";
+ }
+ if (roomsToshow == 4) {
+ height = "50%";
+ }
+ var i = 0;
+ for (var t = 0; t < roomIds.length; t++) {
+ var property = roomIds[t];
+ var text = "<div class='roompadding' id ='roompadding_" + rooms[property].id + "'></div>";
+ $("body").append(text);
+
+
+ var obj = document.getElementById("roompadding_" + rooms[property].id);
+ obj.style.height = height;
+ obj.style.width = width;
+ text = "<div class='room' id ='room_" + rooms[property].id + "'></div>";
+
+ $("#roompadding_" + rooms[property].id).append(text);
+
+
+ text = "<div id='header_" + rooms[property].id + "' class='row'>" +
+ "<div class='header col-2'>" +
+ "<div class='font' id='roomHeader_" + rooms[property].id + "'></div>" +
+ "</div>" +
+ "<div class='col-1 courseText header'>" +
+ "<div class='font' id='courseHeading_" + rooms[property].id + "'></div>" +
+ "</div>" +
+ "<div class='square .col-2' id='square_" + rooms[property].id + "'>" +
+ "<div class='FreeSeatsFont' id='freeSeatsHeader_" + rooms[property].id + "'></div>" +
+ "</div>" +
+ "</div>";
+ $("#room_" + rooms[property].id).append(text);
+
+ if (rooms[property].name != null) {
+ $("#roomHeader_" + rooms[property].id).text(rooms[property].name);
+ }
+
+ if (roomsToshow == 2) {
+ document.getElementById("square_" + rooms[property].id).style.width = "6vw";
+ document.getElementById("square_" + rooms[property].id).style.height = "6vw";
+ document.getElementById("roomHeader_" + rooms[property].id).style.fontSize = "1.8vw";
+ document.getElementById("freeSeatsHeader_" + rooms[property].id).style.fontSize = "4.5vw";
+ document.getElementById("courseHeading_" + rooms[property].id).style.fontSize = "1.8vw";
+ var headers = document.getElementsByClassName('header');
+ for (var j = 0; j < headers.length; j++) {
+ headers[j].style.height = "6vw";
+ }
+ }
+ if (roomsToshow == 3) {
+ document.getElementById("square_" + rooms[property].id).style.width = "4vw";
+ document.getElementById("square_" + rooms[property].id).style.height = "4vw";
+ document.getElementById("roomHeader_" + rooms[property].id).style.fontSize = "1.2vw";
+ document.getElementById("freeSeatsHeader_" + rooms[property].id).style.fontSize = "2.5vw";
+ document.getElementById("courseHeading_" + rooms[property].id).style.fontSize = "1.2vw";
+ var headers = document.getElementsByClassName('header');
+ for (var j = 0; j < headers.length; j++) {
+ headers[j].style.height = "4vw";
+ }
+
+ }
+ if (roomsToshow == 4) {
+ document.getElementById("square_" + rooms[property].id).style.width = "4vw";
+ document.getElementById("square_" + rooms[property].id).style.height = "4vw";
+ document.getElementById("roomHeader_" + rooms[property].id).style.fontSize = "1.5vw";
+ document.getElementById("freeSeatsHeader_" + rooms[property].id).style.fontSize = "2.5vw";
+ document.getElementById("courseHeading_" + rooms[property].id).style.fontSize = "1.5vw";
+
+ var headers = document.getElementsByClassName('header');
+ for (var j = 0; j < headers.length; j++) {
+ headers[j].style.height = "4vw";
+ }
+ }
+
+
+ if (rooms[property].config.mode == 1) {
+ setUpCalendar(rooms[property].config.scale + "%", rooms[property].config.daystoshow, rooms[property]);
+ preInitRoom(rooms[property]);
+
+ } else if (rooms[property].config.mode == 2) {
+ setUpCalendar("100%", rooms[property].config.daystoshow, rooms[property]);
+
+ } else if (rooms[property].config.mode == 3) {
+ preInitRoom(rooms[property]);
+ getOpeningTimes(rooms[property]);
+ } else if (rooms[property].config.mode == 4) {
+
+ setUpCalendar("100%", rooms[property].config.daystoshow, rooms[property]);
+ preInitRoom(rooms[property]);
+ generateProgressBar(rooms[property].id);
+ }
+
+ i++;
+ }
+
+ setInterval(mainUpdateLoop, 1000);
+ }
+
+ /**
+ * Helper function to generate id string used in query functions
+ * @param list A string, wicht contains ids or not(for now)
+ * @param id An ID which should be added to the list
+ */
+ function addIdToUpdateList(list, id) {
+ if (list == "") {
+ list += id;
+ } else {
+ list += ("," + id);
+ }
+ return list;
+ }
+
+
+ var timeSteps = 10;
+ /**
+ * Main Update loop, this loop runs every 1 seconds and calls all
+ * function which should be called periodically
+ */
+ function mainUpdateLoop() {
+
+ // check ervery 10 sec if rooms need new calendar data or room data
+ // groups request
+
+ if (timeSteps > 9) {
+ timeSteps = 0;
+
+ var calendarUpdateIds = "";
+ var rommUpdateIds = "";
+ for (var property in rooms) {
+ if (rooms[property].config.lastCalendarUpdate == null || rooms[property].config.lastCalendarUpdate + rooms[property].config.calupdate < MyDate().getTime()) {
+
+ calendarUpdateIds = addIdToUpdateList(calendarUpdateIds, rooms[property].id);
+ rooms[property].config.lastCalendarUpdate = MyDate().getTime();
+ }
+ if (rooms[property].config.lastRoomUpdate == null || rooms[property].config.lastRoomUpdate + rooms[property].config.roomupdate < MyDate().getTime()) {
+ rommUpdateIds = addIdToUpdateList(rommUpdateIds, rooms[property].id);
+ rooms[property].config.lastRoomUpdate = MyDate().getTime();
+ }
+ }
+
+
+ if (calendarUpdateIds != "") {
+ queryCalendars(calendarUpdateIds);
+ }
+ if (rommUpdateIds != "") {
+ queryRooms(rommUpdateIds);
+ }
+ }
+
+
+ // switches calendar and roomlayout in mode 4
+
+ for (var property in rooms) {
+ if (rooms[property].config.mode == 4) {
+ if (rooms[property].lastSwitchTime == null
+ || rooms[property].lastSwitchTime + rooms[property].config.switchtime < MyDate().getTime()) {
+
+
+ switchLayout(rooms[property]);
+ MoveProgressBar(rooms[property].id, rooms[property].config.switchtime);
+
+ if (rooms[property].lastSwitchTime == null) {
+ rooms[property].lastSwitchTime = MyDate().getTime();
+ } else {
+ rooms[property].lastSwitchTime = rooms[property].lastSwitchTime + rooms[property].config.switchtime;
+ }
+ }
+
+ //
+ }
+ // Updateing All room Headers
+
+ UpdateRoomHeader(rooms[property]);
+ }
+
+
+ // reload site at midnight
+ var now = new MyDate();
+ if (date != null) {
+ if (date.getDate() != now.getDate()) {
+ location.reload(true);
+ }
+ }
+ date = now;
+ timeSteps++;
+
+ }
+
+ /**
+ * Generates a room Object and adds it to the rooms array
+ * @param id ID of the room
+ * @param name Name of the room
+ * @param config Config Json of the room
+ */
+ function addRoom(id, name, config) {
+ var room = {
+ id: id,
+ name: name,
+ timetable: null,
+ currentEvent: null,
+ nextEventEnd: null,
+ timeTilFree: null,
+ state: null,
+ openingTimes: null,
+ config: config,
+ currentfreePcs: 0,
+ layout: null,
+ freePcs: 0,
+ resized: true,
+ lastCalendarUpdate: null,
+ lastRoomUpdate: null,
+ getState: function () {
+ if (this.state == null) {
+ ComputeCurrentState(this);
+ return this.state;
+ }
+ if (this.state.end != "") {
+ if (this.state.end < new MyDate()) {
+ ComputeCurrentState(this);
+ }
+ }
+ return this.state;
+ }
+
+
+ };
+ getParamerter(room);
+ rooms[id] = room;
+ return room;
+
+ }
+
+ /**
+ * inilizes the Calendar for an room
+ * @param percent Percentages of how mucht width the Calendar div should get (only used in mode 1)
+ * @param daysToShow How many days the calendar should show
+ * @param room Room Object
+ */
+ function setUpCalendar(percent, daysToShow, room) {
+ generateCalendarDiv(percent, room);
+ var $calendar = $("#calendar_" + room.id).weekCalendar({
+ timeslotsPerHour: 1,
+ timeslotHeight: 30,
+ daysToShow: daysToShow,
+ height: function ($calendar) {
+ var height = $(window).height();
+ if (roomsToshow == 4) {
+ height = height / 2;
+ }
+
+ height = height - document.getElementById('header_' + room.id).clientHeight - 5;
+ if (room.config.mode == 1 && room.config.vertical) {
+ height = height * (room.config.scale / 100)
+ }
+ return height;
+ },
+ eventRender: function (calEvent, $event) {
+ if (calEvent.end.getTime() < new MyDate().getTime()) {
+ $event.css("backgroundColor", "#aaa");
+ $event.find(".time").css({"backgroundColor": "#999", "border": "1px solid #888"});
+ } else if (calEvent.end.getTime() > new MyDate().getTime() && calEvent.start.getTime() < new MyDate().getTime()) {
+ $event.css("backgroundColor", "#25B002");
+ $event.find(".time").css({"backgroundColor": "#25B002", "border": "1px solid #888"});
+ }
+ },
+ date: MyDate(),
+ dateFormat: "j.n",
+ timeFormat: "G:i",
+ scrollToHourMillis: 500,
+ use24Hour: true,
+ readonly: true,
+ showHeader: false,
+ hourLine: true,
+ shortDays: t("shortDays", room.config.language),
+ longDays: t("longDays", room.config.language),
+ buttons: false,
+ timeSeparator: " " + t("to", room.config.language) + " ",
+ startOnFirstDayOfWeek: false,
+ displayFreeBusys: true,
+ defaultFreeBusy: {free: false}
+ });
+ getOpeningTimes(room);
+
+
+ }
+
+ /**
+ * downloads openingTimes for an room
+ * @param room Room Object
+ */
+ function getOpeningTimes(room) {
+ $.getJSON("../../../api.php?do=locationinfo&action=openingtime&id=" + room.id, function (result) {
+ if (Object.prototype.toString.call(result) === '[object Array]') {
+ if (result.length > 0) {
+ SetOpeningTimes(result[0].openingtime, room);
+ }
+ }
+ scaleCalendar(room);
+
+ })
+ .error(function () {
+ scaleCalendar(room);
+
+ })
+ }
+
+
+ /**
+ * Generates the Calendar Div, depending on it's width
+ * @param width width of the Calendar Div
+ * @param room Room Object
+ */
+
+ function generateCalendarDiv(width, room) {
+ var div = document.createElement("div");
+ div.id = "calendar_" + room.id;
+ div.className = "calendar";
+ if (room.config.vertical && room.config.mode == 1) {
+ width = 100 + "%";
+ div.float = "Top";
+ }
+ div.style.width = width;
+ //document.body.appendChild(div);
+ $("#room_" + room.id).append(div);
+
+ }
+
+ /**
+ * sets the opening Time in the calendar plugin and saves it in the room object
+ * @param parsedOpenings Dictonary of Opening times.
+ * @param room Room Object
+ */
+
+ function SetOpeningTimes(parsedOpenings, room) {
+ var opening = 24;
+ var close = 0;
+ room.openingTimesCalendar = [];
+ room.openingTimes = [parsedOpenings['Sunday'], parsedOpenings['Monday'], parsedOpenings['Tuesday'],
+ parsedOpenings['Wednesday'], parsedOpenings['Thursday'],
+ parsedOpenings['Friday'], parsedOpenings['Saturday']];
+ if (room.config.mode == 3) {
+ return;
+ }
+ for (var i = 0; i < 7; i++) {
+ var tmp = room.openingTimes[i];
+ if (tmp != null) {
+ for (var d = 0; d < tmp.length; d++) {
+ var day = getNextDayOfWeek(new MyDate(), i);
+ room.openingTimesCalendar.push({
+ "start": new Date(day.getFullYear(), day.getMonth(), day.getDate(),
+ tmp[d]['HourOpen'], tmp[d]['MinutesOpen']),
+ "end": new Date(day.getFullYear(), day.getMonth(),
+ day.getDate(), tmp[d]['HourClose'], tmp[d]['MinutesClose']),
+ "free": true
+ });
+ if (parseInt(tmp[d]['HourOpen']) < opening) {
+ opening = tmp[d]['HourOpen'];
+ }
+ if (parseInt(tmp[d]['HourClose']) > close) {
+ close = tmp[d]['HourClose'];
+ if (parseInt(tmp[d]['MinutesClose']) != 0) {
+ close++;
+ }
+ }
+ }
+ }
+ }
+ if (parsedOpenings.length == 0) {
+ opening = 0;
+ close = 24;
+ }
+ room.openTimes = close - opening;
+ $('#calendar_' + room.id).weekCalendar("option", "businessHours", {
+ start: parseInt(opening),
+ end: parseInt(close),
+ limitDisplay: true
+ });
+ }
+
+ /**
+ * querys the Calendar data
+ * @param ids ID'S of rooms to query as string, for e.g.: "5,17,8" or "5"
+ */
+ function queryCalendars(ids) {
+ var url = "../../../api.php?do=locationinfo&action=calendar&id=" + ids;
+
+ // Todo reimplement Frontend methode if needed
+ /*
+ if(!(room.config.calendarqueryurl === undefined)) {
+ url = room.config.calendarqueryurl;
+ }
+ */
+ $.ajax({
+ url: url,
+ dataType: 'json',
+ cache: false,
+ timeout: 30000,
+ success: function (result) {
+ var l = result.length;
+ for (var i = 0; i < l; i++) {
+ updateCalendar(result[i].calendar, rooms[result[i].id]);
+ }
+
+
+ }, error: function () {
+
+ }
+ });
+ }
+
+ /**
+ * applays new calendar data to the calendar plugin and also saves it to the room object
+ * @param json Calendar data JSON
+ * @param room Room Object
+ */
+ function updateCalendar(json, room) {
+
+ if (!json) {
+ console.log("Error: Calendar data was an empty string.");
+ return;
+ }
+ try {
+ room.timetable = json;
+ if (room.config.mode != 3) {
+ var cal = $('#calendar_' + room.id);
+ cal.weekCalendar("option", "data", json);
+ cal.weekCalendar("refresh");
+ cal.weekCalendar("option", "defaultFreeBusy", {free: false});
+ cal.weekCalendar("updateFreeBusy", room.openingTimesCalendar);
+ }
+ ComputeCurrentState(room);
+ } catch (e) {
+ console.log("Error: Couldnt add calendar data");
+ console.log(e);
+ }
+ }
+
+ /**
+ * scales calendar, called once on create and on window resize
+ * @param room Room Object
+ */
+ function scaleCalendar(room) {
+ if (room.config.mode == 3) {
+ return;
+ }
+ if (room.openTimes == null) {
+ room.openTimes = 24;
+ }
+ var cal = $('#calendar_' + room.id);
+ var columnWidth = document.getElementById("calendar_" + room.id).getElementsByClassName("wc-day-1")[0].clientWidth;
+
+ if (room.config.scaledaysauto) {
+ var result = (cal.weekCalendar("option", "daysToShow") * columnWidth) / 100;
+ result = parseInt(Math.min(Math.max(Math.abs(result), 1), 7));
+ if (result != parseInt(cal.weekCalendar("option", "daysToShow"))) {
+
+ cal.weekCalendar("option", "daysToShow", Math.abs(result));
+ }
+ }
+ if (((!room.config.scaledaysauto) || cal.weekCalendar("option", "daysToShow") == 1) && columnWidth < 85) {
+ cal.weekCalendar("option", "useShortDayNames", true);
+ } else {
+ cal.weekCalendar("option", "useShortDayNames", false);
+ }
+ var clientHeight = $(window).height();
+ if (roomsToshow == 4) {
+ clientHeight = clientHeight / 2;
+ }
+
+ clientHeight = clientHeight - document.getElementById('header_' + room.id).clientHeight
+ - document.getElementsByClassName("wc-time-column-header")[0].clientHeight - 2;
+
+ if (room.config.mode == 1 && room.config.vertical) {
+
+ clientHeight = clientHeight * (room.config.scale / 100);
+ clientHeight -= 22;
+ }
+ clientHeight -= 6;
+ var height = clientHeight / (room.openTimes * cal.weekCalendar("option", "timeslotsPerHour"));
+
+
+ if (height < 30) {
+ height = 30;
+ }
+ // Scale calendar font
+ if (height > 120) {
+ cal.weekCalendar("option", "textSize", 28);
+ }
+ else if (height > 100) {
+ cal.weekCalendar("option", "textSize", 24);
+ } else if (height > 80) {
+ cal.weekCalendar("option", "textSize", 22);
+ } else if (height > 70) {
+ cal.weekCalendar("option", "textSize", 20);
+ } else if (height > 60) {
+ cal.weekCalendar("option", "textSize", 14);
+ } else {
+ cal.weekCalendar("option", "textSize", 13);
+ }
+ cal.weekCalendar("option", "timeslotHeight", height);
+ if (room.openingTimesCalendar != null) {
+ cal.weekCalendar("updateFreeBusy", room.openingTimesCalendar);
+ }
+ cal.weekCalendar("resizeCalendar");
+ cal.weekCalendar("scrollToHour");
+
+ }
+
+ /**
+ * used for countdown
+ * computes the time difference between 2 Date objects
+ * @param a Date Object
+ * @param b Date Object
+ * @param room Room Object
+ * @returns time string
+ */
+ function GetTimeDiferenceAsString(a, b, room) {
+ if (a == null || b == null) {
+ return "";
+ }
+ var milliseconds = a.getTime() - b.getTime();
+ var seconds = Math.floor((milliseconds / 1000) % 60);
+ milliseconds -= seconds * 1000;
+ var minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
+ milliseconds -= minutes * 1000 * 60;
+ var hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
+
+ var days = Math.floor((milliseconds / (1000 * 60 * 60 * 24)) % 31);
+ if (seconds < 10) {
+ seconds = "0" + seconds;
+ }
+ if (minutes < 10) {
+ minutes = "0" + minutes;
+ }
+ if (days != 0) {
+ // dont show?
+ return "";
+ }
+ if (room.config.einkmode) {
+ return hours + ":" + minutes;
+ }
+ return hours + ":" + minutes + ":" + seconds;
+ }
+
+ /**
+ * returns next closing time of a given room
+ * @param room
+ * @returns Date Object of next closing
+ */
+ function GetNextClosing(room) {
+ var now = new MyDate();
+ var day = now.getDay();
+ var offset = 0;
+ var bestdate;
+ for (var a = 0; a < 7; a++) {
+ var tmp = room.openingTimes[day];
+ if (tmp != null) {
+ for (var i = 0; i < tmp.length; i++) {
+ var closeDate = new MyDate();
+ closeDate.setDate(now.getDate() + offset);
+ closeDate.setHours(tmp[i].HourClose);
+ closeDate.setMinutes(tmp[i].MinutesClose);
+ closeDate.setSeconds(0);
+ if (closeDate > now) {
+ if (!IsOpen(new Date(closeDate.getTime() + 60000), room)) {
+ if (bestdate == null || bestdate > closeDate) {
+ bestdate = closeDate;
+ }
+ }
+ }
+ }
+ }
+ offset++;
+ day++;
+ if (day > 6) {
+ day = 0;
+ }
+ }
+ return bestdate;
+ }
+
+
+ /**
+ * checks if a room is on a given date/time open
+ * @param date Date Object
+ * @param room Room object
+ * @returns bool for open or not
+ */
+ function IsOpen(date, room) {
+ if (room.openingTimes == null) {
+ return false;
+ }
+ var tmp = room.openingTimes[date.getDay()];
+ if (tmp == null) {
+ return false;
+ }
+ for (var i = 0; i < tmp.length; i++) {
+ var openDate = new MyDate();
+ openDate.setHours(tmp[i].HourOpen);
+ openDate.setMinutes(tmp[i].MinutesOpen);
+ var closeDate = new MyDate();
+ closeDate.setHours(tmp[i].HourClose);
+ closeDate.setMinutes(tmp[i].MinutesClose);
+ if (openDate < date && closeDate > date) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Retruns next Opening
+ * @param room Room Object
+ * @returns bestdate Date Object of next opening
+ */
+ function GetNextOpening(room) {
+ var now = new MyDate();
+ var day = now.getDay();
+ var offset = 0;
+ var bestdate;
+ for (var a = 0; a < 7; a++) {
+ if (room.openingTimes == null) {
+ return null;
+ }
+ var tmp = room.openingTimes[day];
+ if (tmp != null) {
+ for (var i = 0; i < tmp.length; i++) {
+ var openDate = new MyDate();
+ openDate.setDate(now.getDate() + offset);
+ openDate.setHours(tmp[i].HourOpen);
+ openDate.setMinutes(tmp[i].MinutesOpen);
+ if (openDate > now) {
+ if (!IsOpen(new Date(openDate.getTime() - 60000), room)) {
+ if (bestdate == null || bestdate > openDate) {
+ bestdate = openDate;
+ }
+ }
+ }
+ }
+ }
+ offset++;
+ day++;
+ if (day > 6) {
+ day = 0;
+ }
+ }
+ return bestdate;
+ }
+
+
+ /**
+ * Sets the free PCs number in the right corner and updates the sqare color acordingly
+ * @param id Room id
+ * @param seats Number of free PC's in the room
+ */
+ function SetFreeSeats(id, seats) {
+ if (seats > 0) {
+ $("#freeSeatsHeader_" + id).text(seats);
+ $("#square_" + id).css('background-color', '#00dd10');
+ } else if (seats == -1) {
+ $("#freeSeatsHeader_" + id).text("");
+ $("#square_" + id).css('background-color', 'red');
+ } else {
+ $("#freeSeatsHeader_" + id).text("0");
+ $("#square_" + id).css('background-color', 'red');
+ }
+ }
+
+ /**
+ * Updates the Header of an Room
+ * @param room Room Object
+ */
+ function UpdateRoomHeader(room) {
+ var tmp = room.getState();
+ if (tmp.state == "closed") {
+ $("#courseHeading_" + room.id).text(t("closed", room.config.language) + " " + GetTimeDiferenceAsString(tmp.end, new MyDate(), room));
+ SetFreeSeats(room.id, room.freePcs);
+ } else if (tmp.state == "ClaendarEvent") {
+ $("#courseHeading_" + room.id).text(tmp.title);
+ SetFreeSeats(room.id, -1);
+ } else if (tmp.state == "Free") {
+ $("#courseHeading_" + room.id).text(t("free", room.config.language) + " " + GetTimeDiferenceAsString(tmp.end, new MyDate(), room));
+ SetFreeSeats(room.id, room.freePcs);
+ } else if (tmp.state == "FreeNoEnd") {
+ $("#courseHeading_" + room.id).text(t("free", room.config.language));
+ SetFreeSeats(room.id, room.freePcs);
+ }
+ }
+
+ /**
+ * computes state of a room, states are:
+ * closed, FreeNoEnd, Free, ClaendarEvent.
+ * @param Room Object
+ */
+ function ComputeCurrentState(room) {
+ if (!IsOpen(new MyDate(), room)) {
+ room.state = {state: "closed", end: GetNextOpening(room), title: "", next: ""};
+
+ return;
+ }
+ var closing = GetNextClosing(room);
+
+ var event = getNextEvent(room.timetable);
+
+ // no event and no closing
+ if (closing == null && event == null) {
+ room.state = {state: "FreeNoEnd", end: "", title: "", next: ""};
+ return;
+ }
+
+ // no event so closing is next
+ if (event == null) {
+ room.state = {state: "Free", end: closing, title: "", next: "closing"};
+ return;
+ }
+
+ // event is at the moment
+ if ((closing == null || event.start.getTime() < closing.getTime()) && event.start.getTime() < new MyDate()) {
+ room.state = {state: "ClaendarEvent", end: event.end, title: event.title, next: ""};
+ return;
+ }
+
+ // no closing so event is next
+ if (closing == null) {
+ room.state = {state: "Free", end: event.start, title: "", next: "event"};
+ return;
+ }
+
+ // event sooner then closing
+ if (event.start.getTime() < closing) {
+ room.state = {state: "Free", end: event.start, title: "", next: "event"};
+ } else if (event.start.getTime() > closing) {
+ room.state = {state: "Free", end: closing, title: "", next: "closing"};
+ }
+
+ }
+
+
+ /**
+ * returns next event from a given json of events
+ * @param json Json which contains the calendar data.
+ * @returns event next Carlendar Event
+ */
+ function getNextEvent(json) {
+ var event;
+ var now = new MyDate();
+ if (json == null) {
+ return;
+ }
+ for (var i = 0; i < json.length; i++) {
+ //event is now active
+ if (json[i].start.getTime() < now.getTime() && json[i].end.getTime() > now.getTime()) {
+ return json[i];
+ }
+ //first element to consider
+ if (event == null) {
+ if (json[i].start.getTime() > now.getTime()) {
+ event = json[i];
+ }
+ }
+ if (json[i].start.getTime() > now.getTime() && event.start.getTime() > json[i].start.getTime()) {
+ event = json[i];
+ }
+ }
+ return event;
+ }
+
+ function getNextDayOfWeek(date, dayOfWeek) {
+ // Code to check that date and dayOfWeek are valid left as an exercise ;)
+
+ var resultDate = new Date(date.getTime());
+
+ resultDate.setDate(date.getDate() + (7 + dayOfWeek - date.getDay()) % 7);
+
+ return resultDate;
+ }
+ /*
+ /========================================== Room Layout =============================================
+ */
+
+
+ var picSizeX = 3.8;
+ var picSizeY = 3;
+
+ /**
+ * Generates the RoomLayout Div
+ * @param width The width the RoomLayout should have (in percent).
+ * @param room Room Object
+ */
+ function generateRoomLayoutDiv(width, room) {
+ var div = document.createElement("div");
+ div.id = "roomLayout_" + room.id;
+ div.className = "roomLayoutDesign";
+ if ((room.config.vertical && room.config.mode == 1) || (room.config.mode == 3) || (room.config.mode == 4)) {
+ width = 100 + "%";
+ div.float = "Top";
+ }
+
+ div.style.width = width;
+ if (room.config.mode == 4) {
+ div.style.display = "none";
+ }
+ //document.body.appendChild(div);
+ $("#room_" + room.id).append(div);
+
+
+ }
+ /**
+ * Donwloads Room Layout Json (which contains the pc information for a given room)
+ * @param room Room Object
+ */
+ function preInitRoom(room) {
+
+ $.getJSON("../../../api.php?do=locationinfo&action=locationinfo&id=" + room.id + "&coords=1", function (result) {
+
+ generateRoomLayoutDiv((100 - room.config.scale) + "%", room);
+ if (result && result[0] && result[0].computer) {
+ initRoom(result[0].computer, room);
+ } else {
+ initRoom([], room);
+ }
+
+ }).error(function () {
+
+ generateRoomLayoutDiv((100 - room.config.scale) + "%", room);
+
+ })
+ }
+
+
+ /**
+ * Main funciton for generating the Room Layout
+ * @param layout Layout Json
+ * @param room Room Object
+ */
+ function initRoom(layout, room) {
+ var maxX;
+ var maxY;
+ var minY;
+ var minX;
+ var xDifference;
+ var yDifference;
+ room.layout = layout;
+ if (layout == null || layout.length == 0) {
+ return;
+ }
+ if (room.config.rotation != 0) {
+ rotateRoom(room.config.rotation, layout);
+ }
+
+
+ for (var i = 0; i < layout.length; i++) {
+ if (!isNaN(parseInt(layout[i].x)) && !isNaN(parseInt(layout[i].y)) && layout[i].y != null && layout[i].y != null) {
+ if (minX === undefined) {
+ minX = parseInt(layout[i].x);
+ }
+ if (minY === undefined) {
+ minY = parseInt(layout[i].y);
+ }
+ if (maxX === undefined) {
+ maxX = parseInt(layout[i].x);
+ }
+ if (maxY === undefined) {
+ maxY = parseInt(layout[i].y);
+ }
+ if (parseInt(layout[i].x) < parseInt(minX)) {
+ minX = parseInt(layout[i].x);
+ }
+ if (parseInt(layout[i].y) < parseInt(minY)) {
+ minY = parseInt(layout[i].y);
+ }
+ if (parseInt(layout[i].x) > parseInt(maxX)) {
+ maxX = parseInt(layout[i].x);
+ }
+ if (parseInt(layout[i].y) > parseInt(maxY)) {
+ maxY = parseInt(layout[i].y);
+ }
+ }
+ }
+
+ xDifference = maxX - minX;
+ yDifference = maxY - minY;
+
+ room.xDifference = xDifference;
+ room.yDifference = yDifference;
+ room.minX = minX;
+ room.minY = minY;
+ room.maxX = maxX;
+ room.maxY = maxY;
+
+ generateOffsetAndScale(room);
+ setUpRoom(room, layout);
+ scaleRoom(room);
+ UpdatePc(layout, room);
+
+ }
+
+ /**
+ * Computes offsets and scaling's for the RoomLayout
+ * @param room Room Object
+ */
+ function generateOffsetAndScale(room) {
+
+ var clientHeight = $(window).height();
+ if (roomsToshow == 4) {
+ clientHeight = clientHeight / 2;
+ }
+
+ clientHeight = clientHeight - document.getElementById('header_' + room.id).clientHeight - 5;
+
+ if (roomsToshow > 1) {
+ clientHeight -= 5;
+ }
+ if (room.config.vertical && room.config.mode == 1) {
+ clientHeight = clientHeight * (1 - (room.config.scale / 100));
+ }
+ var roomLayout = document.getElementById('roomLayout_' + room.id);
+
+ var clientWidth = roomLayout.clientWidth;
+ //roomLayout.style.height = clientHeight + "px";
+
+ var scaleX;
+ if (room.xDifference != 0) {
+ scaleX = clientWidth / room.xDifference;
+ } else {
+ scaleX = clientWidth;
+ }
+ var scaleY;
+ if (room.yDifference != 0) {
+ scaleY = clientHeight / room.yDifference;
+ } else {
+ scaleY = clientHeight;
+ }
+ var scaleYs = (clientHeight - (picSizeY * scaleY)) / room.yDifference;
+ var scaleXs = (clientWidth - (picSizeX * scaleX)) / room.xDifference;
+ if (scaleYs <= 0) {
+ scaleYs = 9999;
+ }
+ if (scaleXs <= 0) {
+ scaleXs = 9999;
+ }
+
+
+ var tmp = [scaleYs, scaleY, scaleXs, scaleX, (clientHeight * 0.9) / picSizeY, (clientWidth * 0.9) / picSizeX];
+ room.scale = Math.min.apply(Math, tmp);
+ room.xOffset = 0 - room.minX;
+ room.yOffset = 0 - room.minY;
+ room.xOffset += ((1 / 2 * (clientWidth - (((room.maxX + room.xOffset) * room.scale) + picSizeX * room.scale))) / room.scale);
+ room.yOffset += ((1 / 2 * (clientHeight - (((room.maxY + room.yOffset) * room.scale) + picSizeY * room.scale))) / room.scale);
+ }
+
+
+ /**
+ * adds images for each pc to Room Layout
+ * @param room Room Object
+ * @param layout Layout json
+ */
+ function setUpRoom(room, layout) {
+ for (var i = 0; i < layout.length; i++) {
+
+ if (layout[i].y != null && layout[i].x != null && !isNaN(layout[i].y) && !isNaN(layout[i].x)) {
+ var text = "<div class= 'PCImgDiv' id ='layout_PC_div_" + room.id + "_" + layout[i].id + "'>" +
+
+ "<div class= 'OverlayDiv' id ='layout_PC_overlay_" + room.id + "_" + layout[i].id + "'>" +
+ "</div>" +
+ "<img class= 'pcImg' id ='layout_PC_" + room.id + "_" + layout[i].id + "'> </img>" +
+ "</div>";
+
+ $('#roomLayout_' + room.id).append(text);
+ if (layout[i].hasOwnProperty('overlay')) {
+ for (var a = 0; a < layout[i].overlay.length; a++) {
+ addOverlay($('#layout_PC_overlay_' + room.id + "_" + layout[i].id), layout[i].overlay[a]);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds an overlay to an div(here the PC's shown in the RoomLayout).
+ * @param object Object where the overlay should be added
+ * @param overlayName name of the overlay (image name without ending)
+ */
+ function addOverlay(object, overlayName) {
+ var a = [".svg", ".png", "jpg"];
+ var imgname;
+ for (var i = 0; i < a.length; a++) {
+ if (imageExists("img/overlay/" + overlayName + a[i])) {
+ imgname = "img/overlay/" + overlayName + a[i];
+ break;
+ }
+
+ }
+ if (imgname == null) {
+ return;
+ }
+ var text = $("<img class='overlay' src='" + imgname + "'></img>");
+ text.addClass("overlay-" + overlayName);
+
+ object.append(text);
+
+
+ }
+
+
+ var imgExists = {};
+
+ /**
+ * checks if images exists on the webserver(chaches it also)
+ * @param image_url Path to the image
+ */
+ function imageExists(image_url) {
+ if (!imgExists.hasOwnProperty(image_url)) {
+ var http = new XMLHttpRequest();
+ http.open('HEAD', image_url, false);
+ http.send();
+ imgExists[image_url] = http.status != 404;
+ }
+ return imgExists[image_url];
+
+ }
+
+ /**
+ * Querys Pc states
+ * @param ids Room ID's which should be queried. Format for e.g.: "20,5,6"
+ */
+ function queryRooms(ids) {
+ $.ajax({
+ url: "../../../api.php?do=locationinfo&action=locationinfo&id=" + ids + "&coords=0",
+ dataType: 'json',
+ cache: false,
+ timeout: 30000,
+ success: function (result) {
+ for (var i = 0; i < result.length; i++) {
+ UpdatePc(result[i].computer, rooms[result[i].id]);
+ }
+ }
+ })
+ }
+
+
+ /**
+ * Adjust pc coordinate depending on room rotation
+ * @param r Rotation, from 0 - 3 (int)
+ * @param layout Layout json
+ */
+ function rotateRoom(r, layout) {
+ for (var z = 0; z < r; z++) {
+ for (var i = 0; i < layout.length; i++) {
+ var x = parseInt(layout[i].x);
+ var y = parseInt(layout[i].y);
+ layout[i].x = y;
+ layout[i].y = -x;
+ }
+ }
+ }
+
+ /**
+ * Positions the computer images in the roomLayout div accoring to ther potion and div size
+ * @param room Room object
+ */
+ function scaleRoom(room) {
+ if (room.layout == null) {
+ return;
+ }
+ for (var i = 0; i < room.layout.length; i++) {
+ if (room.layout[i].y != null && room.layout[i].x != null && !isNaN(room.layout[i].y) && !isNaN(room.layout[i].x)) {
+ var tmp = document.getElementById("layout_PC_div_" + room.id + "_" + room.layout[i].id);
+ var img = document.getElementById("layout_PC_" + room.id + "_" + room.layout[i].id);
+ if (tmp != null) {
+ tmp.style.width = (picSizeX * room.scale);
+ tmp.setAttribute("style", "width:" + (picSizeX * room.scale) + "px");
+ tmp.style.height = (picSizeY * room.scale) + "px";
+ tmp.style.left = ((parseInt(room.layout[i].x) + room.xOffset) * room.scale) + "px";
+ tmp.style.top = ((parseInt(room.layout[i].y) + room.yOffset) * room.scale ) + "px";
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Updates the PC's (images) in the room layout. Also Updates how many pc's are free.
+ * @param update Update Json from query for one(!) room
+ * @param room Room object
+ */
+ function UpdatePc(update, room) {
+ if (update === undefined || update == null) {
+ return;
+ }
+ var freePcs = 0;
+ for (var i = 0; i < update.length; i++) {
+ var imgobj = document.getElementById("layout_PC_" + room.id + "_" + update[i].id);
+
+
+ var img;
+ // Pc free
+ if (update[i].pcState == "IDLE") {
+ if (supportSvg) {
+ img = "img/pc_free";
+ } else {
+ imgobj.style.backgroundColor = "green";
+ }
+ freePcs++;
+ // Pc in use
+ } else if (update[i].pcState == "OCCUPIED") {
+ if (supportSvg) {
+ img = "img/pc_used";
+ } else {
+ imgobj.style.backgroundColor = "red";
+ }
+ // PC off
+ } else if (update[i].pcState == "OFF") {
+ if (supportSvg) {
+ img = "img/pc_off";
+ } else {
+ imgobj.style.backgroundColor = "black";
+ }
+ freePcs++;
+ } else {
+ if (supportSvg) {
+ img = "img/pc_defect";
+ } else {
+ imgobj.style.backgroundColor = "black";
+ }
+ }
+
+ if (imgobj != null && supportSvg) {
+ if (room.config.einkmode) {
+ img = img + "_eink";
+ }
+ imgobj.src = img + ".svg";
+ }
+
+ }
+
+ room.freePcs = freePcs;
+
+
+ }
+
+ /*
+ /========================================== Misc =============================================
+ */
+ var resizeTimeout;
+
+ // called when browser window changes size
+ // scales calendar and room layout acordingly
+
+ $(window).resize(function () {
+ clearTimeout(resizeTimeout);
+ resizeTimeout = setTimeout(function () {
+
+ for (var property in rooms) {
+
+ rooms[property].resized = true;
+ if (rooms[property].config.mode != null) {
+ if (rooms[property].config.mode != 3) {
+ scaleCalendar(rooms[property]);
+
+ }
+ if (rooms[property].config.mode != 2) {
+ generateOffsetAndScale(rooms[property]);
+ scaleRoom(rooms[property]);
+ }
+ }
+ }
+ }, 50);
+ });
+
+
+ /**
+ * returns parameter value from the url
+ * @param sParam
+ * @returns value for given parameter
+ */
+ var getUrlParameter = function getUrlParameter(sParam) {
+ var sPageURL = decodeURIComponent(window.location.search.substring(1)),
+ sURLVariables = sPageURL.split('&'),
+ sParameterName,
+ i;
+
+ for (i = 0; i < sURLVariables.length; i++) {
+ sParameterName = sURLVariables[i].split('=');
+
+ if (sParameterName[0] === sParam) {
+ return sParameterName[1] === undefined ? true : sParameterName[1];
+ }
+ }
+ };
+
+ /**
+ * Function for translation
+ * @param toTranslate key which we wan't to translate
+ * @param lang languages in which should be translated
+ * @returns r translated string
+ */
+ function t(toTranslate, lang) {
+
+ var r;
+ if (lang === undefined) {
+ r = translation['en'][toTranslate];
+ } else {
+ r = translation[lang][toTranslate];
+ }
+ return r;
+ }
+
+
+ /**
+ * Used in Mode 4, switches given room from Timetable to Roomlayout and vice versa
+ * @param room
+ */
+ function switchLayout(room) {
+ var car = document.getElementById("calendar_" + room.id);
+ var roomLayout = document.getElementById("roomLayout_" + room.id);
+
+ if (car.style.display == "none") {
+ roomLayout.style.display = "none";
+ car.style.display = "block";
+ if (room.resized) {
+ scaleCalendar(room);
+ room.resized = false;
+ }
+ } else {
+ car.style.display = "none";
+ roomLayout.style.display = "block";
+ if (room.resized) {
+ generateOffsetAndScale(room);
+ scaleRoom(room);
+ room.resized = false;
+ }
+ }
+ }
+
+
+ /**
+ * adds a progressbar to a given room (id) used in mode 4
+ * @param id room id
+ */
+ function generateProgressBar(id) {
+
+ var div = document.createElement("div");
+ div.id = "progressBar_" + id;
+ div.classList.add("progressbar");
+ //document.body.appendChild(div);
+ $("#room_" + id).append(div);
+
+ }
+ /**
+ * Animates the progressbar used in mode 4
+ * @param roomId
+ * @param time
+ * @constructor
+ */
+ function MoveProgressBar(roomId, time) {
+ var elem = document.getElementById("progressBar_" + roomId);
+ var width = 1;
+ var id = setInterval(frame, time / 100);
+
+ function frame() {
+ if (width >= 100) {
+ clearInterval(id);
+ } else {
+ width++;
+ elem.style.width = width + '%';
+ }
+ }
+ }
+
+
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/modules-available/locationinfo/frontend/img/overlay/rollstuhl.svg b/modules-available/locationinfo/frontend/img/overlay/rollstuhl.svg
new file mode 100755
index 00000000..8237d543
--- /dev/null
+++ b/modules-available/locationinfo/frontend/img/overlay/rollstuhl.svg
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ width="240"
+ height="360"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.44.1"
+ sodipodi:docname="Rollstuhl_aus_Zusatzzeichen_1044-10.svg"
+ sodipodi:docbase="L:\Wiki\pedia\ksh\svg">
+ <metadata
+ id="metadata4235">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>Symbol: Rollstuhl</dc:title>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Purodha Blissenbach</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:source>
+http://commons.mediawiki.org/wiki/Image:Rollstuhl_Zusatzzeichen_1044-10.svg</dc:source>
+ <cc:license
+ rdf:resource="http://web.resource.org/cc/PublicDomain" />
+ </cc:Work>
+ <cc:License
+ rdf:about="http://web.resource.org/cc/PublicDomain">
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/Reproduction" />
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/Distribution" />
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ inkscape:window-height="734"
+ inkscape:window-width="1001"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="1"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="white"
+ id="base"
+ inkscape:zoom="1.0806949"
+ inkscape:cx="150"
+ inkscape:cy="171.76267"
+ inkscape:window-x="169"
+ inkscape:window-y="84"
+ inkscape:current-layer="svg2"
+ height="360px"
+ width="240px"
+ inkscape:showpageshadow="false"
+ gridempspacing="20"
+ showgrid="true" />
+ <defs
+ id="defs4" />
+ <g
+ transform="matrix(7.357704,0,0,7.357704,434.9445,-2099.79)"
+ id="g8408">
+ <path
+ d="M -35.212572,318.50098 C -35.211894,322.66083 -38.583928,326.03341 -42.743778,326.03341 C -46.903628,326.03341 -50.275662,322.66083 -50.274984,318.50098 C -50.275662,314.34113 -46.903628,310.96855 -42.743778,310.96855 C -38.583928,310.96855 -35.211894,314.34113 -35.212572,318.50098 L -35.212572,318.50098 z "
+ transform="matrix(1.103775,0,0,1.103775,8.653227,-30.10701)"
+ style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:black;stroke-width:1.81196344;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path3069" />
+ <path
+ d="M -36.78532,312.21483 C -36.78532,316.98239 -36.78532,314.68153 -36.78532,314.68153"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:white;stroke-width:1.76695538px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path6617" />
+ <path
+ d="M -47.292192,317.15326 C -41.96024,317.15326 -44.051937,317.15326 -44.051937,317.15326"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:white;stroke-width:1.976125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path5730" />
+ <path
+ d="M -44.56103,313.67108 C -39.229078,313.67108 -41.320775,313.67108 -41.320775,313.67108"
+ style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:white;stroke-width:1.976125px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path8406" />
+ <path
+ d="M -55.667836,330.11748 C -54.347105,325.16278 -51.67252,315.22275 -51.515523,314.89338 C -51.186883,314.20393 -51.121917,313.65381 -50.034883,313.60631 L -42.886955,313.69168 L -42.886955,310.66214 L -50.333498,310.66214 C -50.178853,309.98746 -50.383012,309.31278 -49.164921,308.6381 L -42.934292,308.6381 L -42.934292,302.66225 C -42.934292,301.09614 -37.064554,301.09983 -37.064554,302.66225 L -37.064554,314.73308 C -37.064554,315.9654 -38.193966,316.81589 -39.857413,316.81589 L -49.324732,316.81589 L -53.490353,330.16481 C -53.745584,331.11734 -55.667836,330.83456 -55.667836,330.11748 z "
+ style="fill:black;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path6630" />
+ <path
+ d="M -53.443015,298.59131 C -53.442426,300.16032 -54.714197,301.43257 -56.283211,301.43257 C -57.852224,301.43257 -59.123995,300.16032 -59.123406,298.59131 C -59.123995,297.0223 -57.852224,295.75005 -56.283211,295.75005 C -54.714197,295.75005 -53.442426,297.0223 -53.443015,298.59131 z "
+ transform="matrix(1.083333,0,0,1.083333,20.67569,-26.8545)"
+ style="opacity:1;fill:black;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path6632" />
+ </g>
+</svg>
diff --git a/modules-available/locationinfo/frontend/img/pc_defect.svg b/modules-available/locationinfo/frontend/img/pc_defect.svg
new file mode 100644
index 00000000..219175e2
--- /dev/null
+++ b/modules-available/locationinfo/frontend/img/pc_defect.svg
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="-184 310.3 241.8 172.7" style="enable-background:new -184 310.3 241.8 172.7;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#3C3C3B;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st1{stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st2{fill:#FFFFFF;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st3{fill:#3C3C3B;}
+ .st4{fill:none;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st5{fill:#C6C6C6;}
+</style>
+<path id="XMLID_1_" class="st0" d="M50.1,450.7h-226.4c-4.3,0-7.7-3.5-7.7-7.7V318c0-4.3,3.5-7.7,7.7-7.7H50.1
+ c4.3,0,7.7,3.5,7.7,7.7v125C57.8,447.3,54.3,450.7,50.1,450.7z"/>
+<rect id="XMLID_3_" x="-90.9" y="454.2" class="st0" width="55.6" height="19.4"/>
+<g id="XMLID_8_">
+ <rect id="XMLID_4_" x="-173.7" y="321" class="st1" width="221.1" height="119"/>
+ <path id="XMLID_6_" class="st2" d="M38.2,447.7h-2.6c-1.6,0-2.9-1.3-2.9-2.9l0,0c0-1.6,1.3-2.9,2.9-2.9h2.6c1.6,0,2.9,1.3,2.9,2.9
+ l0,0C41.1,446.4,39.8,447.7,38.2,447.7z"/>
+</g>
+<g id="XMLID_10_">
+ <g id="XMLID_14_">
+ <g id="XMLID_72_">
+ <path id="XMLID_73_" class="st3" d="M8.7,470.2h-143.6c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4H8.7c3.5,0,6.4-2.9,6.4-6.4
+ S12.2,470.2,8.7,470.2z"/>
+ </g>
+ <g id="XMLID_56_">
+ <path id="XMLID_67_" class="st4" d="M8.7,470.2h-143.6c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4H8.7c3.5,0,6.4-2.9,6.4-6.4
+ S12.2,470.2,8.7,470.2z"/>
+ </g>
+ </g>
+ <g id="XMLID_26_">
+ <path id="XMLID_39_" class="st3" d="M-138.1,473.3h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8
+ l0,0C-141.3,476.4-139.9,473.3-138.1,473.3z"/>
+ <path id="XMLID_38_" class="st4" d="M-138.1,473.3h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8
+ l0,0C-141.3,476.4-139.9,473.3-138.1,473.3z"/>
+ </g>
+ <g id="XMLID_13_">
+ <g id="XMLID_69_">
+ <rect id="XMLID_70_" x="-141.3" y="476.5" class="st3" width="156.4" height="6.4"/>
+ </g>
+ <g id="XMLID_63_">
+ <rect id="XMLID_66_" x="-141.3" y="476.5" class="st4" width="156.4" height="6.4"/>
+ </g>
+ </g>
+</g>
+<g id="XMLID_2_">
+ <g id="XMLID_21_">
+ <path id="XMLID_25_" class="st4" d="M0.3,498.7"/>
+ </g>
+</g>
+<g>
+ <g>
+ <polygon class="st5" points="-45,321 -45.1,321.1 -46.8,323.1 -60.1,338.9 -42.7,390.6 -63.6,385.7 -48.8,420.6 -82,373.3
+ -59.4,374.1 -83.5,345.3 -76.8,321 "/>
+ </g>
+</g>
+</svg>
diff --git a/modules-available/locationinfo/frontend/img/pc_defect_eink.svg b/modules-available/locationinfo/frontend/img/pc_defect_eink.svg
new file mode 100644
index 00000000..219175e2
--- /dev/null
+++ b/modules-available/locationinfo/frontend/img/pc_defect_eink.svg
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="-184 310.3 241.8 172.7" style="enable-background:new -184 310.3 241.8 172.7;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#3C3C3B;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st1{stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st2{fill:#FFFFFF;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st3{fill:#3C3C3B;}
+ .st4{fill:none;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st5{fill:#C6C6C6;}
+</style>
+<path id="XMLID_1_" class="st0" d="M50.1,450.7h-226.4c-4.3,0-7.7-3.5-7.7-7.7V318c0-4.3,3.5-7.7,7.7-7.7H50.1
+ c4.3,0,7.7,3.5,7.7,7.7v125C57.8,447.3,54.3,450.7,50.1,450.7z"/>
+<rect id="XMLID_3_" x="-90.9" y="454.2" class="st0" width="55.6" height="19.4"/>
+<g id="XMLID_8_">
+ <rect id="XMLID_4_" x="-173.7" y="321" class="st1" width="221.1" height="119"/>
+ <path id="XMLID_6_" class="st2" d="M38.2,447.7h-2.6c-1.6,0-2.9-1.3-2.9-2.9l0,0c0-1.6,1.3-2.9,2.9-2.9h2.6c1.6,0,2.9,1.3,2.9,2.9
+ l0,0C41.1,446.4,39.8,447.7,38.2,447.7z"/>
+</g>
+<g id="XMLID_10_">
+ <g id="XMLID_14_">
+ <g id="XMLID_72_">
+ <path id="XMLID_73_" class="st3" d="M8.7,470.2h-143.6c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4H8.7c3.5,0,6.4-2.9,6.4-6.4
+ S12.2,470.2,8.7,470.2z"/>
+ </g>
+ <g id="XMLID_56_">
+ <path id="XMLID_67_" class="st4" d="M8.7,470.2h-143.6c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4H8.7c3.5,0,6.4-2.9,6.4-6.4
+ S12.2,470.2,8.7,470.2z"/>
+ </g>
+ </g>
+ <g id="XMLID_26_">
+ <path id="XMLID_39_" class="st3" d="M-138.1,473.3h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8
+ l0,0C-141.3,476.4-139.9,473.3-138.1,473.3z"/>
+ <path id="XMLID_38_" class="st4" d="M-138.1,473.3h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8
+ l0,0C-141.3,476.4-139.9,473.3-138.1,473.3z"/>
+ </g>
+ <g id="XMLID_13_">
+ <g id="XMLID_69_">
+ <rect id="XMLID_70_" x="-141.3" y="476.5" class="st3" width="156.4" height="6.4"/>
+ </g>
+ <g id="XMLID_63_">
+ <rect id="XMLID_66_" x="-141.3" y="476.5" class="st4" width="156.4" height="6.4"/>
+ </g>
+ </g>
+</g>
+<g id="XMLID_2_">
+ <g id="XMLID_21_">
+ <path id="XMLID_25_" class="st4" d="M0.3,498.7"/>
+ </g>
+</g>
+<g>
+ <g>
+ <polygon class="st5" points="-45,321 -45.1,321.1 -46.8,323.1 -60.1,338.9 -42.7,390.6 -63.6,385.7 -48.8,420.6 -82,373.3
+ -59.4,374.1 -83.5,345.3 -76.8,321 "/>
+ </g>
+</g>
+</svg>
diff --git a/modules-available/locationinfo/frontend/img/pc_free.svg b/modules-available/locationinfo/frontend/img/pc_free.svg
new file mode 100644
index 00000000..d49fc9da
--- /dev/null
+++ b/modules-available/locationinfo/frontend/img/pc_free.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="119.9 123 241.8 172.7" style="enable-background:new 119.9 123 241.8 172.7;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#3C3C3B;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st1{fill:#00983A;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st2{fill:#3C3C3B;}
+ .st3{fill:none;stroke:#3C3C3B;stroke-miterlimit:10;}
+</style>
+<path id="XMLID_1_" class="st0" d="M354,263.4H127.6c-4.3,0-7.7-3.5-7.7-7.7v-125c0-4.3,3.5-7.7,7.7-7.7H354c4.3,0,7.7,3.5,7.7,7.7
+ v125C361.7,260,358.2,263.4,354,263.4z"/>
+<rect id="XMLID_3_" x="213" y="266.9" class="st0" width="55.6" height="19.4"/>
+<g id="XMLID_8_">
+ <rect id="XMLID_4_" x="130.3" y="132.6" class="st1" width="221.1" height="119"/>
+ <path id="XMLID_6_" class="st1" d="M342.1,260.4h-2.6c-1.6,0-2.9-1.3-2.9-2.9l0,0c0-1.6,1.3-2.9,2.9-2.9h2.6c1.6,0,2.9,1.3,2.9,2.9
+ l0,0C345,259.1,343.7,260.4,342.1,260.4z"/>
+</g>
+<g id="XMLID_10_">
+ <g id="XMLID_14_">
+ <g id="XMLID_72_">
+ <path id="XMLID_73_" class="st2" d="M312.6,282.9H169c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4h143.6c3.5,0,6.4-2.9,6.4-6.4
+ S316.1,282.9,312.6,282.9z"/>
+ </g>
+ <g id="XMLID_56_">
+ <path id="XMLID_67_" class="st3" d="M312.6,282.9H169c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4h143.6c3.5,0,6.4-2.9,6.4-6.4
+ S316.1,282.9,312.6,282.9z"/>
+ </g>
+ </g>
+ <g id="XMLID_26_">
+ <path id="XMLID_39_" class="st2" d="M165.8,286h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8l0,0
+ C162.6,289.1,164,286,165.8,286z"/>
+ <path id="XMLID_38_" class="st3" d="M165.8,286h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8l0,0
+ C162.6,289.1,164,286,165.8,286z"/>
+ </g>
+ <g id="XMLID_13_">
+ <g id="XMLID_69_">
+ <rect id="XMLID_70_" x="162.6" y="289.2" class="st2" width="156.4" height="6.4"/>
+ </g>
+ <g id="XMLID_63_">
+ <rect id="XMLID_66_" x="162.6" y="289.2" class="st3" width="156.4" height="6.4"/>
+ </g>
+ </g>
+</g>
+<g id="XMLID_2_">
+ <g id="XMLID_21_">
+ <path id="XMLID_25_" class="st3" d="M304.2,311.4"/>
+ </g>
+</g>
+</svg>
diff --git a/modules-available/locationinfo/frontend/img/pc_free_eink.svg b/modules-available/locationinfo/frontend/img/pc_free_eink.svg
new file mode 100644
index 00000000..39418e7b
--- /dev/null
+++ b/modules-available/locationinfo/frontend/img/pc_free_eink.svg
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="-184 310.3 241.8 172.7" style="enable-background:new -184 310.3 241.8 172.7;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#3C3C3B;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st1{fill:#FFFFFF;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st2{fill:#3C3C3B;}
+ .st3{fill:none;stroke:#3C3C3B;stroke-miterlimit:10;}
+</style>
+<path id="XMLID_1_" class="st0" d="M50.1,450.7h-226.4c-4.3,0-7.7-3.5-7.7-7.7V318c0-4.3,3.5-7.7,7.7-7.7H50.1
+ c4.3,0,7.7,3.5,7.7,7.7v125C57.8,447.3,54.3,450.7,50.1,450.7z"/>
+<rect id="XMLID_3_" x="-90.9" y="454.2" class="st0" width="55.6" height="19.4"/>
+<g id="XMLID_8_">
+ <rect id="XMLID_4_" x="-173.7" y="321" class="st1" width="221.1" height="119"/>
+ <path id="XMLID_6_" class="st1" d="M38.2,447.7h-2.6c-1.6,0-2.9-1.3-2.9-2.9l0,0c0-1.6,1.3-2.9,2.9-2.9h2.6c1.6,0,2.9,1.3,2.9,2.9
+ l0,0C41.1,446.4,39.8,447.7,38.2,447.7z"/>
+</g>
+<g id="XMLID_10_">
+ <g id="XMLID_14_">
+ <g id="XMLID_72_">
+ <path id="XMLID_73_" class="st2" d="M8.7,470.2h-143.6c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4H8.7c3.5,0,6.4-2.9,6.4-6.4
+ S12.2,470.2,8.7,470.2z"/>
+ </g>
+ <g id="XMLID_56_">
+ <path id="XMLID_67_" class="st3" d="M8.7,470.2h-143.6c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4H8.7c3.5,0,6.4-2.9,6.4-6.4
+ S12.2,470.2,8.7,470.2z"/>
+ </g>
+ </g>
+ <g id="XMLID_26_">
+ <path id="XMLID_39_" class="st2" d="M-138.1,473.3h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8
+ l0,0C-141.3,476.4-139.9,473.3-138.1,473.3z"/>
+ <path id="XMLID_38_" class="st3" d="M-138.1,473.3h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8
+ l0,0C-141.3,476.4-139.9,473.3-138.1,473.3z"/>
+ </g>
+ <g id="XMLID_13_">
+ <g id="XMLID_69_">
+ <rect id="XMLID_70_" x="-141.3" y="476.5" class="st2" width="156.4" height="6.4"/>
+ </g>
+ <g id="XMLID_63_">
+ <rect id="XMLID_66_" x="-141.3" y="476.5" class="st3" width="156.4" height="6.4"/>
+ </g>
+ </g>
+</g>
+<g id="XMLID_2_">
+ <g id="XMLID_21_">
+ <path id="XMLID_25_" class="st3" d="M0.3,498.7"/>
+ </g>
+</g>
+<g>
+ <g>
+ <path class="st2" d="M-5.9,350.7c0,0.7-0.3,1.4-0.8,1.9L-81,427c-0.5,0.6-1.1,0.8-1.9,0.8H-83h-0.1c-0.7,0-1.3-0.3-1.9-0.8
+ l-34.4-34.3c-0.6-0.7-0.8-1.4-0.8-2c0-0.6,0.3-1.3,0.8-2l14.7-14.7c1.3-1.2,2.7-1.2,4,0l17.7,17.8l57.6-57.7
+ c0.7-0.6,1.4-0.8,2-0.8c0.6,0,1.3,0.3,2,0.8l14.7,14.7C-6.2,349.3-5.9,350-5.9,350.7z"/>
+ </g>
+</g>
+</svg>
diff --git a/modules-available/locationinfo/frontend/img/pc_off.svg b/modules-available/locationinfo/frontend/img/pc_off.svg
new file mode 100644
index 00000000..9a98f7b5
--- /dev/null
+++ b/modules-available/locationinfo/frontend/img/pc_off.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="119.9 123 241.8 172.7" style="enable-background:new 119.9 123 241.8 172.7;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#3C3C3B;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st1{stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st2{fill:#3C3C3B;}
+ .st3{fill:none;stroke:#3C3C3B;stroke-miterlimit:10;}
+</style>
+<path id="XMLID_1_" class="st0" d="M354,263.4H127.6c-4.3,0-7.7-3.5-7.7-7.7v-125c0-4.3,3.5-7.7,7.7-7.7H354c4.3,0,7.7,3.5,7.7,7.7
+ v125C361.7,260,358.2,263.4,354,263.4z"/>
+<rect id="XMLID_3_" x="213" y="266.9" class="st0" width="55.6" height="19.4"/>
+<g id="XMLID_8_">
+ <rect id="XMLID_4_" x="130.3" y="132.6" class="st1" width="221.1" height="119"/>
+ <path id="XMLID_6_" class="st1" d="M342.1,260.4h-2.6c-1.6,0-2.9-1.3-2.9-2.9l0,0c0-1.6,1.3-2.9,2.9-2.9h2.6c1.6,0,2.9,1.3,2.9,2.9
+ l0,0C345,259.1,343.7,260.4,342.1,260.4z"/>
+</g>
+<g id="XMLID_10_">
+ <g id="XMLID_14_">
+ <g id="XMLID_72_">
+ <path id="XMLID_73_" class="st2" d="M312.6,282.9H169c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4h143.6c3.5,0,6.4-2.9,6.4-6.4
+ S316.1,282.9,312.6,282.9z"/>
+ </g>
+ <g id="XMLID_56_">
+ <path id="XMLID_67_" class="st3" d="M312.6,282.9H169c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4h143.6c3.5,0,6.4-2.9,6.4-6.4
+ S316.1,282.9,312.6,282.9z"/>
+ </g>
+ </g>
+ <g id="XMLID_26_">
+ <path id="XMLID_39_" class="st2" d="M165.8,286h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8l0,0
+ C162.6,289.1,164,286,165.8,286z"/>
+ <path id="XMLID_38_" class="st3" d="M165.8,286h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8l0,0
+ C162.6,289.1,164,286,165.8,286z"/>
+ </g>
+ <g id="XMLID_13_">
+ <g id="XMLID_69_">
+ <rect id="XMLID_70_" x="162.6" y="289.2" class="st2" width="156.4" height="6.4"/>
+ </g>
+ <g id="XMLID_63_">
+ <rect id="XMLID_66_" x="162.6" y="289.2" class="st3" width="156.4" height="6.4"/>
+ </g>
+ </g>
+</g>
+<g id="XMLID_2_">
+ <g id="XMLID_21_">
+ <path id="XMLID_25_" class="st3" d="M304.2,311.4"/>
+ </g>
+</g>
+</svg>
diff --git a/modules-available/locationinfo/frontend/img/pc_off_eink.svg b/modules-available/locationinfo/frontend/img/pc_off_eink.svg
new file mode 100644
index 00000000..9a98f7b5
--- /dev/null
+++ b/modules-available/locationinfo/frontend/img/pc_off_eink.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="119.9 123 241.8 172.7" style="enable-background:new 119.9 123 241.8 172.7;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#3C3C3B;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st1{stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st2{fill:#3C3C3B;}
+ .st3{fill:none;stroke:#3C3C3B;stroke-miterlimit:10;}
+</style>
+<path id="XMLID_1_" class="st0" d="M354,263.4H127.6c-4.3,0-7.7-3.5-7.7-7.7v-125c0-4.3,3.5-7.7,7.7-7.7H354c4.3,0,7.7,3.5,7.7,7.7
+ v125C361.7,260,358.2,263.4,354,263.4z"/>
+<rect id="XMLID_3_" x="213" y="266.9" class="st0" width="55.6" height="19.4"/>
+<g id="XMLID_8_">
+ <rect id="XMLID_4_" x="130.3" y="132.6" class="st1" width="221.1" height="119"/>
+ <path id="XMLID_6_" class="st1" d="M342.1,260.4h-2.6c-1.6,0-2.9-1.3-2.9-2.9l0,0c0-1.6,1.3-2.9,2.9-2.9h2.6c1.6,0,2.9,1.3,2.9,2.9
+ l0,0C345,259.1,343.7,260.4,342.1,260.4z"/>
+</g>
+<g id="XMLID_10_">
+ <g id="XMLID_14_">
+ <g id="XMLID_72_">
+ <path id="XMLID_73_" class="st2" d="M312.6,282.9H169c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4h143.6c3.5,0,6.4-2.9,6.4-6.4
+ S316.1,282.9,312.6,282.9z"/>
+ </g>
+ <g id="XMLID_56_">
+ <path id="XMLID_67_" class="st3" d="M312.6,282.9H169c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4h143.6c3.5,0,6.4-2.9,6.4-6.4
+ S316.1,282.9,312.6,282.9z"/>
+ </g>
+ </g>
+ <g id="XMLID_26_">
+ <path id="XMLID_39_" class="st2" d="M165.8,286h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8l0,0
+ C162.6,289.1,164,286,165.8,286z"/>
+ <path id="XMLID_38_" class="st3" d="M165.8,286h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8l0,0
+ C162.6,289.1,164,286,165.8,286z"/>
+ </g>
+ <g id="XMLID_13_">
+ <g id="XMLID_69_">
+ <rect id="XMLID_70_" x="162.6" y="289.2" class="st2" width="156.4" height="6.4"/>
+ </g>
+ <g id="XMLID_63_">
+ <rect id="XMLID_66_" x="162.6" y="289.2" class="st3" width="156.4" height="6.4"/>
+ </g>
+ </g>
+</g>
+<g id="XMLID_2_">
+ <g id="XMLID_21_">
+ <path id="XMLID_25_" class="st3" d="M304.2,311.4"/>
+ </g>
+</g>
+</svg>
diff --git a/modules-available/locationinfo/frontend/img/pc_used.svg b/modules-available/locationinfo/frontend/img/pc_used.svg
new file mode 100644
index 00000000..d5d10c10
--- /dev/null
+++ b/modules-available/locationinfo/frontend/img/pc_used.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="119.9 123 241.8 172.7" style="enable-background:new 119.9 123 241.8 172.7;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#3C3C3B;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st1{fill:#FF0000;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st2{fill:#3C3C3B;}
+ .st3{fill:none;stroke:#3C3C3B;stroke-miterlimit:10;}
+</style>
+<path id="XMLID_1_" class="st0" d="M354,263.4H127.6c-4.3,0-7.7-3.5-7.7-7.7v-125c0-4.3,3.5-7.7,7.7-7.7H354c4.3,0,7.7,3.5,7.7,7.7
+ v125C361.7,260,358.2,263.4,354,263.4z"/>
+<rect id="XMLID_3_" x="213" y="266.9" class="st0" width="55.6" height="19.4"/>
+<g id="XMLID_8_">
+ <rect id="XMLID_4_" x="130.3" y="132.6" class="st1" width="221.1" height="119"/>
+ <path id="XMLID_6_" class="st1" d="M342.1,260.4h-2.6c-1.6,0-2.9-1.3-2.9-2.9l0,0c0-1.6,1.3-2.9,2.9-2.9h2.6c1.6,0,2.9,1.3,2.9,2.9
+ l0,0C345,259.1,343.7,260.4,342.1,260.4z"/>
+</g>
+<g id="XMLID_10_">
+ <g id="XMLID_14_">
+ <g id="XMLID_72_">
+ <path id="XMLID_73_" class="st2" d="M312.6,282.9H169c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4h143.6c3.5,0,6.4-2.9,6.4-6.4
+ S316.1,282.9,312.6,282.9z"/>
+ </g>
+ <g id="XMLID_56_">
+ <path id="XMLID_67_" class="st3" d="M312.6,282.9H169c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4h143.6c3.5,0,6.4-2.9,6.4-6.4
+ S316.1,282.9,312.6,282.9z"/>
+ </g>
+ </g>
+ <g id="XMLID_26_">
+ <path id="XMLID_39_" class="st2" d="M165.8,286h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8l0,0
+ C162.6,289.1,164,286,165.8,286z"/>
+ <path id="XMLID_38_" class="st3" d="M165.8,286h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8l0,0
+ C162.6,289.1,164,286,165.8,286z"/>
+ </g>
+ <g id="XMLID_13_">
+ <g id="XMLID_69_">
+ <rect id="XMLID_70_" x="162.6" y="289.2" class="st2" width="156.4" height="6.4"/>
+ </g>
+ <g id="XMLID_63_">
+ <rect id="XMLID_66_" x="162.6" y="289.2" class="st3" width="156.4" height="6.4"/>
+ </g>
+ </g>
+</g>
+<g id="XMLID_2_">
+ <g id="XMLID_21_">
+ <path id="XMLID_25_" class="st3" d="M304.2,311.4"/>
+ </g>
+</g>
+</svg>
diff --git a/modules-available/locationinfo/frontend/img/pc_used_eink.svg b/modules-available/locationinfo/frontend/img/pc_used_eink.svg
new file mode 100644
index 00000000..ededaa58
--- /dev/null
+++ b/modules-available/locationinfo/frontend/img/pc_used_eink.svg
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="-184 310.3 241.8 172.7" style="enable-background:new -184 310.3 241.8 172.7;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#3C3C3B;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st1{fill:#FFFFFF;stroke:#3C3C3B;stroke-miterlimit:10;}
+ .st2{fill:#3C3C3B;}
+ .st3{fill:none;stroke:#3C3C3B;stroke-miterlimit:10;}
+</style>
+<path id="XMLID_1_" class="st0" d="M50.1,450.7h-226.4c-4.3,0-7.7-3.5-7.7-7.7V318c0-4.3,3.5-7.7,7.7-7.7H50.1
+ c4.3,0,7.7,3.5,7.7,7.7v125C57.8,447.3,54.3,450.7,50.1,450.7z"/>
+<rect id="XMLID_3_" x="-90.9" y="454.2" class="st0" width="55.6" height="19.4"/>
+<g id="XMLID_8_">
+ <rect id="XMLID_4_" x="-173.7" y="321" class="st1" width="221.1" height="119"/>
+ <path id="XMLID_6_" class="st1" d="M38.2,447.7h-2.6c-1.6,0-2.9-1.3-2.9-2.9l0,0c0-1.6,1.3-2.9,2.9-2.9h2.6c1.6,0,2.9,1.3,2.9,2.9
+ l0,0C41.1,446.4,39.8,447.7,38.2,447.7z"/>
+</g>
+<g id="XMLID_10_">
+ <g id="XMLID_14_">
+ <g id="XMLID_72_">
+ <path id="XMLID_73_" class="st2" d="M8.7,470.2h-143.6c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4H8.7c3.5,0,6.4-2.9,6.4-6.4
+ S12.2,470.2,8.7,470.2z"/>
+ </g>
+ <g id="XMLID_56_">
+ <path id="XMLID_67_" class="st3" d="M8.7,470.2h-143.6c-3.5,0-6.4,2.9-6.4,6.4s2.9,6.4,6.4,6.4H8.7c3.5,0,6.4-2.9,6.4-6.4
+ S12.2,470.2,8.7,470.2z"/>
+ </g>
+ </g>
+ <g id="XMLID_26_">
+ <path id="XMLID_39_" class="st2" d="M-138.1,473.3h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8
+ l0,0C-141.3,476.4-139.9,473.3-138.1,473.3z"/>
+ <path id="XMLID_38_" class="st3" d="M-138.1,473.3h150c1.8,0,3.2,3,3.2,4.8l0,0c0,1.8-1.4,4.8-3.2,4.8h-150c-1.8,0-3.2-3-3.2-4.8
+ l0,0C-141.3,476.4-139.9,473.3-138.1,473.3z"/>
+ </g>
+ <g id="XMLID_13_">
+ <g id="XMLID_69_">
+ <rect id="XMLID_70_" x="-141.3" y="476.5" class="st2" width="156.4" height="6.4"/>
+ </g>
+ <g id="XMLID_63_">
+ <rect id="XMLID_66_" x="-141.3" y="476.5" class="st3" width="156.4" height="6.4"/>
+ </g>
+ </g>
+</g>
+<g id="XMLID_2_">
+ <g id="XMLID_21_">
+ <path id="XMLID_25_" class="st3" d="M0.3,498.7"/>
+ </g>
+</g>
+<g>
+ <g id="x">
+ <g>
+ <polygon class="st2" points="-27.1,402.1 -48.7,380.5 -27.1,358.9 -41.5,344.5 -63.1,366.1 -84.7,344.5 -99.1,358.9 -77.5,380.5
+ -99.1,402.1 -84.7,416.5 -63.1,394.9 -41.5,416.5 "/>
+ </g>
+ </g>
+</g>
+</svg>
diff --git a/modules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.css b/modules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.css
new file mode 100755
index 00000000..aae8f956
--- /dev/null
+++ b/modules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.css
@@ -0,0 +1,284 @@
+.wc-container {
+ font-size: 14px;
+ font-family: arial, helvetica;
+}
+
+.wc-toolbar {
+ padding: 1em;
+ font-size:0.8em;
+}
+
+.wc-toolbar .wc-nav {
+ float:left;
+}
+
+.wc-toolbar .wc-display {
+ float: right;
+}
+
+.wc-toolbar button {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.wc-toolbar .wc-title {
+ text-align: center;
+ padding:0;
+ margin:0;
+}
+
+.wc-container table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+.wc-container table td {
+ margin: 0;
+ padding: 0;
+}
+
+.wc-header {
+ background: #eee;
+ border-width:1px 0;
+ border-style:solid;
+}
+.wc-header table{
+ width: 100%;
+ table-layout:fixed;
+}
+
+.wc-grid-timeslot-header,
+.wc-header .wc-time-column-header {
+ width: 45px;
+}
+
+.wc-header .wc-scrollbar-shim {
+ width: 16px;
+}
+
+.wc-header .wc-day-column-header {
+ text-align: center;
+ padding: 0.4em;
+}
+
+.wc-header .wc-user-header{
+ text-align: center;
+ padding: 0.4em 0;
+ overflow:hidden;
+}
+.wc-grid-timeslot-header {
+ background: #eee;
+}
+
+.wc-scrollable-grid {
+ overflow: auto;
+ overflow-x: hidden !important;
+ overflow-y: auto !important;
+ position: relative;
+ background-color: #fff;
+ width: 100%;
+}
+
+
+table.wc-time-slots {
+ width: 100%;
+ table-layout: fixed;
+ cursor: default;
+ overflow:hidden;
+}
+
+.wc-day-column {
+ width: 13.5%;
+ overflow: visible;
+ vertical-align: top;
+}
+.wc-day-column-header{border-width: 0 0 1px 3px; border-style: solid;border-color:transparent;}
+.wc-scrollable-grid .wc-day-column-last,
+.wc-scrollable-grid .wc-day-column-middle{border-width: 0 0 0 1px; border-style: dashed;}
+.wc-scrollable-grid .wc-day-column-first{border-width: 0 0 0 3px; border-style: double;}
+
+.wc-day-column-inner {
+ width: 100%;
+ position:relative;
+}
+
+.wc-no-height-wrapper{
+ position:relative;
+ overflow: visible;
+ height: 0px;
+}
+
+.wc-time-slot-wrapper {
+/* top: 3px;*/
+}
+.wc-oddeven-wrapper .wc-full-height-column{
+/* top: 2px; */
+ /* Modern Browsers */ opacity: 0.4;
+ /* IE 8 */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)";
+ /* IE 5-7 */ filter: alpha(opacity=40);
+ /* Netscape */ -moz-opacity: 0.4;
+ /* Safari 1 */ -khtml-opacity: 0.4;
+}
+.wc-freebusy-wrapper .wc-freebusy{
+/* top: 1px;*/
+ /* Modern Browsers */ opacity: 0.4;
+ /* IE 8 */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)";
+ /* IE 5-7 */ filter: alpha(opacity=40);
+ /* Netscape */ -moz-opacity: 0.4;
+ /* Safari 1 */ -khtml-opacity: 0.4;
+}
+
+.wc-time-slots {
+ position: absolute;
+ width: 100%;
+}
+
+.wc-column-odd,
+.wc-column-even.ui-state-hover{background-image:none;border:none;}
+
+.wc-header .wc-today.ui-state-active{background-image:none;}
+.wc-header .wc-today.wc-day-column-header{border-width:0 3px; border-style: solid;}
+.wc-header .wc-user-header{border-width:0;}
+
+.wc-time-slots .wc-day-column.ui-state-default{background:transparent;}
+.wc-time-slots .wc-today.ui-state-active{background-image:none;}
+.wc-header .wc-today.ui-state-active.wc-day-column-middle{border-width:0;}
+.wc-header .wc-today.ui-state-active.wc-day-column-first{border-left-width:3px;}
+.wc-header .wc-today.ui-state-active.wc-day-column-last{border-right-width:3px;}
+
+.wc-full-height-column{
+ display:block;
+/* width:100%;*/
+}
+
+
+.wc-time-header-cell {
+ padding: 5px;
+ height: 80px; /* reference height */
+}
+
+
+.wc-time-slot {
+ border-bottom: 1px dotted #ddd;
+}
+
+.wc-hour-header {
+ text-align: right;
+}
+.wc-hour-header.ui-state-active,
+.wc-hour-header.ui-state-default{
+ border-width:0 0 1px 0;
+}
+
+.wc-hour-end, .wc-hour-header {
+ border-bottom: 1px solid #ccc;
+ color: #555;
+}
+
+.wc-business-hours {
+ background-color: #E6EEF1;
+ border-bottom: 1px solid #ccc;
+ color: #333;
+ font-size: 1em;
+ font-weight: bold;
+}
+
+.wc-business-hours .wc-am-pm {
+ font-size: 0.6em;
+}
+
+.wc-day-header-cell {
+ text-align: center;
+ vertical-align: middle;
+ padding: 5px;
+}
+
+
+
+.wc-time-slot-header .wc-header-cell {
+ text-align: right;
+ padding-right: 10px;
+}
+
+.wc-cal-event {
+ background-color: #68a1e5;
+ /* Modern Browsers */ opacity: 0.8;
+ /* IE 8 */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
+ /* IE 5-7 */ filter: alpha(opacity=80);
+ /* Netscape */ -moz-opacity: 0.8;
+ /* Safari 1 */ -khtml-opacity: 0.8;
+ position: absolute;
+ text-align: center;
+ overflow: hidden;
+ cursor: pointer;
+ color: #fff;
+ width: 100%;
+ display: none;
+}
+
+
+.wc-cal-event-delete {
+ float: right;
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+}
+
+.wc-cal-event.ui-resizable-resizing {
+ cursor: s-resize;
+}
+
+.wc-cal-event .wc-time {
+ background-color: #2b72d0;
+ border: 1px solid #1b62c0;
+ color: #fff;
+ padding: 0;
+ font-weight: bold;
+}
+
+.wc-container .ui-draggable .wc-time {
+ cursor: move;
+}
+
+.wc-cal-event .wc-title {
+ position: relative;
+}
+
+.wc-container .ui-resizable-s {
+ height: 10px;
+ line-height: 10px;
+ bottom: -2px;
+ font-size: .75em;
+}
+
+
+.wc-container .ui-draggable-dragging {
+ z-index: 1000;
+}
+
+.free-busy-free{}
+.free-busy-busy{
+ background:url("./libs/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png") repeat scroll 50% 50% #666666;
+}
+
+/** hourLine */
+
+.wc-hourline {
+ height: 0pt;
+ border-top: 2px solid #FF7F6E;
+ overflow: hidden;
+ position: absolute;
+ width: inherit;
+}
+
+/* IE6 hacks */
+* html .wc-no-height-wrapper{position:absolute;}
+* html .wc-time-slot-wrapper{top:3px;}
+* html .wc-grid-row-oddeven{top:2px;}
+* html .wc-grid-row-freebusy{top:1px;}
+
+/* IE7 hacks */
+*:first-child+html .wc-no-height-wrapper{position:relative;}
+*:first-child+html .wc-time-slot-wrapper{top:3px;}
+*:first-child+html .wc-grid-row-oddeven{top:2px;}
+*:first-child+html .wc-grid-row-freebusy{top:1px;}
+*:first-child+html .wc-time-slots .wc-today{/* due to rendering issues, no background */background:none;}
diff --git a/modules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.iml b/modules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.iml
new file mode 100755
index 00000000..0f7b5ef4
--- /dev/null
+++ b/modules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.iml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module relativePaths="true" type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$" />
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module>
+
diff --git a/modules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.js b/modules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.js
new file mode 100755
index 00000000..9d83afca
--- /dev/null
+++ b/modules-available/locationinfo/frontend/jquery-week-calendar/jquery.weekcalendar.js
@@ -0,0 +1,2961 @@
+/*
+ * jQuery.weekCalendar v2.0-dev
+ *
+ * for support join us at the google group:
+ * - http://groups.google.com/group/jquery-week-calendar
+ * have a look to the wiki for documentation:
+ * - http://wiki.github.com/themouette/jquery-week-calendar/
+ * something went bad ? report an issue:
+ * - http://github.com/themouette/jquery-week-calendar/issues
+ * get the last version on github:
+ * - http://github.com/themouette/jquery-week-calendar
+ *
+ * Copyright (c) 2009 Rob Monie
+ * Copyright (c) 2010 Julien MUETTON
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * If you're after a monthly calendar plugin, check out this one :
+ * http://arshaw.com/fullcalendar/
+ */
+var startdate;
+
+function SetUpDate(d) {
+ startdate = d.getTime()-new Date().getTime();
+}
+
+function MyDate() {
+ return new Date(startdate +new Date().getTime());
+}
+
+(function($) {
+
+ // check the jquery version
+ var _v = $.fn.jquery.split('.'),
+ _jQuery14OrLower = (10 * _v[0] + _v[1]) < 15;
+ $.widget('ui.weekCalendar', (function() {
+ var _currentAjaxCall, _hourLineTimeout;
+
+ return {
+ options: {
+ date: new MyDate(),
+ timeFormat: null,
+ dateFormat: 'M d, Y',
+ alwaysDisplayTimeMinutes: true,
+ use24Hour: false,
+ daysToShow: 7,
+ minBodyHeight: 100,
+ firstDayOfWeek: function(calendar) {
+ if ($(calendar).weekCalendar('option', 'daysToShow') != 5) {
+ return 0;
+ } else {
+ //workweek
+ return 1;
+ }
+ }, // 0 = Sunday, 1 = Monday, 2 = Tuesday, ... , 6 = Saturday
+ useShortDayNames: false,
+ timeSeparator: ' to ',
+ startParam: 'start',
+ endParam: 'end',
+ businessHours: {start: 8, end: 18, limitDisplay: false},
+ newEventText: 'New Event',
+ timeslotHeight: 20,
+ defaultEventLength: 2,
+ timeslotsPerHour: 4,
+ minDate: null,
+ maxDate: null,
+ showHeader: true,
+ buttons: true,
+ buttonText: {
+ today: 'today',
+ lastWeek: 'previous',
+ nextWeek: 'next'
+ },
+ switchDisplay: {},
+ scrollToHourMillis: 500,
+ allowEventDelete: false,
+ allowCalEventOverlap: false,
+ overlapEventsSeparate: false,
+ totalEventsWidthPercentInOneColumn: 100,
+ readonly: false,
+ allowEventCreation: true,
+ hourLine: false,
+ deletable: function(calEvent, element) {
+ return true;
+ },
+ draggable: function(calEvent, element) {
+ return true;
+ },
+ resizable: function(calEvent, element) {
+ return true;
+ },
+ eventClick: function(calEvent, element, dayFreeBusyManager,
+ calendar, clickEvent) {
+ },
+ eventRender: function(calEvent, element) {
+ return element;
+ },
+ eventAfterRender: function(calEvent, element) {
+ return element;
+ },
+ eventRefresh: function(calEvent, element) {
+ return element;
+ },
+ eventDrag: function(calEvent, element) {
+ },
+ eventDrop: function(calEvent, element) {
+ },
+ eventResize: function(calEvent, element) {
+ },
+ eventNew: function(calEvent, element, dayFreeBusyManager,
+ calendar, mouseupEvent) {
+ },
+ eventMouseover: function(calEvent, $event) {
+ },
+ eventMouseout: function(calEvent, $event) {
+ },
+ eventDelete: function(calEvent, element, dayFreeBusyManager,
+ calendar, clickEvent) {
+ calendar.weekCalendar('removeEvent',calEvent.id);
+ },
+ calendarBeforeLoad: function(calendar) {
+ },
+ calendarAfterLoad: function(calendar) {
+ },
+ noEvents: function() {
+ },
+ eventHeader: function(calEvent, calendar) {
+ var options = calendar.weekCalendar('option');
+ var one_hour = 3600000;
+ var displayTitleWithTime = calEvent.end.getTime() - calEvent.start.getTime() <= (one_hour / options.timeslotsPerHour);
+ if (displayTitleWithTime) {
+ return calendar.weekCalendar(
+ 'formatTime', calEvent.start) +
+ ': ' + calEvent.title;
+ } else {
+ return calendar.weekCalendar(
+ 'formatTime', calEvent.start) +
+ options.timeSeparator +
+ calendar.weekCalendar(
+ 'formatTime', calEvent.end);
+ }
+ },
+ eventBody: function(calEvent, calendar) {
+ return calEvent.title;
+ },
+ shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+ longMonths: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ longDays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+ /* multi-users options */
+ /**
+ * the available users for calendar.
+ * if you want to display users separately, enable the
+ * showAsSeparateUsers option.
+ * if you provide a list of user and do not enable showAsSeparateUsers
+ * option, then only the events that belongs to one or several of
+ * given users will be displayed
+ * @type {array}
+ */
+ users: [],
+ /**
+ * should the calendar be displayed with separate column for each
+ * users.
+ * note that this option does nothing if you do not provide at least
+ * one user.
+ * @type {boolean}
+ */
+ showAsSeparateUsers: true,
+ /**
+ * callback used to read user id from a user object.
+ * @param {Object} user the user to retrieve the id from.
+ * @param {number} index the user index from user list.
+ * @param {jQuery} calendar the calendar object.
+ * @return {int|String} the user id.
+ */
+ getUserId: function(user, index, calendar) {
+ return index;
+ },
+ /**
+ * callback used to read user name from a user object.
+ * @param {Object} user the user to retrieve the name from.
+ * @param {number} index the user index from user list.
+ * @param {jQuery} calendar the calendar object.
+ * @return {String} the user name.
+ */
+ getUserName: function(user, index, calendar) {
+ return user;
+ },
+ /**
+ * reads the id(s) of user(s) for who the event should be displayed.
+ * @param {Object} calEvent the calEvent to read informations from.
+ * @param {jQuery} calendar the calendar object.
+ * @return {number|String|Array} the user id(s) to appened events for.
+ */
+ getEventUserId: function(calEvent, calendar) {
+ return calEvent.userId;
+ },
+ /**
+ * sets user id(s) to the calEvent
+ * @param {Object} calEvent the calEvent to set informations to.
+ * @param {jQuery} calendar the calendar object.
+ * @return {Object} the calEvent with modified user id.
+ */
+ setEventUserId: function(userId, calEvent, calendar) {
+ calEvent.userId = userId;
+ return calEvent;
+ },
+ /* freeBusy options */
+ /**
+ * should the calendar display freebusys ?
+ * @type {boolean}
+ */
+ displayFreeBusys: false,
+ /**
+ * read the id(s) for who the freebusy is available
+ * @param {Object} calEvent the calEvent to read informations from.
+ * @param {jQuery} calendar the calendar object.
+ * @return {number|String|Array} the user id(s) to appened events for.
+ */
+ getFreeBusyUserId: function(calFreeBusy, calendar) {
+ return calFreeBusy.userId;
+ },
+ /**
+ * the default freeBusy object, used to manage default state
+ * @type {Object}
+ */
+ defaultFreeBusy: {free: false},
+ /**
+ * function used to display the freeBusy element
+ * @type {Function}
+ * @param {Object} freeBusy the freeBusy timeslot to render.
+ * @param {jQuery} $freeBusy the freeBusy HTML element.
+ * @param {jQuery} calendar the calendar element.
+ */
+ freeBusyRender: function(freeBusy, $freeBusy, calendar) {
+ if (!freeBusy.free) {
+ $freeBusy.addClass('free-busy-busy');
+ }
+ else {
+ $freeBusy.addClass('free-busy-free');
+ }
+ return $freeBusy;
+ },
+ /* other options */
+ /**
+ * true means start on first day of week, false means starts on
+ * startDate.
+ * @param {jQuery} calendar the calendar object.
+ * @type {Function|bool}
+ */
+ startOnFirstDayOfWeek: function(calendar) {
+ return $(calendar).weekCalendar('option', 'daysToShow') >= 5;
+ },
+ /**
+ * should the columns be rendered alternatively using odd/even
+ * class
+ * @type {boolean}
+ */
+ displayOddEven: false,
+ textSize: 13,
+ /**
+ * the title attribute for the calendar. possible placeholders are:
+ * <ul>
+ * <li>%start%</li>
+ * <li>%end%</li>
+ * <li>%date%</li>
+ * </ul>
+ * @type {Function|string}
+ * @param {number} option daysToShow.
+ * @return {String} the title attribute for the calendar.
+ */
+ title: '%start% - %end%',
+ /**
+ * default options to pass to callback
+ * you can pass a function returning an object or a litteral object
+ * @type {object|function(#calendar)}
+ */
+ jsonOptions: {},
+ headerSeparator: '<br />',
+ /**
+ * returns formatted header for day display
+ * @type {function(date,calendar)}
+ */
+ getHeaderDate: null,
+ preventDragOnEventCreation: false,
+ /**
+ * the event on which to bind calendar resize
+ * @type {string}
+ */
+ resizeEvent: 'resize.weekcalendar'
+ },
+
+ /***********************
+ * Initialise calendar *
+ ***********************/
+ _create: function() {
+ var self = this;
+ self._computeOptions();
+ self._setupEventDelegation();
+ self._renderCalendar();
+ self._loadCalEvents();
+ self._resizeCalendar();
+ self._scrollToHour(self.options.date.getHours(), true);
+
+ if (this.options.resizeEvent) {
+ $(window).unbind(this.options.resizeEvent);
+ $(window).bind(this.options.resizeEvent, function() {
+ self._resizeCalendar();
+ });
+ }
+
+ },
+
+ /********************
+ * public functions *
+ ********************/
+ /*
+ * Refresh the events for the currently displayed week.
+ */
+ refresh: function() {
+ //reload with existing week
+ this._loadCalEvents(this.element.data('startDate'));
+ },
+
+ resizeCalendar:function(){
+ this._resizeCalendar();
+ },
+ scrollToHour:function(){
+ this._scrollToHour(this._getCurrentScrollHour(), false);
+ },
+ /*
+ * Clear all events currently loaded into the calendar
+ */
+ clear: function() {
+ this._clearCalendar();
+ },
+
+ /*
+ * Go to this week
+ */
+ today: function() {
+ this._clearCalendar();
+ this._loadCalEvents(new MyDate());
+ },
+
+ /*
+ * Go to the previous week relative to the currently displayed week
+ */
+ prevWeek: function() {
+ //minus more than 1 day to be sure we're in previous week - account for daylight savings or other anomolies
+ var newDate = new Date(this.element.data('startDate').getTime() - (MILLIS_IN_WEEK / 6));
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+
+ /*
+ * Go to the next week relative to the currently displayed week
+ */
+ nextWeek: function() {
+ //add 8 days to be sure of being in prev week - allows for daylight savings or other anomolies
+ var newDate = new Date(this.element.data('startDate').getTime() + MILLIS_IN_WEEK + MILLIS_IN_DAY);
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+
+ /*
+ * Reload the calendar to whatever week the date passed in falls on.
+ */
+ gotoWeek: function(date) {
+ this._clearCalendar();
+ this._loadCalEvents(date);
+ },
+
+ /*
+ * Reload the calendar to whatever week the date passed in falls on.
+ */
+ gotoDate: function(date) {
+ this._clearCalendar();
+ this._loadCalEvents(date);
+ },
+
+ /**
+ * change the number of days to show
+ */
+ setDaysToShow: function(daysToShow) {
+ var self = this;
+ var hour = self._getCurrentScrollHour();
+ self.options.daysToShow = daysToShow;
+ $(self.element).html('');
+ self._renderCalendar();
+ self._loadCalEvents();
+ self._resizeCalendar();
+ self._scrollToHour(hour, false);
+
+ if (this.options.resizeEvent) {
+ $(window).unbind(this.options.resizeEvent);
+ $(window).bind(this.options.resizeEvent, function() {
+ self._resizeCalendar();
+ });
+ }
+ },
+
+ /*
+ * Remove an event based on it's id
+ */
+ removeEvent: function(eventId) {
+
+ var self = this;
+
+ self.element.find('.wc-cal-event').each(function() {
+ if ($(this).data('calEvent').id === eventId) {
+ $(this).remove();
+ return false;
+ }
+ });
+
+ //this could be more efficient rather than running on all days regardless...
+ self.element.find('.wc-day-column-inner').each(function() {
+ self._adjustOverlappingEvents($(this));
+ });
+ },
+
+ /*
+ * Removes any events that have been added but not yet saved (have no id).
+ * This is useful to call after adding a freshly saved new event.
+ */
+ removeUnsavedEvents: function() {
+
+ var self = this;
+
+ self.element.find('.wc-new-cal-event').each(function() {
+ $(this).remove();
+ });
+
+ //this could be more efficient rather than running on all days regardless...
+ self.element.find('.wc-day-column-inner').each(function() {
+ self._adjustOverlappingEvents($(this));
+ });
+ },
+
+ /*
+ * update an event in the calendar. If the event exists it refreshes
+ * it's rendering. If it's a new event that does not exist in the calendar
+ * it will be added.
+ */
+ updateEvent: function(calEvent) {
+ this._updateEventInCalendar(calEvent);
+ },
+
+ /*
+ * Returns an array of timeslot start and end times based on
+ * the configured grid of the calendar. Returns in both date and
+ * formatted time based on the 'timeFormat' config option.
+ */
+ getTimeslotTimes: function(date) {
+ var options = this.options;
+ var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
+ var startDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), firstHourDisplayed);
+
+ var times = [],
+ startMillis = startDate.getTime();
+ for (var i = 0; i < options.timeslotsPerDay; i++) {
+ var endMillis = startMillis + options.millisPerTimeslot;
+ times[i] = {
+ start: new Date(startMillis),
+ startFormatted: this.formatTime(new Date(startMillis), options.timeFormat),
+ end: new Date(endMillis),
+ endFormatted: this.formatTime(new Date(endMillis), options.timeFormat)
+ };
+ startMillis = endMillis;
+ }
+ return times;
+ },
+
+ formatDate: function(date, format) {
+ if (format) {
+ return this._formatDate(date, format);
+ } else {
+ return this._formatDate(date, this.options.dateFormat);
+ }
+ },
+
+ formatTime: function(date, format) {
+ if (format) {
+ return this._formatDate(date, format);
+ } else if (this.options.timeFormat) {
+ return this._formatDate(date, this.options.timeFormat);
+ } else if (this.options.use24Hour) {
+ return this._formatDate(date, 'H:i');
+ } else {
+ return this._formatDate(date, 'h:i a');
+ }
+ },
+
+ serializeEvents: function() {
+ var self = this;
+ var calEvents = [];
+
+ self.element.find('.wc-cal-event').each(function() {
+ calEvents.push($(this).data('calEvent'));
+ });
+ return calEvents;
+ },
+
+ next: function() {
+ if (this._startOnFirstDayOfWeek()) {
+ return this.nextWeek();
+ }
+ var newDate = new Date(this.element.data('startDate').getTime());
+ newDate.setDate(newDate.getDate() + this.options.daysToShow);
+
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+
+ prev: function() {
+ if (this._startOnFirstDayOfWeek()) {
+ return this.prevWeek();
+ }
+ var newDate = new Date(this.element.data('startDate').getTime());
+ newDate.setDate(newDate.getDate() - this.options.daysToShow);
+
+ this._clearCalendar();
+ this._loadCalEvents(newDate);
+ },
+ getCurrentFirstDay: function() {
+ return this._dateFirstDayOfWeek(this.options.date || new MyDate());
+ },
+ getCurrentLastDay: function() {
+ return this._addDays(this.getCurrentFirstDay(), this.options.daysToShow - 1);
+ },
+
+ /*********************
+ * private functions *
+ *********************/
+ _setOption: function(key, value) {
+ var self = this;
+ if (self.options[key] != value) {
+ // event callback change, no need to re-render the events
+ if (key == 'beforeEventNew') {
+ self.options[key] = value;
+ return;
+ }
+
+ // this could be made more efficient at some stage by caching the
+ // events array locally in a store but this should be done in conjunction
+ // with a proper binding model.
+
+ var currentEvents = self.element.find('.wc-cal-event').map(function() {
+ return $(this).data('calEvent');
+ });
+
+ var newOptions = {};
+ newOptions[key] = value;
+ self._renderEvents({events: currentEvents, options: newOptions}, self.element.find('.wc-day-column-inner'));
+ }
+ },
+
+ // compute dynamic options based on other config values
+ _computeOptions: function() {
+ var options = this.options;
+ if (options.businessHours.limitDisplay) {
+ options.timeslotsPerDay = options.timeslotsPerHour * (options.businessHours.end - options.businessHours.start);
+ options.millisToDisplay = (options.businessHours.end - options.businessHours.start) * 3600000; // 60 * 60 * 1000
+ options.millisPerTimeslot = options.millisToDisplay / options.timeslotsPerDay;
+ } else {
+ options.timeslotsPerDay = options.timeslotsPerHour * 24;
+ options.millisToDisplay = MILLIS_IN_DAY;
+ options.millisPerTimeslot = MILLIS_IN_DAY / options.timeslotsPerDay;
+ }
+ },
+
+ /*
+ * Resize the calendar scrollable height based on the provided function in options.
+ */
+ _resizeCalendar: function() {
+ var options = this.options;
+ if (options && $.isFunction(options.height)) {
+ var calendarHeight = options.height(this.element);
+ var headerHeight = this.element.find('.wc-header').outerHeight();
+ var navHeight = this.element.find('.wc-toolbar').outerHeight();
+ var scrollContainerHeight = Math.max(calendarHeight - navHeight - headerHeight, options.minBodyHeight);
+ var timeslotHeight = this.element.find('.wc-time-slots').outerHeight();
+ this.element.find('.wc-scrollable-grid').height(scrollContainerHeight);
+ if (timeslotHeight <= scrollContainerHeight) {
+ this.element.find('.wc-scrollbar-shim').width(0);
+ }
+ else {
+ this.element.find('.wc-scrollbar-shim').width(this._findScrollBarWidth());
+ }
+ this._trigger('resize', this.element);
+ }
+ },
+
+ _findScrollBarWidth: function() {
+ var parent = $('<div style="width:50px;height:50px;overflow:auto"><div/></div>').appendTo('body');
+ var child = parent.children();
+ var width = child.innerWidth() - child.height(99).innerWidth();
+ parent.remove();
+ return width || /* default to 16 that is the average */ 16;
+ },
+
+ /*
+ * configure calendar interaction events that are able to use event
+ * delegation for greater efficiency
+ */
+ _setupEventDelegation: function() {
+ var self = this;
+ var options = this.options;
+
+ this.element.click(function(event) {
+ var $target = $(event.target),
+ freeBusyManager;
+
+ // click is disabled
+ if ($target.data('preventClick')) {
+ return;
+ }
+
+ var $calEvent = $target.hasClass('wc-cal-event') ?
+ $target :
+ $target.parents('.wc-cal-event');
+ if (!$calEvent.length || !$calEvent.data('calEvent')) {
+ return;
+ }
+
+ freeBusyManager = self.getFreeBusyManagerForEvent($calEvent.data('calEvent'));
+
+ if (options.allowEventDelete && $target.hasClass('wc-cal-event-delete')) {
+ options.eventDelete($calEvent.data('calEvent'), $calEvent, freeBusyManager, self.element, event);
+ } else {
+ options.eventClick($calEvent.data('calEvent'), $calEvent, freeBusyManager, self.element, event);
+ }
+ }).mouseover(function(event) {
+ var $target = $(event.target);
+ var $calEvent = $target.hasClass('wc-cal-event') ?
+ $target :
+ $target.parents('.wc-cal-event');
+
+ if (!$calEvent.length || !$calEvent.data('calEvent')) {
+ return;
+ }
+
+ if (self._isDraggingOrResizing($calEvent)) {
+ return;
+ }
+
+ options.eventMouseover($calEvent.data('calEvent'), $calEvent, event);
+ }).mouseout(function(event) {
+ var $target = $(event.target);
+ var $calEvent = $target.hasClass('wc-cal-event') ?
+ $target :
+ $target.parents('.wc-cal-event');
+
+ if (!$calEvent.length || !$calEvent.data('calEvent')) {
+ return;
+ }
+
+ if (self._isDraggingOrResizing($calEvent)) {
+ return;
+ }
+
+ options.eventMouseout($calEvent.data('calEvent'), $calEvent, event);
+ });
+ },
+
+ /**
+ * check if a ui draggable or resizable is currently being dragged or
+ * resized.
+ */
+ _isDraggingOrResizing: function($target) {
+ return $target.hasClass('ui-draggable-dragging') ||
+ $target.hasClass('ui-resizable-resizing');
+ },
+
+ /*
+ * Render the main calendar layout
+ */
+ _renderCalendar: function() {
+ var $calendarContainer, $weekDayColumns;
+ var self = this;
+ var options = this.options;
+
+ $calendarContainer = $('<div class=\"ui-widget wc-container\">').appendTo(self.element);
+
+ //render the different parts
+ // nav links
+ self._renderCalendarButtons($calendarContainer);
+ // header
+ self._renderCalendarHeader($calendarContainer);
+ // body
+ self._renderCalendarBody($calendarContainer);
+
+ $weekDayColumns = $calendarContainer.find('.wc-day-column-inner');
+ $weekDayColumns.each(function(i, val) {
+ if (!options.readonly) {
+ self._addDroppableToWeekDay($(this));
+ if (options.allowEventCreation) {
+ self._setupEventCreationForWeekDay($(this));
+ }
+ }
+ });
+ },
+
+ /**
+ * render the nav buttons on top of the calendar
+ */
+ _renderCalendarButtons: function($calendarContainer) {
+ var self = this, options = this.options;
+ if ( !options.showHeader ) return;
+ if (options.buttons) {
+ var calendarNavHtml = '';
+
+ calendarNavHtml += '<div class=\"ui-widget-header wc-toolbar\">';
+ calendarNavHtml += '<div class=\"wc-display\"></div>';
+ calendarNavHtml += '<div class=\"wc-nav\">';
+ calendarNavHtml += '<button class=\"wc-prev\">' + options.buttonText.lastWeek + '</button>';
+ calendarNavHtml += '<button class=\"wc-today\">' + options.buttonText.today + '</button>';
+ calendarNavHtml += '<button class=\"wc-next\">' + options.buttonText.nextWeek + '</button>';
+ calendarNavHtml += '</div>';
+ calendarNavHtml += '<h1 class=\"wc-title\"></h1>';
+ calendarNavHtml += '</div>';
+
+ $(calendarNavHtml).appendTo($calendarContainer);
+
+ $calendarContainer.find('.wc-nav .wc-today')
+ .button({
+ icons: {primary: 'ui-icon-home'}})
+ .click(function() {
+ self.today();
+ return false;
+ });
+
+ $calendarContainer.find('.wc-nav .wc-prev')
+ .button({
+ text: false,
+ icons: {primary: 'ui-icon-seek-prev'}})
+ .click(function() {
+ self.element.weekCalendar('prev');
+ return false;
+ });
+
+ $calendarContainer.find('.wc-nav .wc-next')
+ .button({
+ text: false,
+ icons: {primary: 'ui-icon-seek-next'}})
+ .click(function() {
+ self.element.weekCalendar('next');
+ return false;
+ });
+
+ // now add buttons to switch display
+ if (this.options.switchDisplay && $.isPlainObject(this.options.switchDisplay)) {
+ var $container = $calendarContainer.find('.wc-display');
+ $.each(this.options.switchDisplay, function(label, option) {
+ var _id = 'wc-switch-display-' + option;
+ var _input = $('<input type="radio" id="' + _id + '" name="wc-switch-display" class="wc-switch-display"/>');
+ var _label = $('<label for="' + _id + '"></label>');
+ _label.html(label);
+ _input.val(option);
+ if (parseInt(self.options.daysToShow, 10) === parseInt(option, 10)) {
+ _input.attr('checked', 'checked');
+ }
+ $container
+ .append(_input)
+ .append(_label);
+ });
+ $container.find('input').change(function() {
+ self.setDaysToShow(parseInt($(this).val(), 10));
+ });
+ }
+ $calendarContainer.find('.wc-nav, .wc-display').buttonset();
+ var _height = $calendarContainer.find('.wc-nav').outerHeight();
+ $calendarContainer.find('.wc-title')
+ .height(_height)
+ .css('line-height', _height + 'px');
+ }else{
+ var calendarNavHtml = '';
+ calendarNavHtml += '<div class=\"ui-widget-header wc-toolbar\">';
+ calendarNavHtml += '<h1 class=\"wc-title\"></h1>';
+ calendarNavHtml += '</div>';
+ $(calendarNavHtml).appendTo($calendarContainer);
+
+ }
+ },
+
+ /**
+ * render the calendar header, including date and user header
+ */
+ _renderCalendarHeader: function($calendarContainer) {
+ var self = this, options = this.options,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ rowspan = '', colspan = '', calendarHeaderHtml;
+
+ if (showAsSeparatedUser) {
+ rowspan = ' rowspan=\"2\"';
+ colspan = ' colspan=\"' + options.users.length + '\" ';
+ }
+
+ //first row
+ calendarHeaderHtml = '<div class=\"ui-widget-content wc-header\">';
+ calendarHeaderHtml += '<table><tbody><tr><td class=\"wc-time-column-header\"></td>';
+ for (var i = 1; i <= options.daysToShow; i++) {
+ calendarHeaderHtml += '<td class=\"wc-day-column-header wc-day-' + i + '\"' + colspan + '></td>';
+ }
+ calendarHeaderHtml += '<td class=\"wc-scrollbar-shim\"' + rowspan + '></td></tr>';
+
+ //users row
+ if (showAsSeparatedUser) {
+ calendarHeaderHtml += '<tr><td class=\"wc-time-column-header\"></td>';
+ var uLength = options.users.length,
+ _headerClass = '';
+
+ for (var i = 1; i <= options.daysToShow; i++) {
+ for (var j = 0; j < uLength; j++) {
+ _headerClass = [];
+ if (j == 0) {
+ _headerClass.push('wc-day-column-first');
+ }
+ if (j == uLength - 1) {
+ _headerClass.push('wc-day-column-last');
+ }
+ if (!_headerClass.length) {
+ _headerClass = 'wc-day-column-middle';
+ }
+ else {
+ _headerClass = _headerClass.join(' ');
+ }
+ calendarHeaderHtml += '<td class=\"' + _headerClass + ' wc-user-header wc-day-' + i + ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
+// calendarHeaderHtml+= "<div class=\"wc-user-header wc-day-" + i + " wc-user-" + self._getUserIdFromIndex(j) +"\" >";
+ calendarHeaderHtml += self._getUserName(j);
+// calendarHeaderHtml+= "</div>";
+ calendarHeaderHtml += '</td>';
+ }
+ }
+ calendarHeaderHtml += '</tr>';
+ }
+ //close the header
+ calendarHeaderHtml += '</tbody></table></div>';
+
+ $(calendarHeaderHtml).appendTo($calendarContainer);
+ },
+
+ /**
+ * render the calendar body.
+ * Calendar body is composed of several distinct parts.
+ * Each part is displayed in a separated row to ease rendering.
+ * for further explanations, see each part rendering function.
+ */
+ _renderCalendarBody: function($calendarContainer) {
+ var self = this, options = this.options,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ $calendarBody, $calendarTableTbody;
+ // create the structure
+ $calendarBody = '<div class=\"wc-scrollable-grid\">';
+ $calendarBody += '<table class=\"wc-time-slots\">';
+ $calendarBody += '<tbody>';
+ $calendarBody += '</tbody>';
+ $calendarBody += '</table>';
+ $calendarBody += '</div>';
+ $calendarBody = $($calendarBody);
+ $calendarTableTbody = $calendarBody.find('tbody');
+
+ self._renderCalendarBodyTimeSlots($calendarTableTbody);
+ self._renderCalendarBodyOddEven($calendarTableTbody);
+ self._renderCalendarBodyFreeBusy($calendarTableTbody);
+ self._renderCalendarBodyEvents($calendarTableTbody);
+
+ $calendarBody.appendTo($calendarContainer);
+
+ //set the column height
+ $calendarContainer.find('.wc-full-height-column').height(options.timeslotHeight * options.timeslotsPerDay);
+ //set the timeslot height
+ $calendarContainer.find('.wc-time-slot').height(options.timeslotHeight - 1); //account for border
+ //init the time row header height
+ /**
+ TODO if total height for an hour is less than 11px, there is a display problem.
+ Find a way to handle it
+ */
+ $calendarContainer.find('.wc-time-header-cell').css({
+ height: (options.timeslotHeight * options.timeslotsPerHour) - 11,
+ padding: 5
+ });
+ //add the user data to every impacted column
+ if (showAsSeparatedUser) {
+ for (var i = 0, uLength = options.users.length; i < uLength; i++) {
+ $calendarContainer.find('.wc-user-' + self._getUserIdFromIndex(i))
+ .data('wcUser', options.users[i])
+ .data('wcUserIndex', i)
+ .data('wcUserId', self._getUserIdFromIndex(i));
+ }
+ }
+ },
+
+ /**
+ * render the timeslots separation
+ */
+ _renderCalendarBodyTimeSlots: function($calendarTableTbody) {
+ var options = this.options,
+ renderRow, i, j,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ start = (options.businessHours.limitDisplay ? options.businessHours.start : 0),
+ end = (options.businessHours.limitDisplay ? options.businessHours.end : 24),
+ rowspan = 1;
+
+ //calculate the rowspan
+ if (options.displayOddEven) { rowspan += 1; }
+ if (options.displayFreeBusys) { rowspan += 1; }
+ if (rowspan > 1) {
+ rowspan = ' rowspan=\"' + rowspan + '\"';
+ }
+ else {
+ rowspan = '';
+ }
+
+ renderRow = '<tr class=\"wc-grid-row-timeslot\">';
+ renderRow += '<td class=\"wc-grid-timeslot-header\"' + rowspan + '></td>';
+ renderRow += '<td colspan=\"' + options.daysToShow * (showAsSeparatedUser ? options.users.length : 1) + '\">';
+ renderRow += '<div class=\"wc-no-height-wrapper wc-time-slot-wrapper\">';
+ renderRow += '<div class=\"wc-time-slots\">';
+
+ for (i = start; i < end; i++) {
+ for (j = 0; j < options.timeslotsPerHour - 1; j++) {
+ renderRow += '<div class=\"wc-time-slot\"></div>';
+ }
+ renderRow += '<div class=\"wc-time-slot wc-hour-end\"></div>';
+ }
+
+ renderRow += '</div>';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ renderRow += '</tr>';
+
+ $(renderRow).appendTo($calendarTableTbody);
+ },
+
+ /**
+ * render the odd even columns
+ */
+ _renderCalendarBodyOddEven: function($calendarTableTbody) {
+ if (this.options.displayOddEven) {
+ var options = this.options,
+ renderRow = '<tr class=\"wc-grid-row-oddeven\">',
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ oddEven,
+ // let's take advantage of the jquery ui framework
+ oddEvenClasses = {'odd': 'wc-column-odd', 'even': 'ui-state-hover wc-column-even'};
+
+ //now let's display oddEven placeholders
+ for (var i = 1; i <= options.daysToShow; i++) {
+ if (!showAsSeparatedUser) {
+ oddEven = (oddEven == 'odd' ? 'even' : 'odd');
+ renderRow += '<td class=\"wc-day-column day-' + i + '\">';
+ renderRow += '<div class=\"wc-no-height-wrapper wc-oddeven-wrapper\">';
+ renderRow += '<div class=\"wc-full-height-column ' + oddEvenClasses[oddEven] + '\"></div>';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ }
+ else {
+ var uLength = options.users.length;
+ for (var j = 0; j < uLength; j++) {
+ oddEven = (oddEven == 'odd' ? 'even' : 'odd');
+ renderRow += '<td class=\"wc-day-column day-' + i + '\">';
+ renderRow += '<div class=\"wc-no-height-wrapper wc-oddeven-wrapper\">';
+ renderRow += '<div class=\"wc-full-height-column ' + oddEvenClasses[oddEven] + '\" ></div>';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ }
+ }
+ }
+ renderRow += '</tr>';
+
+ $(renderRow).appendTo($calendarTableTbody);
+ }
+ },
+
+ /**
+ * render the freebusy placeholders
+ */
+ _renderCalendarBodyFreeBusy: function($calendarTableTbody) {
+ if (this.options.displayFreeBusys) {
+ var self = this, options = this.options,
+ renderRow = '<tr class=\"wc-grid-row-freebusy\">',
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
+ renderRow += '</td>';
+
+ //now let's display freebusy placeholders
+ for (var i = 1; i <= options.daysToShow; i++) {
+ if (options.displayFreeBusys) {
+ if (!showAsSeparatedUser) {
+ renderRow += '<td class=\"wc-day-column day-' + i + '\">';
+ renderRow += '<div class=\"wc-no-height-wrapper wc-freebusy-wrapper\">';
+ renderRow += '<div class=\"wc-full-height-column wc-column-freebusy wc-day-' + i + '\"></div>';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ }
+ else {
+ var uLength = options.users.length;
+ for (var j = 0; j < uLength; j++) {
+ renderRow += '<td class=\"wc-day-column day-' + i + '\">';
+ renderRow += '<div class=\"wc-no-height-wrapper wc-freebusy-wrapper\">';
+ renderRow += '<div class=\"wc-full-height-column wc-column-freebusy wc-day-' + i;
+ renderRow += ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
+ renderRow += '</div>';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ }
+ }
+ }
+ }
+
+ renderRow += '</tr>';
+
+ $(renderRow).appendTo($calendarTableTbody);
+ }
+ },
+
+ /**
+ * render the calendar body for event placeholders
+ */
+ _renderCalendarBodyEvents: function($calendarTableTbody) {
+ var self = this, options = this.options,
+ renderRow,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ start = (options.businessHours.limitDisplay ? options.businessHours.start : 0),
+ end = (options.businessHours.limitDisplay ? options.businessHours.end : 24);
+ renderRow = '<tr class=\"wc-grid-row-events\">';
+ renderRow += '<td class=\"wc-grid-timeslot-header\">';
+ for (var i = start; i < end; i++) {
+ var bhClass = (options.businessHours.start <= i && options.businessHours.end > i) ? 'ui-state-active wc-business-hours' : 'ui-state-default';
+ renderRow += '<div class=\"wc-hour-header ' + bhClass + '\">';
+ if (options.use24Hour) {
+ renderRow += '<div class=\"wc-time-header-cell\">' + self._24HourForIndex(i) + '</div>';
+ }
+ else {
+ renderRow += '<div class=\"wc-time-header-cell\">' + self._hourForIndex(i) + '<span class=\"wc-am-pm\">' + self._amOrPm(i) + '</span></div>';
+ }
+ renderRow += '</div>';
+ }
+ renderRow += '</td>';
+
+ //now let's display events placeholders
+ var _columnBaseClass = 'ui-state-default wc-day-column';
+ for (var i = 1; i <= options.daysToShow; i++) {
+ if (!showAsSeparatedUser) {
+ renderRow += '<td class=\"' + _columnBaseClass + ' wc-day-column-first wc-day-column-last day-' + i + '\">';
+ renderRow += '<div class=\"wc-full-height-column wc-day-column-inner day-' + i + '\"></div>';
+ renderRow += '</td>';
+ }
+ else {
+ var uLength = options.users.length;
+ var columnclass;
+ for (var j = 0; j < uLength; j++) {
+ columnclass = [];
+ if (j == 0) {
+ columnclass.push('wc-day-column-first');
+ }
+ if (j == uLength - 1) {
+ columnclass.push('wc-day-column-last');
+ }
+ if (!columnclass.length) {
+ columnclass = 'wc-day-column-middle';
+ }
+ else {
+ columnclass = columnclass.join(' ');
+ }
+ renderRow += '<td class=\"' + _columnBaseClass + ' ' + columnclass + ' day-' + i + '\">';
+ renderRow += '<div class=\"wc-full-height-column wc-day-column-inner day-' + i;
+ renderRow += ' wc-user-' + self._getUserIdFromIndex(j) + '\">';
+ renderRow += '</div>';
+ renderRow += '</td>';
+ }
+ }
+ }
+
+ renderRow += '</tr>';
+
+ $(renderRow).appendTo($calendarTableTbody);
+ },
+
+ /*
+ * setup mouse events for capturing new events
+ */
+ _setupEventCreationForWeekDay: function($weekDay) {
+ var self = this;
+ var options = this.options;
+ $weekDay.mousedown(function(event) {
+ var $target = $(event.target);
+ if ($target.hasClass('wc-day-column-inner')) {
+
+ var $newEvent = $('<div class=\"wc-cal-event wc-new-cal-event wc-new-cal-event-creating\"></div>');
+
+ $newEvent.css({lineHeight: (options.timeslotHeight - 2) + 'px', fontSize: (options.timeslotHeight / 2) + 'px'});
+ $target.append($newEvent);
+
+ var columnOffset = $target.offset().top;
+ var clickY = event.pageY - columnOffset;
+ var clickYRounded = (clickY - (clickY % options.timeslotHeight)) / options.timeslotHeight;
+ var topPosition = clickYRounded * options.timeslotHeight;
+ $newEvent.css({top: topPosition});
+
+ if (!options.preventDragOnEventCreation) {
+ $target.bind('mousemove.newevent', function(event) {
+ $newEvent.show();
+ $newEvent.addClass('ui-resizable-resizing');
+ var height = Math.round(event.pageY - columnOffset - topPosition);
+ var remainder = height % options.timeslotHeight;
+ //snap to closest timeslot
+ if (remainder < 0) {
+ var useHeight = height - remainder;
+ $newEvent.css('height', useHeight < options.timeslotHeight ? options.timeslotHeight : useHeight);
+ } else {
+ $newEvent.css('height', height + (options.timeslotHeight - remainder));
+ }
+ }).mouseup(function() {
+ $target.unbind('mousemove.newevent');
+ $newEvent.addClass('ui-corner-all');
+ });
+ }
+ }
+
+ }).mouseup(function(event) {
+ var $target = $(event.target);
+
+ var $weekDay = $target.closest('.wc-day-column-inner');
+ var $newEvent = $weekDay.find('.wc-new-cal-event-creating');
+
+ if ($newEvent.length) {
+ var createdFromSingleClick = !$newEvent.hasClass('ui-resizable-resizing');
+
+ //if even created from a single click only, default height
+ if (createdFromSingleClick) {
+ $newEvent.css({height: options.timeslotHeight * options.defaultEventLength}).show();
+ }
+ var top = parseInt($newEvent.css('top'));
+ var eventDuration = self._getEventDurationFromPositionedEventElement($weekDay, $newEvent, top);
+
+ $newEvent.remove();
+ var newCalEvent = {start: eventDuration.start, end: eventDuration.end, title: options.newEventText};
+ var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
+
+ if (showAsSeparatedUser) {
+ newCalEvent = self._setEventUserId(newCalEvent, $weekDay.data('wcUserId'));
+ }
+ else if (!options.showAsSeparateUsers && options.users && options.users.length == 1) {
+ newCalEvent = self._setEventUserId(newCalEvent, self._getUserIdFromIndex(0));
+ }
+
+ var freeBusyManager = self.getFreeBusyManagerForEvent(newCalEvent);
+
+ var $renderedCalEvent = self._renderEvent(newCalEvent, $weekDay);
+
+ if (!options.allowCalEventOverlap) {
+ self._adjustForEventCollisions($weekDay, $renderedCalEvent, newCalEvent, newCalEvent);
+ self._positionEvent($weekDay, $renderedCalEvent);
+ } else {
+ self._adjustOverlappingEvents($weekDay);
+ }
+
+ var proceed = self._trigger('beforeEventNew', event, {
+ 'calEvent': newCalEvent,
+ 'createdFromSingleClick': createdFromSingleClick,
+ 'calendar': self.element
+ });
+ if (proceed) {
+ options.eventNew(newCalEvent, $renderedCalEvent, freeBusyManager, self.element, event);
+ }
+ else {
+ $($renderedCalEvent).remove();
+ }
+ }
+ });
+ },
+
+ /*
+ * load calendar events for the week based on the date provided
+ */
+ _loadCalEvents: function(dateWithinWeek) {
+
+ var date, weekStartDate, weekEndDate, $weekDayColumns;
+ var self = this;
+ var options = this.options;
+ date = this._fixMinMaxDate(dateWithinWeek || options.date);
+ // if date is not provided
+ // or was not set
+ // or is different than old one
+ if ((!date || !date.getTime) ||
+ (!options.date || !options.date.getTime) ||
+ date.getTime() != options.date.getTime()
+ ) {
+ // trigger the changedate event
+ this._trigger('changedate', this.element, date);
+ }
+ this.options.date = date;
+ weekStartDate = self._dateFirstDayOfWeek(date);
+ weekEndDate = self._dateLastMilliOfWeek(date);
+
+ options.calendarBeforeLoad(self.element);
+
+ self.element.data('startDate', weekStartDate);
+ self.element.data('endDate', weekEndDate);
+
+ $weekDayColumns = self.element.find('.wc-day-column-inner');
+
+ self._updateDayColumnHeader($weekDayColumns);
+
+ //load events by chosen means
+ if (typeof options.data == 'string') {
+ if (options.loading) {
+ options.loading(true);
+ }
+ if (_currentAjaxCall) {
+ // first abort current request.
+ if (!_jQuery14OrLower) {
+ _currentAjaxCall.abort();
+ } else {
+ // due to the fact that jquery 1.4 does not detect a request was
+ // aborted, we need to replace the onreadystatechange and
+ // execute the "complete" callback.
+ _currentAjaxCall.onreadystatechange = null;
+ _currentAjaxCall.abort();
+ _currentAjaxCall = null;
+ if (options.loading) {
+ options.loading(false);
+ }
+ }
+ }
+ var jsonOptions = self._getJsonOptions();
+ jsonOptions[options.startParam || 'start'] = Math.round(weekStartDate.getTime() / 1000);
+ jsonOptions[options.endParam || 'end'] = Math.round(weekEndDate.getTime() / 1000);
+ _currentAjaxCall = $.ajax({
+ url: options.data,
+ data: jsonOptions,
+ dataType: 'json',
+ error: function(XMLHttpRequest, textStatus, errorThrown) {
+ // only prevent error with jQuery 1.5
+ // see issue #34. thanks to dapplebeforedawn
+ // (https://github.com/themouette/jquery-week-calendar/issues#issue/34)
+ // for 1.5+, aborted request mean errorThrown == 'abort'
+ // for prior version it means !errorThrown && !XMLHttpRequest.status
+ // fixes #55
+ if (errorThrown != 'abort' && XMLHttpRequest.status != 0) {
+ alert('unable to get data, error:' + textStatus);
+ }
+ },
+ success: function(data) {
+ self._renderEvents(data, $weekDayColumns);
+ },
+ complete: function() {
+ _currentAjaxCall = null;
+ if (options.loading) {
+ options.loading(false);
+ }
+ }
+ });
+ }
+ else if ($.isFunction(options.data)) {
+ options.data(weekStartDate, weekEndDate,
+ function(data) {
+ self._renderEvents(data, $weekDayColumns);
+ });
+ }
+ else if (options.data) {
+ self._renderEvents(options.data, $weekDayColumns);
+ }
+
+ self._disableTextSelect($weekDayColumns);
+ },
+
+ /**
+ * Draws a thin line which indicates the current time.
+ */
+ _drawCurrentHourLine: function() {
+ var self = this;
+ var d = new MyDate(),
+ options = this.options,
+ businessHours = options.businessHours;
+
+ self._scrollToHour(d.getHours() ,false);
+ // first, we remove the old hourline if it exists
+ $('.wc-hourline', this.element).remove();
+
+ // the line does not need to be displayed
+ if (businessHours.limitDisplay && d.getHours() > businessHours.end) {
+ return;
+ }
+
+ // then we recreate it
+ var paddingStart = businessHours.limitDisplay ? businessHours.start : 0;
+ var nbHours = d.getHours() - paddingStart + d.getMinutes() / 60;
+ var positionTop = nbHours * options.timeslotHeight * options.timeslotsPerHour;
+ var lineWidth = $('.wc-scrollable-grid .wc-today', this.element).width() + 3;
+ $('.wc-scrollable-grid .wc-today', this.element).append(
+ $('<div>', {
+ 'class': 'wc-hourline',
+ style: 'top: ' + positionTop + 'px; width: ' + lineWidth + 'px'
+ })
+ );
+ },
+
+ /*
+ * update the display of each day column header based on the calendar week
+ */
+ _updateDayColumnHeader: function($weekDayColumns) {
+ var self = this;
+ var options = this.options;
+ var currentDay = self._cloneDate(self.element.data('startDate'));
+ var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
+ var todayClass = 'ui-state-active wc-today';
+
+ self.element.find('.wc-header td.wc-day-column-header').each(function(i, val) {
+ $(this).html(self._getHeaderDate(currentDay));
+ if (self._isToday(currentDay)) {
+ $(this).addClass(todayClass);
+ } else {
+ $(this).removeClass(todayClass);
+ }
+ currentDay = self._addDays(currentDay, 1);
+
+ });
+
+ currentDay = self._cloneDate(self.element.data('startDate'));
+ if (showAsSeparatedUser)
+ {
+ self.element.find('.wc-header td.wc-user-header').each(function(i, val) {
+ if (self._isToday(currentDay)) {
+ $(this).addClass(todayClass);
+ } else {
+ $(this).removeClass(todayClass);
+ }
+ currentDay = ((i + 1) % options.users.length) ? currentDay : self._addDays(currentDay, 1);
+ });
+ }
+
+ currentDay = self._cloneDate(self.element.data('startDate'));
+
+ $weekDayColumns.each(function(i, val) {
+
+ $(this).data('startDate', self._cloneDate(currentDay));
+ $(this).data('endDate', new Date(currentDay.getTime() + (MILLIS_IN_DAY)));
+ if (self._isToday(currentDay)) {
+ $(this).parent()
+ .addClass(todayClass)
+ .removeClass('ui-state-default');
+ } else {
+ $(this).parent()
+ .removeClass(todayClass)
+ .addClass('ui-state-default');
+ }
+
+ if (!showAsSeparatedUser || !((i + 1) % options.users.length)) {
+ currentDay = self._addDays(currentDay, 1);
+ }
+ });
+
+ //now update the freeBusy placeholders
+ if (options.displayFreeBusys) {
+ currentDay = self._cloneDate(self.element.data('startDate'));
+ self.element.find('.wc-grid-row-freebusy .wc-column-freebusy').each(function(i, val) {
+ $(this).data('startDate', self._cloneDate(currentDay));
+ $(this).data('endDate', new Date(currentDay.getTime() + (MILLIS_IN_DAY)));
+ if (!showAsSeparatedUser || !((i + 1) % options.users.length)) {
+ currentDay = self._addDays(currentDay, 1);
+ }
+ });
+ }
+
+ // now update the calendar title
+ if (this.options.title) {
+ var date = this.options.date,
+ start = self._cloneDate(self.element.data('startDate')),
+ end = self._dateLastDayOfWeek(new Date(this._cloneDate(self.element.data('endDate')).getTime() - (MILLIS_IN_DAY))),
+ title = this._getCalendarTitle(),
+ date_format = options.dateFormat;
+
+ // replace the placeholders contained in the title
+ title = title.replace('%start%', self._formatDate(start, date_format));
+ title = title.replace('%end%', self._formatDate(end, date_format));
+ title = title.replace('%date%', self._formatDate(date, date_format));
+
+ $('.wc-toolbar .wc-title', self.element).html(title);
+ }
+ //self._clearFreeBusys();
+ },
+
+ /**
+ * Gets the calendar raw title.
+ */
+ _getCalendarTitle: function() {
+ if ($.isFunction(this.options.title)) {
+ return this.options.title(this.options.daysToShow);
+ }
+
+ return this.options.title || '';
+ },
+
+ /**
+ * Render the events into the calendar
+ */
+ _renderEvents: function(data, $weekDayColumns) {
+ var self = this;
+ var options = this.options;
+ var eventsToRender, nbRenderedEvents = 0;
+
+ if (data.options) {
+ var updateLayout = false;
+ // update options
+ $.each(data.options, function(key, value) {
+ if (value !== options[key]) {
+ options[key] = value;
+ updateLayout = updateLayout || $.ui.weekCalendar.updateLayoutOptions[key];
+ }
+ });
+
+ self._computeOptions();
+
+ if (updateLayout) {
+ var hour = self._getCurrentScrollHour();
+ self.element.empty();
+ self._renderCalendar();
+ $weekDayColumns = self.element.find('.wc-time-slots .wc-day-column-inner');
+ self._updateDayColumnHeader($weekDayColumns);
+ self._resizeCalendar();
+ self._scrollToHour(hour, false);
+ }
+ }
+ this._clearCalendar();
+
+ if ($.isArray(data)) {
+ eventsToRender = self._cleanEvents(data);
+ } else if (data.events) {
+ eventsToRender = self._cleanEvents(data.events);
+ self._renderFreeBusys(data);
+ }
+
+ $.each(eventsToRender, function(i, calEvent) {
+ // render a multi day event as various event :
+ // thanks to http://github.com/fbeauchamp/jquery-week-calendar
+ var initialStart = new Date(calEvent.start);
+ var initialEnd = new Date(calEvent.end);
+ var maxHour = self.options.businessHours.limitDisplay ? self.options.businessHours.end : 24;
+ var minHour = self.options.businessHours.limitDisplay ? self.options.businessHours.start : 0;
+ var start = new Date(initialStart);
+ var startDate = self._formatDate(start, 'Ymd');
+ var endDate = self._formatDate(initialEnd, 'Ymd');
+ var $weekDay;
+ var isMultiday = false;
+
+ while (startDate < endDate) {
+ calEvent.start = start;
+
+ // end of this virual calEvent is set to the end of the day
+ calEvent.end.setFullYear(start.getFullYear());
+ calEvent.end.setDate(start.getDate());
+ calEvent.end.setMonth(start.getMonth());
+ calEvent.end.setHours(maxHour, 0, 0);
+
+ if (($weekDay = self._findWeekDayForEvent(calEvent, $weekDayColumns))) {
+ self._renderEvent(calEvent, $weekDay);
+ nbRenderedEvents += 1;
+ }
+
+ // start is set to the begin of the new day
+ start.setDate(start.getDate() + 1);
+ start.setHours(minHour, 0, 0);
+
+ startDate = self._formatDate(start, 'Ymd');
+ isMultiday = true;
+ }
+
+ if (start <= initialEnd) {
+ calEvent.start = start;
+ calEvent.end = initialEnd;
+
+ if (((isMultiday && calEvent.start.getTime() != calEvent.end.getTime()) || !isMultiday) && ($weekDay = self._findWeekDayForEvent(calEvent, $weekDayColumns))) {
+ self._renderEvent(calEvent, $weekDay);
+ nbRenderedEvents += 1;
+ }
+ }
+
+ // put back the initial start date
+ calEvent.start = initialStart;
+ });
+
+ $weekDayColumns.each(function() {
+ self._adjustOverlappingEvents($(this));
+ });
+
+ options.calendarAfterLoad(self.element);
+
+ _hourLineTimeout && clearInterval(_hourLineTimeout);
+
+ if (options.hourLine) {
+ self._drawCurrentHourLine();
+
+ _hourLineTimeout = setInterval(function() {
+ self._drawCurrentHourLine();
+ }, 60 * 1000); // redraw the line each minute
+ }
+
+ !nbRenderedEvents && options.noEvents();
+ },
+
+ /*
+ * Render a specific event into the day provided. Assumes correct
+ * day for calEvent date
+ */
+ _renderEvent: function(calEvent, $weekDay) {
+ var self = this;
+ var options = this.options;
+ if (calEvent.start.getTime() > calEvent.end.getTime()) {
+ return; // can't render a negative height
+ }
+
+ var eventClass, eventHtml, $calEventList, $modifiedEvent;
+
+ eventClass = calEvent.id ? 'wc-cal-event' : 'wc-cal-event wc-new-cal-event';
+ eventHtml = '<div class=\"' + eventClass + ' ui-corner-all\">';
+ eventHtml += '<div class=\"wc-time ui-corner-top\"></div>';
+ eventHtml += '<div class=\"wc-title\"></div></div>';
+
+ $weekDay.each(function() {
+ var $calEvent = $(eventHtml);
+ $modifiedEvent = options.eventRender(calEvent, $calEvent);
+ $calEvent = $modifiedEvent ? $modifiedEvent.appendTo($(this)) : $calEvent.appendTo($(this));
+ $calEvent.css({lineHeight: (options.textSize + 2) + 'px', fontSize: options.textSize + 'px'});
+
+ self._refreshEventDetails(calEvent, $calEvent);
+ self._positionEvent($(this), $calEvent);
+
+ //add to event list
+ if ($calEventList) {
+ $calEventList = $calEventList.add($calEvent);
+ }
+ else {
+ $calEventList = $calEvent;
+ }
+ });
+ $calEventList.show();
+
+ if (!options.readonly && options.resizable(calEvent, $calEventList)) {
+ self._addResizableToCalEvent(calEvent, $calEventList, $weekDay);
+ }
+ if (!options.readonly && options.draggable(calEvent, $calEventList)) {
+ self._addDraggableToCalEvent(calEvent, $calEventList);
+ }
+ options.eventAfterRender(calEvent, $calEventList);
+
+ return $calEventList;
+
+ },
+ addEvent: function() {
+ return this._renderEvent.apply(this, arguments);
+ },
+
+ _adjustOverlappingEvents: function($weekDay) {
+ var self = this;
+ if (self.options.allowCalEventOverlap) {
+ var groupsList = self._groupOverlappingEventElements($weekDay);
+ $.each(groupsList, function() {
+ var curGroups = this;
+ $.each(curGroups, function(groupIndex) {
+ var curGroup = this;
+
+ // do we want events to be displayed as overlapping
+ if (self.options.overlapEventsSeparate) {
+ var newWidth = self.options.totalEventsWidthPercentInOneColumn / curGroups.length;
+ var newLeft = groupIndex * newWidth;
+ } else {
+ // TODO what happens when the group has more than 10 elements
+ var newWidth = self.options.totalEventsWidthPercentInOneColumn - ((curGroups.length - 1) * 10);
+ var newLeft = groupIndex * 10;
+ }
+ $.each(curGroup, function() {
+ // bring mouseovered event to the front
+ if (!self.options.overlapEventsSeparate) {
+ $(this).bind('mouseover.z-index', function() {
+ var $elem = $(this);
+ $.each(curGroup, function() {
+ $(this).css({'z-index': '1'});
+ });
+ $elem.css({'z-index': '3'});
+ });
+ }
+ $(this).css({width: newWidth + '%', left: newLeft + '%', right: 0});
+ });
+ });
+ });
+ }
+ },
+
+
+ /*
+ * Find groups of overlapping events
+ */
+ _groupOverlappingEventElements: function($weekDay) {
+ var $events = $weekDay.find('.wc-cal-event:visible');
+ var sortedEvents = $events.sort(function(a, b) {
+ return $(a).data('calEvent').start.getTime() - $(b).data('calEvent').start.getTime();
+ });
+
+ var lastEndTime = new Date(0, 0, 0);
+ var groups = [];
+ var curGroups = [];
+ var $curEvent;
+ $.each(sortedEvents, function() {
+ $curEvent = $(this);
+ //checks, if the current group list is not empty, if the overlapping is finished
+ if (curGroups.length > 0) {
+ if (lastEndTime.getTime() <= $curEvent.data('calEvent').start.getTime()) {
+ //finishes the current group list by adding it to the resulting list of groups and cleans it
+
+ groups.push(curGroups);
+ curGroups = [];
+ }
+ }
+
+ //finds the first group to fill with the event
+ for (var groupIndex = 0; groupIndex < curGroups.length; groupIndex++) {
+ if (curGroups[groupIndex].length > 0) {
+ //checks if the event starts after the end of the last event of the group
+ if (curGroups[groupIndex][curGroups[groupIndex].length - 1].data('calEvent').end.getTime() <= $curEvent.data('calEvent').start.getTime()) {
+ curGroups[groupIndex].push($curEvent);
+ if (lastEndTime.getTime() < $curEvent.data('calEvent').end.getTime()) {
+ lastEndTime = $curEvent.data('calEvent').end;
+ }
+ return;
+ }
+ }
+ }
+ //if not found, creates a new group
+ curGroups.push([$curEvent]);
+ if (lastEndTime.getTime() < $curEvent.data('calEvent').end.getTime()) {
+ lastEndTime = $curEvent.data('calEvent').end;
+ }
+ });
+ //adds the last groups in result
+ if (curGroups.length > 0) {
+ groups.push(curGroups);
+ }
+ return groups;
+ },
+
+
+ /*
+ * find the weekday in the current calendar that the calEvent falls within
+ */
+ _findWeekDayForEvent: function(calEvent, $weekDayColumns) {
+
+ var $weekDay,
+ options = this.options,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ user_ids = this._getEventUserId(calEvent);
+
+ if (!$.isArray(user_ids)) {
+ user_ids = [user_ids];
+ }
+
+ $weekDayColumns.each(function(index, curDay) {
+ if ($(this).data('startDate').getTime() <= calEvent.start.getTime() &&
+ $(this).data('endDate').getTime() >= calEvent.end.getTime() &&
+ (!showAsSeparatedUser || $.inArray($(this).data('wcUserId'), user_ids) !== -1)
+ ) {
+ if ($weekDay) {
+ $weekDay = $weekDay.add($(curDay));
+ }
+ else {
+ $weekDay = $(curDay);
+ }
+ }
+ });
+
+ return $weekDay;
+ },
+
+ /*
+ * update the events rendering in the calendar. Add if does not yet exist.
+ */
+ _updateEventInCalendar: function(calEvent) {
+ var self = this;
+ self._cleanEvent(calEvent);
+
+ if (calEvent.id) {
+ self.element.find('.wc-cal-event').each(function() {
+ if ($(this).data('calEvent').id === calEvent.id || $(this).hasClass('wc-new-cal-event')) {
+ $(this).remove();
+ // return false;
+ }
+ });
+ }
+
+ var $weekDays = self._findWeekDayForEvent(calEvent, self.element.find('.wc-grid-row-events .wc-day-column-inner'));
+ if ($weekDays) {
+ $weekDays.each(function(index, weekDay) {
+ var $weekDay = $(weekDay);
+ var $calEvent = self._renderEvent(calEvent, $weekDay);
+ self._adjustForEventCollisions($weekDay, $calEvent, calEvent, calEvent);
+ self._refreshEventDetails(calEvent, $calEvent);
+ self._positionEvent($weekDay, $calEvent);
+ self._adjustOverlappingEvents($weekDay);
+ });
+ }
+ },
+
+ /*
+ * Position the event element within the weekday based on it's start / end dates.
+ */
+ _positionEvent: function($weekDay, $calEvent) {
+ var options = this.options;
+ var calEvent = $calEvent.data('calEvent');
+ var pxPerMillis = $weekDay.height() / options.millisToDisplay;
+ var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
+ var startMillis = this._getDSTdayShift(calEvent.start).getTime() - this._getDSTdayShift(new Date(calEvent.start.getFullYear(), calEvent.start.getMonth(), calEvent.start.getDate(), firstHourDisplayed)).getTime();
+ var eventMillis = this._getDSTdayShift(calEvent.end).getTime() - this._getDSTdayShift(calEvent.start).getTime();
+ var pxTop = pxPerMillis * startMillis;
+ var pxHeight = pxPerMillis * eventMillis;
+ //var pxHeightFallback = pxPerMillis * (60 / options.timeslotsPerHour) * 60 * 1000;
+ $calEvent.css({top: pxTop, height: pxHeight || (pxPerMillis * 3600000 / options.timeslotsPerHour)});
+ },
+
+ /*
+ * Determine the actual start and end times of a calevent based on it's
+ * relative position within the weekday column and the starting hour of the
+ * displayed calendar.
+ */
+ _getEventDurationFromPositionedEventElement: function($weekDay, $calEvent, top) {
+ var options = this.options;
+ var startOffsetMillis = options.businessHours.limitDisplay ? options.businessHours.start * 3600000 : 0;
+ var start = new Date($weekDay.data('startDate').getTime() + startOffsetMillis + Math.round(top / options.timeslotHeight) * options.millisPerTimeslot);
+ var end = new Date(start.getTime() + ($calEvent.height() / options.timeslotHeight) * options.millisPerTimeslot);
+ return {start: this._getDSTdayShift(start, -1), end: this._getDSTdayShift(end, -1)};
+ },
+
+ /*
+ * If the calendar does not allow event overlap, adjust the start or end date if necessary to
+ * avoid overlapping of events. Typically, shortens the resized / dropped event to it's max possible
+ * duration based on the overlap. If no satisfactory adjustment can be made, the event is reverted to
+ * it's original location.
+ */
+ _adjustForEventCollisions: function($weekDay, $calEvent, newCalEvent, oldCalEvent, maintainEventDuration) {
+ var options = this.options;
+
+ if (options.allowCalEventOverlap) {
+ return;
+ }
+ var adjustedStart, adjustedEnd;
+ var self = this;
+
+ $weekDay.find('.wc-cal-event').not($calEvent).each(function() {
+ var currentCalEvent = $(this).data('calEvent');
+
+ //has been dropped onto existing event overlapping the end time
+ if (newCalEvent.start.getTime() < currentCalEvent.end.getTime() &&
+ newCalEvent.end.getTime() >= currentCalEvent.end.getTime()) {
+
+ adjustedStart = currentCalEvent.end;
+ }
+
+
+ //has been dropped onto existing event overlapping the start time
+ if (newCalEvent.end.getTime() > currentCalEvent.start.getTime() &&
+ newCalEvent.start.getTime() <= currentCalEvent.start.getTime()) {
+
+ adjustedEnd = currentCalEvent.start;
+ }
+ //has been dropped inside existing event with same or larger duration
+ if (oldCalEvent.resizable == false ||
+ (newCalEvent.end.getTime() <= currentCalEvent.end.getTime() &&
+ newCalEvent.start.getTime() >= currentCalEvent.start.getTime())) {
+
+ adjustedStart = oldCalEvent.start;
+ adjustedEnd = oldCalEvent.end;
+ return false;
+ }
+
+ });
+
+
+ newCalEvent.start = adjustedStart || newCalEvent.start;
+
+ if (adjustedStart && maintainEventDuration) {
+ newCalEvent.end = new Date(adjustedStart.getTime() + (oldCalEvent.end.getTime() - oldCalEvent.start.getTime()));
+ self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, oldCalEvent);
+ } else {
+ newCalEvent.end = adjustedEnd || newCalEvent.end;
+ }
+
+
+ //reset if new cal event has been forced to zero size
+ if (newCalEvent.start.getTime() >= newCalEvent.end.getTime()) {
+ newCalEvent.start = oldCalEvent.start;
+ newCalEvent.end = oldCalEvent.end;
+ }
+
+ $calEvent.data('calEvent', newCalEvent);
+ },
+
+ /**
+ * Add draggable capabilities to an event
+ */
+ _addDraggableToCalEvent: function(calEvent, $calEvent) {
+ var options = this.options;
+
+ $calEvent.draggable({
+ handle: '.wc-time',
+ containment: 'div.wc-time-slots',
+ snap: '.wc-day-column-inner',
+ snapMode: 'inner',
+ snapTolerance: options.timeslotHeight - 1,
+ revert: 'invalid',
+ opacity: 0.5,
+ grid: [$calEvent.outerWidth() + 1, options.timeslotHeight],
+ start: function(event, ui) {
+ var $calEvent = ui.draggable || ui.helper;
+ options.eventDrag(calEvent, $calEvent);
+ }
+ });
+ },
+
+ /*
+ * Add droppable capabilites to weekdays to allow dropping of calEvents only
+ */
+ _addDroppableToWeekDay: function($weekDay) {
+ var self = this;
+ var options = this.options;
+ $weekDay.droppable({
+ accept: '.wc-cal-event',
+ drop: function(event, ui) {
+ var $calEvent = ui.draggable;
+ var top = Math.round(parseInt(ui.position.top));
+ var eventDuration = self._getEventDurationFromPositionedEventElement($weekDay, $calEvent, top);
+ var calEvent = $calEvent.data('calEvent');
+ var newCalEvent = $.extend(true, {}, calEvent, {start: eventDuration.start, end: eventDuration.end});
+ var showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length;
+ if (showAsSeparatedUser) {
+ // we may have dragged the event on column with a new user.
+ // nice way to handle that is:
+ // - get the newly dragged on user
+ // - check if user is part of the event
+ // - if yes, nothing changes, if not, find the old owner to remove it and add new one
+ var newUserId = $weekDay.data('wcUserId');
+ var userIdList = self._getEventUserId(calEvent);
+ var oldUserId = $(ui.draggable.parents('.wc-day-column-inner').get(0)).data('wcUserId');
+ if (!$.isArray(userIdList)) {
+ userIdList = [userIdList];
+ }
+ if ($.inArray(newUserId, userIdList) == -1) {
+ // remove old user
+ var _index = $.inArray(oldUserId, userIdList);
+ userIdList.splice(_index, 1);
+ // add new user ?
+ if ($.inArray(newUserId, userIdList) == -1) {
+ userIdList.push(newUserId);
+ }
+ }
+ newCalEvent = self._setEventUserId(newCalEvent, ((userIdList.length == 1) ? userIdList[0] : userIdList));
+ }
+ self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent, true);
+ var $weekDayColumns = self.element.find('.wc-day-column-inner');
+
+ //trigger drop callback
+ options.eventDrop(newCalEvent, calEvent, $calEvent);
+
+ var $newEvent = self._renderEvent(newCalEvent, self._findWeekDayForEvent(newCalEvent, $weekDayColumns));
+ $calEvent.hide();
+
+ $calEvent.data('preventClick', true);
+
+ var $weekDayOld = self._findWeekDayForEvent($calEvent.data('calEvent'), self.element.find('.wc-time-slots .wc-day-column-inner'));
+
+ if ($weekDayOld.data('startDate') != $weekDay.data('startDate')) {
+ self._adjustOverlappingEvents($weekDayOld);
+ }
+ self._adjustOverlappingEvents($weekDay);
+
+ setTimeout(function() {
+ $calEvent.remove();
+ }, 1000);
+
+ }
+ });
+ },
+
+ /*
+ * Add resizable capabilities to a calEvent
+ */
+ _addResizableToCalEvent: function(calEvent, $calEvent, $weekDay) {
+ var self = this;
+ var options = this.options;
+ $calEvent.resizable({
+ grid: options.timeslotHeight,
+ containment: $weekDay,
+ handles: 's',
+ minHeight: options.timeslotHeight,
+ stop: function(event, ui) {
+ var $calEvent = ui.element;
+ var newEnd = new Date($calEvent.data('calEvent').start.getTime() + Math.max(1, Math.round(ui.size.height / options.timeslotHeight)) * options.millisPerTimeslot);
+ if (self._needDSTdayShift($calEvent.data('calEvent').start, newEnd))
+ newEnd = self._getDSTdayShift(newEnd, -1);
+ var newCalEvent = $.extend(true, {}, calEvent, {start: calEvent.start, end: newEnd});
+ self._adjustForEventCollisions($weekDay, $calEvent, newCalEvent, calEvent);
+
+ //trigger resize callback
+ options.eventResize(newCalEvent, calEvent, $calEvent);
+ self._refreshEventDetails(newCalEvent, $calEvent);
+ self._positionEvent($weekDay, $calEvent);
+ self._adjustOverlappingEvents($weekDay);
+ $calEvent.data('preventClick', true);
+ setTimeout(function() {
+ $calEvent.removeData('preventClick');
+ }, 500);
+ }
+ });
+ $('.ui-resizable-handle', $calEvent).text('=');
+ },
+
+ /*
+ * Refresh the displayed details of a calEvent in the calendar
+ */
+ _refreshEventDetails: function(calEvent, $calEvent) {
+ var suffix = '';
+ if (!this.options.readonly &&
+ this.options.allowEventDelete &&
+ this.options.deletable(calEvent,$calEvent)) {
+ suffix = '<div class="wc-cal-event-delete ui-icon ui-icon-close"></div>';
+ }
+ $calEvent.find('.wc-time').html(this.options.eventHeader(calEvent, this.element) + suffix);
+ $calEvent.find('.wc-title').html(this.options.eventBody(calEvent, this.element));
+ $calEvent.data('calEvent', calEvent);
+ this.options.eventRefresh(calEvent, $calEvent);
+ },
+
+ /*
+ * Clear all cal events from the calendar
+ */
+ _clearCalendar: function() {
+ this.element.find('.wc-day-column-inner div').remove();
+ this._clearFreeBusys();
+ },
+
+ /*
+ * Scroll the calendar to a specific hour
+ */
+ _scrollToHour: function(hour, animate) {
+ var self = this;
+ var options = this.options;
+ var $scrollable = this.element.find('.wc-scrollable-grid');
+ var slot = hour;
+ if (self.options.businessHours.limitDisplay) {
+ if (hour <= self.options.businessHours.start) {
+ slot = 0;
+ } else if (hour >= self.options.businessHours.end) {
+ slot = self.options.businessHours.end - self.options.businessHours.start - 1;
+ } else {
+ slot = hour - self.options.businessHours.start;
+ }
+ }
+
+ var $target = this.element.find('.wc-grid-timeslot-header .wc-hour-header:eq(' + slot + ')');
+
+ $scrollable.animate({scrollTop: 0}, 0, function() {
+ var targetOffset = $target.offset().top;
+ var scroll = targetOffset - $scrollable.offset().top - $target.outerHeight();
+ if (animate) {
+ $scrollable.animate({scrollTop: scroll}, options.scrollToHourMillis);
+ }
+ else {
+ $scrollable.animate({scrollTop: scroll}, 0);
+ }
+ });
+ },
+
+ /*
+ * find the hour (12 hour day) for a given hour index
+ */
+ _hourForIndex: function(index) {
+ if (index === 0) { //midnight
+ return 12;
+ } else if (index < 13) { //am
+ return index;
+ } else { //pm
+ return index - 12;
+ }
+ },
+
+ _24HourForIndex: function(index) {
+ if (index === 0) { //midnight
+ return '00:00';
+ } else if (index < 10) {
+ return '0' + index + ':00';
+ } else {
+ return index + ':00';
+ }
+ },
+
+ _amOrPm: function(hourOfDay) {
+ return hourOfDay < 12 ? 'AM' : 'PM';
+ },
+
+ _isToday: function(date) {
+ var clonedDate = this._cloneDate(date);
+ this._clearTime(clonedDate);
+ var today = new MyDate();
+ this._clearTime(today);
+ return today.getTime() === clonedDate.getTime();
+ },
+
+ /*
+ * Clean events to ensure correct format
+ */
+ _cleanEvents: function(events) {
+ var self = this;
+ $.each(events, function(i, event) {
+ self._cleanEvent(event);
+ });
+ return events;
+ },
+
+ /*
+ * Clean specific event
+ */
+ _cleanEvent: function(event) {
+ if (event.date) {
+ event.start = event.date;
+ }
+ event.start = this._cleanDate(event.start);
+ event.end = this._cleanDate(event.end);
+ if (!event.end) {
+ event.end = this._addDays(this._cloneDate(event.start), 1);
+ }
+ },
+
+ /*
+ * Disable text selection of the elements in different browsers
+ */
+ _disableTextSelect: function($elements) {
+ $elements.each(function() {
+ $(this).attr('unselectable', 'on')
+ .css({
+ '-moz-user-select': '-moz-none',
+ '-moz-user-select': 'none',
+ '-o-user-select': 'none',
+ '-khtml-user-select': 'none', /* you could also put this in a class */
+ '-webkit-user-select': 'none',/* and add the CSS class here instead */
+ '-ms-user-select': 'none',
+ 'user-select': 'none'
+ }).bind('selectstart', function () { return false; });
+
+ });
+ },
+
+ /*
+ * returns the date on the first millisecond of the week
+ */
+ _dateFirstDayOfWeek: function(date) {
+ var self = this;
+ var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
+ var adjustedDate = new Date(midnightCurrentDate);
+ adjustedDate.setDate(adjustedDate.getDate() - self._getAdjustedDayIndex(midnightCurrentDate));
+
+ return adjustedDate;
+ },
+
+ /*
+ * returns the date on the first millisecond of the last day of the week
+ */
+ _dateLastDayOfWeek: function(date) {
+ var self = this;
+ var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
+ var adjustedDate = new Date(midnightCurrentDate);
+ var daysToAdd = (self.options.daysToShow - 1 - self._getAdjustedDayIndex(midnightCurrentDate));
+ adjustedDate.setDate(adjustedDate.getDate() + daysToAdd);
+
+ return adjustedDate;
+ },
+
+ /**
+ * fix the date if it is not within given options
+ * minDate and maxDate
+ */
+ _fixMinMaxDate: function(date) {
+ var minDate, maxDate;
+ date = this._cleanDate(date);
+
+ // not less than minDate
+ if (this.options.minDate) {
+ minDate = this._cleanDate(this.options.minDate);
+ // midnight on minDate
+ minDate = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate());
+ if (date.getTime() < minDate.getTime()) {
+ this._trigger('reachedmindate', this.element, date);
+ }
+ date = this._cleanDate(Math.max(date.getTime(), minDate.getTime()));
+ }
+
+ // not more than maxDate
+ if (this.options.maxDate) {
+ maxDate = this._cleanDate(this.options.maxDate);
+ // apply correction for max date if not startOnFirstDayOfWeek
+ // to make sure no further date is displayed.
+ // otherwise, the complement will still be shown
+ if (!this._startOnFirstDayOfWeek()) {
+ var day = maxDate.getDate() - this.options.daysToShow + 1;
+ maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), day);
+ }
+ // microsecond before midnight on maxDate
+ maxDate = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate(), 23, 59, 59, 999);
+ if (date.getTime() > maxDate.getTime()) {
+ this._trigger('reachedmaxdate', this.element, date);
+ }
+ date = this._cleanDate(Math.min(date.getTime(), maxDate.getTime()));
+ }
+
+ return date;
+ },
+
+ /*
+ * gets the index of the current day adjusted based on options
+ */
+ _getAdjustedDayIndex: function(date) {
+ if (!this._startOnFirstDayOfWeek()) {
+ return 0;
+ }
+
+ var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
+ var currentDayOfStandardWeek = midnightCurrentDate.getDay();
+ var days = [0, 1, 2, 3, 4, 5, 6];
+ this._rotate(days, this._firstDayOfWeek());
+ return days[currentDayOfStandardWeek];
+ },
+
+ _firstDayOfWeek: function() {
+ if ($.isFunction(this.options.firstDayOfWeek)) {
+ return this.options.firstDayOfWeek(this.element);
+ }
+ return this.options.firstDayOfWeek;
+ },
+
+ /*
+ * returns the date on the last millisecond of the week
+ */
+ _dateLastMilliOfWeek: function(date) {
+ var lastDayOfWeek = this._dateLastDayOfWeek(date);
+ lastDayOfWeek = this._cloneDate(lastDayOfWeek);
+ lastDayOfWeek.setDate(lastDayOfWeek.getDate() + 1);
+ return lastDayOfWeek;
+
+ },
+
+ /*
+ * Clear the time components of a date leaving the date
+ * of the first milli of day
+ */
+ _clearTime: function(d) {
+ d.setHours(0);
+ d.setMinutes(0);
+ d.setSeconds(0);
+ d.setMilliseconds(0);
+ return d;
+ },
+
+ /*
+ * add specific number of days to date
+ */
+ _addDays: function(d, n, keepTime) {
+ d.setDate(d.getDate() + n);
+ if (keepTime) {
+ return d;
+ }
+ return this._clearTime(d);
+ },
+
+ /*
+ * Rotate an array by specified number of places.
+ */
+ _rotate: function(a /*array*/, p /* integer, positive integer rotate to the right, negative to the left... */) {
+ for (var l = a.length, p = (Math.abs(p) >= l && (p %= l), p < 0 && (p += l), p), i, x; p; p = (Math.ceil(l / p) - 1) * p - l + (l = p)) {
+ for (i = l; i > p; x = a[--i], a[i] = a[i - p], a[i - p] = x) {}
+ }
+ return a;
+ },
+
+ _cloneDate: function(d) {
+ return new Date(d.getTime());
+ },
+
+ /**
+ * Return a Date instance for different representations.
+ * Valid representations are:
+ * * timestamps
+ * * Date objects
+ * * textual representations (only these accepted by the Date
+ * constructor)
+ *
+ * @return {Date} The clean date object.
+ */
+ _cleanDate: function(d) {
+ if (typeof d === 'string') {
+ // if is numeric
+ if (!isNaN(Number(d))) {
+ return this._cleanDate(parseInt(d, 10));
+ }
+
+ // this is a human readable date
+ return Date.parse(d) || new Date(d);
+ }
+
+ if (typeof d == 'number') {
+ return new Date(d);
+ }
+
+ return d;
+ },
+
+ /*
+ * date formatting is adapted from
+ * http://jacwright.com/projects/javascript/date_format
+ */
+ _formatDate: function(date, format) {
+ var returnStr = '';
+ for (var i = 0; i < format.length; i++) {
+ var curChar = format.charAt(i);
+ if (i != 0 && format.charAt(i - 1) == '\\') {
+ returnStr += curChar;
+ }
+ else if (this._replaceChars[curChar]) {
+ returnStr += this._replaceChars[curChar](date, this);
+ } else if (curChar != '\\') {
+ returnStr += curChar;
+ }
+ }
+ return returnStr;
+ },
+
+ _replaceChars: {
+ // Day
+ d: function(date) { return (date.getDate() < 10 ? '0' : '') + date.getDate(); },
+ D: function(date, calendar) { return calendar.options.shortDays[date.getDay()]; },
+ j: function(date) { return date.getDate(); },
+ l: function(date, calendar) { return calendar.options.longDays[date.getDay()]; },
+ N: function(date) { var _d = date.getDay(); return _d ? _d : 7; },
+ S: function(date) { return (date.getDate() % 10 == 1 && date.getDate() != 11 ? 'st' : (date.getDate() % 10 == 2 && date.getDate() != 12 ? 'nd' : (date.getDate() % 10 == 3 && date.getDate() != 13 ? 'rd' : 'th'))); },
+ w: function(date) { return date.getDay(); },
+ z: function(date) { var d = new Date(date.getFullYear(), 0, 1); return Math.ceil((date - d) / 86400000); }, // Fixed now
+ // Week
+ W: function(date) { var d = new Date(date.getFullYear(), 0, 1); return Math.ceil((((date - d) / 86400000) + d.getDay() + 1) / 7); }, // Fixed now
+ // Month
+ F: function(date, calendar) { return calendar.options.longMonths[date.getMonth()]; },
+ m: function(date) { return (date.getMonth() < 9 ? '0' : '') + (date.getMonth() + 1); },
+ M: function(date, calendar) { return calendar.options.shortMonths[date.getMonth()]; },
+ n: function(date) { return date.getMonth() + 1; },
+ t: function(date) { var d = date; return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate() }, // Fixed now, gets #days of date
+ // Year
+ L: function(date) { var year = date.getFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); }, // Fixed now
+ o: function(date) { var d = new Date(date.valueOf()); d.setDate(d.getDate() - ((date.getDay() + 6) % 7) + 3); return d.getFullYear();}, //Fixed now
+ Y: function(date) { return date.getFullYear(); },
+ y: function(date) { return ('' + date.getFullYear()).substr(2); },
+ // Time
+ a: function(date) { return date.getHours() < 12 ? 'am' : 'pm'; },
+ A: function(date) { return date.getHours() < 12 ? 'AM' : 'PM'; },
+ B: function(date) { return Math.floor((((date.getUTCHours() + 1) % 24) + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now
+ g: function(date) { return date.getHours() % 12 || 12; },
+ G: function(date) { return date.getHours(); },
+ h: function(date) { return ((date.getHours() % 12 || 12) < 10 ? '0' : '') + (date.getHours() % 12 || 12); },
+ H: function(date) { return (date.getHours() < 10 ? '0' : '') + date.getHours(); },
+ i: function(date) { return (date.getMinutes() < 10 ? '0' : '') + date.getMinutes(); },
+ s: function(date) { return (date.getSeconds() < 10 ? '0' : '') + date.getSeconds(); },
+ u: function(date) { var m = date.getMilliseconds(); return (m < 10 ? '00' : (m < 100 ? '0' : '')) + m; },
+ // Timezone
+ e: function(date) { return 'Not Yet Supported'; },
+ I: function(date) { return 'Not Yet Supported'; },
+ O: function(date) { return (-date.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(date.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(date.getTimezoneOffset() / 60)) + '00'; },
+ P: function(date) { return (-date.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(date.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(date.getTimezoneOffset() / 60)) + ':00'; }, // Fixed now
+ T: function(date) { var m = date.getMonth(); date.setMonth(0); var result = date.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); date.setMonth(m); return result;},
+ Z: function(date) { return -date.getTimezoneOffset() * 60; },
+ // Full Date/Time
+ c: function(date, calendar) { return calendar._formatDate(date, 'Y-m-d\\TH:i:sP'); }, // Fixed now
+ r: function(date, calendar) { return calendar._formatDate(date, 'D, d M Y H:i:s O'); },
+ U: function(date) { return date.getTime() / 1000; }
+ },
+
+ /* USER MANAGEMENT FUNCTIONS */
+
+ getUserForId: function(id) {
+ return $.extend({}, this.options.users[this._getUserIndexFromId(id)]);
+ },
+
+ /**
+ * return the user name for header
+ */
+ _getUserName: function(index) {
+ var self = this;
+ var options = this.options;
+ var user = options.users[index];
+ if ($.isFunction(options.getUserName)) {
+ return options.getUserName(user, index, self.element);
+ }
+ else {
+ return user;
+ }
+ },
+ /**
+ * return the user id for given index
+ */
+ _getUserIdFromIndex: function(index) {
+ var self = this;
+ var options = this.options;
+ if ($.isFunction(options.getUserId)) {
+ return options.getUserId(options.users[index], index, self.element);
+ }
+ return index;
+ },
+ /**
+ * returns the associated user index for given ID
+ */
+ _getUserIndexFromId: function(id) {
+ var self = this;
+ var options = this.options;
+ for (var i = 0; i < options.users.length; i++) {
+ if (self._getUserIdFromIndex(i) == id) {
+ return i;
+ }
+ }
+ return 0;
+ },
+ /**
+ * return the user ids for given calEvent.
+ * default is calEvent.userId field.
+ */
+ _getEventUserId: function(calEvent) {
+ var self = this;
+ var options = this.options;
+ if (options.showAsSeparateUsers && options.users && options.users.length) {
+ if ($.isFunction(options.getEventUserId)) {
+ return options.getEventUserId(calEvent, self.element);
+ }
+ return calEvent.userId;
+ }
+ return [];
+ },
+ /**
+ * sets the event user id on given calEvent
+ * default is calEvent.userId field.
+ */
+ _setEventUserId: function(calEvent, userId) {
+ var self = this;
+ var options = this.options;
+ if ($.isFunction(options.setEventUserId)) {
+ return options.setEventUserId(userId, calEvent, self.element);
+ }
+ calEvent.userId = userId;
+ return calEvent;
+ },
+ /**
+ * return the user ids for given freeBusy.
+ * default is freeBusy.userId field.
+ */
+ _getFreeBusyUserId: function(freeBusy) {
+ var self = this;
+ var options = this.options;
+ if ($.isFunction(options.getFreeBusyUserId)) {
+ return options.getFreeBusyUserId(freeBusy.getOption(), self.element);
+ }
+ return freeBusy.getOption('userId');
+ },
+
+ /* FREEBUSY MANAGEMENT */
+
+ /**
+ * ckean the free busy managers and remove all the freeBusy
+ */
+ _clearFreeBusys: function() {
+ if (this.options.displayFreeBusys) {
+ var self = this,
+ options = this.options,
+ $freeBusyPlaceholders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy');
+ $freeBusyPlaceholders.each(function() {
+ $(this).data('wcFreeBusyManager', new FreeBusyManager({
+ start: self._cloneDate($(this).data('startDate')),
+ end: self._cloneDate($(this).data('endDate')),
+ defaultFreeBusy: options.defaultFreeBusy || {}
+ }));
+ });
+ self.element.find('.wc-grid-row-freebusy .wc-freebusy').remove();
+ }
+ },
+ /**
+ * retrieve placeholders for given freebusy
+ */
+ _findWeekDaysForFreeBusy: function(freeBusy, $weekDays) {
+ var $returnWeekDays,
+ options = this.options,
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ self = this,
+ userList = self._getFreeBusyUserId(freeBusy);
+ if (!$.isArray(userList)) {
+ userList = userList != 'undefined' ? [userList] : [];
+ }
+ if (!$weekDays) {
+ $weekDays = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy');
+ }
+ $weekDays.each(function() {
+ var manager = $(this).data('wcFreeBusyManager'),
+ has_overlap = manager.isWithin(freeBusy.getStart()) ||
+ manager.isWithin(freeBusy.getEnd()) ||
+ freeBusy.isWithin(manager.getStart()) ||
+ freeBusy.isWithin(manager.getEnd()),
+ userId = $(this).data('wcUserId');
+ if (has_overlap && (!showAsSeparatedUser || ($.inArray(userId, userList) != -1))) {
+ $returnWeekDays = $returnWeekDays ? $returnWeekDays.add($(this)) : $(this);
+ }
+ });
+ return $returnWeekDays;
+ },
+
+ /**
+ * used to render all freeBusys
+ */
+ _renderFreeBusys: function(freeBusys) {
+ if (this.options.displayFreeBusys) {
+ var self = this,
+ $freeBusyPlaceholders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
+ freebusysToRender;
+ //insert freebusys to dedicated placeholders freebusy managers
+ if ($.isArray(freeBusys)) {
+ freebusysToRender = self._cleanFreeBusys(freeBusys);
+ } else if (freeBusys.freebusys) {
+ freebusysToRender = self._cleanFreeBusys(freeBusys.freebusys);
+ }
+ else {
+ freebusysToRender = [];
+ }
+
+ $.each(freebusysToRender, function(index, freebusy) {
+ var $placeholders = self._findWeekDaysForFreeBusy(freebusy, $freeBusyPlaceholders);
+ if ($placeholders) {
+ $placeholders.each(function() {
+ var manager = $(this).data('wcFreeBusyManager');
+ manager.insertFreeBusy(new FreeBusy(freebusy.getOption()));
+ $(this).data('wcFreeBusyManager', manager);
+ });
+ }
+ });
+
+ //now display freebusys on place holders
+ self._refreshFreeBusys($freeBusyPlaceholders);
+ }
+ },
+ /**
+ * refresh freebusys for given placeholders
+ */
+ _refreshFreeBusys: function($freeBusyPlaceholders) {
+ if (this.options.displayFreeBusys && $freeBusyPlaceholders) {
+ var self = this,
+ options = this.options,
+ start = (options.businessHours.limitDisplay ? options.businessHours.start : 0),
+ end = (options.businessHours.limitDisplay ? options.businessHours.end : 24);
+
+ $freeBusyPlaceholders.each(function() {
+ var $placehoder = $(this);
+ var s = self._cloneDate($placehoder.data('startDate')),
+ e = self._cloneDate(s);
+ s.setHours(start);
+ e.setHours(end);
+ $placehoder.find('.wc-freebusy').remove();
+ $.each($placehoder.data('wcFreeBusyManager').getFreeBusys(s, e), function() {
+ self._renderFreeBusy(this, $placehoder);
+ });
+ });
+ }
+ },
+ /**
+ * render a freebusy item on dedicated placeholders
+ */
+ _renderFreeBusy: function(freeBusy, $freeBusyPlaceholder) {
+ if (this.options.displayFreeBusys) {
+ var self = this,
+ options = this.options,
+ freeBusyHtml = '<div class="wc-freebusy"></div>';
+
+ var $fb = $(freeBusyHtml);
+ $fb.data('wcFreeBusy', new FreeBusy(freeBusy.getOption()));
+ this._positionFreeBusy($freeBusyPlaceholder, $fb);
+ $fb = options.freeBusyRender(freeBusy.getOption(), $fb, self.element);
+ if ($fb) {
+ $fb.appendTo($freeBusyPlaceholder);
+ }
+ }
+ },
+ /*
+ * Position the freebusy element within the weekday based on it's start / end dates.
+ */
+ _positionFreeBusy: function($placeholder, $freeBusy) {
+ var options = this.options;
+ var freeBusy = $freeBusy.data('wcFreeBusy');
+ var pxPerMillis = $placeholder.height() / options.millisToDisplay;
+ var firstHourDisplayed = options.businessHours.limitDisplay ? options.businessHours.start : 0;
+ var startMillis = freeBusy.getStart().getTime() - new Date(freeBusy.getStart().getFullYear(), freeBusy.getStart().getMonth(), freeBusy.getStart().getDate(), firstHourDisplayed).getTime();
+ var eventMillis = freeBusy.getEnd().getTime() - freeBusy.getStart().getTime();
+ var pxTop = pxPerMillis * startMillis;
+ var pxHeight = pxPerMillis * eventMillis;
+ $freeBusy.css({top: pxTop, height: pxHeight});
+ },
+ /*
+ * Clean freebusys to ensure correct format
+ */
+ _cleanFreeBusys: function(freebusys) {
+ var self = this,
+ freeBusyToReturn = [];
+ if (!$.isArray(freebusys)) {
+ var freebusys = [freebusys];
+ }
+ $.each(freebusys, function(i, freebusy) {
+ freeBusyToReturn.push(new FreeBusy(self._cleanFreeBusy(freebusy)));
+ });
+ return freeBusyToReturn;
+ },
+
+ /*
+ * Clean specific freebusy
+ */
+ _cleanFreeBusy: function(freebusy) {
+ if (freebusy.date) {
+ freebusy.start = freebusy.date;
+ }
+ freebusy.start = this._cleanDate(freebusy.start);
+ freebusy.end = this._cleanDate(freebusy.end);
+ return freebusy;
+ },
+
+ /**
+ * retrives the first freebusy manager matching demand.
+ */
+ getFreeBusyManagersFor: function(date, users) {
+ var calEvent = {
+ start: date,
+ end: date
+ };
+ this._setEventUserId(calEvent, users);
+ return this.getFreeBusyManagerForEvent(calEvent);
+ },
+ /**
+ * retrives the first freebusy manager for given event.
+ */
+ getFreeBusyManagerForEvent: function(newCalEvent) {
+ var self = this,
+ options = this.options,
+ freeBusyManager;
+ if (options.displayFreeBusys) {
+ var $freeBusyPlaceHoders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
+ freeBusy = new FreeBusy({start: newCalEvent.start, end: newCalEvent.end}),
+ showAsSeparatedUser = options.showAsSeparateUsers && options.users && options.users.length,
+ userId = showAsSeparatedUser ? self._getEventUserId(newCalEvent) : null;
+ if (!$.isArray(userId)) {
+ userId = [userId];
+ }
+ $freeBusyPlaceHoders.each(function() {
+ var manager = $(this).data('wcFreeBusyManager'),
+ has_overlap = manager.isWithin(freeBusy.getEnd()) ||
+ manager.isWithin(freeBusy.getEnd()) ||
+ freeBusy.isWithin(manager.getStart()) ||
+ freeBusy.isWithin(manager.getEnd());
+ if (has_overlap && (!showAsSeparatedUser || $.inArray($(this).data('wcUserId'), userId) != -1)) {
+ freeBusyManager = $(this).data('wcFreeBusyManager');
+ return false;
+ }
+ });
+ }
+ return freeBusyManager;
+ },
+ /**
+ * appends the freebusys to replace the old ones.
+ * @param {array|object} freeBusys freebusy(s) to apply.
+ */
+ updateFreeBusy: function(freeBusys) {
+ var self = this,
+ options = this.options;
+ if (options.displayFreeBusys) {
+ var $toRender,
+ $freeBusyPlaceHoders = self.element.find('.wc-grid-row-freebusy .wc-column-freebusy'),
+ _freeBusys = self._cleanFreeBusys(freeBusys);
+
+ $.each(_freeBusys, function(index, _freeBusy) {
+
+ var $weekdays = self._findWeekDaysForFreeBusy(_freeBusy, $freeBusyPlaceHoders);
+ //if freebusy has a placeholder
+ if ($weekdays && $weekdays.length) {
+ $weekdays.each(function(index, day) {
+ var manager = $(day).data('wcFreeBusyManager');
+ manager.insertFreeBusy(_freeBusy);
+ $(day).data('wcFreeBusyManager', manager);
+ });
+ $toRender = $toRender ? $toRender.add($weekdays) : $weekdays;
+ }
+ });
+ self._refreshFreeBusys($toRender);
+ }
+ },
+
+ /* NEW OPTIONS MANAGEMENT */
+
+ /**
+ * checks wether or not the calendar should be displayed starting on first day of week
+ */
+ _startOnFirstDayOfWeek: function() {
+ return jQuery.isFunction(this.options.startOnFirstDayOfWeek) ? this.options.startOnFirstDayOfWeek(this.element) : this.options.startOnFirstDayOfWeek;
+ },
+
+ /**
+ * finds out the current scroll to apply it when changing the view
+ */
+ _getCurrentScrollHour: function() {
+ var self = this;
+ var options = this.options;
+ var $scrollable = this.element.find('.wc-scrollable-grid');
+ var scroll = $scrollable.scrollTop();
+ if (self.options.businessHours.limitDisplay) {
+ scroll = scroll + options.businessHours.start * options.timeslotHeight * options.timeslotsPerHour;
+ }
+ return Math.round(scroll / (options.timeslotHeight * options.timeslotsPerHour)) + 1;
+ },
+ _getJsonOptions: function() {
+ if ($.isFunction(this.options.jsonOptions)) {
+ return $.extend({}, this.options.jsonOptions(this.element));
+ }
+ if ($.isPlainObject(this.options.jsonOptions)) {
+ return $.extend({}, this.options.jsonOptions);
+ }
+ return {};
+ },
+ _getHeaderDate: function(date) {
+ var options = this.options;
+ if (options.getHeaderDate && $.isFunction(options.getHeaderDate))
+ {
+ return options.getHeaderDate(date, this.element);
+ }
+ var dayName = options.useShortDayNames ? options.shortDays[date.getDay()] : options.longDays[date.getDay()];
+ return dayName + (options.headerSeparator) + this._formatDate(date, options.dateFormat);
+ },
+
+
+
+ /**
+ * returns corrected date related to DST problem
+ */
+ _getDSTdayShift: function(date, shift) {
+ var start = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0);
+ var offset1 = start.getTimezoneOffset();
+ var offset2 = date.getTimezoneOffset();
+ if (offset1 == offset2)
+ return date;
+ shift = shift ? shift : 1;
+ return new Date(date.getTime() - shift * (offset1 > offset2 ? -1 : 1) * (Math.max(offset1, offset2) - Math.min(offset1, offset2)) * 60000);
+ },
+ _needDSTdayShift: function(date1, date2) {
+ return date1.getTimezoneOffset() != date2.getTimezoneOffset();
+ }
+
+
+
+ }; // end of widget function return
+ })() //end of widget function closure execution
+ ); // end of $.widget("ui.weekCalendar"...
+
+ $.extend($.ui.weekCalendar, {
+ version: '2.0-dev',
+ updateLayoutOptions: {
+ startOnFirstDayOfWeek: true,
+ firstDayOfWeek: true,
+ daysToShow: true,
+ displayOddEven: true,
+ timeFormat: true,
+ dateFormat: true,
+ use24Hour: true,
+ useShortDayNames: true,
+ businessHours: true,
+ timeslotHeight: true,
+ timeslotsPerHour: true,
+ buttonText: true,
+ height: true,
+ shortMonths: true,
+ longMonths: true,
+ shortDays: true,
+ longDays: true,
+ textSize: true,
+ users: true,
+ showAsSeparateUsers: true,
+ displayFreeBusys: true
+ }
+ });
+
+ var MILLIS_IN_DAY = 86400000;
+ var MILLIS_IN_WEEK = MILLIS_IN_DAY * 7;
+
+ /* FREE BUSY MANAGERS */
+ var FreeBusyProto = {
+ getStart: function() {return this.getOption('start')},
+ getEnd: function() {return this.getOption('end')},
+ getOption: function() {
+ if (!arguments.length) { return this.options }
+ if (typeof(this.options[arguments[0]]) !== 'undefined') {
+ return this.options[arguments[0]];
+ }
+ else if (typeof(arguments[1]) !== 'undefined') {
+ return arguments[1];
+ }
+ return null;
+ },
+ setOption: function(key, value) {
+ if (arguments.length == 1) {
+ $.extend(this.options, arguments[0]);
+ return this;
+ }
+ this.options[key] = value;
+ return this;
+ },
+ isWithin: function(dateTime) {return Math.floor(dateTime.getTime() / 1000) >= Math.floor(this.getStart().getTime() / 1000) && Math.floor(dateTime.getTime() / 1000) <= Math.floor(this.getEnd().getTime() / 1000)},
+ isValid: function() {return this.getStart().getTime() < this.getEnd().getTime()}
+ };
+
+ /**
+ * @constructor
+ * single user freebusy manager.
+ */
+ var FreeBusy = function(options) {
+ this.options = $.extend({}, options || {});
+ };
+ $.extend(FreeBusy.prototype, FreeBusyProto);
+
+ var FreeBusyManager = function(options) {
+ this.options = $.extend({
+ defaultFreeBusy: {}
+ }, options || {});
+ this.freeBusys = [];
+ this.freeBusys.push(new FreeBusy($.extend({
+ start: this.getStart(),
+ end: this.getEnd()
+ }, this.options.defaultFreeBusy)));
+ };
+ $.extend(FreeBusyManager.prototype, FreeBusyProto, {
+ /**
+ * return matching freeBusys.
+ * if you do not pass any argument, returns all freebusys.
+ * if you only pass a start date, only matchinf freebusy will be returned.
+ * if you pass 2 arguments, then all freebusys available within the time period will be returned
+ * @param {Date} start [optionnal] if you do not pass end date, will return the freeBusy within which this date falls.
+ * @param {Date} end [optionnal] the date where to stop the search.
+ * @return {Array} an array of FreeBusy matching arguments.
+ */
+ getFreeBusys: function() {
+ switch (arguments.length) {
+ case 0:
+ return this.freeBusys;
+ case 1:
+ var freeBusy = [];
+ var start = arguments[0];
+ if (!this.isWithin(start)) {
+ return freeBusy;
+ }
+ $.each(this.freeBusys, function() {
+ if (this.isWithin(start)) {
+ freeBusy.push(this);
+ }
+ if (Math.floor(this.getEnd().getTime() / 1000) > Math.floor(start.getTime() / 1000)) {
+ return false;
+ }
+ });
+ return freeBusy;
+ default:
+ //we assume only 2 first args are revealants
+ var freeBusy = [];
+ var start = arguments[0], end = arguments[1];
+ var tmpFreeBusy = new FreeBusy({start: start, end: end});
+ if (end.getTime() < start.getTime() || this.getStart().getTime() > end.getTime() || this.getEnd().getTime() < start.getTime()) {
+ return freeBusy;
+ }
+ $.each(this.freeBusys, function() {
+ if (this.getStart().getTime() >= end.getTime()) {
+ return false;
+ }
+ if (tmpFreeBusy.isWithin(this.getStart()) && tmpFreeBusy.isWithin(this.getEnd())) {
+ freeBusy.push(this);
+ }
+ else if (this.isWithin(tmpFreeBusy.getStart()) && this.isWithin(tmpFreeBusy.getEnd())) {
+ var _f = new FreeBusy(this.getOption());
+ _f.setOption('end', tmpFreeBusy.getEnd());
+ _f.setOption('start', tmpFreeBusy.getStart());
+ freeBusy.push(_f);
+ }
+ else if (this.isWithin(tmpFreeBusy.getStart()) && this.getStart().getTime() < start.getTime()) {
+ var _f = new FreeBusy(this.getOption());
+ _f.setOption('start', tmpFreeBusy.getStart());
+ freeBusy.push(_f);
+ }
+ else if (this.isWithin(tmpFreeBusy.getEnd()) && this.getEnd().getTime() > end.getTime()) {
+ var _f = new FreeBusy(this.getOption());
+ _f.setOption('end', tmpFreeBusy.getEnd());
+ freeBusy.push(_f);
+ }
+ });
+ return freeBusy;
+ }
+ },
+ insertFreeBusy: function(freeBusy) {
+ var freeBusy = new FreeBusy(freeBusy.getOption());
+ //first, if inserted freebusy is bigger than manager
+ if (freeBusy.getStart().getTime() < this.getStart().getTime()) {
+ freeBusy.setOption('start', this.getStart());
+ }
+ if (freeBusy.getEnd().getTime() > this.getEnd().getTime()) {
+ freeBusy.setOption('end', this.getEnd());
+ }
+ var start = freeBusy.getStart(), end = freeBusy.getEnd(),
+ startIndex = 0, endIndex = this.freeBusys.length - 1,
+ newFreeBusys = [];
+ var pushNewFreeBusy = function(_f) {if (_f.isValid()) newFreeBusys.push(_f);};
+
+ $.each(this.freeBusys, function(index) {
+ //within the loop, we have following vars:
+ // curFreeBusyItem: the current iteration freeBusy, part of manager freeBusys list
+ // start: the insterted freeBusy start
+ // end: the inserted freebusy end
+ var curFreeBusyItem = this;
+ if (curFreeBusyItem.isWithin(start) && curFreeBusyItem.isWithin(end)) {
+ /*
+ we are in case where inserted freebusy fits in curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *-------------*
+ obviously, start and end indexes are this item.
+ */
+ startIndex = index;
+ endIndex = index;
+ if (start.getTime() == curFreeBusyItem.getStart().getTime() && end.getTime() == curFreeBusyItem.getEnd().getTime()) {
+ /*
+ in this case, inserted freebusy is exactly curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *-----------------------------*
+
+ just replace curFreeBusyItem with freeBusy.
+ */
+ var _f1 = new FreeBusy(freeBusy.getOption());
+ pushNewFreeBusy(_f1);
+ }
+ else if (start.getTime() == curFreeBusyItem.getStart().getTime()) {
+ /*
+ in this case inserted freebusy starts with curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *--------------*
+
+ just replace curFreeBusyItem with freeBusy AND the rest.
+ */
+ var _f1 = new FreeBusy(freeBusy.getOption());
+ var _f2 = new FreeBusy(curFreeBusyItem.getOption());
+ _f2.setOption('start', end);
+ pushNewFreeBusy(_f1);
+ pushNewFreeBusy(_f2);
+ }
+ else if (end.getTime() == curFreeBusyItem.getEnd().getTime()) {
+ /*
+ in this case inserted freebusy ends with curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *--------------*
+
+ just replace curFreeBusyItem with before part AND freeBusy.
+ */
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('end', start);
+ var _f2 = new FreeBusy(freeBusy.getOption());
+ pushNewFreeBusy(_f1);
+ pushNewFreeBusy(_f2);
+ }
+ else {
+ /*
+ in this case inserted freebusy is within curFreeBusyItem:
+ curFreeBusyItem: *-----------------------------*
+ freeBusy: *--------------*
+
+ just replace curFreeBusyItem with before part AND freeBusy AND the rest.
+ */
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ var _f2 = new FreeBusy(freeBusy.getOption());
+ var _f3 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('end', start);
+ _f3.setOption('start', end);
+ pushNewFreeBusy(_f1);
+ pushNewFreeBusy(_f2);
+ pushNewFreeBusy(_f3);
+ }
+ /*
+ as work is done, no need to go further.
+ return false
+ */
+ return false;
+ }
+ else if (curFreeBusyItem.isWithin(start) && curFreeBusyItem.getEnd().getTime() != start.getTime()) {
+ /*
+ in this case, inserted freebusy starts within curFreeBusyItem:
+ curFreeBusyItem: *----------*
+ freeBusy: *-------------------*
+
+ set start index AND insert before part, we'll insert freebusy later
+ */
+ if (curFreeBusyItem.getStart().getTime() != start.getTime()) {
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('end', start);
+ pushNewFreeBusy(_f1);
+ }
+ startIndex = index;
+ }
+ else if (curFreeBusyItem.isWithin(end) && curFreeBusyItem.getStart().getTime() != end.getTime()) {
+ /*
+ in this case, inserted freebusy starts within curFreeBusyItem:
+ curFreeBusyItem: *----------*
+ freeBusy: *-------------------*
+
+ set end index AND insert freebusy AND insert after part if needed
+ */
+ pushNewFreeBusy(new FreeBusy(freeBusy.getOption()));
+ if (end.getTime() < curFreeBusyItem.getEnd().getTime()) {
+ var _f1 = new FreeBusy(curFreeBusyItem.getOption());
+ _f1.setOption('start', end);
+ pushNewFreeBusy(_f1);
+ }
+ endIndex = index;
+ return false;
+ }
+ });
+ //now compute arguments
+ var tmpFB = this.freeBusys;
+ this.freeBusys = [];
+
+ if (startIndex) {
+ this.freeBusys = this.freeBusys.concat(tmpFB.slice(0, startIndex));
+ }
+ this.freeBusys = this.freeBusys.concat(newFreeBusys);
+ if (endIndex < tmpFB.length) {
+ this.freeBusys = this.freeBusys.concat(tmpFB.slice(endIndex + 1));
+ }
+/* if(start.getDate() == 1){
+ console.info('insert from '+freeBusy.getStart() +' to '+freeBusy.getEnd());
+ console.log('index from '+ startIndex + ' to ' + endIndex);
+ var str = [];
+ $.each(tmpFB, function(i){str.push(i + ": " + this.getStart().getHours() + ' > ' + this.getEnd().getHours() + ' ' + (this.getOption('free') ? 'free' : 'busy'))});
+ console.log(str.join('\n'));
+
+ console.log('insert');
+ var str = [];
+ $.each(newFreeBusys, function(i){str.push(this.getStart().getHours() + ' > ' + this.getEnd().getHours())});
+ console.log(str.join(', '));
+
+ console.log('results');
+ var str = [];
+ $.each(this.freeBusys, function(i){str.push(i + ": " + this.getStart().getHours() + ' > ' + this.getEnd().getHours() + ' ' + (this.getOption('free') ? 'free' :'busy'))});
+ console.log(str.join('\n'));
+ }*/
+ return this;
+ }
+ });
+})(jQuery);
+
diff --git a/modules-available/locationinfo/frontend/panel.html b/modules-available/locationinfo/frontend/panel.html
new file mode 100644
index 00000000..bd22408e
--- /dev/null
+++ b/modules-available/locationinfo/frontend/panel.html
@@ -0,0 +1,858 @@
+<!DOCTYPE html>
+<html lang="de">
+<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8">
+<head>
+ <script type='text/javascript' src='../../../script/jquery.js'></script>
+
+ <style type='text/css'>
+ body {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ background-color: lightgrey;
+ }
+
+ .main {
+ background-color: lightgrey;
+
+ }
+
+ .child {
+ background-color: white;
+ display: inline-block;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ padding: 1vmin;
+ float: left;
+ }
+
+ .parent {
+ background-color: white;
+ display: inline-block;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ padding: 5px;
+ float: left;
+
+ }
+
+ .childWithBorder {
+ display: inline-flex;
+ padding: 0.4vmin;
+
+ }
+
+ .outermost {
+
+ }
+
+ .row {
+ float: left
+ }
+
+ .border {
+ display: inline-flex;
+ padding: 5px;
+ }
+
+ .borderout {
+ display: inline-flex;
+ padding: 0.4vmin;
+ }
+
+ .courseFont {
+ padding: 0.5vmin;
+ font-size: 2vmin;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: bold;
+ overflow: hidden;
+ }
+
+ .headerFont {
+ font-size: 4vmin;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: bold;
+ border: 0px;
+ border-bottom: 0.2vmin;
+ margin-bottom: 1vmin;
+ border-color: grey;
+ border-style: solid;
+
+ }
+
+ .divPcOn, .divPcPcUsed, .divPcPcOff, .divPcPcDefect {
+ width: 4vmin;
+ height: 4vmin;
+ text-align: center;
+ font-size: 3vmin;
+ font-weight: 800;
+ border-radius: 0.4vmin;
+
+ }
+
+ .divPcOn {
+ background-color: green;
+ text-align: center;
+ }
+
+ .divPcPcUsed {
+
+ background-color: red;
+ }
+
+ .divPcPcOff {
+ background-color: darkgrey;
+ }
+
+ .divPcPcDefect {
+ background-color: black;
+ color: white;
+ }
+
+ .divAroundPcStates {
+ display: flex;
+ justify-content: flex-end;
+
+ }
+
+ .paperEffect {
+ margin: 0 auto;
+ background-color: #fff;
+ -webkit-box-shadow: 0 0 0.2vmin rgba(0, 0, 0, 0.4), inset 0 0 1vmin rgba(0, 0, 0, 0.1);
+ -moz-box-shadow: 0 0 0.2vmin rgba(0, 0, 0, 0.4), inset 0 0 1vmin rgba(0, 0, 0, 0.1);
+ box-shadow: 0 0 0.2vmin rgba(0, 0, 0, 0.4), inset 0 0 1vmin rgba(0, 0, 0, 0.1);
+ border-radius: 1px;
+ }
+
+
+ </style>
+ <script type='text/javascript'>
+
+ var rooms = {};
+ var startdate;
+ var roomidsString = "";
+ var lastPcUpdate;
+ var lastTimeTableUpdate;
+
+ // Todo change these
+ var pcStateUpdateTime = 60; // in seconds
+ var TimeTableUpdateTime = 60 * 30; //in seconds
+
+
+ $(document).ready(function () {
+ //temp
+ SetUpDate(new Date());
+ init();
+ });
+
+ function init() {
+ var ids = getUrlParameter("id");
+ $.getJSON("../../../api.php?do=locationinfo&action=locationtree&id=" + ids, function (result) {
+ generateLayout(result);
+
+ setInterval(update, 1000);
+ arrange();
+ });
+
+ }
+
+ /**
+ * Main Fuction for aranging the divs
+ */
+ function arrange() {
+ var height = $(window).height();
+ var width = $(window).width();
+ var childs = $(".childWithBorder");
+ var parents = [];
+ for (var i = 0; i < childs.length; i++) {
+ var parent = $(childs[i]).parent();
+ if ($.inArray(parent[0], parents) == -1) {
+ parents.push(parent[0]);
+ }
+ }
+ for (var i = 0; i < parents.length; i++) {
+ childs = $(parents[i]).children();
+
+ if (checkForLineBreak(parents[i], childs) > 0) {
+ makeItFit(parents[i], childs, childs.length / 2);
+ }
+
+ }
+
+ }
+ /**
+ * rekursive calls itselfs and trys to find the best number off childs
+ * which should be side by side.
+ * @param parent Parent div
+ * @param childs Child divs
+ * @param breakAfter number after the divs should go in the next row
+ */
+ function makeItFit(parent, childs, breakAfter) {
+ breakAfter = Math.abs(breakAfter);
+ var width = checkIfFit(parent, childs, breakAfter)
+ if (width > 0) {
+ $(parent).width(width + 20);
+ } else {
+ makeItFit(parent, childs, breakAfter - 1);
+ }
+ }
+
+ /**
+ * checks if a given number off divs(side by side) would fit on the screen
+ * @param parent Parent div
+ * @param childs Child divs
+ * @param breakAfter number after the divs should go in the next row
+ * @returns -1 if it wouldn't fit else the width the div should have
+ */
+ function checkIfFit(parent, childs, breakAfter) {
+ var parentWidth = $(parent).width();
+ var maxWidth = 0;
+ var curWidth = 0;
+ var t = 0;
+ for (var i = 0; i < childs.length; i++) {
+ var childWidth = $(childs[i]).width();
+ if ((curWidth + childWidth > parentWidth && breakAfter > 1)) {
+ return -1;
+ }
+ curWidth += childWidth;
+ t++;
+ if (t == breakAfter) {
+ maxWidth = Math.max(curWidth, maxWidth);
+ curWidth = 0;
+ t = 0;
+ }
+
+ }
+ return maxWidth
+ }
+
+ /**
+ * return the number of Linebreaks the divs would make
+ * @param parent Parent div
+ * @param childs Child divs
+ * @returns the number of Linebreaks the divs would make
+ */
+ function checkForLineBreak(parent, childs) {
+ var linebreaks = 0;
+ var curWidth = 0;
+ var parentWidth = $(parent).width();
+ for (var i = 0; i < childs.length; i++) {
+ var childWidth = $(childs[i]).width();
+ if (curWidth + childWidth < parentWidth) {
+ curWidth += childWidth;
+ } else {
+ linebreaks++;
+
+ curWidth = childWidth;
+ }
+ }
+ return linebreaks;
+ }
+
+ function SetUpDate(d) {
+ startdate = d.getTime() - new Date().getTime();
+ }
+
+ function MyDate() {
+ return new Date(startdate + new Date().getTime());
+ }
+
+ function generateLayout(json) {
+ var ids = getUrlParameter("id");
+ ids = ids.split(',');
+ for (var t = 0; t < ids.length; t++) {
+
+ for (var i = 0; i < json.length; i++) {
+ if (ids[t] == json[i].locationid) {
+ var el = generateObject(json[i], ($("#main")), true);
+ }
+
+ }
+ }
+
+ }
+
+ /**
+ * generates the divs, decidecs if parent or child
+ * @param json Room tree json
+ * @param myParent parent div
+ * @param outermost if the object is a root node
+ * @returns generated div
+ */
+ function generateObject(json, myParent, outermost) {
+ var obj;
+ if (!json.children || json.children.length == 0) {
+ obj = generateChild(myParent, json.locationid, json.locationname, outermost);
+ } else {
+ obj = generateParent(myParent, json.locationid, json.locationname, outermost);
+ for (var i = 0; i < json.children.length; i++) {
+ generateObject(json.children[i], $("#parent_" + json.locationid));
+ }
+ }
+ return obj;
+
+ }
+
+ /**
+ * Helper function to generate id string used in query functions
+ * @param list A string, wicht contains ids or not(for now)
+ * @param id An ID which should be added to the list
+ */
+ function addIdToUpdateList(list, id) {
+ if (list == "") {
+ list += id;
+ } else {
+ list += ("," + id);
+ }
+ return list;
+ }
+
+
+ var timeSteps = 10;
+ function update() {
+
+ if (timeSteps > 9) {
+ timeSteps = 0;
+ var calendarUpdateIds = "";
+ var rommUpdateIds = "";
+ for (var property in rooms) {
+ if (rooms[property].lastCalendarUpdate == null || rooms[property].lastCalendarUpdate + rooms[property].config.calupdates < MyDate().getTime()) {
+ calendarUpdateIds = addIdToUpdateList(calendarUpdateIds, rooms[property].id);
+ rooms[property].lastCalendarUpdate = MyDate().getTime();
+ }
+ if (rooms[property].lastRoomUpdate == null || rooms[property].lastRoomUpdate + rooms[property].config.roomupdate < MyDate().getTime()) {
+ rommUpdateIds = addIdToUpdateList(rommUpdateIds, rooms[property].id);
+ rooms[property].lastRoomUpdate = MyDate().getTime();
+
+ }
+ }
+ if (calendarUpdateIds != "") {
+ queryCalendars(calendarUpdateIds);
+ }
+ if (rommUpdateIds != "") {
+ queryRooms(rommUpdateIds);
+ }
+ }
+ // TODO
+ for (var property in rooms) {
+ upDateRoomState(rooms[property]);
+ }
+ timeSteps++;
+
+ }
+
+
+ function UpdateTimeTables(json) {
+ var l = json.length;
+ for (var i = 0; i < l; i++) {
+ rooms[json[i].id].timetable = json[i].calendar;
+ for (var property in rooms[json[i].id].timetable) {
+ rooms[json[i].id].timetable[property].start = new Date(rooms[json[i].id].timetable[property].start);
+ rooms[json[i].id].timetable[property].end = new Date(rooms[json[i].id].timetable[property].end);
+ }
+ ComputeCurrentState(rooms[json[i].id]);
+ }
+ }
+
+ /**
+ * Querys Pc states
+ * @param ids Room ID's which should be queried. Format for e.g.: "20,5,6"
+ */
+ function queryRooms(ids) {
+ $.ajax({
+ url: "../../../api.php?do=locationinfo&action=pcstates&id=" + ids,
+ dataType: 'json',
+ cache: false,
+ timeout: 30000,
+ success: function (result) {
+ var l = result.length;
+ if (result[0] == null) {
+ console.log("Error: Backend reported null back for RoomUpdate, this might happend if the room isn't" +
+ "configurated.");
+ return;
+ }
+ updatePcStates(result);
+ }, error: function () {
+
+ }
+ })
+ }
+
+ /**
+ * Updates a room visualy
+ * @param room A room to update
+ */
+ function upDateRoomState(room) {
+ if (room === undefined) {
+ console.log("error");
+ return;
+ }
+
+ var state = room.getState();
+
+ if (state.state == "ClaendarEvent") {
+ updateCourseText(room.id, state.titel);
+ updateCoursTimer(room.id, GetTimeDiferenceAsString(state.end, MyDate()));
+ } else if (state.state == "Free") {
+ updateCourseText(room.id, "Frei");
+ updateCoursTimer(room.id, GetTimeDiferenceAsString(state.end, MyDate()));
+ } else if (state.state == "FreeNoEnd") {
+ updateCourseText(room.id, "Frei");
+ updateCoursTimer(room.id, "");
+ }
+ else if (state.state == "closed") {
+ updateCourseText(room.id, "Geschlossen");
+ updateCoursTimer(room.id, "");
+ }
+
+ }
+
+ /**
+ * Updates for all rooms the PC's states
+ * @param json Json with information about the PC's states
+ */
+ function updatePcStates(json) {
+ var l = json.length;
+ for (var i = 0; i < l; i++) {
+ updateRoomUsage(json[i].id, json[i].idle, json[i].occupied, json[i].off, json[i].broken)
+ }
+
+ }
+ /**
+ * Generates a room Object and adds it to the rooms array
+ * @param id ID of the room
+ * @param name Name of the room
+ * @param config Config Json of the room
+ */
+ function addRoom(id, name, config) {
+ var room = {
+ id: id,
+ name: name,
+ timetable: null,
+ currentEvent: null,
+ nextEventEnd: null,
+ timeTilFree: null,
+ state: null,
+ openingTimes: null,
+ config: config,
+ lastCalendarUpdate: null,
+ lastRoomUpdate: null,
+ getState: function () {
+ if (this.state == null) {
+ ComputeCurrentState(this);
+ return this.state;
+ }
+ if (this.state.end != "") {
+ if (this.state.end < new MyDate()) {
+ ComputeCurrentState(this);
+ }
+ }
+ return this.state;
+ }
+
+
+ };
+ if (room.config.calupdate === undefined || room.config.calupdate < 1) {
+ room.config.calupdate = 1;
+ }
+ room.config.calupdate = room.config.calupdate * 60 * 1000;
+ if (room.config.roomupdate === undefined || room.config.roomupdate < 1) {
+ room.config.roomupdate = 1;
+ }
+ room.config.roomupdate = room.config.roomupdate * 1000;
+
+ rooms[id] = room;
+
+ if (roomidsString == "") {
+ roomidsString = id;
+ } else {
+ roomidsString = roomidsString + "," + id;
+ }
+ }
+
+
+ /**
+ * computes state of a room, states are:
+ * closed, FreeNoEnd, Free, ClaendarEvent.
+ * @param Room Object
+ */
+ function ComputeCurrentState(room) {
+ if (!IsOpenNow(room)) {
+ room.state = {state: "closed", end: GetNextOpening(room), titel: "", next: ""};
+
+ return;
+ }
+ var closing = GetNextClosing(room);
+
+ var event = getNextEvent(room.timetable);
+ // no event and no closing
+ if (closing == null && event == null) {
+ room.state = {state: "FreeNoEnd", end: "", titel: "", next: ""};
+ return;
+ }
+
+ // no event so closing is next
+ if (event == null) {
+ room.state = {state: "Free", end: closing, titel: "", next: "closing"};
+ return;
+ }
+
+ // event is at the moment
+ if ((closing == null || event.start.getTime() < closing.getTime()) && event.start.getTime() < new MyDate()) {
+ room.state = {state: "ClaendarEvent", end: event.end, titel: event.title, next: ""};
+ return;
+ }
+
+ // no closing so event is next
+ if (closing == null) {
+ room.state = {state: "Free", end: event.start, titel: "", next: "event"};
+ return;
+ }
+
+ // event sooner then closing
+ if (event.start.getTime() < closing) {
+ room.state = {state: "Free", end: event.start, titel: "", next: "event"};
+ } else if (event.start.getTime() > closing) {
+ room.state = {state: "Free", end: closing, titel: "", next: "closing"};
+ }
+ }
+ /**
+ * checks if a room is open
+ * @param room Room object
+ * @returns bool for open or not
+ */
+ function IsOpenNow(room) {
+ var now = new MyDate();
+ if (room.openingTimes == null) {
+
+ // changes from falls needs testing
+ return true;
+ }
+ var tmp = room.openingTimes[now.getDay()];
+ if (tmp == null) {
+ return false;
+ }
+ for (var i = 0; i < tmp.length; i++) {
+ var openDate = new MyDate();
+ openDate.setHours(tmp[i].HourOpen);
+ openDate.setMinutes(tmp[i].MinutesOpen);
+ var closeDate = new MyDate();
+ closeDate.setHours(tmp[i].HourClose);
+ closeDate.setMinutes(tmp[i].MinutesClose);
+ if (openDate < now && closeDate > now) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * returns next event from a given json of events
+ * @param json Json which contains the calendar data.
+ * @returns event next Carlendar Event
+ */
+ function getNextEvent(json) {
+ if (json == null) {
+ return;
+ }
+ var event;
+ var now = new MyDate();
+ for (var i = 0; i < json.length; i++) {
+ //event is now active
+ if (json[i].start.getTime() < now.getTime() && json[i].end.getTime() > now.getTime()) {
+ return json[i];
+ }
+ //first element to consider
+ if (event == null) {
+ if (json[i].start.getTime() > now.getTime()) {
+ event = json[i];
+ }
+ }
+ if (json[i].start.getTime() > now.getTime() && event.start.getTime() > json[i].start.getTime()) {
+ event = json[i];
+ }
+ }
+ return event;
+ }
+
+ /**
+ * Retruns next Opening
+ * @param room Room Object
+ * @returns bestdate Date Object of next opening
+ */
+ function GetNextOpening(room) {
+ var now = new MyDate();
+ var day = now.getDay();
+ var offset = 0;
+ var bestdate;
+ for (var a = 0; a < 7; a++) {
+ if (room.openingTimes == null) {
+ return null;
+ }
+ var tmp = room.openingTimes[day];
+ if (tmp != null) {
+ for (var i = 0; i < tmp.length; i++) {
+ var openDate = new MyDate();
+ openDate.setDate(now.getDate() + offset);
+ openDate.setHours(tmp[i].HourOpen);
+ openDate.setMinutes(tmp[i].MinutesOpen);
+ if (openDate > now) {
+ if (!IsOpen(new Date(openDate.getTime() - 60000))) {
+ if (bestdate == null || bestdate > openDate) {
+ bestdate = openDate;
+ }
+ }
+ }
+ }
+ }
+ offset++;
+ day++;
+ if (day > 6) {
+ day = 0;
+ }
+ }
+ return bestdate;
+ }
+
+ /**
+ * returns next closing time of a given room
+ * @param room
+ * @returns Date Object of next closing
+ */
+ function GetNextClosing(room) {
+ var now = new MyDate();
+ var day = now.getDay();
+ var offset = 0;
+ var bestdate;
+ for (var a = 0; a < 7; a++) {
+ //Test
+ if (room.openingTimes === null) {
+ return null;
+ }
+ var tmp = room.openingTimes[day];
+ if (tmp != null) {
+ for (var i = 0; i < tmp.length; i++) {
+ var closeDate = new MyDate();
+ closeDate.setDate(now.getDate() + offset);
+ closeDate.setHours(tmp[i].HourClose);
+ closeDate.setMinutes(tmp[i].MinutesClose);
+ if (closeDate > now) {
+ if (!IsOpen(new Date(closeDate.getTime() + 60000))) {
+ if (bestdate == null || bestdate > closeDate) {
+ bestdate = closeDate;
+ }
+ }
+ }
+ }
+ }
+ offset++;
+ day++;
+ if (day > 6) {
+ day = 0;
+ }
+ }
+ return bestdate;
+ }
+
+ /**
+ * Updates the Course Text of a child
+ * @param id of the child
+ * @param on PC's on
+ * @param used PC's used
+ * @param off PC's that are off
+ * @param defect PC's that are defect
+ */
+ function updateRoomUsage(id, on, used, off, defect) {
+ $("#div_pc_On_" + id).text(on);
+ $("#div_pc_Used_" + id).text(used);
+ $("#div_pc_Off_" + id).text(off);
+ $("#div_pc_Defect_" + id).text(defect);
+ }
+
+ /**
+ * Updates the Course Text of a child
+ * @param id of the child
+ * @param text Text
+ */
+ function updateCourseText(id, text) {
+ $("#div_course" + id).text(text);
+ }
+
+ /**
+ * Updates the Course time of a child
+ * @param id of the child
+ * @param time Time value
+ */
+ function updateCoursTimer(id, time) {
+ $("#div_Time_" + id).text(time);
+ }
+ /**
+ * generates a new Div
+ * @param target Div it should be inserted
+ * @returns generated div
+ */
+ function generateRow(target) {
+ var text = "<div class='row' ></div>";
+ return $(target).append(text);
+ }
+
+ /**
+ * generates a Div, used for a child node
+ * @param target Div it should be inserted
+ * @param id ID of the Object it represents
+ * @param name Name of the Object it represents
+ * @param outermost if the object is a root node
+ * @returns generated div
+ */
+ function generateChild(target, id, name, outermost) {
+
+ var c = "";
+ if (outermost) {
+ c = "outermost";
+ }
+
+ var text = "<div class='childWithBorder'>" +
+ "<div class='child paperEffect " + c + "'>" +
+ "<div class='headerFont'>" + name + "</div>" +
+ "<div class='divAroundPcStates'>" +
+ "<div id = 'div_pc_On_" + id + "' class='divPcOn '>" + 0 + "</div>" +
+ "<div id = 'div_pc_Used_" + id + "' class='divPcPcUsed'>" + 0 + "</div>" +
+ "<div id = 'div_pc_Off_" + id + "' class='divPcPcOff'>" + 0 + "</div>" +
+ "<div id = 'div_pc_Defect_" + id + "' class='divPcPcDefect'>" + 0 + "</div>" +
+ "</div>" +
+ "<div class='aroundCourse'>" +
+ "<div id = 'div_course" + id + "'class='courseFont'></div>" +
+ "<div id = 'div_Time_" + id + "'class='courseFont'></div></div></div></div>";
+ var obj = $(target).append(text);
+ getConfig((id));
+ return obj
+
+ }
+
+ /**
+ * generates a Div, used for a parent node
+ * @param target Div it should be inserted
+ * @param id ID of the Object it represents
+ * @param name Name of the Object it represents
+ * @param outermost if the object is a root node
+ * @returns generated div
+ */
+ function generateParent(target, id, name, outermost) {
+ var c = "";
+ if (outermost) {
+ c = "outermost";
+ }
+
+ var text = "<div class='border " + c + "'>" +
+ "<div class='parent paperEffect'>" +
+ "<div class='headerFont'>" + name + "</div>" +
+ "<div id='parent_" + id + "'</div>" +
+ "</div></div>";
+ return $(target).append(text);
+ }
+
+ /**
+ * Downloads the config of a room
+ * @param id ID of the room
+ */
+ function getConfig(id) {
+ $.ajax({
+ url: "../../../api.php?do=locationinfo&action=config&id=" + id,
+ dataType: 'json',
+ cache: false,
+ timeout: 30000,
+ success: function (result) {
+ if (result.room != null) {
+ delete result.time;
+ room = addRoom(id, result.room, result);
+ }
+ }, error: function () {
+ //Todo Error handling:
+ }
+ })
+ }
+
+ /**
+ * returns parameter value from the url
+ * @param sParam
+ * @returns value for given parameter
+ */
+ var getUrlParameter = function getUrlParameter(sParam) {
+ var sPageURL = decodeURIComponent(window.location.search.substring(1)),
+ sURLVariables = sPageURL.split('&'),
+ sParameterName,
+ i;
+
+ for (i = 0; i < sURLVariables.length; i++) {
+ sParameterName = sURLVariables[i].split('=');
+
+ if (sParameterName[0] === sParam) {
+ return sParameterName[1] === undefined ? true : sParameterName[1];
+ }
+ }
+ };
+
+
+ /**
+ * querys the Calendar data
+ * @param ids ID'S of rooms to query as string, for e.g.: "5,17,8" or "5"
+ */
+ function queryCalendars(ids) {
+ var url = "../../../api.php?do=locationinfo&action=calendar&id=" + ids;
+
+ // Todo reimplement Frontend methode if needed
+ /*
+ if(!(room.config.calendarqueryurl === undefined)) {
+ url = room.config.calendarqueryurl;
+ }
+ */
+ $.ajax({
+ url: url,
+ dataType: 'json',
+ cache: false,
+ timeout: 30000,
+ success: function (result) {
+ UpdateTimeTables(result);
+
+
+ }, error: function () {
+
+ }
+ });
+ }
+
+
+ /**
+ * used for countdown
+ * computes the time difference between 2 Date objects
+ * @param a Date Object
+ * @param b Date Object
+ * @returns time string
+ */
+ function GetTimeDiferenceAsString(a, b) {
+ if (a == null || b == null) {
+ return "";
+ }
+ var milliseconds = a.getTime() - b.getTime();
+ var seconds = Math.floor((milliseconds / 1000) % 60);
+ milliseconds -= seconds * 1000;
+ var minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
+ milliseconds -= minutes * 1000 * 60;
+ var hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
+
+ var days = Math.floor((milliseconds / (1000 * 60 * 60 * 24)) % 31);
+ if (seconds < 10) {
+ seconds = "0" + seconds;
+ }
+ if (minutes < 10) {
+ minutes = "0" + minutes;
+ }
+ if (days != 0) {
+ // dont show?
+ return "";
+ }
+ return hours + ":" + minutes + ":" + seconds;
+ }
+ </script>
+</head>
+<body class="">
+<h1>Raum Übersicht</h1>
+<div id="main"></div>
+</body>
+</html>
diff --git a/modules-available/locationinfo/hooks/translation.inc.php b/modules-available/locationinfo/hooks/translation.inc.php
new file mode 100644
index 00000000..e83dfd2d
--- /dev/null
+++ b/modules-available/locationinfo/hooks/translation.inc.php
@@ -0,0 +1,24 @@
+<?php
+
+$HANDLER = array();
+
+if (Module::isAvailable('locationinfo')) {
+ $HANDLER['subsections'] = array();
+ foreach (CourseBackend::getList() as $backend) {
+ // Define subsections
+ $HANDLER['subsections'][] = 'backend-' . $backend;
+ // Grep handlers to detect tags
+ $HANDLER['grep_backend-' . $backend] = function($module) use ($backend) {
+ $b = CourseBackend::getInstance($backend);
+ if ($b === false)
+ return array();
+ $props = $b->getCredentialDefinitions();
+ $return = array();
+ foreach ($props as $prop) {
+ $return[$prop->property] = true;
+ $return[$prop->property . '_helptext'] = true;
+ }
+ return $return;
+ };
+ }
+}
diff --git a/modules-available/locationinfo/inc/coursebackend.inc.php b/modules-available/locationinfo/inc/coursebackend.inc.php
new file mode 100644
index 00000000..447a5598
--- /dev/null
+++ b/modules-available/locationinfo/inc/coursebackend.inc.php
@@ -0,0 +1,365 @@
+<?php
+
+/**
+ * Base class for course query backends
+ */
+abstract class CourseBackend
+{
+
+ /*
+ * Static part for handling interfaces
+ */
+
+ /**
+ * @var array list of known backends
+ */
+ private static $backendTypes = false;
+ /**
+ * @var boolean|string false = no error, error message otherwise
+ */
+ protected $error;
+ /**
+ * @var int as internal serverId
+ */
+ protected $serverId;
+ /**
+ * @const int max number of additional locations to fetch (for backends that benefit from request coalesc.)
+ */
+ const MAX_ADDIDIONAL_LOCATIONS = 5;
+
+ /**
+ * CourseBackend constructor.
+ */
+ public final function __construct()
+ {
+ $this->error = false;
+ }
+
+ /**
+ * Load all known backend types. This is done
+ * by including *.inc.php from inc/coursebackend/.
+ */
+ public static function loadDb()
+ {
+ if (self::$backendTypes !== false)
+ return;
+ self::$backendTypes = array();
+ foreach (glob(dirname(__FILE__) . '/coursebackend/coursebackend_*.inc.php', GLOB_NOSORT) as $file) {
+ require_once $file;
+ preg_match('#coursebackend_([^/\.]+)\.inc\.php$#i', $file, $out);
+ if (!class_exists('coursebackend_' . $out[1])) {
+ trigger_error("Backend type source unit $file doesn't seem to define class CourseBackend_{$out[1]}", E_USER_ERROR);
+ }
+ self::$backendTypes[$out[1]] = true;
+ }
+ }
+
+ /**
+ * Get all known config module types.
+ *
+ * @return array list of modules
+ */
+ public static function getList()
+ {
+ self::loadDb();
+ return array_keys(self::$backendTypes);
+ }
+
+ /**
+ * Get fresh instance of ConfigModule subclass for given module type.
+ *
+ * @param string $moduleType name of module type
+ * @return \CourseBackend module instance
+ */
+ public static function getInstance($moduleType)
+ {
+ self::loadDb();
+ if (!isset(self::$backendTypes[$moduleType])) {
+ error_log('Unknown module type: ' . $moduleType);
+ return false;
+ }
+ if (!is_object(self::$backendTypes[$moduleType])) {
+ $class = "coursebackend_$moduleType";
+ self::$backendTypes[$moduleType] = new $class;
+ }
+ return self::$backendTypes[$moduleType];
+ }
+
+ /**
+ * @return string return display name of backend
+ */
+ public abstract function getDisplayName();
+
+
+ /**
+ * @returns \BackendProperty[] list of properties that need to be set
+ */
+ public abstract function getCredentialDefinitions();
+
+ /**
+ * @return boolean true if the connection works, false otherwise
+ */
+ public abstract function checkConnection();
+
+ /**
+ * uses json to setCredentials, the json must follow the form given in
+ * getCredentials
+ *
+ * @param array $data assoc array with data required by backend
+ * @returns bool if the credentials were in the correct format
+ */
+ public abstract function setCredentialsInternal($data);
+
+ /**
+ * @return int desired caching time of results, in seconds. 0 = no caching
+ */
+ public abstract function getCacheTime();
+
+ /**
+ * @return int age after which timetables are no longer refreshed should be
+ * greater then CacheTime
+ */
+ public abstract function getRefreshTime();
+
+ /**
+ * Internal version of fetch, to be overridden by subclasses.
+ *
+ * @param $roomIds array with local ID as key and serverId as value
+ * @return array a recursive array that uses the roomID as key
+ * and has the schedule array as value. A shedule array contains an array in this format:
+ * ["start"=>'JJJJ-MM-DD HH:MM:SS',"end"=>'JJJJ-MM-DD HH:MM:SS',"title"=>string]
+ */
+ protected abstract function fetchSchedulesInternal($roomId);
+
+ /**
+ * Method for fetching the schedule of the given rooms on a server.
+ *
+ * @param array $roomId array of room ID to fetch
+ * @return array|bool array containing the timetables as value and roomid as key as result, or false on error
+ */
+ public final function fetchSchedule($requestedLocationIds)
+ {
+ if (!is_array($requestedLocationIds)) {
+ $this->error = 'No array of roomids was given to fetchSchedule';
+ return false;
+ }
+ if (empty($requestedLocationIds))
+ return array();
+ $NOW = time();
+ $dbquery1 = Database::simpleQuery("SELECT locationid, calendar, serverlocationid, lastcalendarupdate
+ FROM locationinfo_locationconfig WHERE locationid IN (:locations)",
+ array('locations' => array_values($requestedLocationIds)));
+ $returnValue = [];
+ $remoteIds = [];
+ while ($row = $dbquery1->fetch(PDO::FETCH_ASSOC)) {
+ //Check if in cache if lastUpdate is null then it is interpreted as 1970
+ if ($row['lastcalendarupdate'] + $this->getCacheTime() > $NOW) {
+ $returnValue[$row['locationid']] = json_decode($row['calendar']);
+ } else {
+ $remoteIds[$row['locationid']] = $row['serverlocationid'];
+ }
+
+ }
+ // No need for additional round trips to backend
+ if (empty($remoteIds)) {
+ return $returnValue;
+ }
+ // Check if we should refresh other rooms recently requested by front ends
+ if ($this->getRefreshTime() > $this->getCacheTime()) {
+ $dbquery4 = Database::simpleQuery("SELECT locationid, serverlocationid FROM locationinfo_locationconfig
+ WHERE serverid = :serverid AND serverlocationid NOT IN (:skiplist)
+ AND lastcalendarupdate BETWEEN :lowerage AND :upperage
+ LIMIT " . self::MAX_ADDIDIONAL_LOCATIONS, array(
+ 'serverid' => $this->serverId,
+ 'skiplist' => array_values($remoteIds),
+ 'lowerage' => $NOW - $this->getRefreshTime(),
+ 'upperage' => $NOW - $this->getCacheTime(),
+ ));
+ while ($row = $dbquery4->fetch(PDO::FETCH_ASSOC)) {
+ $remoteIds[$row['locationid']] = $row['serverlocationid'];
+ }
+ }
+ $backendResponse = $this->fetchSchedulesInternal($remoteIds);
+ if ($backendResponse === false) {
+ return false;
+ }
+
+ if ($this->getCacheTime() > 0) {
+ // Caching requested by backend, write to DB
+ foreach ($backendResponse as $serverRoomId => $calendar) {
+ $value = json_encode($calendar);
+ Database::simpleQuery("UPDATE locationinfo_locationconfig SET calendar = :ttable, lastcalendarupdate = :now
+ WHERE serverid = :serverid AND serverlocationid = :serverlocationid", array(
+ 'serverid' => $this->serverId,
+ 'serverlocationid' => $serverRoomId,
+ 'ttable' => $value,
+ 'now' => $NOW
+ ));
+ }
+ }
+ // Add rooms that were requested to the final return value
+ foreach ($remoteIds as $location => $serverRoomId) {
+ if (isset($backendResponse[$serverRoomId]) && in_array($location, $requestedLocationIds)) {
+ // Only add if we can map it back to our location id AND it was not an unsolicited coalesced refresh
+ $returnValue[$location] = $backendResponse[$serverRoomId];
+ }
+ }
+
+ return $returnValue;
+ }
+
+ public final function setCredentials($serverId, $data)
+ {
+ foreach ($this->getCredentialDefinitions() as $prop) {
+ if (!isset($data[$prop->property])) {
+ $data[$prop->property] = $prop->default;
+ }
+ if (in_array($prop->type, ['string', 'bool', 'int'])) {
+ settype($data[$prop->property], $prop->type);
+ } else {
+ settype($data[$prop->property], 'string');
+ }
+ }
+ if ($this->setCredentialsInternal($data)) {
+ $this->serverId = $serverId;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return false if there was no error string with error message if there was one
+ */
+ public final function getError()
+ {
+ return $this->error;
+ }
+
+ /**
+ * Query path in array-representation of XML document.
+ * e.g. 'path/syntax/foo/wanteditem'
+ * This works for intermediate nodes (that have more children)
+ * and leaf nodes. The result is always an array on success, or
+ * false if not found.
+ */
+ protected function getArrayPath($array, $path)
+ {
+ if (!is_array($path)) {
+ // Convert 'path/syntax/foo/wanteditem' to array for further processing and recursive calls
+ $path = explode('/', $path);
+ }
+ do {
+ // Get next element from array, loop to ignore empty elements (so double slashes in the path are allowed)
+ $element = array_shift($path);
+ } while (empty($element) && !empty($path));
+ if (!isset($array[$element])) {
+ // Current path element does not exist - error
+ return false;
+ }
+ if (empty($path)) {
+ // Path is now empty which means we're at 'wanteditem' from out example above
+ if (!is_array($array[$element]) || !isset($array[$element][0])) {
+ // If it's a leaf node of the array, wrap it in plain array, so the function will
+ // always return an array on success
+ return array($array[$element]);
+ }
+ // 'wanteditem' is not a unique leaf node, return as is
+ // This means it's either a plain array, in case there are multiple 'wanteditem' elements on the same level
+ // or it's an associative array if 'wanteditem' has any sub-nodes
+ return $array[$element];
+ }
+ // Recurse
+ if (!is_array($array[$element])) {
+ // We're in the middle of the requested path, but the current element is already a leaf node with no
+ // children - error
+ return false;
+ }
+ if (isset($array[$element][0])) {
+ // The currently handled element of the path exists multiple times on the current level, so it is
+ // wrapped in a plain array - recurse into each one of them and merge the results
+ $return = [];
+ foreach ($array[$element] as $item) {
+ $test = $this->getArrayPath($item, $path);
+ If (gettype($test) == "array") {
+ $return = array_merge($return, $test);
+ }
+
+ }
+ return $return;
+ }
+ // Unique non-leaf node - simple recursion
+ return $this->getArrayPath($array[$element], $path);
+ }
+
+ /**
+ * @param string $response xml document to convert
+ * @return bool|array array representation of the xml if possible, false otherwise
+ */
+ protected function xmlStringToArray($response)
+ {
+ $cleanresponse = preg_replace('/(<\/?)(\w+):([^>]*>)/', '$1$2$3', $response);
+ try {
+ $xml = new SimpleXMLElement($cleanresponse);
+ } catch (Exception $e) {
+ $this->error = 'Could not parse reply as XML, got ' . get_class($e) . ': ' . $e->getMessage();
+ return false;
+ }
+ $array = json_decode(json_encode((array)$xml), true);
+ return $array;
+ }
+
+}
+
+/**
+ * Class BackendProperty describes a property a backend requires to define its functionality
+ */
+class BackendProperty {
+ public $property;
+ public $type;
+ public $default;
+ public function __construct($property, $type, $default = '')
+ {
+ $this->property = $property;
+ $this->type = $type;
+ $this->default = $default;
+ }
+
+ /**
+ * Initialize additional fields of this class that are only required
+ * for rendering the server configuration dialog.
+ *
+ * @param string $backendId target backend id
+ * @param mixed $current current value of this property.
+ */
+ public function initForRender($current = null) {
+ if (is_array($this->type)) {
+ $this->template = 'dropdown';
+ $this->select_list = [];
+ foreach ($this->type as $item) {
+ $this->select_list[] = [
+ 'option' => $item,
+ 'active' => $item == $current,
+ ];
+ }
+ } elseif ($this->type === 'bool') {
+ $this->template = $this->type;
+ } else {
+ $this->template = 'generic';
+ }
+ if ($this->type === 'string') {
+ $this->inputtype = 'text';
+ } elseif ($this->type === 'int') {
+ $this->inputtype = 'number';
+ } elseif ($this->type === 'password') {
+ $this->inputtype = Property::getPasswordFieldType();
+ }
+ $this->currentvalue = $current === null ? $this->default : $current;
+ }
+ public $inputtype;
+ public $template;
+ public $title;
+ public $helptext;
+ public $currentvalue;
+ public $select_list;
+ public $credentialsHtml;
+}
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php
new file mode 100644
index 00000000..a9756dda
--- /dev/null
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php
@@ -0,0 +1,133 @@
+<?php
+
+class CourseBackend_Davinci extends CourseBackend
+{
+
+ private $location;
+ private $verifyHostname = true;
+ private $verifyCert = true;
+
+ public function setCredentialsInternal($data)
+ {
+ if (empty($data['baseUrl'])) {
+ $this->error = "No url is given";
+ return false;
+ }
+ $location = preg_replace('#/+(davinciis\.dll)?\W*$#i', '', $data['baseUrl']);
+ $this->location = $location . "/DAVINCIIS.dll?";
+ $this->verifyHostname = $data['verifyHostname'];
+ $this->verifyCert = $data['verifyCert'];
+ return true;
+ }
+
+ public function checkConnection()
+ {
+ if (empty($this->location)) {
+ $this->error = "Credentials are not set";
+ } else {
+ $data = $this->fetchRoomRaw('someroomid123');
+ if ($data !== false && strpos($data, 'DAVINCI SERVER') === false) {
+ $this->error = "Unknown reply; this doesn't seem to be a DAVINCI server.";
+ }
+ }
+ return $this->error === false;
+ }
+
+ public function getCredentialDefinitions()
+ {
+ return [
+ new BackendProperty('baseUrl', 'string'),
+ new BackendProperty('verifyCert', 'bool', true),
+ new BackendProperty('verifyHostname', 'bool', true)
+ ];
+ }
+
+ public function getDisplayName()
+ {
+ return 'Davinci';
+ }
+
+ public function getCacheTime()
+ {
+ return 30 * 60;
+ }
+
+ public function getRefreshTime()
+ {
+ return 0;
+ }
+
+ /**
+ * @param $roomId string name of the room
+ * @return array|bool if successful the arrayrepresentation of the timetable
+ */
+ private function fetchRoomRaw($roomId)
+ {
+ $startDate = new DateTime('today 0:00');
+ $endDate = new DateTime('+7 days 0:00');
+ $url = $this->location . "content=xml&type=room&name=" . urlencode($roomId)
+ . "&startdate=" . $startDate->format('d.m.Y') . "&enddate=" . $endDate->format('d.m.Y');
+ $ch = curl_init();
+ $options = array(
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_SSL_VERIFYHOST => $this->verifyHostname ? 2 : 0,
+ CURLOPT_SSL_VERIFYPEER => $this->verifyCert ? 1 : 0,
+ CURLOPT_URL => $url,
+ );
+
+ curl_setopt_array($ch, $options);
+ $output = curl_exec($ch);
+ if ($output === false) {
+ $this->error = 'Curl error: ' . curl_error($ch);
+ return false;
+ } else {
+ $this->error = false;
+ ///Operation completed successfully
+ }
+ curl_close($ch);
+ return $output;
+
+ }
+
+ public function fetchSchedulesInternal($requestedRoomIds)
+ {
+ $schedules = [];
+ foreach ($requestedRoomIds as $roomId) {
+ $return = $this->fetchRoomRaw($roomId);
+ if ($return === false) {
+ continue;
+ }
+ $return = $this->xmlStringToArray($return);
+ if ($return === false) {
+ continue;
+ }
+ $lessons = $this->getArrayPath($return, '/Lessons/Lesson');
+ if ($lessons === false) {
+ $this->error = "Cannot find /Lessons/Lesson in XML";
+ continue;
+ }
+ $timetable = [];
+ foreach ($lessons as $lesson) {
+ if (!isset($lesson['Date']) || !isset($lesson['Start']) || !isset($lesson['Finish'])) {
+ $this->error = 'Lesson is missing Date, Start or Finish';
+ continue;
+ }
+ $date = $lesson['Date'];
+ $date = substr($date, 0, 4) . '-' . substr($date, 4, 2) . '-' . substr($date, 6, 2);
+ $start = $lesson['Start'];
+ $start = substr($start, 0, 2) . ':' . substr($start, 2, 2);
+ $end = $lesson['Finish'];
+ $end = substr($end, 0, 2) . ':' . substr($end, 2, 2);
+ $subject = isset($lesson['Subject']) ? $lesson['Subject'] : '???';
+ $timetable[] = array(
+ 'title' => $subject,
+ 'start' => $date . " " . $start . ':00',
+ 'end' => $date . " " . $end . ':00'
+ );
+ }
+ $schedules[$roomId] = $timetable;
+ }
+ return $schedules;
+ }
+}
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php
new file mode 100644
index 00000000..0e5d654e
--- /dev/null
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php
@@ -0,0 +1,113 @@
+<?php
+
+class CourseBackend_Dummy extends CourseBackend
+{
+ private $pw;
+
+ /**
+ * uses json to setCredentials, the json must follow the form given in
+ * getCredentials
+ *
+ * @param array $data with the credentials
+ * @param string $url address of the server
+ * @param int $serverId ID of the server
+ * @returns bool if the credentials were in the correct format
+ */
+ public function setCredentialsInternal($json)
+ {
+ $x = $json;
+ $this->pw = $x['password'];
+
+ if ($this->pw === "mfg") {
+ $this->error = false;
+ return true;
+ } else {
+ $this->error = "USE mfg as password!";
+ return false;
+ }
+ }
+
+ /**
+ * @return boolean true if the connection works, false otherwise
+ */
+ public function checkConnection()
+ {
+ if ($this->pw == "mfg") {
+ $this->error = false;
+ return true;
+ } else {
+ $this->error = "USE mfg as password!";
+ return false;
+ }
+ }
+
+ /**
+ * @returns array with parameter name as key and and an array with type, help text and mask as value
+ */
+ public function getCredentialDefinitions()
+ {
+ $options = ["opt1", "opt2", "opt3", "opt4", "opt5", "opt6", "opt7", "opt8"];
+ return [
+ new BackendProperty('username', 'string', 'default-user'),
+ new BackendProperty('password', 'password'),
+ new BackendProperty('integer', 'int', 7),
+ new BackendProperty('option', $options),
+ new BackendProperty('CheckTheBox', 'bool'),
+ new BackendProperty('CB2t', 'bool', true)
+ ];
+ }
+
+ /**
+ * @return string return display name of backend
+ */
+ public function getDisplayName()
+ {
+ return 'Dummy with array';
+ }
+
+ /**
+ * @return int desired caching time of results, in seconds. 0 = no caching
+ */
+ public function getCacheTime()
+ {
+ return 0;
+ }
+
+ /**
+ * @return int age after which timetables are no longer refreshed should be
+ * greater then CacheTime
+ */
+ public function getRefreshTime()
+ {
+ return 0;
+ }
+
+ /**
+ * Internal version of fetch, to be overridden by subclasses.
+ *
+ * @param $roomIds array with local ID as key and serverId as value
+ * @return array a recursive array that uses the roomID as key
+ * and has the schedule array as value. A shedule array contains an array in this format:
+ * ["start"=>'JJJJ-MM-DD HH:MM:SS',"end"=>'JJJJ-MM-DD HH:MM:SS',"title"=>string]
+ */
+ public function fetchSchedulesInternal($roomId)
+ {
+ $a = array();
+ foreach ($roomId as $id) {
+ $x['id'] = $id;
+ $calendar['title'] = "test exam";
+ $calendar['start'] = "2017-3-08 13:00:00";
+ $calendar['end'] = "2017-3-08 16:00:00";
+ $calarray = array();
+ $calarray[] = $calendar;
+ $x['calendar'] = $calarray;
+ $a[$id] = $calarray;
+ }
+
+
+ return $a;
+ }
+
+}
+
+?>
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php
new file mode 100644
index 00000000..65f52e6b
--- /dev/null
+++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php
@@ -0,0 +1,352 @@
+<?php
+
+class CourseBackend_HisInOne extends CourseBackend
+{
+ private $username = '';
+ private $password = '';
+ private $open = true;
+ private $location;
+ private $verifyHostname = true;
+ private $verifyCert = true;
+
+
+ public function setCredentialsInternal($data)
+ {
+ if (!$data['open']) {
+ // If not using OpenCourseService, require credentials
+ foreach (['username', 'password'] as $field) {
+ if (empty($data[$field])) {
+ $this->error = 'setCredentials: Missing field ' . $field;
+ return false;
+ }
+ }
+ }
+ if (empty($data['baseUrl'])) {
+ $this->error = "No url is given";
+ return false;
+ }
+
+ $this->error = false;
+ $this->username = $data['username'] . "\t" . $data['role'];
+ $this->password = $data['password'];
+ $this->open = $data['open'] !== 'CourseService';
+ $url = preg_replace('#(/+qisserver(/+services\d+(/+OpenCourseService)?)?)?\W*$#i', '', $data['baseUrl']);
+ if ($this->open) {
+ $this->location = $url . "/qisserver/services2/OpenCourseService";
+ } else {
+ $this->location = $url . "/qisserver/services2/CourseService";
+ }
+ $this->verifyHostname = $data['verifyHostname'];
+ $this->verifyCert = $data['verifyCert'];
+
+ return true;
+ }
+
+ public function getCredentialDefinitions()
+ {
+ return [
+ new BackendProperty('baseUrl', 'string'),
+ new BackendProperty('username', 'string'),
+ new BackendProperty('role', 'string'),
+ new BackendProperty('password', 'password'),
+ new BackendProperty('open', ['OpenCourseService', 'CourseService'], 'OpenCourseService'),
+ new BackendProperty('verifyCert', 'bool', true),
+ new BackendProperty('verifyHostname', 'bool', true)
+ ];
+ }
+
+ public function checkConnection()
+ {
+ if (empty($this->location)) {
+ $this->error = "Credentials are not set";
+ } else {
+ $this->findUnit(123456789, true);
+ }
+ return $this->error === false;
+ }
+
+ /**
+ * @param int $roomId his in one room id to get
+ * @param bool $connectionCheckOnly true will only check if no soapError is returned, return value will be empty
+ * @return array|bool if successful an array with the event ids that take place in the room
+ */
+ public function findUnit($roomId, $connectionCheckOnly = false)
+ {
+ $termYear = date('Y');
+ $termType1 = date('n');
+ if ($termType1 > 3 && $termType1 < 10) {
+ $termType = 2;
+ } elseif ($termType1 > 10) {
+ $termType = 1;
+ $termYear = $termYear + 1;
+ } else {
+ $termType = 1;
+ }
+ $doc = new DOMDocument('1.0', 'utf-8');
+ $doc->formatOutput = true;
+ $envelope = $doc->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 'SOAP-ENV:Envelope');
+ $doc->appendChild($envelope);
+ if ($this->open) {
+ $envelope->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns1', 'http://www.his.de/ws/OpenCourseService');
+ } else {
+ $envelope->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns1', 'http://www.his.de/ws/CourseService');
+ $header = $this->getHeader($doc);
+ $envelope->appendChild($header);
+ }
+ //Body of the request
+ $body = $doc->createElement('SOAP-ENV:Body');
+ $envelope->appendChild($body);
+ $findUnit = $doc->createElement('ns1:findUnit');
+ $body->appendChild($findUnit);
+ $findUnit->appendChild($doc->createElement('termYear', $termYear));
+ if ($termType1 != 3 && $termType1 != 10) {
+ $findUnit->appendChild($doc->createElement('termTypeValueId', $termType));
+ }
+ $findUnit->appendChild($doc->createElement('ns1:roomId', $roomId));
+
+ $soap_request = $doc->saveXML();
+ $response1 = $this->postToServer($soap_request, "findUnit");
+ if ($this->error !== false) {
+ return false;
+ }
+ $response2 = $this->xmlStringToArray($response1);
+ if (!is_array($response2)) {
+ if ($this->error === false) {
+ $this->error = 'Cannot convert XML response to array';
+ }
+ return false;
+ }
+ if (!isset($response2['soapenvBody'])) {
+ $this->error = 'findUnit(' . $roomId . '): Backend reply is missing element soapenvBody';
+ return false;
+ }
+ if (isset($response2['soapenvBody']['soapenvFault'])) {
+ $this->error = $response2['soapenvBody']['soapenvFault']['faultcode'] . " " . $response2['soapenvBody']['soapenvFault']['faultstring'];
+ return false;
+ }
+ // We only need to check if the connection is working (URL ok, credentials ok, ..) so bail out early
+ if ($connectionCheckOnly) {
+ return array();
+ }
+ if ($this->open) {
+ $path = '/soapenvBody/hisfindUnitResponse/hisunits/hisunit/hisid';
+ } else {
+ $path = '/soapenvBody/hisfindUnitResponse/hisunitIds/hisid';
+ }
+ $id = $this->getArrayPath($response2, $path);
+ if ($id === false) {
+ $this->error = 'Cannot find ' . $path;
+ }
+ return $id;
+ }
+
+ /**
+ * @param $doc DOMDocument
+ * @return DOMElement
+ */
+ private function getHeader($doc)
+ {
+ $header = $doc->createElement('SOAP-ENV:Header');
+ $security = $doc->createElementNS('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd', 'ns2:Security');
+ $mustunderstand = $doc->createAttribute('SOAP-ENV:mustUnderstand');
+ $mustunderstand->value = 1;
+ $security->appendChild($mustunderstand);
+ $header->appendChild($security);
+ $token = $doc->createElement('ns2:UsernameToken');
+ $security->appendChild($token);
+ $user = $doc->createElement('ns2:Username', $this->username);
+ $token->appendChild($user);
+ $pass = $doc->createElement('ns2:Password', $this->password);
+ $type = $doc->createAttribute('Type');
+ $type->value = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText';
+ $pass->appendChild($type);
+ $token->appendChild($pass);
+ return $header;
+ }
+
+ /**
+ * @param $request string with xml SOAP request
+ * @param $action string with the name of the SOAP action
+ * @return bool|string if successful the answer xml from the SOAP server
+ */
+ private function postToServer($request, $action)
+ {
+ $header = array(
+ "Content-type: text/xml;charset=\"utf-8\"",
+ "SOAPAction: \"" . $action . "\"",
+ "Content-length: " . strlen($request),
+ );
+
+ $soap_do = curl_init();
+
+ $options = array(
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_SSL_VERIFYHOST => $this->verifyHostname ? 2 : 0,
+ CURLOPT_SSL_VERIFYPEER => $this->verifyCert ? 1 : 0,
+ CURLOPT_URL => $this->location,
+ CURLOPT_POSTFIELDS => $request,
+ CURLOPT_HTTPHEADER => $header,
+ );
+
+ curl_setopt_array($soap_do, $options);
+
+ $output = curl_exec($soap_do);
+
+ if ($output === false) {
+ $this->error = 'Curl error: ' . curl_error($soap_do);
+ } else {
+ $this->error = false;
+ ///Operation completed successfully
+ }
+ curl_close($soap_do);
+ return $output;
+ }
+
+ public function getCacheTime()
+ {
+ return 30 * 60;
+ }
+
+
+ public function getRefreshTime()
+ {
+ return 60 * 60;
+ }
+
+
+ public function getDisplayName()
+ {
+ return "HisInOne";
+ }
+
+ public function fetchSchedulesInternal($requestedRoomIds)
+ {
+ if (empty($requestedRoomIds)) {
+ return array();
+ }
+ $tTables = [];
+ //get all eventIDs in a given room
+ $eventIds = [];
+ foreach ($requestedRoomIds as $roomId) {
+ $roomEventIds = $this->findUnit($roomId);
+ if ($roomEventIds === false) {
+ error_log($this->error);
+ $this->error = false;
+ // TODO: Error gets swallowed
+ continue;
+ }
+ $tTables[$roomId] = [];
+ $eventIds = array_merge($eventIds, $roomEventIds);
+ }
+ $eventIds = array_unique($eventIds);
+ if (empty($eventIds)) {
+ return $tTables;
+ }
+ $eventDetails = [];
+ //get all information on each event
+ foreach ($eventIds as $eventId) {
+ $event = $this->readUnit(intval($eventId));
+ if ($event === false) {
+ error_log($this->error);
+ $this->error = false;
+ // TODO: Error gets swallowed
+ continue;
+ }
+ $eventDetails = array_merge($eventDetails, $event);
+ }
+ $currentWeek = $this->getCurrentWeekDates();
+ foreach ($eventDetails as $event) {
+ foreach (array('/hisdefaulttext',
+ '/hisshorttext',
+ '/hisshortcomment',
+ '/hisplanelements/hisplanelement/hisdefaulttext') as $path) {
+ $name = $this->getArrayPath($event, $path);
+ if (!empty($name) && !empty($name[0]))
+ break;
+ $name = false;
+ }
+ if ($name === false) {
+ $name = ['???'];
+ }
+ $unitPlannedDates = $this->getArrayPath($event,
+ '/hisplanelements/hisplanelement/hisplannedDates/hisplannedDate/hisindividualDates/hisindividualDate');
+ if ($unitPlannedDates === false) {
+ $this->error = 'Cannot find ./hisplanelements/hisplanelement/hisplannedDates/hisplannedDate/hisindividualDates/hisindividualDate';
+ return false;
+ }
+ foreach ($unitPlannedDates as $plannedDate) {
+ $eventRoomId = $this->getArrayPath($plannedDate, '/hisroomId')[0];
+ $eventDate = $this->getArrayPath($plannedDate, '/hisexecutiondate')[0];
+ if (in_array($eventRoomId, $requestedRoomIds) && in_array($eventDate, $currentWeek)) {
+ $startTime = $this->getArrayPath($plannedDate, '/hisstarttime')[0];
+ $endTime = $this->getArrayPath($plannedDate, '/hisendtime')[0];
+ $tTables[$eventRoomId][] = array(
+ 'title' => $name[0],
+ 'start' => $eventDate . " " . $startTime,
+ 'end' => $eventDate . " " . $endTime
+ );
+ }
+ }
+ }
+ return $tTables;
+ }
+
+
+ /**
+ * @param $unit int ID of the subject in HisInOne database
+ * @return bool|array false if there was an error otherwise an array with the information about the subject
+ */
+ public function readUnit($unit)
+ {
+ $doc = new DOMDocument('1.0', 'utf-8');
+ $doc->formatOutput = true;
+ $envelope = $doc->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 'SOAP-ENV:Envelope');
+ $doc->appendChild($envelope);
+ if ($this->open) {
+ $envelope->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns1', 'http://www.his.de/ws/OpenCourseService');
+ } else {
+ $envelope->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns1', 'http://www.his.de/ws/CourseService');
+ $header = $this->getHeader($doc);
+ $envelope->appendChild($header);
+ }
+ //body of the request
+ $body = $doc->createElement('SOAP-ENV:Body');
+ $envelope->appendChild($body);
+ $readUnit = $doc->createElement('ns1:readUnit');
+ $body->appendChild($readUnit);
+ $readUnit->appendChild($doc->createElement('ns1:unitId', $unit));
+
+ $soap_request = $doc->saveXML();
+ $response1 = $this->postToServer($soap_request, "readUnit");
+ if ($response1 === false) {
+ return false;
+ }
+ $response2 = $this->xmlStringToArray($response1);
+ if ($response2 === false)
+ return false;
+ if (!isset($response2['soapenvBody'])) {
+ $this->error = 'findUnit(' . $unit . '): Backend reply is missing element soapenvBody';
+ return false;
+ }
+ if (isset($response2['soapenvBody']['soapenvFault'])) {
+ $this->error = 'SOAP-Fault' . $response2['soapenvBody']['soapenvFault']['faultcode'] . " " . $response2['soapenvBody']['soapenvFault']['faultstring'];
+ return false;
+ }
+ return $this->getArrayPath($response2, '/soapenvBody/hisreadUnitResponse/hisunit');
+ }
+
+ /**
+ * @return array with days of the current week in datetime format
+ */
+ private function getCurrentWeekDates()
+ {
+ $returnValue = array();
+ $startDate = time();
+ for ($i = 0; $i <= 7; $i++) {
+ $returnValue[] = date('Y-m-d', strtotime("+{$i} day 12:00", $startDate));
+ }
+ return $returnValue;
+ }
+
+}
diff --git a/modules-available/locationinfo/inc/locationinfo.inc.php b/modules-available/locationinfo/inc/locationinfo.inc.php
new file mode 100644
index 00000000..6b13ad9a
--- /dev/null
+++ b/modules-available/locationinfo/inc/locationinfo.inc.php
@@ -0,0 +1,63 @@
+<?php
+
+class LocationInfo
+{
+
+ /**
+ * Gets the pc data and returns it's state.
+ *
+ * @param array $pc The pc data from the db. Array('logintime' =>, 'lastseen' =>, 'lastboot' =>)
+ * @return int pc state
+ */
+ public static function getPcState($pc)
+ {
+ /* pcState:
+ * [0] = IDLE (NOT IN USE)
+ * [1] = OCCUPIED (IN USE)
+ * [2] = OFF
+ * [3] = 10 days offline (BROKEN?)
+ */
+ // TODO USE STATE NAME instead of numbers
+
+ $logintime = (int)$pc['logintime'];
+ $lastseen = (int)$pc['lastseen'];
+ $lastboot = (int)$pc['lastboot'];
+ $NOW = time();
+
+ if ($NOW - $lastseen > 14 * 86400) {
+ return "BROKEN";
+ } elseif (($NOW - $lastseen > 610) || $lastboot === 0) {
+ return "OFF";
+ } elseif ($logintime === 0) {
+ return "IDLE";
+ } elseif ($logintime > 0) {
+ return "OCCUPIED";
+ }
+ return -1;
+ }
+
+ /**
+ * Set current error message of given server. Pass null or false to clear.
+ *
+ * @param int $serverId id of server
+ * @param string $message error message to set, null or false clears error.
+ */
+ public static function setServerError($serverId, $message)
+ {
+ if ($message === false || $message === null) {
+ Database::exec("UPDATE `locationinfo_coursebackend` SET error = NULL
+ WHERE serverid = :id", array('id' => $serverId));
+ } else {
+ if (empty($message)) {
+ $message = '<empty error message>';
+ }
+ $error = json_encode(array(
+ 'timestamp' => time(),
+ 'error' => (string)$message
+ ));
+ Database::exec("UPDATE `locationinfo_coursebackend` SET error = :error
+ WHERE serverid = :id", array('id' => $serverId, 'error' => $error));
+ }
+ }
+
+}
diff --git a/modules-available/locationinfo/install.inc.php b/modules-available/locationinfo/install.inc.php
new file mode 100644
index 00000000..9cab652a
--- /dev/null
+++ b/modules-available/locationinfo/install.inc.php
@@ -0,0 +1,34 @@
+<?php
+
+$res = array();
+
+// TODO: serverid NULL, constraint to serverlist on delete set NULL
+$res[] = tableCreate('locationinfo_locationconfig', '
+ `locationid` INT(10) UNSIGNED NOT NULL,
+ `serverid` INT(11) NOT NULL,
+ `serverlocationid` VARCHAR(150),
+ `hidden` BOOLEAN NOT NULL DEFAULT 0,
+ `openingtime` BLOB,
+ `config` BLOB,
+ `calendar` BLOB,
+ `lastcalendarupdate` INT(11) NOT NULL DEFAULT 0,
+ PRIMARY KEY (`locationid`)
+');
+
+// TODO: KEY `servername` (`servername`)
+$res[] = tableCreate('locationinfo_coursebackend', '
+ `serverid` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `servername` VARCHAR(200) NOT NULL,
+ `servertype` VARCHAR(100) NOT NULL,
+ `credentials` BLOB,
+ `error` VARCHAR(1000),
+ PRIMARY KEY (`serverid`)
+');
+
+// Create response for browser
+
+if (in_array(UPDATE_DONE, $res)) {
+ finalResponse(UPDATE_DONE, 'Tables created successfully');
+}
+
+finalResponse(UPDATE_NOOP, 'Everything already up to date');
diff --git a/modules-available/locationinfo/lang/de/backend-davinci.json b/modules-available/locationinfo/lang/de/backend-davinci.json
new file mode 100644
index 00000000..edf155bf
--- /dev/null
+++ b/modules-available/locationinfo/lang/de/backend-davinci.json
@@ -0,0 +1,8 @@
+{
+ "baseUrl": "Basis-URL",
+ "baseUrl_helptext": "URL zur Davinci-Installation",
+ "verifyCert": "Zertifikat pr\u00fcfen",
+ "verifyCert_helptext": "Wenn das Zertifikat abgelaufen ist, oder von keiner bekannten CA ausgestellt wurde, wird die Verbindung abgelehnt.",
+ "verifyHostname": "Hostnamen pr\u00fcfen",
+ "verifyHostname_helptext": "Der im Zertifikat angegebene Hostname muss mit dem Hostnamen aus der URL \u00fcbereinstimmen, sonst wird die Verbindung abgelehnt."
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/de/backend-dummy.json b/modules-available/locationinfo/lang/de/backend-dummy.json
new file mode 100644
index 00000000..cd558860
--- /dev/null
+++ b/modules-available/locationinfo/lang/de/backend-dummy.json
@@ -0,0 +1,14 @@
+{
+ "CB2t": "DBzwo",
+ "CB2t_helptext": "CBzwo Help",
+ "CheckTheBox": "CheckBox",
+ "CheckTheBox_helptext": "Die checkbox ist ein wenig nutzlos",
+ "integer": "Zahl",
+ "integer_helptext": "Ein Zahlen felde?!",
+ "option": "Irgendein Array",
+ "option_helptext": "LALALA Hilfs- Text bla bla",
+ "password": "Passwort 1",
+ "password_helptext": "Bla passwort bla bla",
+ "username": "Benutzer",
+ "username_helptext": "Das ist halt ein Username feld.."
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/de/backend-hisinone.json b/modules-available/locationinfo/lang/de/backend-hisinone.json
new file mode 100644
index 00000000..6ea1a933
--- /dev/null
+++ b/modules-available/locationinfo/lang/de/backend-hisinone.json
@@ -0,0 +1,16 @@
+{
+ "baseUrl": "Basis-URL",
+ "baseUrl_helptext": "URL zur HisInOne-Installation",
+ "open": "Service",
+ "open_helptext": "Legt den zu verwendenden Web Service fest. OpenCourseService bietet anonymisierte Belegungspl\u00e4ne und erfordert keine Authentifizierung und ist i.d.R. bevorzugt.",
+ "password": "Passwort",
+ "password_helptext": "Das Passwort, das in HisInOne verwendet wird.",
+ "role": "Rolle",
+ "role_helptext": "Die Rolle die der Nutzername in HisInOne verwendet.",
+ "username": "Nutzername",
+ "username_helptext": "Der Nutzername, der in HisInOne verwendet wird.",
+ "verifyCert": "Zertifikat pr\u00fcfen",
+ "verifyCert_helptext": "Wenn das Zertifikat abgelaufen ist, oder von keiner bekannten CA ausgestellt wurde, wird die Verbindung abgelehnt.",
+ "verifyHostname": "Hostnamen pr\u00fcfen",
+ "verifyHostname_helptext": "Der im Zertifikat angegebene Hostname muss mit dem Hostnamen aus der URL \u00fcbereinstimmen, sonst wird die Verbindung abgelehnt."
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/de/messages.json b/modules-available/locationinfo/lang/de/messages.json
new file mode 100644
index 00000000..52a48a02
--- /dev/null
+++ b/modules-available/locationinfo/lang/de/messages.json
@@ -0,0 +1,8 @@
+{
+ "no-days-selected": "Es wurden keine Tage ausgewählt.",
+ "added-x-entries": "Eintr\u00e4ge hinzugef\u00fcgt: {{0}}",
+ "deleted-x-entries": "Eintr\u00e4ge gelöscht: {{0}}",
+ "openingtime-updated": "Öffnungszeiten aktualisiert.",
+ "config-saved": "Einstellungen erfolgreich gespeichert.",
+ "auth-failed": "[{{0}}] {{1}} Error: {{2}}"
+}
diff --git a/modules-available/locationinfo/lang/de/module.json b/modules-available/locationinfo/lang/de/module.json
index 166909c3..2fd14353 100644
--- a/modules-available/locationinfo/lang/de/module.json
+++ b/modules-available/locationinfo/lang/de/module.json
@@ -1,4 +1,3 @@
{
- "module_name": "Mein erstes Modul",
- "page_title": "Mein erster Seitentitel"
-} \ No newline at end of file
+ "module_name": "Infoscreen"
+}
diff --git a/modules-available/locationinfo/lang/de/template-tags.json b/modules-available/locationinfo/lang/de/template-tags.json
index ce98ce38..ed92d240 100644
--- a/modules-available/locationinfo/lang/de/template-tags.json
+++ b/modules-available/locationinfo/lang/de/template-tags.json
@@ -1,3 +1,74 @@
{
- "lang_hello": "Hallo"
+ "lang_addServer": "Server",
+ "lang_autoScale": "Auto Tage",
+ "lang_autoscaleTooltip": "Berechnet sich die optimale anzahl an Tagen, anhand der Bildschirmbreite, die der Kalender anzeigt.",
+ "lang_buildingTable": "Geb\u00e4ude \/ Raum Liste",
+ "lang_calendar": "Kalender",
+ "lang_calupdateTooltip": "Zeit nachdem der Kalender geupdated wird (in minuten)",
+ "lang_checkConnection": "Verbindung pr\u00fcfen",
+ "lang_closingTime": "Schlie\u00dfungszeit",
+ "lang_config": "Einstellungen",
+ "lang_configupdateTooltip": "Zeit nachder die Einstellungen geupdated werden (in minuten)",
+ "lang_credentials": "Anmeldung",
+ "lang_customUrl": "Benutzerdefinierter URL",
+ "lang_customUrlTooltip": "Dieser URL \u00fcberscheibt die Einstellungen von dem Raum.",
+ "lang_day": "Tag",
+ "lang_daysToShow": "Tage",
+ "lang_daysToShowTooltip": "Legt die gew\u00fcnschte Anzahl an Tagen im Kalender fest.",
+ "lang_deleteConfirmation": "Bist du sicher?",
+ "lang_display": "Anzeige",
+ "lang_ecoMode": "E-Ink modus",
+ "lang_ecoTooltip": "Anstelle der Farb-basierten PC-Status Bilder, werden Symbol-basierte PC Bilder verwendet.",
+ "lang_entryName": "Name",
+ "lang_expertMode": "Experten Modus",
+ "lang_general": "Allgemein",
+ "lang_language": "Sprache",
+ "lang_languageTooltip": "Die Sprache, welche vom Frontend benutzt wird.",
+ "lang_locationIsHidden": "Versteckt",
+ "lang_locationIsHidden_title": "Wenn aktiv, dann liefert die API keine Informationen \u00fcber diesen Raum.",
+ "lang_locationName": "Name",
+ "lang_locationSettings": "Einstellungen",
+ "lang_mainHeader": "Infoscreen",
+ "lang_mode": "Modus",
+ "lang_mode1": "Kalender & Raum",
+ "lang_mode2": "Kalender",
+ "lang_mode3": "Raum",
+ "lang_mode4": "Wechselnd",
+ "lang_modeTooltip": "Die Anzeigemodi, welche das frontend unterst\u00fctzt.",
+ "lang_monTilFr": "Montag - Freitag",
+ "lang_nameTooltip": "Legt den Namen des Servers fest.",
+ "lang_noServer": "<Kein Server>",
+ "lang_openingTime": "\u00d6ffnungszeiten",
+ "lang_room": "Raum",
+ "lang_roomId": "Raum ID",
+ "lang_roomIdTooltip": "Die ID vom Raum, welche der Server braucht, um die Kalender Daten abzurufen.",
+ "lang_roomupdateTooltip": "Zeit nachder die Pcs geupdated werden (in sekunden)",
+ "lang_rotation": "Rotation",
+ "lang_rotation0": "0\u00b0",
+ "lang_rotation1": "90\u00b0 \u27f2",
+ "lang_rotation2": "180\u00b0",
+ "lang_rotation3": "90\u00b0 \u27f3",
+ "lang_rotationTooltip": "Rotiert den Raum.",
+ "lang_saturday": "Samstag",
+ "lang_scale": "Kalender breite",
+ "lang_scaleTooltip": "[10-90] Legt die Kalenderbreite fest. (In Prozent)",
+ "lang_sec": "sec",
+ "lang_server": "Server",
+ "lang_serverTable": "Server Liste",
+ "lang_serverTooltip": "Legt fest, von welchem Server die Kalender Daten bezogen werden.",
+ "lang_serverType": "Typ",
+ "lang_shortFriday": "Fr",
+ "lang_shortMonday": "Mo",
+ "lang_shortSaturday": "Sa",
+ "lang_shortSunday": "So",
+ "lang_shortThursday": "Do",
+ "lang_shortTuesday": "Di",
+ "lang_shortWednesday": "Mi",
+ "lang_sunday": "Sonntag",
+ "lang_switchTime": "Wechselintervall",
+ "lang_switchTimeTooltip": "[1-120] Legt die Zeit fest, die vergeht bis ein wechsel erfolgt (in sekunden)",
+ "lang_typeTooltip": "Legt fest um welchen Typ von Server es sich handelt.",
+ "lang_updateRates": "Anfragraten",
+ "lang_vertical": "Vertikaler Modus",
+ "lang_verticalTooltip": "Legt fest, ob der Kalender und der Raum \u00fcbereinander angezeigt werden soll."
} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/en/backend-davinci.json b/modules-available/locationinfo/lang/en/backend-davinci.json
new file mode 100644
index 00000000..e85abebe
--- /dev/null
+++ b/modules-available/locationinfo/lang/en/backend-davinci.json
@@ -0,0 +1,8 @@
+{
+ "baseUrl": "Base URL",
+ "baseUrl_helptext": "URL to Davinci installation",
+ "verifyCert": "Verify certificate",
+ "verifyCert_helptext": "If the certificate expired or was not signed by a known CA, the connection will be aborted.",
+ "verifyHostname": "Verify host name",
+ "verifyHostname_helptext": "The certificate's host name must match the host name given in the URL, otherwise the connection will be aborted."
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/en/backend-dummy.json b/modules-available/locationinfo/lang/en/backend-dummy.json
new file mode 100644
index 00000000..07bf804c
--- /dev/null
+++ b/modules-available/locationinfo/lang/en/backend-dummy.json
@@ -0,0 +1,14 @@
+{
+ "CB2t": "sausages",
+ "CB2t_helptext": "cb2",
+ "CheckTheBox": "CheckBox",
+ "CheckTheBox_helptext": "Check this if you are bored",
+ "integer": "A integer value",
+ "integer_helptext": "What is this for?!",
+ "option": "Option Array",
+ "option_helptext": "LALALA OPtion title bla bla",
+ "password": "Password 1",
+ "password_helptext": "Password 1 title alalala :D",
+ "username": "Username",
+ "username_helptext": "A fkn username field"
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/en/backend-hisinone.json b/modules-available/locationinfo/lang/en/backend-hisinone.json
new file mode 100644
index 00000000..0ff12c18
--- /dev/null
+++ b/modules-available/locationinfo/lang/en/backend-hisinone.json
@@ -0,0 +1,16 @@
+{
+ "baseUrl": "Base URL",
+ "baseUrl_helptext": "URL to HisInOne installation",
+ "open": "Service",
+ "open_helptext": "Sets the Web Service to use. OpenCourseService is anonymized and doesn't require authentication, so it's usually the preferred way.",
+ "password": "Password",
+ "password_helptext": "Account password. Only required if using CourseService",
+ "role": "Role",
+ "role_helptext": "Role of the user accessing the CourseService.",
+ "username": "Username",
+ "username_helptext": "Authenticating user (only required for CourseService).",
+ "verifyCert": "Verify certificate",
+ "verifyCert_helptext": "Wenn das Zertifikat abgelaufen ist, oder von keiner bekannten CA ausgestellt wurde, wird die Verbindung abgelehnt.",
+ "verifyHostname": "Verify host name",
+ "verifyHostname_helptext": "Der im Zertifikat angegebene Hostname muss mit dem Hostnamen aus der URL \u00fcbereinstimmen, sonst wird die Verbindung abgelehnt."
+} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/en/messages.json b/modules-available/locationinfo/lang/en/messages.json
new file mode 100644
index 00000000..b85b56cd
--- /dev/null
+++ b/modules-available/locationinfo/lang/en/messages.json
@@ -0,0 +1,8 @@
+{
+ "no-days-selected": "No days selected.",
+ "added-x-entries": "Entries added: {{0}}",
+ "deleted-x-entries": "Entries deleted: {{0}}",
+ "openingtime-updated": "Openingtime updated.",
+ "config-saved": "Config successfully saved.",
+ "auth-failed": "[{{0}}] {{1}} Error: {{2}}"
+}
diff --git a/modules-available/locationinfo/lang/en/module.json b/modules-available/locationinfo/lang/en/module.json
index b2bcbb0c..2fd14353 100644
--- a/modules-available/locationinfo/lang/en/module.json
+++ b/modules-available/locationinfo/lang/en/module.json
@@ -1,4 +1,3 @@
{
- "module_name": "My first module",
- "page_title": "My first page title"
-} \ No newline at end of file
+ "module_name": "Infoscreen"
+}
diff --git a/modules-available/locationinfo/lang/en/template-tags.json b/modules-available/locationinfo/lang/en/template-tags.json
index c30739e5..c43f7608 100644
--- a/modules-available/locationinfo/lang/en/template-tags.json
+++ b/modules-available/locationinfo/lang/en/template-tags.json
@@ -1,3 +1,74 @@
{
- "lang_hello": "Hello"
+ "lang_addServer": "Server",
+ "lang_autoScale": "Auto Days",
+ "lang_autoscaleTooltip": "Calculates the optimum amount of days to show from the display width.",
+ "lang_buildingTable": "Building \/ Room List",
+ "lang_calendar": "Calendar",
+ "lang_calupdateTooltip": "Time the calender querys for updates (in minutes)",
+ "lang_checkConnection": "Check connection",
+ "lang_closingTime": "Closing time",
+ "lang_config": "Config",
+ "lang_configupdateTooltip": "Time interval the config gets updated (in minutes)",
+ "lang_credentials": "Login",
+ "lang_customUrl": "Custom URL",
+ "lang_customUrlTooltip": "This URL will override the config settings from the room.",
+ "lang_day": "Day",
+ "lang_daysToShow": "Days",
+ "lang_daysToShowTooltip": "Defines the amount of days to show in the calendar",
+ "lang_deleteConfirmation": "Are you sure?",
+ "lang_display": "Display",
+ "lang_ecoMode": "E-Ink mode",
+ "lang_ecoTooltip": "Symbolic based pc state pictures are used instead of the colour base ones.",
+ "lang_entryName": "Name",
+ "lang_expertMode": "Expert mode",
+ "lang_general": "General",
+ "lang_language": "Language",
+ "lang_languageTooltip": "The language the frontend uses.",
+ "lang_locationIsHidden": "Hidden",
+ "lang_locationIsHidden_title": "If checked the API doesn't return information about the room.",
+ "lang_locationName": "Name",
+ "lang_locationSettings": "Settings",
+ "lang_mainHeader": "Infoscreen",
+ "lang_mode": "Mode",
+ "lang_mode1": "Calendar & Room",
+ "lang_mode2": "Calendar",
+ "lang_mode3": "Room",
+ "lang_mode4": "Switching",
+ "lang_modeTooltip": "The display modes the frontend supports.",
+ "lang_monTilFr": "Monday - Friday",
+ "lang_nameTooltip": "Defines the name of the server.",
+ "lang_noServer": "<no server>",
+ "lang_openingTime": "Opening times",
+ "lang_room": "Room",
+ "lang_roomId": "Room ID",
+ "lang_roomIdTooltip": "The ID of the room the server needs, for querying the calendar data.",
+ "lang_roomupdateTooltip": "Time the PCs in the room gets updated (in seconds)",
+ "lang_rotation": "Rotation",
+ "lang_rotation0": "0\u00b0",
+ "lang_rotation1": "90\u00b0 \u27f2",
+ "lang_rotation2": "180\u00b0",
+ "lang_rotation3": "90\u00b0 \u27f3",
+ "lang_rotationTooltip": "Rotates the room.",
+ "lang_saturday": "Saturday",
+ "lang_scale": "Calendar width",
+ "lang_scaleTooltip": "[10-90] Defines the calendar width. (in percent)",
+ "lang_sec": "sec",
+ "lang_server": "Server",
+ "lang_serverTable": "Server List",
+ "lang_serverTooltip": "Defines from which server the room queries the calendar data.",
+ "lang_serverType": "Type",
+ "lang_shortFriday": "Fri",
+ "lang_shortMonday": "Mon",
+ "lang_shortSaturday": "Sat",
+ "lang_shortSunday": "Sun",
+ "lang_shortThursday": "Thu",
+ "lang_shortTuesday": "Tue",
+ "lang_shortWednesday": "Wed",
+ "lang_sunday": "Sunday",
+ "lang_switchTime": "Switchtime",
+ "lang_switchTimeTooltip": "[1-120] Sets the time between switching (in seconds)",
+ "lang_typeTooltip": "Defines on which type of server you want to connect to.",
+ "lang_updateRates": "Update rates",
+ "lang_vertical": "Vertical mode",
+ "lang_verticalTooltip": "Defines whether the room and calendar are shown above each other."
} \ No newline at end of file
diff --git a/modules-available/locationinfo/lang/pt/template-tags.json b/modules-available/locationinfo/lang/pt/template-tags.json
deleted file mode 100644
index e7981844..00000000
--- a/modules-available/locationinfo/lang/pt/template-tags.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "lang_hello": "Olá"
-} \ No newline at end of file
diff --git a/modules-available/locationinfo/page.inc.php b/modules-available/locationinfo/page.inc.php
index ff73107a..eb50193e 100644
--- a/modules-available/locationinfo/page.inc.php
+++ b/modules-available/locationinfo/page.inc.php
@@ -3,17 +3,38 @@
class Page_LocationInfo extends Page
{
+ private $action;
+
/**
* Called before any page rendering happens - early hook to check parameters etc.
*/
protected function doPreprocess()
{
User::load();
-
if (!User::isLoggedIn()) {
Message::addError('main.no-permission');
Util::redirect('?do=Main'); // does not return
}
+
+ $this->action = Request::post('action');
+ if ($this->action === 'updateOpeningTimeExpert') {
+ $this->updateOpeningTimeExpert();
+ } elseif ($this->action === 'updateOpeningTimeEasy') {
+ $this->updateOpeningTimeEasy();
+ } elseif ($this->action === 'updateConfig') {
+ $this->updateLocationConfig();
+ } elseif ($this->action === 'deleteServer') {
+ $this->deleteServer();
+ } elseif ($this->action === 'checkConnection') {
+ $this->checkConnection(Request::post('serverid', 0, 'int'));
+ } elseif ($this->action === 'updateServerSettings') {
+ $this->updateServerSettings();
+ } elseif (Request::isPost()) {
+ Messages::addWarning('main.invalid-action', $this->action);
+ }
+ if (Request::isPost()) {
+ Util::redirect('?do=locationinfo');
+ }
}
/**
@@ -21,9 +42,549 @@ class Page_LocationInfo extends Page
*/
protected function doRender()
{
- Render::addTemplate('_page', array(
- 'foo' => 'bar',
- 'now' => date('d.m.Y H:i:s')
+ $this->getInfoScreenTable();
+ }
+
+ /**
+ * Deletes the server from the db.
+ */
+ private function deleteServer()
+ {
+ $id = Request::post('serverid', false, 'int');
+ if ($id === false) {
+ Messages::addError('server-id-missing');
+ return;
+ }
+ Database::exec("DELETE FROM `locationinfo_coursebackend` WHERE serverid=:id", array('id' => $id));
+ }
+
+ /**
+ * Updated the config in the db.
+ */
+ private function updateLocationConfig()
+ {
+ $result = array();
+
+ $locationid = Request::post('id', 0, 'int');
+ if ($locationid <= 0) {
+ Message::addError('location.invalid-location-id', $locationid);
+ Util::redirect('?do=locationinfo');
+ }
+ $result['language'] = Request::post('language', 'en', 'string');
+ $result['mode'] = Request::post('mode', 1, 'int');
+ $result['vertical'] = Request::post('vertical', false, 'bool');
+ $result['eco'] = Request::post('eco', false, 'bool');
+ $result['scaledaysauto'] = Request::post('scaledaysauto', false, 'bool');
+ $result['daystoshow'] = Request::post('daystoshow', 7, 'int');
+ $result['rotation'] = Request::post('rotation', 0, 'int');
+ $result['scale'] = Request::post('scale', 50, 'int');
+ $result['switchtime'] = Request::post('switchtime', 20, 'int');
+ $result['calupdate'] = Request::post('calupdate', 120, 'int');
+ $result['roomupdate'] = Request::post('roomupdate', 30, 'int');
+ $result['configupdate'] = Request::post('configupdate', 180, 'int');
+ if ($result['roomupdate'] < 30) {
+ $result['roomupdate'] = 30;
+ }
+ if ($result['calupdate'] < 120) {
+ $result['calupdate'] = 120;
+ }
+ $serverid = Request::post('serverid', 0, 'int');
+ $serverlocationid = Request::post('serverlocationid', '', 'string');
+
+ Database::exec("INSERT INTO `locationinfo_locationconfig` (locationid, serverid, serverlocationid, config, lastcalendarupdate)
+ VALUES (:id, :serverid, :serverlocationid, :config, 0)
+ ON DUPLICATE KEY UPDATE config = VALUES(config), serverid = VALUES(serverid),
+ serverlocationid = VALUES(serverlocationid), lastcalendarupdate = 0", array(
+ 'id' => $locationid,
+ 'config' => json_encode($result),
+ 'serverid' => $serverid,
+ 'serverlocationid' => $serverlocationid,
+ ));
+
+ Message::addSuccess('config-saved');
+ Util::redirect('?do=locationinfo');
+ }
+
+ /**
+ * Updates the server settings in the db.
+ */
+ private function updateServerSettings()
+ {
+ $serverid = Request::post('id', -1, 'int');
+ $servername = Request::post('name', 'unnamed', 'string');
+ $servertype = Request::post('type', '', 'string');
+ $backend = CourseBackend::getInstance($servertype);
+
+ if ($backend === false) {
+ Messages::addError('invalid-backend-type', $servertype);
+ Util::redirect('?do=locationinfo');
+ }
+
+ $tmptypeArray = $backend->getCredentialDefinitions();
+
+ $credentialsJson = array();
+ foreach ($tmptypeArray as $cred) {
+ $credentialsJson[$cred->property] = Request::post('prop-' . $cred->property);
+ }
+ $params = array(
+ 'name' => $servername,
+ 'type' => $servertype,
+ 'credentials' => json_encode($credentialsJson)
+ );
+ if ($serverid === 0) {
+ Database::exec('INSERT INTO `locationinfo_coursebackend` (servername, servertype, credentials)
+ VALUES (:name, :type, :credentials)', $params);
+ $this->checkConnection(Database::lastInsertId());
+ } else {
+ $params['id'] = $serverid;
+ Database::exec('UPDATE `locationinfo_coursebackend`
+ SET servername = :name, servertype = :type, credentials = :credentials
+ WHERE serverid = :id', $params);
+ $this->checkConnection($serverid);
+ }
+ }
+
+ /**
+ * Updates the opening time in the db from the expert mode.
+ */
+ private function updateOpeningTimeExpert()
+ {
+ $days = Request::post('days', array(), 'array');
+ $locationid = Request::post('id', 0, 'int');
+ $openingtime = Request::post('openingtime', array(), 'array');
+ $closingtime = Request::post('closingtime', array(), 'array');
+ $easyMode = Request::post('easyMode', false, 'bool');
+ $delete = Request::post('delete', array(), 'array');
+ $dontadd = Request::post('dontadd', array(), 'array');
+ $count = 0;
+ $result = array();
+ $resulttmp = array();
+ $deleteCounter = 0;
+
+ if (!$easyMode) {
+ $resulttmp = Database::queryFirst("SELECT openingtime FROM `locationinfo_locationconfig` WHERE locationid = :id", array('id' => $locationid));
+ if ($resulttmp !== false) {
+ $resulttmp = json_decode($resulttmp['openingtime'], true);
+ }
+ if (!is_array($resulttmp)) {
+ $resulttmp = array();
+ }
+
+ $index = 0;
+
+ foreach ($resulttmp as $day) {
+ $skip = false;
+
+ foreach ($delete as $del) {
+ if ($del == $index) {
+ $skip = true;
+ break;
+ }
+ }
+ if ($skip) {
+ $index++;
+ $deleteCounter++;
+ continue;
+ }
+
+ $result[] = $day;
+ $index++;
+ }
+ }
+
+ if (!empty($days) && !is_array($days)) {
+ Message::addError('no-days-selected');
+ Util::redirect('?do=locationinfo');
+ } else {
+
+ $dayz = array();
+ $da = array();
+
+ foreach ($days as $d) {
+ if ($d != '-') {
+ $da[] = $d;
+ } else {
+ $dayz[$count] = $da;
+ $da = array();
+ $count++;
+ }
+ }
+
+ $optime = array();
+ for ($x = 0; $x < $count; $x++) {
+ if ($dontadd[$x] == 'dontadd') {
+ continue;
+ }
+ $optime['days'] = $dayz[$x];
+ $optime['openingtime'] = $openingtime[$x];
+ $optime['closingtime'] = $closingtime[$x];
+ $result[] = $optime;
+ }
+ }
+
+ Database::exec("INSERT INTO `locationinfo_locationconfig` (locationid, openingtime)
+ VALUES (:id, :openingtime)
+ ON DUPLICATE KEY UPDATE openingtime = VALUES(openingtime)",
+ array('id' => $locationid, 'openingtime' => json_encode($result)));
+
+ if ($deleteCounter > 0) {
+ Message::addSuccess('deleted-x-entries', $deleteCounter);
+ }
+ if ($count > 0) {
+ Message::addSuccess('added-x-entries', $count);
+ }
+
+ Util::redirect('?do=locationinfo');
+ }
+
+ /**
+ * Updates the opening time in the db from the easy mode.
+ */
+ private function updateOpeningTimeEasy()
+ {
+ $locationid = Request::post('id', 0, 'int');
+ $openingtime = Request::post('openingtime', array(), 'array');
+ $closingtime = Request::post('closingtime', array(), 'array');
+ $result = array();
+
+ $blocks = array(
+ 0 => array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday"),
+ 1 => array("Saturday"),
+ 2 => array("Sunday"),
+ );
+ foreach ($blocks as $idx => $days) {
+ //if (!empty($openingtime[$idx]) && !empty($closingtime[$idx])) {
+ $result[] = array(
+ 'days' => $days,
+ 'openingtime' => $openingtime[$idx],
+ 'closingtime' => $closingtime[$idx],
+ );
+ //}
+ }
+
+ Database::exec("INSERT INTO `locationinfo_locationconfig` (locationid, openingtime)
+ VALUES (:id, :openingtime)
+ ON DUPLICATE KEY UPDATE openingtime = VALUES(openingtime)",
+ array('id' => $locationid, 'openingtime' => json_encode($result)));
+
+ Message::addSuccess('openingtime-updated');
+ Util::redirect('?do=locationinfo');
+ }
+
+ /**
+ * Checks if the server connection to a backend is valid.
+ *
+ * @param int $id Server id which connection should be checked.
+ */
+ private function checkConnection($serverid = 0)
+ {
+ if ($serverid === 0) {
+ Util::traceError('checkConnection called with no server id');
+ }
+
+ $dbresult = Database::queryFirst("SELECT servertype, credentials
+ FROM `locationinfo_coursebackend`
+ WHERE serverid = :serverid", array('serverid' => $serverid));
+
+ $serverInstance = CourseBackend::getInstance($dbresult['servertype']);
+ if ($serverInstance === false) {
+ LocationInfo::setServerError($serverid, 'Unknown backend type: ' . $dbresult['servertype']);
+ return;
+ }
+ $credentialsOk = $serverInstance->setCredentials($serverid, json_decode($dbresult['credentials'], true));
+
+ if ($credentialsOk) {
+ $connectionOk = $serverInstance->checkConnection();
+ }
+
+ LocationInfo::setServerError($serverid, $serverInstance->getError());
+ }
+
+
+ /**
+ * Sets the new hidden value and checks childs and parents.
+ *
+ * @param int $id The location id which was toggled
+ * @param bool $hidden The hidden value true / false
+ */
+ protected function toggleHidden($id, $hidden)
+ {
+ $locs = Location::getLocationsAssoc();
+ if (!isset($locs[$id]))
+ die('Invalid location id');
+ $loc = $locs[$id];
+
+ // The JSON to return, telling the client which locationids to update in the view
+ $return = array();
+ $return[] = array('locationid' => $id, 'hidden' => $hidden);
+
+ // Update the location, plus all child locations
+ $qs = '(?,?)' . str_repeat(',(?,?)', count($loc['children']));
+ $params = array($id, $hidden);
+ foreach ($loc['children'] as $child) {
+ $params[] = $child;
+ $params[] = $hidden;
+ $return[] = array('locationid' => $child, 'hidden' => $hidden);
+ }
+ Database::exec("INSERT INTO locationinfo_locationconfig (locationid, hidden)
+ VALUES $qs ON DUPLICATE KEY UPDATE hidden = VALUES(hidden)", $params);
+
+ // Handle parents - uncheck if not all children are checked
+ while ($loc['parentlocationid'] != 0) {
+ $stats = Database::queryFirst('SELECT Count(*) AS total, Sum(li.hidden > 0) AS hidecount FROM location l
+ LEFT JOIN locationinfo_locationconfig li USING (locationid)
+ WHERE l.parentlocationid = :parent', array('parent' => $loc['parentlocationid']));
+ $hidden = ($stats['total'] == $stats['hidecount']) ? 1 : 0;
+ $params = array('locationid' => $loc['parentlocationid'], 'hidden' => $hidden);
+ Database::exec('INSERT INTO locationinfo_locationconfig (locationid, hidden)
+ VALUES (:locationid, :hidden) ON DUPLICATE KEY UPDATE hidden = VALUES(hidden)', $params);
+ $return[] = $params;
+ $loc = $locs[$loc['parentlocationid']];
+ }
+ return $return;
+ }
+
+ /**
+ * Loads the Infoscreen page in the admin-panel and passes all needed information.
+ */
+ protected function getInfoScreenTable()
+ {
+ $locations = Location::getLocations(0, 0, false, true);
+
+ // Get hidden state of all locations
+ $dbquery = Database::simpleQuery("SELECT li.locationid, li.hidden FROM `locationinfo_locationconfig` AS li");
+
+ while ($row = $dbquery->fetch(PDO::FETCH_ASSOC)) {
+ $locid = (int)$row['locationid'];
+ $locations[$locid]['hidden_checked'] = $row['hidden'] != 0 ? 'checked' : '';
+ }
+
+ // Get a list of all the backend types.
+ $servertypes = array();
+ $s_list = CourseBackend::getList();
+ foreach ($s_list as $s) {
+ $typeInstance = CourseBackend::getInstance($s);
+ $servertypes[$s] = $typeInstance->getDisplayName();
+ }
+
+ // Get the Serverlist from the DB and make it mustache accessable
+ $serverlist = array();
+ $dbquery2 = Database::simpleQuery("SELECT * FROM `locationinfo_coursebackend`");
+ while ($row = $dbquery2->fetch(PDO::FETCH_ASSOC)) {
+ if (isset($servertypes[$row['servertype']])) {
+ $row['typename'] = $servertypes[$row['servertype']];
+ } else {
+ $row['typename'] = '[' . $row['servertype'] . ']';
+ $row['disabled'] = 'disabled';
+ }
+
+ if (!empty($row['error'])) {
+ $row['autherror'] = true;
+ $error = json_decode($row['error'], true);
+ if (isset($error['timestamp'])) {
+ $time = date('Y/m/d H:i:s', $error['timestamp']);
+ } else {
+ $time = '???';
+ }
+ Message::addError('auth-failed', $row['servername'], $time, $error['error']);
+ }
+ $serverlist[] = $row;
+ }
+
+ // Pass the data to the html and render it.
+ Render::addTemplate('location-info', array(
+ 'list' => array_values($locations),
+ 'serverlist' => $serverlist,
+ ));
+ }
+
+ /**
+ * AJAX
+ */
+ protected function doAjax()
+ {
+ User::load();
+ if (!User::isLoggedIn()) {
+ die('Unauthorized');
+ }
+ $action = Request::any('action');
+ $id = Request::any('id', 0, 'int');
+ if ($action === 'timetable') {
+ $this->ajaxTimeTable($id);
+ } elseif ($action === 'config') {
+ $this->ajaxLoadLocationConfig($id);
+ } elseif ($action === 'serverSettings') {
+ $this->ajaxServerSettings($id);
+ } elseif ($action === 'hide') {
+ $this->ajaxHideLocation();
+ }
+ }
+
+ /**
+ * Request to deny displaying the door sign for the
+ * given location. Sends a list of all affected
+ * locations, so the client can update its view.
+ */
+ private function ajaxHideLocation()
+ {
+ $locationId = Request::post('locationid', 0, 'int');
+ $hidden = Request::post('hidden', 0, 'int');
+ Header('Content-Type: application/json; charset=utf-8');
+ $ret = $this->toggleHidden($locationId, $hidden);
+ echo json_encode(array('changed' => $ret));
+ }
+
+ /**
+ * Ajax the server settings.
+ *
+ * @param int $id Serverid
+ */
+ private function ajaxServerSettings($id)
+ {
+ $oldConfig = Database::queryFirst('SELECT servername, servertype, credentials
+ FROM `locationinfo_coursebackend` WHERE serverid = :id', array('id' => $id));
+
+ // Credentials stuff.
+ if ($oldConfig !== false) {
+ $oldCredentials = json_decode($oldConfig['credentials'], true);
+ } else {
+ $oldCredentials = array();
+ }
+
+ // Get a list of all the backend types.
+ $serverBackends = array();
+ $s_list = CourseBackend::getList();
+ foreach ($s_list as $s) {
+ $backendInstance = CourseBackend::getInstance($s);
+ $backend = array(
+ 'backendtype' => $s,
+ 'display' => $backendInstance->getDisplayName(),
+ 'active' => ($oldConfig !== false && $s === $oldConfig['servertype']),
+ );
+ $backend['credentials'] = $backendInstance->getCredentialDefinitions();
+ foreach ($backend['credentials'] as $cred) {
+ if ($backend['active'] && isset($oldCredentials[$cred->property])) {
+ $cred->initForRender($oldCredentials[$cred->property]);
+ } else {
+ $cred->initForRender();
+ }
+ $cred->title = Dictionary::translateFile('backend-' . $s, $cred->property, true);
+ $cred->helptext = Dictionary::translateFile('backend-' . $s, $cred->property . "_helptext");
+ $cred->credentialsHtml = Render::parse('server-prop-' . $cred->template, (array)$cred);
+ }
+ $serverBackends[] = $backend;
+ }
+ echo Render::parse('server-settings', array('id' => $id,
+ 'name' => $oldConfig['servername'],
+ 'currentbackend' => $oldConfig['servertype'],
+ 'backendList' => $serverBackends));
+ }
+
+ /**
+ * Ajax the time table
+ *
+ * @param $id id of the location
+ */
+ private function ajaxTimeTable($id)
+ {
+ $row = Database::queryFirst("SELECT openingtime FROM `locationinfo_locationconfig` WHERE locationid = :id", array('id' => $id));
+ if ($row !== false) {
+ $openingtimes = json_decode($row['openingtime'], true);
+ }
+ if (!is_array($openingtimes)) {
+ $openingtimes = array();
+ }
+ if ($this->isEasyMode($openingtimes)) {
+ echo Render::parse('timetable', array('id' => $id,
+ 'openingtime0' => $openingtimes[0]['openingtime'],
+ 'closingtime0' => $openingtimes[0]['closingtime'],
+ 'openingtime1' => $openingtimes[1]['openingtime'],
+ 'closingtime1' => $openingtimes[1]['closingtime'],
+ 'openingtime2' => $openingtimes[2]['openingtime'],
+ 'closingtime2' => $openingtimes[2]['closingtime'],
+ 'easyMode' => true,
+ 'expertMode' => false));
+
+ } else {
+ $index = 0;
+ foreach ($openingtimes as &$entry) {
+ $entry['days'] = implode(', ', $entry['days']);
+ $entry['index'] = $index++;
+ }
+ echo Render::parse('timetable', array('id' => $id,
+ 'openingtimes' => array_values($openingtimes),
+ 'easyMode' => false,
+ 'expertMode' => true));
+ }
+ }
+
+ /**
+ * Checks if easymode or expert mode is active.
+ *
+ * @param $array Array of the saved openingtimes.
+ * @return bool True if easy mode, false if expert mode
+ */
+ private function isEasyMode($array)
+ {
+ if (empty($array))
+ return true;
+ if (count($array) === 3
+ && $array[0]['days'] == array("Monday", "Tuesday", "Wednesday", "Thursday", "Friday")
+ && $array[1]['days'][0] == "Saturday" && $array[2]['days'][0] == "Sunday"
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Ajax the config of a location.
+ *
+ * @param $id Location ID
+ */
+ private function ajaxLoadLocationConfig($id)
+ {
+ // Get Config data from db
+ $location = Database::queryFirst("SELECT config, serverid, serverlocationid FROM `locationinfo_locationconfig` WHERE locationid = :id", array('id' => $id));
+ if ($location === false) {
+ die("Invalid location id: $id");
+ }
+
+ $config = json_decode($location['config'], true); // TODO: Validate we got an array, fill with defaults otherwise
+
+ // get Server / ID list
+ $dbq = Database::simpleQuery("SELECT serverid, servername FROM locationinfo_coursebackend ORDER BY servername ASC");
+ $serverList = array();
+ while ($row = $dbq->fetch(PDO::FETCH_ASSOC)) {
+ if ($row['serverid'] == $location['serverid']) {
+ $row['selected'] = 'selected';
+ }
+ $serverList[] = $row;
+ }
+ $langs = Dictionary::getLanguages(true);
+ foreach ($langs as &$lang) {
+ if ($lang['cc'] === $config['language']) {
+ $lang['selected'] = 'selected';
+ }
+ }
+
+ echo Render::parse('config', array(
+ 'id' => $id,
+ 'languages' => $langs,
+ 'mode' => $config['mode'],
+ 'vertical_checked' => $config['vertical'] ? 'checked' : '',
+ 'eco_checked' => $config['eco'] ? 'checked' : '',
+ 'scaledaysauto_checked' => $config['scaledaysauto'] ? 'checked' : '',
+ 'daystoshow' => $config['daystoshow'],
+ 'rotation' => $config['rotation'],
+ 'scale' => $config['scale'],
+ 'switchtime' => $config['switchtime'],
+ 'calupdate' => $config['calupdate'],
+ 'roomupdate' => $config['roomupdate'],
+ 'configupdate' => $config['configupdate'],
+ 'serverlist' => $serverList,
+ 'serverid' => $location['serverid'],
+ 'serverlocationid' => $location['serverlocationid']
));
}
diff --git a/modules-available/locationinfo/templates/_page.html b/modules-available/locationinfo/templates/_page.html
deleted file mode 100644
index dfc941ae..00000000
--- a/modules-available/locationinfo/templates/_page.html
+++ /dev/null
@@ -1,4 +0,0 @@
-<div style="border:5px solid red">
- <h1>{{lang_hello}}, {{foo}}</h1>
- ** {{now}} **
-</div> \ No newline at end of file
diff --git a/modules-available/locationinfo/templates/config.html b/modules-available/locationinfo/templates/config.html
new file mode 100644
index 00000000..cce63de1
--- /dev/null
+++ b/modules-available/locationinfo/templates/config.html
@@ -0,0 +1,395 @@
+<form method="post" action="?do=locationinfo" id="configForm">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="updateConfig">
+ <input type="hidden" name="id" value="{{id}}">
+
+ <div class="row">
+ <div class="col-md-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">{{lang_server}}</div>
+ <div class="panel-body">
+ <div class="list-group">
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_server}}</label>
+ </div>
+ <div class="col-sm-7">
+ <select class="form-control" name="serverid">
+ <option value="0">{{lang_noServer}}</option>
+ {{#serverlist}}
+ <option value="{{serverid}}" {{selected}}>{{servername}}</option>
+ {{/serverlist}}
+ </select>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_serverTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_roomId}}</label>
+ </div>
+ <div class="col-sm-7">
+ <input class="form-control" name="serverlocationid" id="serverlocationid" value="{{serverlocationid}}">
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_roomIdTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="col-md-6">
+ <div class="modify-inputs panel panel-default">
+ <div class="panel-heading">{{lang_display}}</div>
+ <div class="panel-body">
+ <div class="list-group">
+
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_language}}</label>
+ </div>
+ <div class="col-sm-7">
+ <select class="form-control" name="language" id="language">
+ {{#languages}}
+ <option value="{{cc}}" id="lang-{{cc}}" {{selected}}>{{name}}</option>
+ {{/languages}}
+ </select>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_languageTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_mode}}</label>
+ </div>
+ <div class="col-sm-7">
+ <select class="form-control" name="mode" id="mode" onchange="modeChange()">
+ <option value="1" id="mode1">{{lang_mode1}}</option>
+ <option value="2" id="mode2">{{lang_mode2}}</option>
+ <option value="3" id="mode3">{{lang_mode3}}</option>
+ <option value="4" id="mode4">{{lang_mode4}}</option>
+ </select>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_modeTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_ecoMode}}</label>
+ </div>
+ <div class="col-sm-7">
+ <input type="checkbox" name="eco" {{eco_checked}}>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_ecoTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="modify-inputs">
+ <div class="row">
+ <div class="col-md-6">
+ <div class="panel panel-default" id="extra-div">
+ <div class="panel-heading">{{lang_mode}}</div>
+ <div class="panel-body">
+ <div class="list-group">
+
+ <div class="list-group-item m1-s m2-h m3-h m4-h">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_vertical}}</label>
+ </div>
+ <div class="col-sm-7">
+ <input type="checkbox" name="vertical" {{vertical_checked}}>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_verticalTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item m1-s m2-s m3-h m4-s">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_autoScale}}</label>
+ </div>
+ <div class="col-sm-7">
+ <input id="scaledaysauto" type="checkbox" name="scaledaysauto" {{scaledaysauto_checked}}>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_autoscaleTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item m1-s m2-s m3-h m4-s">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_daysToShow}}</label>
+ </div>
+ <div class="col-sm-7">
+ <select class="form-control" id="daystoshow" name="daystoshow">
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ <option value="4">4</option>
+ <option value="5">5</option>
+ <option value="6">6</option>
+ <option value="7">7</option>
+ </select>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_daysToShowTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item m1-s m2-h m3-s m4-s">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_rotation}}</label>
+ </div>
+ <div class="col-sm-7">
+ <select class="form-control" id="rotation" name="rotation">
+ <option value="0">{{lang_rotation0}}</option>
+ <option value="3">{{lang_rotation3}}</option>
+ <option value="2">{{lang_rotation2}}</option>
+ <option value="1">{{lang_rotation1}}</option>
+ </select>
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_rotationTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item m1-s m2-h m3-h m4-h">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_scale}}</label>
+ </div>
+ <div class="col-sm-7">
+ <span><span class="range-display"></span>&thinsp;%</span>
+ <input name="scale" type="range" step="1" min="10" max="90" value="{{scale}}">
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_scaleTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item m1-h m2-h m3-h m4-s">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_switchTime}}</label>
+ </div>
+ <div class="col-sm-7">
+ <span><span class="range-display"></span>&thinsp;{{lang_sec}}</span>
+ <input name="switchtime" type="range" step="1" min="1" max="120" value="{{switchtime}}">
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_switchTimeTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="col-md-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">{{lang_updateRates}}</div>
+ <div class="panel-body">
+ <div class="list-group">
+
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_calendar}}</label>
+ </div>
+ <div class="col-sm-7">
+ <input class="form-control" name="calupdate" type="number" min="0"
+ max="1440" value="{{calupdate}}">
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_calupdateTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_room}}</label>
+ </div>
+ <div class="col-sm-7">
+ <input class="form-control" name="roomupdate" type="number" min="0"
+ max="86400" value="{{roomupdate}}">
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_roomupdateTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-sm-3">
+ <label>{{lang_config}}</label>
+ </div>
+ <div class="col-sm-7">
+ <input class="form-control" name="configupdate" type="number" min="0"
+ max="1440" value="{{configupdate}}">
+ </div>
+ <div class="col-sm-2">
+ <a class="btn btn-default helptext" title="{{lang_configupdateTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <a class="btn btn-sm btn-default helptext pull-right" title="{{lang_customUrlTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ {{lang_customUrl}}
+ <div class="clearfix"></div>
+ </div>
+ <div class="panel-body">
+ <textarea rows="5" readonly class="form-control" id="custom-url"></textarea>
+ </div>
+ </div>
+</form>
+
+<script type="text/javascript"><!--
+ // Get list of form elements which affect the generated custom URL
+ var $inputs = $('.modify-inputs input, .modify-inputs select');
+ // Base for displaying the custom URL
+ var customURL = window.location.protocol + "//" + window.location.hostname + "/slx-admin/modules/locationinfo/frontend/doorsign.html?id={{id}}";
+ // Initialize fancy tooltips
+ $('a.helptext').tooltip();
+ // Add listener to range sliders so their label can be updated
+ $('input[type="range"]').change(function () {
+ $(this).siblings().find('.range-display').text($(this).val());
+ });
+ // Set state of input controls that aren't statically initialized server side
+ loadValues();
+ // Update the custom URL
+ buildCustomUrl();
+ // Add listener to all the elements affecting custom URL
+ $inputs.change(function () {
+ $this = $(this);
+ if ($this.attr('type') === 'hidden')
+ return;
+ buildCustomUrl();
+ });
+
+ /**
+ * Modifies the url preview.
+ */
+ function buildCustomUrl() {
+ var str = Array.prototype.reduce.call($inputs, function (acc, val) {
+ if (val.type && val.type === 'radio' && !val.checked)
+ return acc;
+ var v;
+ if (val.type && val.type === 'checkbox') {
+ v = val.checked ? val.value : '';
+ } else {
+ v = val.value;
+ }
+ return acc + '&' + encodeURIComponent(val.name) + '=' + encodeURIComponent(v);
+ }, '');
+
+ $('#custom-url').val(customURL + str);
+ }
+
+ /**
+ * Loads the Values in the config form elements.
+ */
+ function loadValues() {
+ $('.modify-inputs input[type="checkbox"]')
+ .bootstrapSwitch({size: 'small'})
+ .on('switchChange.bootstrapSwitch', function () {
+ buildCustomUrl();
+ if (this.name === 'scaledaysauto') {
+ $('#daystoshow').prop('disabled', this.checked);
+ }
+ });
+
+ $('#daystoshow option[value="{{daystoshow}}"]').attr("selected", "selected");
+ $('#rotation option[value="{{rotation}}"]').attr("selected", "selected");
+ $('#mode option[value="{{mode}}"]').attr("selected", "selected");
+
+ $('#daystoshow').prop('disabled', document.getElementById('scaledaysauto').checked);
+
+ $('input[type="range"]').change();
+ modeChange();
+ }
+
+ /**
+ * If the mode was changed the mode settings have to be adjusted.
+ */
+ function modeChange() {
+ var value = $('#mode').val();
+ $('.m' + value + '-h').hide();
+ $('.m' + value + '-s').show();
+ }
+
+//--></script>
diff --git a/modules-available/locationinfo/templates/location-info.html b/modules-available/locationinfo/templates/location-info.html
new file mode 100644
index 00000000..ce7b6504
--- /dev/null
+++ b/modules-available/locationinfo/templates/location-info.html
@@ -0,0 +1,219 @@
+<div>
+ <h1>{{lang_mainHeader}}</h1>
+
+ <h4>{{lang_serverTable}}</h4>
+
+ <table class="table table-hover">
+ <tr>
+ <th width="1">{{lang_serverType}}</th>
+ <th>{{lang_locationName}}</th>
+ <th width="1"></th>
+ <th width="1"></th>
+ </tr>
+ {{#serverlist}}
+ <form method="post" action="?do=locationinfo">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="serverid" value="{{serverid}}">
+ <tr>
+ <td nowrap>{{typename}}</td>
+ <td nowrap>{{servername}}</td>
+
+ <td align="center" nowrap>
+ <button class="btn btn-sm {{^autherror}}btn-success{{/autherror}}{{#autherror}}btn-danger{{/autherror}}"
+ data-server-edit="{{serverid}}" {{disabled}} type="button">
+ <span style="margin-right: 5px;" class="glyphicon glyphicon-cog"></span>
+ {{lang_locationSettings}}
+ </button>
+ <button class="btn btn-sm btn-primary server-check" {{disabled}} name="action" value="checkConnection"
+ type="submit">
+ <span style="margin-right: 5px;" class="glyphicon glyphicon-refresh"></span>
+ {{lang_checkConnection}}
+ </button>
+ </td>
+ <td align="center" nowrap>
+ <button class="btn btn-sm btn-danger server-delete" type="submit" name="action" value="deleteServer">
+ <span style="margin-right: 5px;" class="glyphicon glyphicon-remove"></span>
+ {{lang_delete}}
+ </button>
+ </td>
+ </tr>
+ </form>
+ {{/serverlist}}
+ </table>
+
+ <div>
+ <button class="btn btn-sm btn-success" id="addServerButton" onclick="addServer()">
+ <span style="margin-right: 5px;" class="glyphicon glyphicon-plus"></span>
+ {{lang_addServer}}
+ </button>
+ </div>
+
+ <h4>{{lang_buildingTable}}</h4>
+ <table class="table table-condensed table-hover" style="margin-bottom:0">
+
+ <tr>
+ <th>{{lang_locationName}}</th>
+ <th width="50" title="{{lang_locationIsHidden_title}}">{{lang_locationIsHidden}}</th>
+ <th width="50">{{lang_openingTime}}</th>
+ <th width="50">{{lang_locationSettings}}</th>
+ </tr>
+
+ {{#list}}
+ <tr id="row{{locationid}}">
+ <td>
+ <div style="display:inline-block;width:{{depth}}em"></div>
+ <a href="modules/locationinfo/frontend/doorsign.html?id={{locationid}}">{{locationname}}</a>
+ </td>
+ <td align="center">
+ <div class="checkbox" style="margin:0">
+ <input class="hidden-toggle" type="checkbox" data-locationid="{{locationid}}" {{hidden_checked}}>
+ <label></label>
+ </div>
+ </td>
+ <td>
+ <a class="btn btn-sm btn-default" role="button" style="width: 100%"
+ onclick="loadTimeModal({{locationid}}, '{{locationname}}');">
+ <span style="margin-right: 5px;" class="glyphicon glyphicon-time"></span>
+ </a>
+ </td>
+ <td>
+ <a class="btn btn-sm btn-default" role="button" style="width: 100%;"
+ onclick="loadLocationConfigModal({{locationid}}, '{{locationname}}');">
+ <span style="margin-right: 5px;" class="glyphicon glyphicon-cog"></span>
+ </a>
+ </td>
+ </tr>
+
+ {{/list}}
+ </table>
+
+ <div class="modal fade" id="myModal" tabindex="-1" role="dialog">
+ <div class="modal-dialog">
+
+ <div class="modal-content">
+ <div class="modal-header" id="myModalHeader"></div>
+ <div class="modal-body" id="myModalBody"></div>
+ <div class="modal-footer">
+ <button type="submit" id="myModalSubmitButton" class="btn btn-primary" form="">{{lang_save}}</button>
+
+ <a class="btn btn-primary" data-dismiss="modal">{{lang_close}}</a>
+ </div>
+ </div>
+
+ </div>
+ </div>
+</div>
+<script type="text/javascript"><!--
+
+ document.addEventListener("DOMContentLoaded", function () {
+
+ /**
+ * React to click on a "hidden" checkbox.
+ */
+ $('.hidden-toggle').change(function() {
+ $input = $(this);
+ $.ajax({
+ type: 'POST',
+ url: '?do=locationinfo&action=hide',
+ data: {locationid: $input.data('locationid'), hidden: $input[0].checked ? 1 : 0, token: TOKEN },
+ dataType: 'json'
+ }).done(function(data) {
+ if (data && $.isArray(data.changed)) {
+ markBoxes(data.changed);
+ } else {
+ $input.replaceWith('ERROR');
+ }
+ }).fail(function () {
+ $input.replaceWith('ERROR');
+ });
+ }).parent().click(function() {
+ $this = $(this);
+ $box = $this.find('input');
+ if (!$this.is($box)) {
+ $box.click();
+ }
+ });
+
+ /**
+ * Confirm deleting a server.
+ */
+ $('.server-delete').click(function(ev) {
+ var del = confirm("{{lang_deleteConfirmation}}");
+ if (!del) ev.preventDefault();
+ });
+
+ /**
+ * Animate refresh icon while page is loading
+ */
+ $('.server-check').click(function() {
+ $(this).find('.glyphicon').addClass('slx-rotation');
+ });
+
+ $('button[data-server-edit]').click(function() {
+ var id = $(this).data('server-edit');
+ loadServerSettingsModal(id);
+ });
+
+ });
+
+ /**
+ * Gets a status array delivered by backend's ajaxHideLocation()
+ * and sets the checkbox states accordingly.
+ */
+ function markBoxes(boxArray) {
+ for (var i = 0; i < boxArray.length; ++i) {
+ if (boxArray[i].locationid) {
+ var lid = parseInt(boxArray[i].locationid);
+ $('input.hidden-toggle[data-locationid="' + lid + '"]').prop('checked', !!boxArray[i].hidden);
+ }
+ }
+ }
+
+ /**
+ * Loads the settings modal of a server.
+ *
+ * @param serverid The id of the server.
+ */
+ function loadServerSettingsModal(serverid) {
+ $('#myModalHeader').text("{{lang_locationSettings}}").css("font-weight", "Bold");
+ $('#myModal .modal-dialog').css('width', '');
+ $('#myModal').modal('show');
+ $('#myModalBody').load("?do=locationinfo&action=serverSettings&id=" + serverid);
+ }
+
+ /**
+ * Load a opening time modal of a location.
+ *
+ * @param locationId The id of the location.
+ * @param locationName The name of the location.
+ */
+ function loadTimeModal(locationId, locationName) {
+ $('#myModalHeader').text("[" + locationId + "] " + locationName).css("font-weight", "Bold");
+ $('#myModal .modal-dialog').css('width', '');
+ $('#myModal').modal('show');
+ $('#myModalBody').load("?do=locationinfo&action=timetable&id=" + locationId);
+ }
+
+ /**
+ * Loads the config modal of a location.
+ *
+ * @param locationId The id of the location
+ * @param locationName the name of the location
+ */
+ function loadLocationConfigModal(locationId, locationName) {
+ $('#myModalHeader').text("[" + locationId + "] " + locationName).css("font-weight", "Bold");
+ $('#myModalSubmitButton').attr("form", "configForm");
+ $('#myModal .modal-dialog').css('width', '90%');
+ $('#myModal').modal('show');
+ $('#myModalBody').load("?do=locationinfo&action=config&id=" + locationId);
+ }
+
+ // ########### Server Table ###########
+
+ /**
+ * Loads a new / empty server settings modal.
+ */
+ function addServer() {
+ loadServerSettingsModal(0);
+ }
+//--></script>
diff --git a/modules-available/locationinfo/templates/server-prop-bool.html b/modules-available/locationinfo/templates/server-prop-bool.html
new file mode 100644
index 00000000..f430d02c
--- /dev/null
+++ b/modules-available/locationinfo/templates/server-prop-bool.html
@@ -0,0 +1,16 @@
+<div class="list-group-item">
+ <div class="row">
+ <div class="col-md-3"><label for="prop-{{property}}">{{title}}</label></div>
+ <div class="col-md-7">
+ <input class="settings-bs-switch" id="prop-{{property}}" type="checkbox" name="prop-{{property}}" value="1"
+ {{#currentvalue}}checked{{/currentvalue}}>
+ </div>
+ <div class="col-md-2">
+ {{#helptext}}
+ <a class="btn btn-default" title="{{helptext}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ {{/helptext}}
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/modules-available/locationinfo/templates/server-prop-dropdown.html b/modules-available/locationinfo/templates/server-prop-dropdown.html
new file mode 100644
index 00000000..80667766
--- /dev/null
+++ b/modules-available/locationinfo/templates/server-prop-dropdown.html
@@ -0,0 +1,19 @@
+<div class="list-group-item">
+ <div class="row">
+ <div class="col-md-3"><label for="prop-{{property}}">{{title}}</label></div>
+ <div class="col-md-7">
+ <select class="form-control" id="prop-{{property}}" name="prop-{{property}}">
+ {{#select_list}}
+ <option {{#active}}selected{{/active}}>{{option}}</option>
+ {{/select_list}}
+ </select>
+ </div>
+ <div class="col-md-2">
+ {{#helptext}}
+ <a class="btn btn-default" title="{{helptext}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ {{/helptext}}
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/modules-available/locationinfo/templates/server-prop-generic.html b/modules-available/locationinfo/templates/server-prop-generic.html
new file mode 100644
index 00000000..9e94c23d
--- /dev/null
+++ b/modules-available/locationinfo/templates/server-prop-generic.html
@@ -0,0 +1,16 @@
+<div class="list-group-item">
+ <div class="row">
+ <div class="col-md-3"><label for="prop-{{property}}">{{title}}</label></div>
+ <div class="col-md-7">
+ <input class="form-control" id="prop-{{property}}" type="{{inputtype}}" name="prop-{{property}}"
+ value="{{currentvalue}}">
+ </div>
+ <div class="col-md-2">
+ {{#helptext}}
+ <a class="btn btn-default" title="{{helptext}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ {{/helptext}}
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/modules-available/locationinfo/templates/server-settings.html b/modules-available/locationinfo/templates/server-settings.html
new file mode 100644
index 00000000..c9b713e6
--- /dev/null
+++ b/modules-available/locationinfo/templates/server-settings.html
@@ -0,0 +1,91 @@
+<div class="panel panel-default">
+ <div class="panel-heading">{{lang_general}}</div>
+ <div class="panel-body">
+ <div class="list-group">
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-md-3">
+ <label>{{lang_entryName}}</label>
+ </div>
+ <div class="col-md-7">
+ <input required class="form-control" name="name" type="text" value="{{name}}" id="name-input"
+ form="form-{{currentbackend}}">
+ </div>
+ <div class="col-md-2">
+ <a class="btn btn-default" title="{{lang_nameTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+ <div class="list-group-item">
+ <div class="row">
+ <div class="col-md-3">
+ <label>{{lang_serverType}}</label>
+ </div>
+ <div class="col-md-7">
+ <select class="form-control" onchange="servertype_changed(this.value)">
+ {{#backendList}}
+ <option value="{{backendtype}}" {{#active}}selected{{/active}}>{{display}}</option>
+ {{/backendList}}
+ </select>
+ </div>
+ <div class="col-md-2">
+ <a class="btn btn-default" id="help-type" title="{{lang_typeTooltip}}">
+ <span class="glyphicon glyphicon-question-sign"></span>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+{{#backendList}}
+<div id="formwrapper-{{backendtype}}" {{^active}}class="collapse"{{/active}}>
+ <form method="post" action="?do=locationinfo" id="form-{{backendtype}}">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="updateServerSettings">
+ <input type="hidden" name="id" value="{{id}}">
+ <input type="hidden" name="type" value="{{backendtype}}">
+
+
+ <div class="panel panel-default">
+ <div class="panel-heading">{{lang_credentials}}</div>
+ <div class="panel-body">
+ <div class="list-group">
+ {{#credentials}}
+ {{{credentialsHtml}}}
+ {{/credentials}}
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
+{{/backendList}}
+<script type="text/javascript">
+ var currentBackend = "{{currentbackend}}";
+
+ /**
+ * Show proper form after switching backend type
+ *
+ * @param {string} value The new currentBackend of the server which credentials needs to be loaded.
+ */
+ function servertype_changed(value) {
+ if (value === currentBackend)
+ return;
+ var newBackend = value;
+ console.log('From ' + currentBackend + ' to ' + value);
+ $('#formwrapper-' + currentBackend).fadeOut('fast', function() {
+ console.log('Fading in ' + newBackend);
+ $('#formwrapper-' + newBackend).fadeIn('fast');
+ $('#myModalSubmitButton, #name-input').attr('form', 'form-' + newBackend);
+ });
+ currentBackend = value;
+ }
+
+ $('a.btn[title]').tooltip();
+ $('#myModalSubmitButton').attr('form', 'form-' + currentBackend);
+ $('.settings-bs-switch').bootstrapSwitch({size:'small'});
+
+</script>
diff --git a/modules-available/locationinfo/templates/timetable.html b/modules-available/locationinfo/templates/timetable.html
new file mode 100644
index 00000000..91a80877
--- /dev/null
+++ b/modules-available/locationinfo/templates/timetable.html
@@ -0,0 +1,216 @@
+<div>
+ <div align="right">
+ <label for="CB_1">{{lang_expertMode}}</label>
+ <input class="bs-switch" name="1" id="CB_1" type="checkbox" {{#expertMode}}checked{{/expertMode}}>
+ </div>
+ <br>
+
+ <div id="easyMode" style="{{#expertMode}}display: none;{{/expertMode}}">
+ <form method="post" action="?do=locationinfo" id="timeFormEasy">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="updateOpeningTimeEasy">
+ <input type="hidden" name="id" value="{{id}}">
+ <input type="hidden" name="easyMode" value="{{easyMode}}">
+
+ <table class="table table-condensed locations" style="margin-bottom:0">
+ <tr>
+ <th>{{lang_day}}</th>
+ <th>{{lang_openingTime}}</th>
+ <th>{{lang_closingTime}}</th>
+ </tr>
+
+ <tr class="tablerow">
+ <td>{{lang_monTilFr}}</td>
+ <td>
+ <div class="input-group bootstrap-timepicker">
+ <span class="input-group-addon">
+ <span class="glyphicon glyphicon-time"></span>
+ </span>
+ <input type="text" class="form-control timepicker2" name="openingtime[]" id="openingtimepicker"
+ pattern="[0-9]{1,2}:[0-9]{2}" value="{{openingtime0}}">
+ </div>
+ </td>
+ <td>
+ <div class="input-group bootstrap-timepicker">
+ <span class="input-group-addon">
+ <span class="glyphicon glyphicon-time"></span>
+ </span>
+ <input type="text" class="form-control timepicker2" name="closingtime[]" id="openingtimepicker"
+ pattern="[0-9]{1,2}:[0-9]{2}" value="{{closingtime0}}">
+ </div>
+ </td>
+ </tr>
+ <tr class="tablerow">
+ <td>{{lang_saturday}}</td>
+ <td>
+ <div class="input-group bootstrap-timepicker">
+ <span class="input-group-addon">
+ <span class="glyphicon glyphicon-time"></span>
+ </span>
+ <input type="text" class="form-control timepicker2" name="openingtime[]" id="openingtimepicker"
+ pattern="[0-9]{1,2}:[0-9]{2}" value="{{openingtime1}}">
+ </div>
+ </td>
+ <td>
+ <div class="input-group bootstrap-timepicker">
+ <span class="input-group-addon">
+ <span class="glyphicon glyphicon-time"></span>
+ </span>
+ <input type="text" class="form-control timepicker2" name="closingtime[]" id="openingtimepicker"
+ pattern="[0-9]{1,2}:[0-9]{2}" value="{{closingtime1}}">
+ </div>
+ </td>
+ </tr>
+ <tr class="tablerow">
+ <td>{{lang_sunday}}</td>
+ <td>
+ <div class="input-group bootstrap-timepicker">
+ <span class="input-group-addon">
+ <span class="glyphicon glyphicon-time"></span>
+ </span>
+ <input type="text" class="form-control timepicker2" name="openingtime[]" id="openingtimepicker"
+ pattern="[0-9]{1,2}:[0-9]{2}" value="{{openingtime2}}">
+ </div>
+ </td>
+ <td>
+ <div class="input-group bootstrap-timepicker">
+ <span class="input-group-addon">
+ <span class="glyphicon glyphicon-time"></span>
+ </span>
+ <input type="text" class="form-control timepicker2" name="closingtime[]" id="openingtimepicker"
+ pattern="[0-9]{1,2}:[0-9]{2}" value="{{closingtime2}}">
+ </div>
+ </td>
+ </tr>
+ </table>
+ </form>
+ </div>
+
+ <div id="expertMode" style="{{#easyMode}}display: none;{{/easyMode}}">
+ <form method="post" action="?do=locationinfo" id="timeFormExpert">
+ <input type="hidden" name="token" value="{{token}}">
+ <input type="hidden" name="action" value="updateOpeningTimeExpert">
+ <input type="hidden" name="id" value="{{id}}">
+ <input type="hidden" name="easyMode" value="{{easyMode}}">
+
+
+ <table class="table table-condensed locations" style="margin-bottom:0">
+ <tr>
+ <th>{{lang_day}}</th>
+ <th>{{lang_openingTime}}</th>
+ <th>{{lang_closingTime}}</th>
+ <th>{{lang_delete}}</th>
+ </tr>
+
+ {{#openingtimes}}
+ <tr class=tablerow>
+ <td>{{days}}</td>
+ <td>{{openingtime}}</td>
+ <td>{{closingtime}}</td>
+ <td align="center"><input type="checkbox" name="delete[]" value="{{index}}"></td>
+ </tr>
+ {{/openingtimes}}
+
+ <tr id="lastOpenTimesTableElement"></tr>
+ </table>
+
+ <br>
+ <a class="btn btn-success btn-sm" onclick=newOpeningTime()>
+ <span class="glyphicon glyphicon-plus-sign"></span>
+ {{lang_openingTime}}
+ </a>
+ </form>
+ </div>
+</div>
+
+<script>
+ setTimepicker();
+ $('#CB_1').bootstrapSwitch();
+ $('#CB_1').on('switchChange.bootstrapSwitch', function (e, data) {
+
+ if (data == false) {
+ $('#expertMode').hide();
+ $('#easyMode').show();
+ $('#myModalSubmitButton').attr("Form", "timeFormEasy");
+ } else {
+ $('#easyMode').hide();
+ $('#expertMode').show();
+ $('#myModalSubmitButton').attr("Form", "timeFormExpert");
+ }
+ });
+
+ if ('{{easyMode}}' == true) {
+ $('#myModalSubmitButton').attr("Form", "timeFormEasy");
+ } else if ('{{expertMode}}' == true) {
+ $('#myModalSubmitButton').attr("Form", "timeFormExpert");
+ }
+
+ /**
+ * Sets the timepicker element.
+ */
+ function setTimepicker() {
+ $('.timepicker2').timepicker({
+ minuteStep: 1,
+ template: 'modal',
+ appendWidgetTo: 'body',
+ showSeconds: false,
+ showMeridian: false,
+ defaultTime: false
+ });
+ }
+
+ /**
+ * Adds a new opening time to the table in expert mode.
+ */
+ function newOpeningTime() {
+ $('#lastOpenTimesTableElement').before('<tr>\
+ <td>\
+ <div class="form-group options">\
+ <label><input required type="checkbox" name="days[]" value="Monday">{{lang_shortMonday}}</label>\
+ <label><input required type="checkbox" name="days[]" value="Tuesday">{{lang_shortTuesday}}</label>\
+ <label><input required type="checkbox" name="days[]" value="Wednesday">{{lang_shortWednesday}}</label>\
+ <label><input required type="checkbox" name="days[]" value="Thursday">{{lang_shortThursday}}</label>\
+ <label><input required type="checkbox" name="days[]" value="Friday">{{lang_shortFriday}}</label>\
+ <label><input required type="checkbox" name="days[]" value="Saturday">{{lang_shortSaturday}}</label>\
+ <label><input required type="checkbox" name="days[]" value="Sunday">{{lang_shortSunday}}</label>\
+ </div>\
+ <input type="hidden" name="days[]" value="-">\
+ </td>\
+ <td>\
+ <div class="input-group bootstrap-timepicker">\
+ <span class="input-group-addon">\
+ <span class="glyphicon glyphicon-time"></span>\
+ </span>\
+ <input required type="text" class="form-control timepicker2" name="openingtime[]" id="openingtimepicker" pattern="[0-9]{1,2}:[0-9]{2}" value="8:00">\
+ </div>\
+ \
+ </td>\
+ <td>\
+ \
+ <div class="input-group bootstrap-timepicker">\
+ <span class="input-group-addon">\
+ <span class="glyphicon glyphicon-time"></span>\
+ </span>\
+ <input required type="text" class="form-control timepicker2" name="closingtime[]" id="closingtimepicker" pattern="[0-9]{1,2}:[0-9]{2}" value="18:00">\
+ </div>\
+ \
+ </td>\
+ <td align="center">\
+ <input type="checkbox" name="dontadd[]" value="dontadd"\
+ </td>\
+ </tr>');
+ setTimepicker();
+
+ $(function () {
+ var requiredCheckboxes = $('.options :checkbox[required]');
+ requiredCheckboxes.change(function () {
+ if (requiredCheckboxes.is(':checked')) {
+ requiredCheckboxes.removeAttr('required');
+ } else {
+ requiredCheckboxes.attr('required', 'required');
+ }
+ });
+ });
+ }
+
+</script>
diff --git a/modules-available/locations/inc/location.inc.php b/modules-available/locations/inc/location.inc.php
index 9977379f..8f3447da 100644
--- a/modules-available/locations/inc/location.inc.php
+++ b/modules-available/locations/inc/location.inc.php
@@ -8,7 +8,7 @@ class Location
private static $treeCache = false;
private static $subnetMapCache = false;
- private static function getTree()
+ public static function getTree()
{
if (self::$treeCache === false) {
self::$treeCache = self::queryLocations();
@@ -61,12 +61,18 @@ class Location
'locationid' => (int)$node['locationid'],
'parentlocationid' => (int)$node['parentlocationid'],
'parents' => $parents,
+ 'children' => empty($node['children']) ? array() : array_map(function ($item) { return $item['locationid']; }, $node['children']),
'locationname' => $node['locationname'],
'depth' => $depth,
'isleaf' => true,
);
if (!empty($node['children'])) {
- $output += self::flattenTreeAssoc($node['children'], array_merge($parents, array((int)$node['locationid'])), $depth + 1);
+ $childNodes = self::flattenTreeAssoc($node['children'], array_merge($parents, array((int)$node['locationid'])), $depth + 1);
+ $output[(int)$node['locationid']]['children'] = array_merge($output[(int)$node['locationid']]['children'],
+ array_reduce($childNodes, function ($carry, $item) {
+ return array_merge($carry, $item['children']);
+ }, array()));
+ $output += $childNodes;
}
}
foreach ($output as &$entry) {
@@ -119,7 +125,7 @@ class Location
return array_values($rows);
}
- public static function buildTree($elements, $parentId = 0)
+ private static function buildTree($elements, $parentId = 0)
{
$branch = array();
$sort = array();
diff --git a/modules-available/rebootcontrol/inc/rebootqueries.inc.php b/modules-available/rebootcontrol/inc/rebootqueries.inc.php
index df3c13d8..62092748 100644
--- a/modules-available/rebootcontrol/inc/rebootqueries.inc.php
+++ b/modules-available/rebootcontrol/inc/rebootqueries.inc.php
@@ -36,8 +36,8 @@ class RebootQueries
{
if (empty($list))
return array();
- $qs = '?' . str_repeat(',?', count($list) - 1);
- $res = Database::simpleQuery("SELECT machineuuid, clientip, locationid FROM machine WHERE machineuuid IN ($qs)", $list);
+ $res = Database::simpleQuery("SELECT machineuuid, clientip, locationid FROM machine
+ WHERE machineuuid IN (:list)", compact('list'));
return $res->fetchAll(PDO::FETCH_ASSOC);
}