diff options
Diffstat (limited to 'modules-available')
43 files changed, 9919 insertions, 30 deletions
diff --git a/modules-available/locationinfo/api.inc.php b/modules-available/locationinfo/api.inc.php index 0d84ebce..a6e2fda3 100644 --- a/modules-available/locationinfo/api.inc.php +++ b/modules-available/locationinfo/api.inc.php @@ -1,7 +1,420 @@ <?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'); + if ($getAction == "roominfo") { + $roomIDs = Request::get('id', 0, 'string'); + $array = filterIdList($roomIDs); + $getCoords = Request::get('coords', 0, 'string'); + if (empty($getCoords)) { + $getCoords = '0'; + } + echo getRoomInfo($array, $getCoords); + } elseif ($getAction == "openingtime") { + $roomIDs = Request::get('id', 0, 'string'); + $array = filterIdList($roomIDs); + echo getOpeningTime($array); + } elseif ($getAction == "config") { + $getRoomID = Request::get('id', 0, 'int'); + getConfig($getRoomID); + } elseif ($getAction == "pcstates") { + $roomIDs = Request::get('id', 0, 'string'); + $array = filterIdList($roomIDs); + echo getPcStates($array); + } elseif ($getAction == "roomtree") { + $roomIDs = Request::get('id', 0, 'string'); + $array = filterIdList($roomIDs); + echo getRoomTree($array); + } elseif ($getAction == "calendar") { + $roomIDs = Request::get('id', 0, 'string'); + $array = filterIdList($roomIDs); + echo getCalendar($array); + } +} + +/** + * Filters the id list. Removes Double / non-int / hidden rooms. + * + * @param $roomids Array of the room ids. + * @return array The filtered array of the room ids. + */ +function filterIdList($roomids) +{ + $idList = explode(',', $roomids); + $filteredIdList = array_filter($idList, 'is_numeric'); + $filteredIdList = array_unique($filteredIdList); + $filteredIdList = filterHiddenRoom($filteredIdList); + + return $filteredIdList; +} + +/** + * Filters the hidden rooms from an array. + * + * @param $idArray Id list + * @return array Filtered id list + */ +function filterHiddenRoom($idArray) +{ + $filteredArray = array(); + if (!empty($idArray)) { + $query = "SELECT locationid, hidden FROM `location_info` WHERE locationid IN ("; + $query .= implode(",", $idArray); + $query .= ")"; + + $dbquery = Database::simpleQuery($query); + + while ($dbresult = $dbquery->fetch(PDO::FETCH_ASSOC)) { + if ($dbresult['hidden'] == false) { + $filteredArray[] = (int)$dbresult['locationid']; + } + } + } + + return $filteredArray; +} + +// ########## <Roominfo> ########## +/** + * Gets the room info of the given rooms. + * + * @param $idList Array list of ids. + * @param bool $coords Defines if coords should be included or not. + * @return string Roominfo JSON + */ +function getRoomInfo($idList, $coords = false) +{ + + $coordinates = (string)$coords; + $dbresult = array(); + + if (!empty($idList)) { + // Build SQL Query for multiple ids. + $query = "SELECT l.locationid, m.machineuuid, m.position, m.logintime, m.lastseen, m.lastboot FROM location_info AS l + LEFT JOIN `machine` AS m ON l.locationid = m.locationid WHERE l.hidden = 0 AND l.locationid IN ("; + + $query .= implode(",", $idList); + $query .= ")"; + + // Execute query. + $dbquery = Database::simpleQuery($query); + + // Fetch db data. + 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()); + } + + // Left join, no data + if (empty($dbdata['machineuuid'])) + continue; + + // Compact the pc data in one array. + $pc['id'] = $dbdata['machineuuid']; + if ($coordinates == '1' || $coordinates == 'true') { + $position = json_decode($dbdata['position'], true); + $pc['x'] = $position['gridCol']; + $pc['y'] = $position['gridRow']; + + if (isset($position['overlays'])) { + $pc['overlay'] = $position['overlays']; + } else { + $pc['overlay'] = 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 json_encode(array_values($dbresult), true); +} + +// ########## </Roominfo> ########### + +// ########## <Openingtime> ########## +/** + * Gets the Opening time of the given locations. + * + * @param $idList Array list of locations + * @return string Opening times JSON + */ +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 location_info 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 json_encode($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) +{ + $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 $locationID ID of the location + */ +function getConfig($locationID) +{ + $dbresult = Database::queryFirst("SELECT l.locationname, li.config, li.serverroomid, s.servertype, s.serverurl FROM `location_info` AS li + RIGHT JOIN `location` AS l ON l.locationid=li.locationid + LEFT JOIN `setting_location_info` AS s ON s.serverid=li.serverid + WHERE l.locationid=:locationID", array('locationID' => $locationID)); + $config = array(); + + if ($dbresult['locationname'] == null) { + $config = array(); + } else { + + if ($dbresult['config'] == null) { + defaultConfig($config); + } else { + $config = json_decode($dbresult['config'], true); + } + + $config['room'] = $dbresult['locationname']; + $date = getdate(); + $config['time'] = $date['year'] . "-" . $date['mon'] . "-" . $date['mday'] . " " . $date['hours'] . ":" . $date['minutes'] . ":" . $date['seconds']; + } + echo json_encode($config, true); +} + +/** + * Creates and returns a default config for room that didn't saved a config yet. + * + * @return Return a default config. + */ +function defaultConfig(&$config) +{ + $config['language'] = 'en'; + $config['mode'] = 1; + $config['vertical'] = false; + $config['eco'] = false; + $config['scaledaysauto'] = true; + $config['daystoshow'] = 7; + $config['rotation'] = 0; + $config['scale'] = 50; + $config['switchtime'] = 20; + $config['calupdate'] = 30; + $config['roomupdate'] = 5; + $config['configupdate'] = 180; + + return $config; +} + +/** + * Gets the pc states of the given locations. + * + * @param $idList Array list of the location ids. + * @return string PC state JSON + */ +function getPcStates($idList) +{ + $pcStates = array(); + + $roominfoList = json_decode(getRoomInfo($idList), true); + foreach ($roominfoList as $roomInfo) { + $result['id'] = $roomInfo['id']; + $idle = 0; + $occupied = 0; + $off = 0; + $broken = 0; + + foreach ($roomInfo['computer'] as $computer) { + if ($computer['pcState'] == "IDLE") { + $idle++; + } elseif ($computer['pcState'] == "OCCUPIED") { + $occupied++; + } elseif ($computer['pcState'] == "OFF") { + $off++; + } elseif ($computer['pcState'] == "BROKEN") { + $broken++; + } + } + + $result['idle'] = $idle; + $result['occupied'] = $occupied; + $result['off'] = $off; + $result['broken'] = $broken; + $pcStates[] = $result; + } + return json_encode($pcStates); +} + +/** + * Gets the room tree of the given locations. + * + * @param int[] $idList Array list of the locations. + * @return string Room tree JSON. + */ +function getRoomTree($idList) +{ + $locations = Location::getTree(); + + $ret = findRooms($locations, $idList); + return json_encode($ret); +} + +function findRooms($locations, $idList) +{ + $ret = array(); + foreach ($locations as $location) { + if (in_array($location['locationid'], $idList)) { + $ret[] = $location; + } elseif (!empty($location['children'])) { + $ret = array_merge($ret, findRooms($location['children'], $idList)); + } + } + return $ret; +} + +// ########## <Calendar> ########### +/** + * Gets the calendar of the given ids. + * + * @param $idList Array list with the location ids. + * @return string Calendar JSON. + */ +function getCalendar($idList) +{ + $serverList = array(); + + if (!empty($idList)) { + // Build SQL query for multiple ids. + $qs = '?' . str_repeat(',?', count($idList) - 1); + $query = "SELECT l.locationid, l.serverid, l.serverroomid, s.serverurl, s.servertype, s.credentials + FROM `location_info` AS l + INNER JOIN setting_location_info AS s ON (s.serverid = l.serverid) + WHERE l.hidden = 0 AND l.locationid IN ($qs) + ORDER BY s.servertype ASC"; + + $dbquery = Database::simpleQuery($query, array_values($idList)); + + while ($dbresult = $dbquery->fetch(PDO::FETCH_ASSOC)) { + if (!isset($serverList[$dbresult['serverid']])) { + $serverList[$dbresult['serverid']] = array( + 'credentials' => json_decode($dbresult['credentials'], true), + 'url' => $dbresult['serverurl'], + '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 location_info SET serverid = 0 WHERE locationid = :lid", + array('lid' => $server['locationid'])); + continue; + } + $credentialsOk = $serverInstance->setCredentials($server['credentials'], $server['url'], $serverid); + + 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 json_encode($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..7017fe0b --- /dev/null +++ b/modules-available/locationinfo/frontend/doorsign.html @@ -0,0 +1,1811 @@ +<!-- + +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=roominfo&id=" + room.id + "&coords=1", function (result) { + generateRoomLayoutDiv((100 - room.config.scale) + "%", room); + if (result[0] == null) { + + return; + } + initRoom(result[0].computer, 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=roominfo&id=" + ids + "&coords=0", + 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; + } + for (var i = 0; i < l; i++) { + + UpdatePc(result[i].computer, rooms[result[i].id]); + } + }, error: function () { + + } + }) + } + + + /** + * 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..cd4db81f --- /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=roomtree&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/inc/coursebackend.inc.php b/modules-available/locationinfo/inc/coursebackend.inc.php new file mode 100644 index 00000000..11d833e6 --- /dev/null +++ b/modules-available/locationinfo/inc/coursebackend.inc.php @@ -0,0 +1,283 @@ +<?php + +/** + * Base class for course query backends + */ +abstract class CourseBackend +{ + + /* + * Static part for handling interfaces + */ + + /** + * @var array list of known backends + * @var boolean true if there was an error + * @var string with the error message + * @var int as internal serverID + * @var string url of the service + */ + private static $backendTypes = false; + public $error; + public $errormsg; + public $serverID; + public $location; + const nrOtherRooms = 5; + + /** + * CourseBackend constructor. + */ + public final function __construct() + { + $this->location = ""; + $this->error = false; + $this->errormsg = ""; + } + + /** + * 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 \ConfigModule 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 array with parameter name as key and and an array with type, help text and mask as value + */ + public abstract function getCredentials(); + + /** + * @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 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 abstract function setCredentials($data, $url, $serverID); + + /** + * @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($roomIDs) + { + if (empty($roomIDs)) { + $this->error = true; + $this->errormsg = 'No roomid was given to fetch Shedule'; + return false; + } + $sqlr = implode(",", $roomIDs); + $sqlr = '(' . $sqlr . ')'; + $q = "SELECT locationid, calendar, serverroomid, lastcalendarupdate FROM location_info WHERE locationid IN " . $sqlr; + $dbquery1 = Database::simpleQuery($q); + $result = []; + $sRoomIDs = []; + $newResult = []; + foreach ($dbquery1->fetchAll(PDO::FETCH_ASSOC) as $row) { + $sRoomID = $row['serverroomid']; + $lastUpdate = $row['lastcalendarupdate']; + $calendar = $row['calendar']; + //Check if in cache if lastUpdate is null then it is interpreted as 1970 + if ($lastUpdate > strtotime("-" . $this->getCacheTime() . "seconds")) { + $result[$row['locationid']] = json_decode($calendar); + } else { + $sRoomIDs[$row['locationid']] = $sRoomID; + } + + } + //Check if we should refresh other rooms recently requested by front ends + if ($this->getCacheTime() > 0) { + $i = 0; //number of rooms getting refreshed + $dbquery4 = Database::simpleQuery("SELECT locationid ,serverroomid, lastcalendarupdate FROM location_info WHERE serverid= :id", array('id' => $this->serverID)); + foreach ($dbquery4->fetchAll(PDO::FETCH_COLUMN) as $row) { + if (isset($row['lastcalendarupdate'])) { + $lastUpdate = $row['lastcalendarupdate']; + if ($lastUpdate < strtotime("-" . $this->getRefreshTime() . "seconds") + && $lastUpdate > strtotime("-" . $this->getCacheTime() . "seconds" + && $i < self::nrOtherRooms)) { + $sRoomIDs[$row['locationid']] = $row['serverroomid']; + $i = $i + 1; + } + } + } + } + //This is true if there is no need to check the HisInOne Server + if (empty($sRoomIDs)) { + return $result; + } + $results = $this->fetchSchedulesInternal($sRoomIDs); + if ($results === false) { + return false; + } + + foreach ($sRoomIDs as $location => $serverRoom) { + $newResult[$location] = $results[$serverRoom]; + } + + if ($this->getCacheTime() > 0) { + foreach ($newResult as $key => $value) { + $value = json_encode($value); + $now = strtotime('Now'); + Database::simpleQuery("UPDATE location_info SET calendar = :ttable, lastcalendarupdate = :now + WHERE locationid = :id ", array( + 'id' => $key, + 'ttable' => $value, + 'now' => $now + )); + } + } + //get all schedules that are wanted from roomIDs + foreach ($roomIDs as $id) { + if (isset($newResult[$id])) { + $result[$id] = $newResult[$id]; + } + } + return $result; + } + + /** + * @return false if there was no error string with error message if there was one + */ + public final function getError() + { + if ($this->error) { + return $this->errormsg; + } + return false; + } + + /** + * 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. + */ + function getAttributes($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->getAttributes($item, $path); + If (gettype($test) == "array") { + $return = array_merge($return, $test); + } + + } + return $return; + } + // Unique non-leaf node - simple recursion + return $this->getAttributes($array[$element], $path); + } +} 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..11882a1e --- /dev/null +++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_davinci.inc.php @@ -0,0 +1,140 @@ +<?php + +class Coursebackend_Davinci extends CourseBackend +{ + + + public function setCredentials($data, $location, $serverID) + { + if ($location == "") { + $this->error = true; + $this->errormsg = "No url is given"; + return !$this->error; + } + $this->location = $location . "/DAVINCIIS.dll?"; + $this->serverID = $serverID; + //Davinci doesn't have credentials + return true; + } + + public function checkConnection() + { + if ($this->location != "") { + $this->fetchSchedulesInternal(['B206']); + return !$this->error; + } + $this->error = true; + $this->errormsg = "Credentials are not set"; + return !$this->error; + } + + public function getCredentials() + { + $return = array(); + return $return; + } + + public function getDisplayName() + { + return 'Davinci'; + } + + public function getCacheTime() + { + return 0; + } + + public function getRefreshTime() + { + return 0; + } + + /** + * @param $response xml document + * @return bool|array array representation of the xml if possible + */ + private function toArray($response) + { + try { + $cleanresponse = preg_replace('/(<\/?)(\w+):([^>]*>)/', "$1$2$3", $response); + $xml = new SimpleXMLElement($cleanresponse); + $array = json_decode(json_encode((array)$xml), true); + } catch (Exception $exception) { + $this->error = true; + $this->errormsg = "url did not answer with a xml, maybe the url is wrong or the room is wrong"; + $array = false; + } + return $array; + } + + /** + * @param $roomId string name of the room + * @return array|bool if successful the arrayrepresentation of the timetable + */ + private function fetchArray($roomId) + { + $startDate = new DateTime('today 0:00'); + $endDate = new DateTime('+7 days 0:00'); + $url = $this->location . "content=xml&type=room&name=" . $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 => false, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_URL => $url, + ); + + curl_setopt_array($ch, $options); + $output = curl_exec($ch); + if ($output === false) { + $this->error = true; + $this->errormsg = 'Curl error: ' . curl_error($ch) . $url; + return false; + } else { + $this->error = false; + $this->errormsg = ""; + ///Operation completed successfully + } + curl_close($ch); + error_log($output); + return $this->toArray($output); + + } + + public function fetchSchedulesInternal($roomIds) + { + $schedules = []; + foreach ($roomIds as $sroomId) { + $return = $this->fetchArray($sroomId); + if ($return === false) { + return false; + } + $lessons = $this->getAttributes($return, 'Lessons/Lesson'); + if (!$lessons) { + $this->error = true; + $this->errormsg = "url send a xml in a wrong format"; + return false; + } + $timetable = []; + foreach ($lessons as $lesson) { + $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 = $lesson['Subject']; + $json = array( + 'title' => $subject, + 'start' => $date . " " . $start . ':00', + 'end' => $date . " " . $end . ':00' + ); + array_push($timetable, $json); + } + $schedules[$sroomId] = $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..484a5286 --- /dev/null +++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_dummy.inc.php @@ -0,0 +1,116 @@ +<?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 setCredentials($json, $location, $serverID) + { + $x = $json; + $this->pw = $x['password']; + + if ($this->pw === "mfg") { + $this->error = false; + return true; + } else { + $this->errormsg = "USE mfg as password!"; + $this->error = true; + 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->errormsg = "USE mfg as password!"; + $this->error = true; + return false; + } + } + + /** + * @returns array with parameter name as key and and an array with type, help text and mask as value + */ + public function getCredentials() + { + $options = ["opt1", "opt2", "opt3", "opt4", "opt5", "opt6", "opt7", "opt8"]; + $credentials = [ + "username" => "string", + "password" => "password", + "integer" => "int", + "option" => $options, + "CheckTheBox" => "bool", + "CB2 t" => "bool" + ]; + return $credentials; + } + + /** + * @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..0e7c5328 --- /dev/null +++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php @@ -0,0 +1,360 @@ +<?php + +class CourseBackend_HisInOne extends CourseBackend +{ + private $username; + private $password; + private $open; + + + public function setCredentials($data, $url, $serverID) + { + if (array_key_exists('password', $data) && array_key_exists('username', $data) && array_key_exists('role', $data) && isset($data['open'])) { + $this->error = false; + $this->password = $data['password']; + $this->username = $data['username'] . "\t" . $data['role']; + $this->open = $data['open']; + if ($url == "") { + $this->error = true; + $this->errormsg = "No url is given"; + return !$this->error; + } + if ($this->open) { + $this->location = $url . "/qisserver/services2/OpenCourseService"; + } else { + $this->location = $url . "/qisserver/services2/CourseService"; + } + $this->serverID = $serverID; + } else { + $this->error = true; + $this->errormsg = "wrong credentials"; + return false; + } + + return true; + } + + public function checkConnection() + { + if ($this->location == "") { + $this->error = true; + $this->errormsg = "Credentials are not set"; + } + $this->findUnit(190); + return !$this->error; + } + + /** + * @param $roomID int + * @return array|bool if successful an array with the subjectIDs that take place in the room + */ + public function findUnit($roomID) + { + $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); + $termYearN = $doc->createElement('termYear', $termYear); + $findUnit->appendChild($termYearN); + if ($termType1 != 3 && $termType1 != 10) { + $termTypeValueId = $doc->createElement('termTypeValueId', $termType); + $findUnit->appendChild($termTypeValueId); + } + $roomIdN = $doc->createElement('ns1:roomId', $roomID); + $findUnit->appendChild($roomIdN); + + $soap_request = $doc->saveXML(); + $response1 = $this->__doRequest($soap_request, "findUnit"); + $id = []; + if ($this->error == true) { + return false; + } + $response2 = $this->toArray($response1); + if ($response2 === false) { + return false; + } + if (isset($response2['soapenvBody']['soapenvFault'])) { + $this->error = true; + $this->errormsg = $response2['soapenvBody']['soapenvFault']['faultcode'] . " " . $response2['soapenvBody']['soapenvFault']['faultstring']; + return false; + } elseif ($this->open) { + $units = $this->getAttributes($response2, 'soapenvBody/hisfindUnitResponse/hisunits/hisunit'); + foreach ($units as $unit) { + $id[] = $unit['hisid']; + } + } elseif (!$this->open) { + $id = $this->getAttributes($response2, 'soapenvBody/hisfindUnitResponse/hisunitIds/hisid'); + } else { + $this->error = true; + $this->errormsg = "url send a xml in a wrong format"; + $id = false; + } + 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 __doRequest($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 => false, + CURLOPT_SSL_VERIFYPEER => false, + 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 = true; + $this->errormsg = 'Curl error: ' . curl_error($soap_do); + } else { + $this->error = false; + $this->errormsg = ""; + ///Operation completed successfully + } + curl_close($soap_do); + return $output; + } + + /** + * @param $response xml document + * @return bool|array array representation of the xml if possible + */ + private function toArray($response) + { + try { + $cleanresponse = preg_replace("/(<\/?)(\w+):([^>]*>)/", "$1$2$3", $response); + $xml = new SimpleXMLElement($cleanresponse); + $array = json_decode(json_encode((array)$xml), true); + } catch (Exception $e) { + $this->error = true; + $this->errormsg = "url did not send a xml"; + $array = false; + } + return $array; + } + + + public function getCacheTime() + { + return 30 * 60; + } + + + public function getRefreshTime() + { + return 60 * 60; + } + + + public function getDisplayName() + { + return "HisInOne"; + } + + + public function getCredentials() + { + $credentials = ["username" => "string", "role" => "string", "password" => "password", "open" => "bool"]; + return $credentials; + } + + + public function fetchSchedulesInternal($param) + { + if (empty($param)) { + $this->error = true; + $this->errormsg = 'Internal Error HisInOne'; + error_log('No roomId was given in HisInOne fetchShedule'); + return false; + } + $tTables = []; + //get all eventIDs in a given room + $eventIDs = []; + foreach ($param as $ID) { + $unitID = $this->findUnit($ID); + if ($unitID == false) { + $this->error = false; + error_log($this->errormsg); + continue; + } + $eventIDs = array_merge($eventIDs, $unitID); + $eventIDs = array_unique($eventIDs); + } + if (empty($eventIDs)) { + foreach ($param as $room) { + $tTables[$room] = []; + } + return $tTables; + } + $events = []; + //get all information on each event + foreach ($eventIDs as $each_event) { + $event = $this->readUnit(intval($each_event)); + if ($event === false) { + $this->error = false; + error_log($this->errormsg); + continue; + } + $events[] = $event; + } + $currentWeek = $this->getCurrentWeekDates(); + foreach ($param as $room) { + $timetable = array(); + //Here I go over the soapresponse + foreach ($events as $event) { + $name = $this->getAttributes($event, '/hisunit/hisdefaulttext'); + if ($name == false) { + //if HisInOne has no default text then there is no name + $name = ['']; + } + $dates = $this->getAttributes($event, + '/hisunit/hisplanelements/hisplanelement/hisplannedDates/hisplannedDate/hisindividualDates/hisindividualDate'); + foreach ($dates as $date) { + $roomID = $this->getAttributes($date, '/hisroomId')[0]; + $datum = $this->getAttributes($date, '/hisexecutiondate')[0]; + if (intval($roomID) == $room && in_array($datum, $currentWeek)) { + $startTime = $this->getAttributes($date, 'hisstarttime')[0]; + $endTime = $this->getAttributes($date, 'hisendtime')[0]; + $json = array( + 'title' => $name[0], + 'start' => $datum . " " . $startTime, + 'end' => $datum . " " . $endTime + ); + array_push($timetable, $json); + } + } + } + $tTables[$room] = $timetable; + } + 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); + $unitId = $doc->createElement('ns1:unitId', $unit); + $readUnit->appendChild($unitId); + + $soap_request = $doc->saveXML(); + $response1 = $this->__doRequest($soap_request, "readUnit"); + if ($response1 == false) { + return false; + } + $response2 = $this->toArray($response1); + if ($response2 != false) { + if (isset($response2['soapenvBody']['soapenvFault'])) { + $this->error = true; + $this->errormsg = 'SOAP-Fault' . $response2['soapenvBody']['soapenvFault']['faultcode'] . " " . $response2['soapenvBody']['soapenvFault']['faultstring']; + return false; + } elseif (isset($response2['soapenvBody']['hisreadUnitResponse'])) { + $this->error = false; + $response3 = $response2['soapenvBody']['hisreadUnitResponse']; + $this->errormsg = ''; + return $response3; + } else { + $this->error = true; + $this->errormsg = "wrong url or the url send a xml in the wrong format"; + return false; + } + } + return false; + } + + /** + * @return array with days of the current week in datetime format + */ + private function getCurrentWeekDates() + { + $DateArray = array(); + $startdate = strtotime('Now'); + for ($i = 0; $i <= 7; $i++) { + $DateArray[] = date('Y-m-d', strtotime("+ {$i} day", $startdate)); + } + return $DateArray; + } + +} diff --git a/modules-available/locationinfo/inc/locationinfo.inc.php b/modules-available/locationinfo/inc/locationinfo.inc.php new file mode 100644 index 00000000..7617d143 --- /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 `setting_location_info` 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 `setting_location_info` 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..659dec8f --- /dev/null +++ b/modules-available/locationinfo/install.inc.php @@ -0,0 +1,163 @@ +<?php + +$res = array(); + +// TODO: serverid NULL, constraint to serverlist on delete set NULL +$res[] = tableCreate('location_info', ' + `locationid` INT(11) NOT NULL, + `serverid` INT(11) NOT NULL, + `serverroomid` VARCHAR(2000), + `hidden` BOOLEAN NOT NULL DEFAULT 0, + `openingtime` VARCHAR(2000), + `config` VARCHAR(2000), + `calendar` VARCHAR(2000), + `lastcalendarupdate` INT(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`locationid`) +'); + +// TODO: KEY `servername` (`servername`) +$res[] = tableCreate('setting_location_info', ' + `serverid` int(10) NOT NULL AUTO_INCREMENT, + `servername` VARCHAR(2000) NOT NULL, + `serverurl` VARCHAR(2000) NOT NULL, + `servertype` VARCHAR(100) NOT NULL, + `credentials` VARCHAR(2000), + `error` VARCHAR(2000), + PRIMARY KEY (`serverid`) +'); + +// Create response for browser + +if (!tableHasColumn('setting_location_info', 'credentials')) { + $ret = Database::exec("ALTER TABLE `setting_location_info` ADD `credentials` VARCHAR(2000) AFTER `servertype`"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Adding column credentials failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (!tableHasColumn('setting_location_info', 'error')) { + $ret = Database::exec("ALTER TABLE `setting_location_info` ADD `error` VARCHAR(2000) AFTER `credentials`"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Adding column error failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (tableHasColumn('setting_location_info', 'login')) { + $ret = Database::exec("ALTER TABLE `setting_location_info` DROP COLUMN login"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Dropping column login failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (tableHasColumn('setting_location_info', 'passwd')) { + $ret = Database::exec("ALTER TABLE `setting_location_info` DROP COLUMN passwd"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Dropping column passwd failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (tableHasColumn('location_info', 'serverroomid')) { + $ret = Database::exec("ALTER TABLE `location_info` MODIFY serverroomid VARCHAR(2000)"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Updateing column serverroomid failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (tableHasColumn('location_info', 'openingtime')) { + $ret = Database::exec("ALTER TABLE `location_info` MODIFY openingtime VARCHAR(2000)"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Updateing column openingtime failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (tableHasColumn('location_info', 'config')) { + $ret = Database::exec("ALTER TABLE `location_info` MODIFY config VARCHAR(2000)"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Updateing column config failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (tableHasColumn('location_info', 'calendar')) { + $ret = Database::exec("ALTER TABLE `location_info` MODIFY calendar VARCHAR(2000)"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Updateing column calendar failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (tableHasColumn('location_info', 'lastcalendarupdate')) { + $ret = Database::exec("ALTER TABLE `location_info` MODIFY lastcalendarupdate INT(11) NOT NULL DEFAULT 0"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Updateing column lastcalendarupdate failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (tableExists('locationinfo')) { + $ret = Database::exec("DROP TABLE `locationinfo`"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Droping table locationinfo failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (!tableHasColumn('location_info', 'config')) { + $ret = Database::exec("ALTER TABLE `location_info` ADD `config` VARCHAR(2000) NOT NULL DEFAULT '' AFTER `openingtime`"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Adding config to location_info failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (!tableHasColumn('location_info', 'calendar')) { + $ret = Database::exec("ALTER TABLE `location_info` ADD `calendar` VARCHAR(2000) NOT NULL DEFAULT '' AFTER `config`"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Adding calendar to location_info failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (!tableHasColumn('location_info', 'serverid')) { + $ret = Database::exec("ALTER TABLE `location_info` ADD `serverid` INT(11) NOT NULL AFTER `locationid`"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Adding serverid to location_info failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (!tableHasColumn('location_info', 'serverroomid')) { + $ret = Database::exec("ALTER TABLE `location_info` ADD `serverroomid` INT(11) NOT NULL AFTER `serverid`"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Adding serverroomid to location_info failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (!tableHasColumn('location_info', 'lastcalendarupdate')) { + $ret = Database::exec("ALTER TABLE `location_info` ADD `lastcalendarupdate` INT(11) NOT NULL AFTER `calendar`"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Adding lastcalendarupdate to location_info failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +if (!tableHasColumn('setting_location_info', 'servername')) { + $ret = Database::exec("ALTER TABLE `setting_location_info` ADD `servername` VARCHAR(2000) NOT NULL AFTER `serverid`"); + if ($ret === false) { + finalResponse(UPDATE_FAILED, 'Adding servername to setting_location_info failed: ' . Database::lastError()); + } + $res[] = UPDATE_DONE; +} + +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/davinci.json b/modules-available/locationinfo/lang/de/davinci.json new file mode 100644 index 00000000..0e0dcd23 --- /dev/null +++ b/modules-available/locationinfo/lang/de/davinci.json @@ -0,0 +1,3 @@ +{ + +}
\ No newline at end of file diff --git a/modules-available/locationinfo/lang/de/dummy.json b/modules-available/locationinfo/lang/de/dummy.json new file mode 100644 index 00000000..f68fb869 --- /dev/null +++ b/modules-available/locationinfo/lang/de/dummy.json @@ -0,0 +1,14 @@ +{ + "username": "Benutzer", + "username_title": "Das ist halt ein Username feld..", + "password": "Passwort 1", + "password_title": "Bla passwort bla bla", + "integer": "Zahl", + "integer_title": "Ein Zahlen felde?!", + "option": "Irgendein Array", + "option_title": "LALALA Hilfs- Text bla bla", + "CheckTheBox": "CheckBox", + "CheckTheBox_title": "Die checkbox ist ein wenig nutzlos", + "CB2 t": "Eine andere cb", + "CB2 t_title": ":P Diese checkbox ist super secret." +} diff --git a/modules-available/locationinfo/lang/de/hisinone.json b/modules-available/locationinfo/lang/de/hisinone.json new file mode 100644 index 00000000..d783c173 --- /dev/null +++ b/modules-available/locationinfo/lang/de/hisinone.json @@ -0,0 +1,10 @@ +{ + "username": "Nutzername", + "username_title": "Der Nutzername, der in HisInOne verwendet wird.", + "password": "Passwort", + "password_title": "Das Passwort, das in HisInOne verwendet wird.", + "open":"open", + "open_title":"Verwende die openCourseService Api.", + "role":"Rolle", + "role_title":"Die Rolle die der Nutzername in HisInOne verwendet." +}
\ 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..3197f333 100644 --- a/modules-available/locationinfo/lang/de/template-tags.json +++ b/modules-available/locationinfo/lang/de/template-tags.json @@ -1,3 +1,97 @@ { - "lang_hello": "Hallo" -}
\ No newline at end of file + "lang_mainHeader": "Infoscreen", + "lang_locationName": "Name", + "lang_locationID": "ID", + "lang_locationIsHidden": "Versteckt", + "lang_locationIsHidden_title": "Wenn aktiv, dann liefert die API keine Informationen über diesen Raum.", + "lang_locationInUse": "Rechner", + "lang_locationSettings": "Einstellungen", + "lang_locationConfig": "Config", + "lang_pcID": "ID", + "lang_pcIP": "IP", + "lang_pcX": "X", + "lang_pcY": "Y", + "lang_pcState": "PC Status", + "lang_day": "Tag", + "lang_openingTime": "Öffnungszeiten", + "lang_closingTime": "Schließungszeit", + "lang_shortMonday": "Mo", + "lang_shortTuesday": "Di", + "lang_shortWednesday": "Mi", + "lang_shortThursday": "Do", + "lang_shortFriday": "Fr", + "lang_shortSaturday": "Sa", + "lang_shortSunday": "So", + "lang_shortMonTilFr": "Mo - Fr", + "lang_monTilFr": "Montag - Freitag", + "lang_saturday": "Samstag", + "lang_sunday": "Sonntag", + "lang_expertMode": "Experten Modus", + "lang_expertMode_title": "Ermöglicht es dir Öffnungzeiten für jeden Tag zu Setzen.", + "lang_nameTooltip": "Legt den Namen des Servers fest.", + "lang_urlTooltip": "Server URL über den das backend versucht Kalender-daten zu holen.", + "lang_typeTooltip": "Legt fest um welchen Typ von Server es sich handelt.", + "lang_language": "Sprache", + "lang_languageEn": "Englisch", + "lang_languageDe": "Deutsch", + "lang_languagePt": "Portugiesisch", + "lang_languageTooltip": "Die Sprache, welche vom Frontend benutzt wird.", + "lang_mode": "Modus", + "lang_mode1": "Kalender & Raum", + "lang_mode2": "Kalender", + "lang_mode3": "Raum", + "lang_mode4": "Wechselnd", + "lang_modeTooltip": "Die Anzeigemodi, welche das frontend unterstützt.", + "lang_ecoMode": "E-Ink modus", + "lang_ecoTooltip": "Anstelle der Farb-basierten PC-Status Bilder, werden Symbol-basierte PC Bilder verwendet.", + "lang_daysToShow": "Tage", + "lang_daysToShowTooltip": "Legt die gewünschte Anzahl an Tagen im Kalender fest.", + "lang_scale": "Kalender breite", + "lang_scaleTooltip": "[10-90] Legt die Kalenderbreite fest. (In Prozent)", + "lang_switchTime": "Wechsel Zeit", + "lang_switchTimeTooltip": "[1-120] Legt die Zeit fest, die vergeht bis ein wechsel erfolgt (in sekunden)", + "lang_rotation": "Rotation", + "lang_rotation0": "0°", + "lang_rotation1": "90° ⟲", + "lang_rotation2": "180°", + "lang_rotation3": "90° ⟳", + "lang_rotationTooltip": "Rotiert den Raum.", + "lang_vertical": "Vertikaler Modus", + "lang_verticalTooltip": "Legt fest, ob der Kalender und der Raum übereinander angezeigt werden soll.", + "lang_updateRates": "Anfragraten", + "lang_calendar": "Kalender", + "lang_calupdateTooltip": "Zeit nachdem der Kalender geupdated wird (in minuten)", + "lang_room": "Raum", + "lang_roomupdateTooltip": "Zeit nachder die Pcs geupdated werden (in sekunden)", + "lang_config": "Einstellungen", + "lang_configupdateTooltip": "Zeit nachder die Einstellungen geupdated werden (in minuten)", + "lang_min": "min", + "lang_sec": "sec", + "lang_autoScale": "Auto Tage", + "lang_autoscaleTooltip": "Berechnet sich die optimale anzahl an Tagen, anhand der Bildschirmbreite, die der Kalender anzeigt.", + "lang_deleteConfirmation": "Bist du sicher?", + "lang_addServer": "Server", + "lang_addServer_title": "Server hinzufügen", + "lang_noServer": "<Kein Server>", + "lang_serverTable": "Server Liste", + "lang_buildingTable": "Gebäude / Raum Liste", + "lang_serverUrl": "URL", + "lang_customUrl": "Benutzerdefinierter URL", + "lang_customUrlTooltip": "Dieser URL überscheibt die Einstellungen von dem Raum.", + "lang_serverType": "Typ", + "lang_serverUser": "Benutzer", + "lang_serverPassword": "Passwort", + "lang_serverID": "Server ID", + "lang_sID": "sID", + "lang_server": "Server", + "lang_serverTooltip": "Legt fest, von welchem Server die Kalender Daten bezogen werden.", + "lang_roomId": "Raum ID", + "lang_roomIdTooltip": "Die ID vom Raum, welche der Server braucht, um die Kalender Daten abzurufen.", + "lang_credentials": "Anmeldung", + "lang_refresh_title": "Überprüft ob die Serververbindung gültig ist.", + "lang_delete": "Löschen", + "lang_refresh": "Aktualisieren", + "lang_general": "Allgemein", + "lang_server": "Server", + "lang_display": "Anzeige" +} diff --git a/modules-available/locationinfo/lang/en/davinci.json b/modules-available/locationinfo/lang/en/davinci.json new file mode 100644 index 00000000..0e0dcd23 --- /dev/null +++ b/modules-available/locationinfo/lang/en/davinci.json @@ -0,0 +1,3 @@ +{ + +}
\ No newline at end of file diff --git a/modules-available/locationinfo/lang/en/dummy.json b/modules-available/locationinfo/lang/en/dummy.json new file mode 100644 index 00000000..800fd80b --- /dev/null +++ b/modules-available/locationinfo/lang/en/dummy.json @@ -0,0 +1,14 @@ +{ + "username": "Username", + "username_title": "A fkn username field", + "password": "Password 1", + "password_title": "Password 1 title alalala :D", + "integer": "A integer value", + "integer_title": "What is this for?!", + "option": "Option Array", + "option_title": "LALALA OPtion title bla bla", + "CheckTheBox": "CheckBox", + "CheckTheBox_title": "Check this if you are bored", + "CB2 t": "Another cb", + "CB2 t_title": "meeh fk it. This cb does nothing. :P" +} diff --git a/modules-available/locationinfo/lang/en/hisinone.json b/modules-available/locationinfo/lang/en/hisinone.json new file mode 100644 index 00000000..3dd14602 --- /dev/null +++ b/modules-available/locationinfo/lang/en/hisinone.json @@ -0,0 +1,10 @@ +{ + "username": "username", + "username_title": "The username used in HisInOne.", + "password": "password", + "password_title": "The password used in HisInOne.", + "open":"open", + "open_title":"Use openCourseService api.", + "role":"role", + "role_title":"The role used by the username in HisInOne." +}
\ 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..8e0b2c8d 100644 --- a/modules-available/locationinfo/lang/en/template-tags.json +++ b/modules-available/locationinfo/lang/en/template-tags.json @@ -1,3 +1,103 @@ { - "lang_hello": "Hello" -}
\ No newline at end of file + "lang_mainHeader": "Infoscreen", + + "lang_locationName": "Name", + "lang_locationID": "ID", + "lang_locationIsHidden": "Hidden", + "lang_locationIsHidden_title": "If checked the API doesn't return information about the room.", + "lang_locationInUse": "Clients", + "lang_locationSettings": "Settings", + "lang_locationConfig": "Config", + + "lang_pcID": "ID", + "lang_pcIP": "IP", + "lang_pcX": "X", + "lang_pcY": "Y", + "lang_pcState": "PC state", + + "lang_day": "Day", + "lang_openingTime": "Opening times", + "lang_closingTime": "Closing time", + + "lang_shortMonday": "Mon", + "lang_shortTuesday": "Tue", + "lang_shortWednesday": "Wed", + "lang_shortThursday": "Thu", + "lang_shortFriday": "Fri", + "lang_shortSaturday": "Sat", + "lang_shortSunday": "Sun", + "lang_shortMonTilFr": "Mon - Fri", + "lang_monTilFr": "Monday - Friday", + "lang_saturday": "Saturday", + "lang_sunday": "Sunday", + "lang_expertMode": "Expert mode", + "lang_expertMode_title": "Allows you to set openingtimes for every day.", + + "lang_nameTooltip": "Defines the name of the server.", + "lang_urlTooltip": "Server URL on which the backend trys to get the calendar data from.", + "lang_typeTooltip": "Defines on which type of server you want to connect to.", + "lang_language": "Language", + "lang_languageEn": "English", + "lang_languageDe": "German", + "lang_languagePt": "Portugues", + "lang_languageTooltip": "The language the frontend uses.", + "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_ecoMode": "E-Ink mode", + "lang_ecoTooltip": "Symbolic based pc state pictures are used instead of the colour base ones.", + "lang_daysToShow": "Days", + "lang_daysToShowTooltip": "Defines the amount of days to show in the calendar", + "lang_scale": "Calendar width", + "lang_scaleTooltip": "[10-90] Defines the calendar width. (in percent)", + "lang_switchTime": "Switchtime", + "lang_switchTimeTooltip": "[1-120] Sets the time between switching (in seconds)", + "lang_rotation": "Rotation", + "lang_rotation0": "0°", + "lang_rotation1": "90° ⟲", + "lang_rotation2": "180°", + "lang_rotation3": "90° ⟳", + "lang_rotationTooltip": "Rotates the room.", + "lang_vertical": "Vertical mode", + "lang_verticalTooltip": "Defines whether the room and calendar are shown above each other.", + "lang_updateRates": "Update rates", + "lang_calendar": "Calendar", + "lang_calupdateTooltip": "Time the calender querys for updates (in minutes)", + "lang_room": "Room", + "lang_roomupdateTooltip": "Time the PCs in the room gets updated (in seconds)", + "lang_config": "Config", + "lang_configupdateTooltip": "Time interval the config gets updated (in minutes)", + "lang_min": "min", + "lang_sec": "sec", + "lang_autoScale": "Auto Days", + "lang_autoscaleTooltip": "Calculates the optimum amount of days to show from the display width.", + "lang_deleteConfirmation": "Are you sure?", + "lang_addServer": "Server", + "lang_addServer_title": "Add server", + "lang_noServer": "<no server>", + "lang_serverTable": "Server List", + "lang_buildingTable": "Building / Room List", + "lang_serverUrl": "URL", + "lang_customUrl": "Custom URL", + "lang_customUrlTooltip": "This URL will override the config settings from the room.", + "lang_serverType": "Type", + "lang_serverUser": "User", + "lang_serverPassword": "Password", + "lang_serverID": "Server ID", + "lang_sID": "sID", + "lang_server": "Server", + "lang_serverTooltip": "Defines from which server the room queries the calendar data.", + "lang_roomId": "Room ID", + "lang_roomIdTooltip": "The ID of the room the server needs, for querying the calendar data.", + "lang_credentials": "Login", + "lang_refresh_title": "Checks if the server connection is valid.", + "lang_delete": "Delete", + "lang_refresh": "Refresh", + "lang_general": "General", + + "lang_server": "Server", + "lang_display": "Display" +} 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..31702dff 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,597 @@ 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 `setting_location_info` 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', 30, 'int'); + $result['roomupdate'] = Request::post('roomupdate', 30, 'int'); + $result['configupdate'] = Request::post('configupdate', 180, 'int'); + $serverid = Request::post('serverid', 0, 'int'); + $serverroomid = Request::post('serverroomid', '', 'string'); + + Database::exec("INSERT INTO `location_info` (locationid, serverid, serverroomid, config, lastcalendarupdate) + VALUES (:id, :serverid, :serverroomid, :config, 0) + ON DUPLICATE KEY UPDATE config = VALUES(config), serverid = VALUES(serverid), + serverroomid = VALUES(serverroomid), lastcalendarupdate = 0", array( + 'id' => $locationid, + 'config' => json_encode($result), + 'serverid' => $serverid, + 'serverroomid' => $serverroomid, + )); + + 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'); + $serverurl = Request::post('url', '', '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->getCredentials(); + + $credentialsJson = array(); + $counter = 0; + foreach ($tmptypeArray as $key => $value) { + $credentialsJson[$key] = Request::post($counter); + $counter++; + } + $params = array( + 'name' => $servername, + 'url' => $serverurl, + 'type' => $servertype, + 'credentials' => json_encode($credentialsJson) + ); + if ($serverid === 0) { + Database::exec('INSERT INTO `setting_location_info` (servername, serverurl, servertype, credentials) + VALUES (:name, :url, :type, :credentials)', $params); + $this->checkConnection(Database::lastInsertId()); + } else { + $params['id'] = $serverid; + Database::exec('UPDATE `setting_location_info` + SET servername = :name, serverurl = :url, 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 `location_info` 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 `location_info` (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 `location_info` (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, serverurl + FROM `setting_location_info` + 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(json_decode($dbresult['credentials'], true), $dbresult['serverurl'], $serverid); + + 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 location_info (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 location_info 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 location_info (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 `location_info` 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 `setting_location_info`"); + 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) + { + $dbresult = Database::queryFirst('SELECT servername, serverurl, servertype, credentials + FROM `setting_location_info` WHERE serverid = :id', array('id' => $id)); + + // Credentials stuff. + $dbcredentials = json_decode($dbresult['credentials'], true); + + // Get a list of all the backend types. + $serverBackends = array(); + $s_list = CourseBackend::getList(); + foreach ($s_list as $s) { + $backend['typ'] = $s; + $backendInstance = CourseBackend::getInstance($s); + $backend['display'] = $backendInstance->getDisplayName(); + + if ($backend['typ'] == $dbresult['servertype']) { + $backend['active'] = true; + } else { + $backend['active'] = false; + } + + $credentials = $backendInstance->getCredentials(); + $backend['credentials'] = array(); + + $counter = 0; + foreach ($credentials as $key => $value) { + $credential['uid'] = $counter; + $credential['name'] = Dictionary::translateFile($s, $key); + $credential['type'] = $value; + $credential['title'] = Dictionary::translateFile($s, $key . "_title"); + + if (Property::getPasswordFieldType() === 'text') { + $credential['mask'] = false; + } else { + if ($value == "password") { + $credential['mask'] = true; + } + } + + if ($backend['typ'] == $dbresult['servertype']) { + foreach ($dbcredentials as $k => $v) { + if ($k == $key) { + $credential['value'] = $v; + break; + } + } + } + + $selection = array(); + + if (is_array($value)) { + + $selfirst = true; + foreach ($value as $opt) { + $option['option'] = $opt; + if (isset($credential['value'])) { + if ($opt == $credential['value']) { + $option['active'] = true; + } else { + $option['active'] = false; + } + } else { + if ($selfirst) { + $option['active'] = true; + $selfirst = false; + } else { + $option['active'] = false; + } + } + + $selection[] = $option; + } + $credential['type'] = "array"; + $credential['array'] = $selection; + } + + $backend['credentials'][] = $credential; + + $counter++; + } + $serverBackends[] = $backend; + } + + echo Render::parse('server-settings', array('id' => $id, + 'name' => $dbresult['servername'], + 'url' => $dbresult['serverurl'], + 'servertype' => $dbresult['servertype'], + 'backendList' => array_values($serverBackends))); + } + + /** + * Ajax the time table + * + * @param $id id of the location + */ + private function ajaxTimeTable($id) + { + $row = Database::queryFirst("SELECT openingtime FROM `location_info` 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, serverroomid FROM `location_info` 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 setting_location_info 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'], + 'serverroomid' => $location['serverroomid'] )); } diff --git a/modules-available/locationinfo/style.css b/modules-available/locationinfo/style.css new file mode 100644 index 00000000..ed1bda76 --- /dev/null +++ b/modules-available/locationinfo/style.css @@ -0,0 +1,14 @@ +.glyphicon-refresh-animate { + -animation: spin .7s infinite linear; + -webkit-animation: spin2 .7s infinite linear; +} + +@-webkit-keyframes spin2 { + from { -webkit-transform: rotate(0deg);} + to { -webkit-transform: rotate(360deg);} +} + +@keyframes spin { + from { transform: scale(1) rotate(0deg);} + to { transform: scale(1) rotate(360deg);} +} 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..e97b72fc --- /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="serverroomid" id="serverroomid" value="{{serverroomid}}"> + </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> %</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> {{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..bd3ea077 --- /dev/null +++ b/modules-available/locationinfo/templates/location-info.html @@ -0,0 +1,215 @@ +<div> + <h1>{{lang_mainHeader}}</h1> + + <h4>{{lang_serverTable}}</h4> + + <table class="table table-condensed table-hover"> + <tr> + <th width="1">{{lang_serverType}}</th> + <th>{{lang_locationName}}</th> + <th>{{lang_serverUrl}}</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 nowrap>{{serverurl}}</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" + title="{{lang_refresh_title}}" type="submit"> + <span style="margin-right: 5px;" class="glyphicon glyphicon-refresh"></span> + {{lang_refresh}} + </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 title="{{lang_addServer_title}}"> + <span style="margin-right: 5px;" class="glyphicon glyphicon-plus"></span> + {{lang_addServer}} + </span> + </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"> + <input class="hidden-toggle" type="checkbox" data-locationid="{{locationid}}" {{hidden_checked}}> + </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'); + }); + }); + + /** + * 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"); + $('#myModalSubmitButton').attr("form", "settingsForm"); + $('#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-settings.html b/modules-available/locationinfo/templates/server-settings.html new file mode 100644 index 00000000..c135543f --- /dev/null +++ b/modules-available/locationinfo/templates/server-settings.html @@ -0,0 +1,181 @@ +<div> + <form method="post" action="?do=locationinfo" id="settingsForm"> + <input type="hidden" name="token" value="{{token}}"> + <input type="hidden" name="action" value="updateServerSettings"> + <input type="hidden" name="id" value="{{id}}"> + + + <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_locationName}}</label> + </div> + <div class="col-md-7"> + <input required class="form-control" id="input-name-{{id}}" name="name" type="text" + value="{{name}}"> + </div> + <div class="col-md-2"> + <a class="btn btn-default" id="help-name" 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_serverUrl}}</label> + </div> + <div class="col-md-7"> + <input required class="form-control" id="input-url-{{id}}" name="url" type="text" + value="{{url}}"> + </div> + <div class="col-md-2"> + <a class="btn btn-default" id="help-url" title="{{lang_urlTooltip}}"> + <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" id="input-type-{{id}}" name="type" value="{{typ}}" + onchange="servertype_changed(this.value);"> + {{#backendList}} + <option id="{{typ}}" value="{{typ}}" {{#active}}selected{{/active}}>{{display}} + {{/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> + + <div class="panel panel-default" id="credentials-div"> + <div class="panel-heading">{{lang_credentials}}</div> + <div class="panel-body"> + <div class="list-group" id="credentials-list"></div> + </div> + </div> + <div id="credentials" class="list-group"></div> + </form> +</div> +<script type="text/javascript"> + var type = "{{servertype}}"; + if (type == "") { + type = $('#input-type-{{id}}').val(); + } + + loadCredentials(true); + initalizeBootstrap(); + + /** + * Initialize the bootstrap elements. + */ + function initalizeBootstrap() { + $('#help-name').tooltip(); + $('#help-url').tooltip(); + $('#help-type').tooltip(); + } + + /** + * Loads the dynamic credentials forms. + * + * @param {bool} useValue If false the form elements will be empty. Default = true. + */ + function loadCredentials(useValue) { + // {{name}} name of auth {{type}} type of auth (string, int etc.) {{value}} value from the db + {{#backendList}} + if (type == "{{typ}}") { + {{#credentials}} + $('#credentials-div').fadeIn('fast'); + + if ("{{type}}" == "string") { + $("#credentials-list").append('<div class="list-group-item"><div class="row">\ + <div class="col-md-3"><label>{{name}}</label></div>\ + <div class="col-md-7">\ + <input required class="form-control" id="input-{{uid}}" type="text" name="{{uid}}" value="{{value}}" form="settingsForm"></div>\ + <div class="col-md-2"><a class="btn btn-default" id="help-{{uid}}" title="{{title}}"><span class="glyphicon glyphicon-question-sign"></span></a></div>\ + </div></div>'); + } else if ("{{type}}" == "int") { + $("#credentials-list").append('<div class="list-group-item"><div class="row">\ + <div class="col-md-3"><label>{{name}}</label></div>\ + <div class="col-md-7">\ + <input required class="form-control" id="input-{{uid}}" type="number" name="{{uid}}" value="{{value}}" form="settingsForm"></div>\ + <div class="col-md-2"><a class="btn btn-default" id="help-{{uid}}" title="{{title}}"><span class="glyphicon glyphicon-question-sign"></span></a></div>\ + </div></div>'); + } else if ("{{type}}" == "password") { + $("#credentials-list").append('<div class="list-group-item"><div class="row">\ + <div class="col-md-3"><label>{{name}}</label></div>\ + <div class="col-md-7">\ + <input required class="form-control" id="input-{{uid}}" {{#mask}}type="password"{{/mask}}{{^mask}}type="text"{{/mask}} name="{{uid}}" value="{{value}}" form="settingsForm"></div>\ + <div class="col-md-2"><a class="btn btn-default" id="help-{{uid}}" title="{{title}}"><span class="glyphicon glyphicon-question-sign"></span></a></div>\ + </div></div>'); + } else if ("{{type}}" == "bool") { + $("#credentials-list").append('<div class="list-group-item"><div class="row">\ + <div class="col-md-3"><label>{{name}}</label></div>\ + <div class="col-md-7">\ + <input class="bs-switch" id="input-{{uid}}" type="checkbox" name="{{uid}}" value="true" form="settingsForm" {{#value}}checked{{/value}}></div>\ + <div class="col-md-2"><a class="btn btn-default" id="help-{{uid}}" title="{{title}}"><span class="glyphicon glyphicon-question-sign"></span></a></div>\ + </div></div>'); + $('#input-{{uid}}').bootstrapSwitch({ + size: "small" + }); + } else if ("{{type}}" == "array") { + $("#credentials-list").append('<div class="list-group-item"><div class="row">\ + <div class="col-md-3"><label>{{name}}</label></div>\ + <div class="col-md-7">\ + <select class="form-control" id="input-selection-{{uid}}" name="{{uid}}" form="settingsForm">\ + {{#array}}\ + <option value="{{option}}" {{#active}}selected{{/active}}>{{option}}</option>\ + {{/array}}\ + </select></div>\ + <div class="col-md-2"><a class="btn btn-default" id="help-{{uid}}" title="{{title}}"><span class="glyphicon glyphicon-question-sign"></span></a></div>\ + </div></div>'); + } + + $('#help-{{uid}}').tooltip(); + if (!useValue) { + $('#input-{{uid}}').val(""); + } + {{/credentials}} + {{^credentials}} + $('#credentials-div').hide(); + {{/credentials}} + } + {{/backendList}} + + } + + /** + * After the servertype switch changed, the new credentials needs to be loaded. + * + * @param {string} value The new type of the server which credentials needs to be loaded. + */ + function servertype_changed(value) { + type = value; + $('#credentials-div').fadeOut('fast', function() { + $('#credentials-list').empty(); + loadCredentials(false); + }); + } + +</script> diff --git a/modules-available/locationinfo/templates/timetable.html b/modules-available/locationinfo/templates/timetable.html new file mode 100644 index 00000000..4790a60c --- /dev/null +++ b/modules-available/locationinfo/templates/timetable.html @@ -0,0 +1,216 @@ +<div> + <div align="right"> + <label for="CB_1" title="{{lang_expertMode_title}}">{{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 73080094..7ac76b3e 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(); |