diff --git a/Layout/default/MaintenanceNotification/Form.php b/Layout/default/MaintenanceNotification/Form.php new file mode 100644 index 000000000..3d5548cf0 --- /dev/null +++ b/Layout/default/MaintenanceNotification/Form.php @@ -0,0 +1,117 @@ + + + +
+
+
+
+ +
+

id) ? "Wartungsmeldung bearbeiten" : "Neue Wartungsmeldung" ?>

+
+
+
+ + +
+
+ +
+
+ +
" enctype="multipart/form-data"> + +
+
+ + + + +
+
+ from) : ""?>" /> +
+
+ from) : ""?>" /> +
+
+ + +
+
+ to) : ""?>"/> +
+
+ to) : ""?>"/> +
+
+ +
+ + + PLZ' mit Leerzeichen oder Komma getrennt +
+ + +
+ +
+ + +
+ + + + + +
+
+ +
+ +
+ + +
+
+ + + +
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/Layout/default/MaintenanceNotification/Index.php b/Layout/default/MaintenanceNotification/Index.php new file mode 100644 index 000000000..e87f2da9f --- /dev/null +++ b/Layout/default/MaintenanceNotification/Index.php @@ -0,0 +1,107 @@ + + + +
+
+
+
+ +
+

Wartungsmeldungen

+
+
+
+ + +
+
+ +
+
+

Filter

+ +
"> +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + ?resetFilter=1">Filter zurücksetzen +
+
+
+ +
+
+ + +
+
+
+

Liste offener Wartungsmeldungen

+
+ + + + + + + + + + + + + + + + + + + + + + + +
BetreffPLZ-BereichVersandterminAussendung abgeschlossenErstelltBearbeitet
subject?>plz) ? implode(", ", $notification->plzs) : ""?>send_ts) ? date("d.m.Y H:i", $notification->send_ts) : ""?>sent) ? "Ja" : "nein"?>create)?> (editor->name?>)edit)?> (creator->name?>) + $notification->id])?>"> + $notification->id])?>" class="text-danger" onclick="if(!confirm('Wartungsmeldung wirklich löschen?')) return false;" title="Wartungsmeldung Löschen"> +
+
+
+ +
+
+ + diff --git a/Layout/default/header.php b/Layout/default/header.php index 6f1b03592..b3f2dd54a 100644 --- a/Layout/default/header.php +++ b/Layout/default/header.php @@ -41,6 +41,8 @@ + + diff --git a/Layout/default/menu.php b/Layout/default/menu.php index a9dba7681..9d4bde680 100644 --- a/Layout/default/menu.php +++ b/Layout/default/menu.php @@ -133,6 +133,7 @@ isAdmin()) : ?>
  • "> Domains
  • isAdmin()) : ?>
  • "> IPAM
  • isAdmin()) : ?>
  • "> Device Congestion
  • + isAdmin()) : ?>
  • "> Wartungsmeldungen
  • diff --git a/application/MaintenanceNotification/MaintenanceNotification.php b/application/MaintenanceNotification/MaintenanceNotification.php new file mode 100644 index 000000000..ad8e915b6 --- /dev/null +++ b/application/MaintenanceNotification/MaintenanceNotification.php @@ -0,0 +1,200 @@ +$name == null) { + + if($name == "plzs") { + if(!$this->plz) return []; + $plzs = json_decode($this->plz); + if(!is_array($plzs)) return []; + $this->plzs = $plzs; + return $this->plzs; + } + + if($name == "creator") { + $user = mfValuecache::singleton()->get("Worker-id-".$this->create_by); + if($user) { + $this->creator = $user; + return $this->creator; + } + $this->creator = new User($this->create_by); + if($this->creator->id) { + mfValuecache::singleton()->set("Worker-id-".$this->create_by, $this->creator); + } + return $this->creator; + } + + if($name == "editor") { + $this->editor = new User($this->edit_by); + return $this->editor; + } + + $classname = ucfirst($name); + $idfield = $name."_id"; + $this->$name = mfValuecache::singleton()->get("mfObjectmodel-$name-".$this->$idfield); + if(!$this->$name) { + $this->$name = new $classname($this->$idfield); + } + + if($this->$name->id) { + mfValuecache::singleton()->set("mfObjectmodel-$name-".$this->$name->id, $this->$name); + return $this->$name; + } else { + return null; + } + } + + return $this->$name; + } + + /******************************** + * Begin static Model functions + */ + + public static function create(Array $data) { + $model = new MaintenanceNotification(); + + $table_fields = [ + "subject_id", "text", "plz", "from", "to", "send_ts", "sent", "sent_by", + "create_by", "edit_by", "create", "edit" + ]; + + foreach($data as $field => $value) { + if(in_array($field, $table_fields)) { + $model->$field = $value; + } + } + + $me = new User(); + $me->loadMe(); + + if($model->create_by === null) { + $model->create_by = $me->id; + } + if($model->edit_by === null) { + $model->edit_by = $me->id; + } + + return $model; + } + + public static function getAll() { + $items = []; + + $db = FronkDB::singleton(); + + $res = $db->select("MaintenanceNotification", "*", "1 = 1 ORDER BY `create`"); + if($db->num_rows($res)) { + while($data = $db->fetch_object($res)) { + $items[] = new MaintenanceNotification($data); + } + } + return $items; + + } + + public static function getFirst($filter) { + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $sql = "SELECT * FROM MaintenanceNotification + WHERE $where + ORDER BY adb_hausnummer_id LIMIT 1"; + //var_dump($sql);exit; + $res = $db->query($sql); + if($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new MaintenanceNotification($data); + if($item->id) { + return $item; + } else { + return null; + } + } + return null; + } + + public static function count($filter) { + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $sql = "SELECT COUNT(*) as cnt FROM MaintenanceNotification + WHERE $where"; + + //mfLoghandler::singleton()->debug($sql); + + $res = $db->query($sql); + if($db->num_rows($res)) { + $data = $db->fetch_object($res); + return $data->cnt; + } + return 0; + } + + public static function search($filter, $limit = false, $order = false) { + //var_dump($filter);exit; + $items = []; + + if(!$order) { + $order = "`create` ASC"; + } + + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $sql = "SELECT * FROM MaintenanceNotification + WHERE $where + ORDER BY $order"; + + if(is_array($limit) && count($limit)) { + if(is_numeric($limit['start']) && is_numeric($limit['count'])) { + $sql .= " LIMIT ".$limit['start'].", ".$limit['count']; + } elseif(is_numeric($limit['count'])) { + $sql .= " LIMIT ".$limit['count']; + } + } + + mfLoghandler::singleton()->debug($sql); + + $res = $db->query($sql); + if($db->num_rows($res)) { + while($data = $db->fetch_object($res)) { + $items[$data->id] = new MaintenanceNotification($data); + } + } + + return $items; + } + + private static function getSqlFilter($filter) { + $where = "1=1 "; + + if(array_key_exists("termination_id", $filter)) { + $termination_id = $filter['termination_id']; + if(is_numeric($termination_id)) { + $where .= " AND MaintenanceNotification.termination_id=$termination_id"; + } + } + + if(array_key_exists("object_type", $filter)) { + $object_type = FronkDB::singleton()->escape($filter["object_type"]); + if($object_type) { + $where .= " AND object_type='$object_type'"; + } + } + + + + + if(array_key_exists("add-where", $filter)) { + $where .= " ".$filter['add-where']; + } + + //var_dump($filter, $where);exit; + return $where; + } +} \ No newline at end of file diff --git a/application/MaintenanceNotification/MaintenanceNotificationController.php b/application/MaintenanceNotification/MaintenanceNotificationController.php new file mode 100644 index 000000000..b1d57f6be --- /dev/null +++ b/application/MaintenanceNotification/MaintenanceNotificationController.php @@ -0,0 +1,162 @@ +needlogin=true; + $me = new User(); + $me->loadMe(); + $this->me = $me; + $this->layout()->set("me",$me); + + if(!$me->is(["Admin"])) { + $this->redirect("Dashboard"); + } + } + + protected function indexAction() { + if($this->request->resetFilter) { + unset($_SESSION[MFAPPNAME.'-MaintenanceNotification-filter']); + } + + $filter = []; + if(is_array($this->request->filter)) { + $filter = $this->request->filter; + $_SESSION[MFAPPNAME.'-MaintenanceNotification-filter'] = $filter; + } else { + if(array_key_exists(MFAPPNAME.'-MaintenanceNotification-filter', $_SESSION) && count($_SESSION[MFAPPNAME.'-MaintenanceNotification-filter'])) { + $filter = $_SESSION[MFAPPNAME.'-MaintenanceNotification-filter']; + } + } + + $this->layout->set("filter", $filter); + $filter = $this->getPreparedFilter($filter); + + + // pagination defaults + $pagination = []; + $pagination['start'] = 0; + $pagination['count'] = 20; + $pagination['maxItems'] = 0; + + if(is_numeric($this->request->s)) { + $pagination['start'] = intval($this->request->s); + } + + $pagination['maxItems'] = MaintenanceNotification::count($filter); + $this->layout()->set("pagination", $pagination); + + $templates = MaintenanceNotification::search($filter, $pagination); + $this->layout()->set("notifications", $templates); + } + + private function getPreparedFilter($filter) : array + { + $new_filter = []; + + if (is_array($filter) && count($filter)) { + foreach ($filter as $name => $value) { + $new_filter[$name] = $value; + } + } + + return $new_filter; + } + + protected function addAction() { + $this->layout()->setTemplate("MaintenanceNotification/Form"); + } + + protected function editAction() { + $id = $this->request->id; + if(!is_numeric($id) || $id < 1) { + $this->layout()->setFlash("Wartungsmeldung nicht gefunden", "error"); + $this->redirect("MaintenanceNotification"); + } + + $notification = new MaintenanceNotification($id); + if(!$notification || !$notification->id) { + $this->layout()->setFlash("Wartungsmeldung nicht gefunden", "error"); + $this->redirect("MaintenanceNotification"); + } + + $this->layout()->set("notification", $notification); + $this->addAction(); + } + + protected function saveAction() { + $r = $this->request; + var_dump($r->get()); + $id = $r->id; + + if(is_numeric($id) && $id > 0) { + $mode = "edit"; + $notification = new MaintenanceNotification($id); + if(!$notification->id) { + $this->layout()->setFlash("Wartungsmeldung nicht gefunden", "error"); + $this->redirect("MaintenanceNotification"); + } + } else { + $mode = "add"; + } + + $data = []; + $data["subject_id"] = $r->subject_id; + $data["text"] = $r->text; + + // plz -> json array + $plzs = $r->plz; + if($plzs) { + $plz_string = preg_replace('/[^0-9]+/', ",", $plzs); + $data["plz"] = json_encode(explode(",", $plz_string)); + } + + // from / to -> date/time -> DateTime + + try { + $from = new DateTime($r->from_date." ".$r->from_time); + } catch(Exception $e) { + $this->layout()->setFlash("Ungültiges Wartungsfenster Von-Zeitpunkt", "error"); + if($mode = "edit") { + return $this->editAction(); + } + return $this->addAction(); + } + $data["from"] = $from->getTimestamp(); + + try { + $to = new DateTime($r->to_date." ".$r->to_time); + } catch(Exception $e) { + $this->layout()->setFlash("Ungültiges Wartungsfenster Bis-Zeitpunkt", "error"); + if($mode = "edit") { + return $this->editAction(); + } + return $this->addAction(); + } + $data["to"] = $to->getTimestamp(); + + // send_ts - feld einbauen -> DateTime + + + //var_dump($data);exit; + + if($mode == "add") { + $notification = MaintenanceNotification::create($data); + } else { + $notification->update($data); + } + + if(!$notification->save()) { + $this->layout()->setFlash("Fehler beim Speichern", "error"); + if($mode = "edit") { + return $this->editAction(); + } + return $this->addAction(); + } + + $this->layout()->setFlash("Wartungsmeldung erfolgreich gespeichert", "success"); + $this->redirect("MaintenanceNotification", "edit", ["id" => $notification->id]); + + + } +} \ No newline at end of file diff --git a/application/MaintenanceNotificationTemplate/MaintenanceNotificationTemplate.php b/application/MaintenanceNotificationTemplate/MaintenanceNotificationTemplate.php new file mode 100644 index 000000000..9e0bc9207 --- /dev/null +++ b/application/MaintenanceNotificationTemplate/MaintenanceNotificationTemplate.php @@ -0,0 +1,192 @@ +$name == null) { + + if($name == "creator") { + $user = mfValuecache::singleton()->get("Worker-id-".$this->create_by); + if($user) { + $this->creator = $user; + return $this->creator; + } + $this->creator = new User($this->create_by); + if($this->creator->id) { + mfValuecache::singleton()->set("Worker-id-".$this->create_by, $this->creator); + } + return $this->creator; + } + + if($name == "editor") { + $this->editor = new User($this->edit_by); + return $this->editor; + } + + $classname = ucfirst($name); + $idfield = $name."_id"; + $this->$name = mfValuecache::singleton()->get("mfObjectmodel-$name-".$this->$idfield); + if(!$this->$name) { + $this->$name = new $classname($this->$idfield); + } + + if($this->$name->id) { + mfValuecache::singleton()->set("mfObjectmodel-$name-".$this->$name->id, $this->$name); + return $this->$name; + } else { + return null; + } + } + + return $this->$name; + } + + /******************************** + * Begin static Model functions + */ + + public static function create(Array $data) { + $model = new MaintenanceNotificationTemplate(); + + $table_fields = [ + "subject", "text", + "create_by", "edit_by", "create", "edit" + ]; + + foreach($data as $field => $value) { + if(in_array($field, $table_fields)) { + $model->$field = $value; + } + } + + $me = new User(); + $me->loadMe(); + + if($model->create_by === null) { + $model->create_by = $me->id; + } + if($model->edit_by === null) { + $model->edit_by = $me->id; + } + + return $model; + } + + public static function getAll() { + $items = []; + + $db = FronkDB::singleton(); + + $res = $db->select("MaintenanceNotificationTemplate", "*", "1 = 1 ORDER BY subject"); + if($db->num_rows($res)) { + while($data = $db->fetch_object($res)) { + $items[] = new MaintenanceNotificationTemplate($data); + } + } + return $items; + + } + + public static function getFirst($filter) { + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $sql = "SELECT * FROM MaintenanceNotificationTemplate + WHERE $where + ORDER BY adb_hausnummer_id LIMIT 1"; + //var_dump($sql);exit; + $res = $db->query($sql); + if($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new MaintenanceNotificationTemplate($data); + if($item->id) { + return $item; + } else { + return null; + } + } + return null; + } + + public static function count($filter) { + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $sql = "SELECT COUNT(*) as cnt FROM MaintenanceNotificationTemplate + WHERE $where"; + + //mfLoghandler::singleton()->debug($sql); + + $res = $db->query($sql); + if($db->num_rows($res)) { + $data = $db->fetch_object($res); + return $data->cnt; + } + return 0; + } + + public static function search($filter, $limit = false, $order = false) { + //var_dump($filter);exit; + $items = []; + + if(!$order) { + $order = "subject ASC"; + } + + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $sql = "SELECT * FROM MaintenanceNotificationTemplate + WHERE $where + ORDER BY $order"; + + if(is_array($limit) && count($limit)) { + if(is_numeric($limit['start']) && is_numeric($limit['count'])) { + $sql .= " LIMIT ".$limit['start'].", ".$limit['count']; + } elseif(is_numeric($limit['count'])) { + $sql .= " LIMIT ".$limit['count']; + } + } + + mfLoghandler::singleton()->debug($sql); + + $res = $db->query($sql); + if($db->num_rows($res)) { + while($data = $db->fetch_object($res)) { + $items[$data->id] = new MaintenanceNotificationTemplate($data); + } + } + + return $items; + } + + private static function getSqlFilter($filter) { + $where = "1=1 "; + + if(array_key_exists("termination_id", $filter)) { + $termination_id = $filter['termination_id']; + if(is_numeric($termination_id)) { + $where .= " AND MaintenanceNotificationTemplate.termination_id=$termination_id"; + } + } + + if(array_key_exists("object_type", $filter)) { + $object_type = FronkDB::singleton()->escape($filter["object_type"]); + if($object_type) { + $where .= " AND object_type='$object_type'"; + } + } + + + + + if(array_key_exists("add-where", $filter)) { + $where .= " ".$filter['add-where']; + } + + //var_dump($filter, $where);exit; + return $where; + } +} \ No newline at end of file diff --git a/db/migrations/20241112130541_create_maintanence_notification.php b/db/migrations/20241112130541_create_maintanence_notification.php new file mode 100644 index 000000000..dd4fad788 --- /dev/null +++ b/db/migrations/20241112130541_create_maintanence_notification.php @@ -0,0 +1,57 @@ +getEnvironment() == "thetool") { + $table = $this->table("MaintenanceNotification"); + $table->addColumn("subject_id", "integer", ["null" => false]); + $table->addColumn("text", "text", ["null" => false]); + $table->addColumn("plz", "json", ["null" => true, "default" => null]); + $table->addColumn("from", "integer", ["null" => true]); + $table->addColumn("to", "integer", ["null" => true]); + $table->addColumn("send_ts", "integer", ["null" => true]); + $table->addColumn("sent", "integer", ["null" => true, "default" => null]); + $table->addColumn("sent_by", "integer", ["null" => true, "default" => null]); + + $table->addColumn("create_by", "integer", ["null" => false]); + $table->addColumn("edit_by", "integer", ["null" => false]); + $table->addColumn("create", "integer", ["null" => false]); + $table->addColumn("edit", "integer", ["null" => false]); + $table->create(); + + $log = $this->table("MaintenanceNotificationLog"); + $log->addColumn("maintenancenotification_id", "integer", ["null" => false]); + $log->addColumn("address_id", "integer", ["null" => true, "default" => null]); + $log->addColumn("email", "string", ["null" => false]); + $log->addColumn("sent", "integer", ["null" => false, "default" => 0]); + + $log->addColumn("create_by", "integer", ["null" => false]); + $log->addColumn("edit_by", "integer", ["null" => false]); + $log->addColumn("create", "integer", ["null" => false]); + $log->addColumn("edit", "integer", ["null" => false]); + $log->create(); + + } + + if($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if($this->getEnvironment() == "thetool") { + $this->table("MaintenanceNotificationLog")->drop()->save(); + $this->table("MaintenanceNotification")->drop()->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } +} diff --git a/db/migrations/20241112155250_create_maintenancenotificationtemplate.php b/db/migrations/20241112155250_create_maintenancenotificationtemplate.php new file mode 100644 index 000000000..a0177d839 --- /dev/null +++ b/db/migrations/20241112155250_create_maintenancenotificationtemplate.php @@ -0,0 +1,37 @@ +getEnvironment() == "thetool") { + $table = $this->table("MaintenanceNotificationTemplate"); + $table->addColumn("subject", "string", ["null" => false]); + $table->addColumn("text", "text", ["null" => false]); + + $table->addColumn("create_by", "integer", ["null" => false]); + $table->addColumn("edit_by", "integer", ["null" => false]); + $table->addColumn("create", "integer", ["null" => false]); + $table->addColumn("edit", "integer", ["null" => false]); + $table->create(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if($this->getEnvironment() == "thetool") { + $this->table("MaintenanceNotificationTemplate")->drop()->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } +} diff --git a/public/js/dayjs.min.js b/public/js/dayjs.min.js new file mode 100644 index 000000000..61916d882 --- /dev/null +++ b/public/js/dayjs.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs=e()}(this,(function(){"use strict";var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",u="hour",a="day",o="week",c="month",f="quarter",h="year",d="date",l="Invalid Date",$=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var e=["th","st","nd","rd"],n=t%100;return"["+t+(e[(n-20)%10]||e[n]||e[0])+"]"}},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},v={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+m(r,2,"0")+":"+m(i,2,"0")},m:function t(e,n){if(e.date()1)return t(u[0])}else{var a=e.name;D[a]=e,i=a}return!r&&i&&(g=i),i||!r&&g},O=function(t,e){if(S(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},b=v;b.l=w,b.i=S,b.w=function(t,e){return O(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=w(t.locale,null,!0),this.parse(t),this.$x=this.$x||t.x||{},this[p]=!0}var m=M.prototype;return m.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(b.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.init()},m.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},m.$utils=function(){return b},m.isValid=function(){return!(this.$d.toString()===l)},m.isSame=function(t,e){var n=O(t);return this.startOf(e)<=n&&n<=this.endOf(e)},m.isAfter=function(t,e){return O(t) (matches[4] ? 0 : -1) && parseInt(matches[1]) < (matches[4] ? 13 : 24) + && (matches[2] === undefined || (parseInt(matches[2]) > -1 && parseInt(matches[2]) < 60)) + && (matches[3] === undefined || (parseInt(matches[3]) > -1 && parseInt(matches[3]) < 60))) + { + let hour = parseInt(matches[1]); + if (matches[4]) + { + hour = hour % 12 + ((matches[4].toUpperCase() == 'P') ? 12 : 0); + } + input_time = dayjs().hour(hour).minute(matches[2] == undefined ? 0 : parseInt(matches[2])).second((matches[3] === undefined) ? 0 : parseInt(matches[3])); + } + else + { + input_time = (options && options.format) + ? dayjs(str, options.format) + : dayjs(str); + } + } + else + { + input_time = dayjs(str); + } + return (input_time && input_time.isValid()) ? input_time : false; +} + +/** + * Return allowed unit text object based on min time, max time, and step + * + * @param {object} options + * @return {object} + */ +function getUnitText(options) +{ + const minTime = options.minTime || dayjs().startOf('day'); + const maxTime = options.maxTime || dayjs().endOf('day'); + const step = options.step || 60; + let valid = { offset: {}, hour: {}, minute: {}, second: {}, meridiem: {}, length: 0 }; + let iTime = minTime.clone(); + const unixOffset = minTime.startOf('day').unix(); + while (iTime.isBefore(maxTime) || iTime.isSame(maxTime, 'second')) + { + valid.offset[iTime.unix() - unixOffset] = true; + valid.hour[iTime.hour()] = true; + valid.minute[iTime.minute()] = true; + valid.second[iTime.second()] = true; + valid.meridiem[(iTime.hour() > 11) ? 1 : 0] = true; + valid.length++; + iTime = iTime.add(step, 'second'); + } + + // Convert valid units to arrays + let unitText = { hour: [], minute: [], second:[], meridiem: [], length: valid.length }; + for (let i = 0; i < 24; i++) + { + unitText.hour.push((i in valid.hour) + ? ((i == 0 || i == 12) ? 12 : i % 12) + : null); + } + ['minute', 'second'].forEach(function (field) { + for (let i = 0; i < 60; i++) + { + unitText[field].push((i in valid[field]) ? ((i < 10) ? '0' + i : i) : null); + } + }); + unitText.meridiem.push((0 in valid.meridiem) ? 'AM' : null); + unitText.meridiem.push((1 in valid.meridiem) ? 'PM' : null); +/* + // Craate position arrays + for (let i = 1; i < 13; i++) + { + position.hour[i] = ((i % 12) in valid.hour || (i % 12 + 12) in valid.hour); + position.minute[i] = (i * 5 % 60) in valid.minute; + position.second[i] = (i * 5 % 60) in valid.second; + position.meridiem[i] = (i % 6 > 0) ? (((i < 6) ? 1 : 0) in valid.meridiem) : false; + } +*/ + return unitText; +} + +/** + * Return whether format string contains a token string + * + * This removes escaped characters from format string prior searching for token + * + * @param {string} format + * @param {string} searchElement + * @return {boolean} + */ +function hasFormat(format, searchElement) +{ + return (format.replace(/\[[^\]]*\]/g).indexOf(searchElement) >= 0); +} + +/** + * Update the view + * + * @param {object} $input the input object + */ +function updateView($input) +{ + const prevView = $input.data('prevview'); + const view = $input.data('view') || 'hour'; + const viewTime = $input.data('viewtime'); + const options = $input.data('options'); + const clock_24 = hasFormat(options.format, 'H'); + const step = options.step || 60; + let submit_disabled = false; + if (60 % step > 0) + { + const minTime = options.minTime || dayjs().startOf('day'); + let viewOffset = viewTime.diff(viewTime.startOf('day'), 'second'); + if (!hasFormat(options.format, 's')) + { + viewOffset -= (viewOffset % 60); + } + submit_disabled = ((viewOffset - minTime.diff(minTime.startOf('day'), 'second')) % step > 0); + } + const input_id = $input.attr('id'); + const $content = jQuery('#' + input_id + '-picker-content').attr('data-view', view); + $content.find('.submit-btn').prop('disabled', submit_disabled + || (options.minTime && viewTime.isBefore(options.minTime, 'second')) + || (options.maxTime && viewTime.isAfter(options.maxTime, 'second'))); + let number, position, format; + switch (view) + { + case 'hour': + number = viewTime.get(view); + position = (number % 12 > 0) ? (number % 12) : 12; + format = clock_24 ? 'H' : 'h'; + if (hasFormat(options.format, format + format)) + { + format += format; + } + break; + case 'minute': + case 'second': + number = viewTime.get(view); + if (number % 5 == 0) + { + position = (number > 0) ? (number / 5) : 12; + } + format = view.charAt(0).repeat(2); + break; + case 'meridiem': + position = (viewTime.hour() > 11) ? 3 : 9; + format = 'A'; + break; + default: + console.warn('Invalid view ' + view); + return false; + } + const text = viewTime.format(format); + $content.find('.timepicker-btns button').removeClass('font-weight-bold').filter('[data-unit="' + view + '"]').addClass('font-weight-bold').text(text); + $content.find('.clock-input-table .chevron-btn').data('unit', view); + + FORMATS[0][1] = clock_24 ? 'H' : 'h'; + FORMATS.forEach(function (formats) { + const text = viewTime.format(formats[1]); + $content.find('.' + formats[2] + '-btn').text(text); + $content.find('.' + formats[2] + '-input').val(text); + }); + + if (view != prevView) + { + $content.find('.clock-input-table button').each(function () { + const $this = jQuery(this); + let pos = parseInt($this.attr('class').match(/\bpos\-(\d+)/)[1]); + let disabled = true; + if (pos > 0) + { + let positions; + switch (view) + { + case 'hour': + positions = [pos, pos % 12, pos % 12 + 12]; + break; + case 'minute': + case 'second': + positions = [pos % 12 * 5]; + break; + case 'meridiem': + positions = (pos % 6 > 0) ? [(pos < 6) ? 1 : 0] : []; + break; + } + positions.forEach(function (pos) { + disabled &&= (options.unitText[view][pos] === null); + }); + $this.prop('disabled', disabled).toggleClass('text-light', disabled).toggleClass(INACTIVE_CLASS, pos != position).toggleClass(ACTIVE_CLASS, pos == position); + } + else + { + $this.text(text); + } + }); + } + $content.data('prevview', view); + return true; +} + +/** + * Update the clock picker in the popover + * + * @param {object} $input the input object + */ +function updatePicker($input) +{ + const input_id = $input.attr('id'); + const options = $input.data('options'); + const now = dayjs(); + const minTime = options.minTime || dayjs().startOf('day'); + const maxTime = options.maxTime || dayjs().endOf('day'); + const step = options.step || 60; + let validSteps = { hour: {}, minute: {}, second: {}, meridiem: {} }; + let viewTime = $input.data('viewtime'); + let iTime = minTime.clone(); + while (iTime.isBefore(maxTime) || iTime.isSame(maxTime, 'second')) + { + validSteps.hour[iTime.hour()] = true; + validSteps.minute[iTime.minute()] = true; + validSteps.second[iTime.second()] = true; + validSteps.meridiem[(iTime.hour() > 11) ? 1 : 0] = true; + if (!iTime.isBefore(minTime) && !iTime.isAfter(maxTime)) + { + if (!viewTime && now.isBefore(iTime)) + { + viewTime = iTime; + } + } + iTime = iTime.add(step, 'second'); + } + if (!viewTime) + { + viewTime = now.endOf(hasFormat(options.format, 's') ? 'second' : 'minute'); + } + $input.data('viewtime', viewTime); + + // Build html + const has_second = (hasFormat(options.format, 's') && step % 60 > 0); + const clock_24 = hasFormat(options.format, 'H'); + const clock_enabled = ((step % 300) == 0) && false; + const viewHour = viewTime.hour(); + let html = '
    ' + + '
    ' + + '
    ' + + '' + + ':' + + '' + + (has_second ? ':' : '') + + (clock_24 ? '' : '') + + '
    ' + + ('' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
    HourMinuteSecondMeridiem
    {{11}}{{12}}{{1}}
    {{10}}{{2}}
    {{9}}{{0}}{{3}}
    {{8}}{{4}}
    {{7}}{{6}}{{5}}
    ').replace(/{{(\w+)}}/g, function (match, position) { + if (position == 0) + { + return ''; + } + + const pos_hour = position % 12; + const pos_minute = pos_hour * 5; + + const meridiem_class = (position % 3 > 0) ? ' text-light' : ''; + const meridiem_text = (position % 6 > 0) ? ((position < 6) ? 'PM' : 'AM') : ' '; + return ''; + }) + + '
    ' + + '' + + '' + + '' + + (has_second ? '' : '') + + (clock_24 ? '' : '') + + '' + + '' + + (has_second ? '' : '') + + (clock_24 ? '' : '') + + '' + + '' + + '' + + '' + + (has_second ? '' : '') + + (clock_24 ? '' : '') + + '
    ::
    ' + + '
    ' + + '' + + '
    ' + + '
    '; + + const $content = jQuery('#' + input_id + '-picker-content'); + const $table = $content.html(html).find('.clock-input-table'); + const $center_btn = jQuery('#' + input_id + '-picker-center-btn'); + $content.parents('.timepicker-popover').attr('data-scheme', (options.scheme == 'auto') ? ((window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : '') : options.scheme); + $content.find('.timepicker-btns button').on('click', function () { + const unit = jQuery(this).blur().data('unit'); + $content.find('.clock-input-table .chevron-btn').data('unit', unit); + updateView($input.data('view', unit)); + }); + const $hour_input = $content.find('.hour-input').on('change', function () { + let hour = this.value.replace(/\D+/, ''); + if (hour.length > 0) + { + hour = parseInt(hour); + if (hour > -1 && hour < 24 && hour in options.unitText.hour) + { + $input.data('viewtime', $input.data('viewtime').hour(hour)); + } + } + updateView($input); + }); + $hour_input.add($content.find('.minute-input, .second-input').on('change', function () { + let number = this.value.replace(/\D+/, ''); + if (number.length > 0) + { + number = parseInt(number); + const unit = jQuery(this).attr('class').match(/(minute|second)\-input/)[1]; + if (number > -1 && number < 60 && number in options.unitText[unit]) + { + $input.data('viewtime', $input.data('viewtime').set(unit, number)); + } + } + updateView($input); + })).on('keyup', function (event) { + if (event.key == 'Enter') + { + $content.find('.submit-btn').triggerHandler('click'); + } + }); + + $content.find('.meridiem-btn').on('click', function () { + $input.data('viewtime', $input.data('viewtime').hour(($input.data('viewtime').hour() + 12) % 24)); + updateView($input); + }).on('keydown', function (event) { + const key = event.key.toUpperCase(); + const hour = $input.data('viewtime').hour(); + let offset = 0; + if (key == 'A' && hour > 12) + { + offset = -12; + } + else if (key == 'P' && hour < 11) + { + offset = 12; + } + if (offset != 0) + { + $input.data('viewtime', $input.data('viewtime').hour(hour + offset)); + updateView($input); + } + }); + $content.find('.input-toggle-btn').attr('tabindex', -1).on('click', function () { + const input = jQuery(this).data('input'); + $content.find('.clock-input').toggleClass('d-none', input != 'clock'); + $content.find('.keyboard-input').toggleClass('d-none', input != 'keyboard'); + $content.find('.input-toggle-btn').each(function () { + const $this = jQuery(this); + $this.toggleClass('d-none', $this.data('input') == input); + }); + $input.popover('update'); + $hour_input.select(); + }); + $content.find('.cancel-btn').on('click', function () { + $input.popover('hide'); + }); + $content.find('.submit-btn').on('click', function () { + $input.val($input.data('viewtime').format($input.data('options').format)).popover('hide').data('view', null).trigger('change'); + }); + $table.find('button').on('click', function () { + const $this = jQuery(this); + let viewTime = $input.data('viewtime'); + let position = $this.attr('class').match(/\bpos\-(\d+)/)[1]; + const view = $input.data('view') || 'hour'; + switch (view) + { + case 'hour': + if (position > 0) + { + viewTime = viewTime.hour(position % 12 + ((viewTime.hour() > 11) ? 12 : 0)); + } + break; + case 'minute': + if (position > 0) + { + viewTime = viewTime.minute(position % 12 * 5); + } + break; + case 'second': + if (position > 0) + { + viewTime = viewTime.second(position % 12 * 5); + } + break; + case 'meridiem': + if (position > 0) + { + viewTime = viewTime.hour(viewTime.hour() % 12 + ((position < 6) ? 12 : 0)); + } + break; + } + $input.data('viewtime', viewTime); + let picked = true; + FORMATS.forEach(function (formats) { + const regex = new RegExp(formats[0], 'i'); + if (picked && regex.test(options.format)) + { + position = null; + let number; + const nextUnit = formats[2]; + switch (nextUnit) + { + case 'hour': + number = viewTime.get(nextUnit) % 12; + position = (number > 0) ? number : 12; + break; + case 'minute': + case 'second': + number = viewTime.get(nextUnit); + if (number % 5 == 0) + { + position = (number > 0) ? (number / 5) : 12; + } + break; + case 'meridiem': + position = (viewTime.hour() > 11) ? 3 : 9; + break; + } + $input.data('view', nextUnit); + picked = false; + } + }); + + if (picked) + { + $input.val(viewTime.format(options.format)).popover('hide').data('view', null).trigger('change'); + } + else + { + updateView($input); + } + }); + $content.find('.chevron-btn').attr('tabindex', -1).on('click', function () { + const $this = jQuery(this).blur(); + const options = $input.data('options'); + const unit = $this.data('unit') || 'hour'; + const step = $this.data('step'); + const viewTime = $input.data('viewtime'); + const number = (unit == 'meridiem') + ? ((viewTime.hour() > 11) ? 1 : 0) + : viewTime.get(unit); + if (!(number in options.unitText[unit])) + { + // Set to number + } + if (options.unitText[unit].length < 2) + { + return; + } + + const unitLength = UNIT_LENGTHS[unit]; + let idx = number; + switch (unit) + { + case 'hour': + case 'minute': + case 'second': + do + { + idx = (idx + step + unitLength) % unitLength; + if (options.unitText[unit][idx] !== null) + { + $center_btn.text(options.unitText[unit][idx]); + $input.data('viewtime', viewTime.set(unit, idx)); + break; + } + } + while (idx != number); + break; + case 'meridiem': + idx = (idx + step + unitLength) % unitLength; + if (options.unitText[unit][idx] !== null) + { + $center_btn.text(options.unitText[unit][idx]); + $input.data('viewtime', viewTime.hour(viewTime.hour() % 12 + ((idx > 0) ? 12 : 0))); + } + break; + } + updateView($input); + }); + updateView($input); +} + +/** + * Add method for initializing plugin + */ +jQuery.fn.timepicker = function (options) { + // Get boostrap version + const bs_version = parseInt(((typeof bootstrap == 'object') ? bootstrap.Dropdown.VERSION : jQuery.fn.dropdown.Constructor.VERSION || '0').replace(/\..+$/, '')); + if (bs_version < 4) + { + console.error('Invalid bootstrap version ' + bs_version + ' detected'); + } + + // Handle functions + if (typeof options == 'string') + { + if (this.length < 1) + { + return undefined; + } + let input_options = this.data('options') || {}; + const single_arg = (arguments.length == 1); + switch (options) + { + case 'format': + if (single_arg) + { + return input_options.format; + } + else if (arguments[1] && typeof arguments[1] == 'string') + { + input_options.format = arguments[1]; + this.data('options', input_options); + } + else + { + console.warn('Invalid format'); + } + break; + case 'defaultTime': + case 'minTime': + case 'maxTime': + if (single_arg) + { + return input_options[options]; + } + else if (arguments[1]) + { + let newTime = parseTime(arguments[1]); + if (newTime && newTime.isValid()) + { + if (options == 'defaultTime') + { + if (input_options.minTime && newTime.isBefore(input_options.minTime)) + { + newTime = false; + console.warn('defaultTime is before minTime'); + } + else if (input_options.maxTime && newTime.isAfter(input_options.maxTime)) + { + newTime = false; + console.warn('defaultTime is after maxTime'); + } + } + if (newTime) + { + input_options[options] = newTime; + input_options.unitText = getUnitText(input_options); + this.data('options', input_options); + } + } + else + { + console.warn('Invalid ' + options); + } + } + else + { + input_options[options] = null; + input_options.unitText = getUnitText(input_options); + this.data('options', input_options); + } + break; + case 'step': + if (single_arg) + { + return input_options[options]; + } + else if (arguments[1]) + { + const step = parseInt(arguments[1]); + if (step > 0 && step < 86400 + && step % (hasFormat(input_options.format, 's') ? 1 : 60) == 0) + { + input_options.step = step; + input_options.unitText = getUnitText(input_options); + this.data('options', input_options); + } + else + { + console.warn('Invalid ' + options); + } + } + else + { + input_options.step = 60; + input_options.unitText = getUnitText(input_options); + this.data('options', input_options); + } + break; + case 'scheme': + if (single_arg) + { + return input_options.scheme; + } + else if (arguments[1] === null || typeof arguments[1] == 'string') + { + input_options.scheme = arguments[1]; + this.data('options', input_options); + } + else + { + console.warn('Invalid scheme'); + } + break; + case 'time': + if (single_arg) + { + return parseTime(this.val(), input_options) || null; + } + else + { + const newTime = (arguments[1]) ? parseTime(arguments[1], input_options) : null; + this.val((newTime && newTime.isValid()) ? newTime.format(input_options.format) : ''); + } + break; + case 'viewTime': + if (single_arg) + { + return this.data('viewtime'); + } + else + { + const newTime = (arguments[1]) ? parseTime(arguments[1], input_options) : null; + this.data('viewtime', newTime); + } + break; + case 'view': + if (single_arg) + { + return this.data('view'); + } + else + { + const view = arguments[1]; + updateView(jQuery(this).data('view', view)); + } + break; + default: + break; + } + return this; + } + + // Initialize code if it hasn't already + if (!initialized) + { + initialized = true; + let table_class = '.timepicker-table '; + jQuery(document.head).append(''); + + // Make popovers close when clicked outside of them + jQuery(document.body).on('mouseup', function (e) { + if (jQuery(e.target).parents('.popover').length == 0) + { + jQuery('.timepicker').popover('hide'); + } + }); + } + + // Process options + if (typeof options == 'undefined') + { + options = {}; + } + const common_options = jQuery.extend({}, jQuery.fn.timepicker.defaults, options); + ['minTime', 'maxTime'].forEach(function (option) { + if (common_options[option]) + { + common_options[option] = parseTime(common_options[option]); + } + }); + + // Initialize the inputs + return this.each(function () { + const $input = jQuery(this); + + // Get input id + let input_id = this.id; + let $toggles = $input.siblings().find('[data-toggle="timepicker"]:not([data-target])'); + if (this.id) + { + $toggles = $toggles.add('[data-toggle="timepicker"][data-target="#' + this.id + '"]'); + } + else + { + input_id = 'input-' + Math.floor(Math.random() * 1000000 + 1); + this.id = input_id; + } + + // Process options + let input_options = jQuery.extend(true, {}, common_options); + let format = $input.data('format') || common_options.format; + if (format) + { + input_options.format = format; + } + let minTime = $input.attr('min') || $input.data('mintime') || common_options.minTime; + if (minTime && (minTime = parseTime(minTime)) && minTime.isValid()) + { + input_options.minTime = minTime; + } + let maxTime = $input.attr('max') || $input.data('maxtime') || common_options.maxTime; + if (maxTime && (maxTime = parseTime(maxTime)) && maxTime.isValid()) + { + input_options.maxTime = maxTime; + } + let defaultTime = $input.data('default') || common_options.defaultTime; + if (defaultTime && (defaultTime = parseTime(defaultTime)) && defaultTime.isValid() + && !(input_options.minTime && defaultTime.isBefore(input_options.minTime)) + && !(input_options.maxTime && defaultTime.isAfter(input_options.maxTime))) + { + input_options.defaultTime = defaultTime; + } + const step = $input.attr('step') || $input.data('step') || common_options.step; + if (step > 0 && step < 86400 && 60 % step > 0) + { + input_options.step = parseInt(step); + } + const scheme = $input.data('scheme') || common_options.scheme; + if (scheme) + { + input_options.scheme = scheme; + } + input_options.unitText = getUnitText(input_options); + $input.data('options', input_options); + if ($input.data('timepicker')) + { + // If timepicker is already initialized, then return + return this; + } + $input.data('timepicker', true).addClass('timepicker'); + + // Set inputmode + if (this.type == 'text' && !this.inputMode) + { + this.inputMode = 'tel'; + } + + const $label = jQuery('label[for="' + input_id + '"]'); + const placement = (window.screen.width > 575) ? 'bottom' : 'top'; + $input.on('change', function () { + this.value = this.value.replace(/^\s+|\s+$/g, ''); + const options = $input.data('options'); + const newTime = parseTime(this.value, options); + this.value = (newTime !== false) ? newTime.format(options.format) : ''; + }).on('shown.bs.popover', function () { + if (window.screen.width > 575) + { + jQuery('#' + input_id + '-picker-content').find('.hour-input').select(); + } + }).on('inserted.bs.popover', function () { + jQuery('.popover').find('[data-dismiss="popover"]').on('click', function () { + $input.popover('hide'); + }); + updatePicker($input, input_options); + }).on('hide.bs.popover', function () { + $input.data('view', null); + }).popover({ + html: true, + placement: placement, + sanitize: false, + title: '' + (($label.length > 0) ? $label.html() : 'Time'), + template: '', + trigger: (($toggles.length > 0) ? 'manual' : 'click'), + popperConfig: { +/* + modifiers: { + hide: { + enabled: false + }, + preventOverflow: { + enabled: false, +// boundariesElement: 'window', + escapeWithReference: true + } + }, +// positionFixed: true +*/ + }, + content: function () { + const options = $input.data('options'); + const viewTime = parseTime($input.val() || options.defaultTime || '', options); + $input.data('viewtime', viewTime); + return '
    '; + } + }); + + $toggles.on('click', function () { + $input.popover('toggle'); + this.blur(); + }); + }); +}; + +/** + * Default options + * + * @type {object} + * @todo add support for additional options + */ +jQuery.fn.timepicker.defaults = { + defaultTime: null, + format: 'hh:mm A', + maxTime: null, + minTime: null, + step: 60, + scheme: 'light' +}; + +/* + * Initialize timepickers + */ +document.addEventListener('DOMContentLoaded', function() { + jQuery('[data-toggle="timepicker"][data-target]').each(function () { + jQuery(jQuery(this).data('target')).timepicker(); + }); +}); + +//------------------------------------- +}());