Merge branch 'fronkdev' into 'master'

Added MaintenanceNotification

See merge request fronk/thetool!722
This commit is contained in:
Frank Schubert
2024-11-12 17:31:53 +00:00
11 changed files with 1840 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/header.php"); ?>
<!-- start page title -->
<div class="row">
<div class="col-12">
<div class="page-title-box">
<div class="page-title-right">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="<?=self::getUrl("Dashboard")?>"><?=MFAPPNAME_SLUG?></a></li>
<li class="breadcrumb-item"><a href="<?=self::getUrl("MaintenanceNotification")?>">Wartungsmeldungen</a></li>
<li class="breadcrumb-item active"><?=($notification && $notification->id) ? "Bearbeiten" : "Neu" ?></li>
</ol>
</div>
<h4 class="page-title"><?=($notification->id) ? "Wartungsmeldung bearbeiten" : "Neue Wartungsmeldung" ?></h4>
</div>
</div>
</div>
<!-- end page title -->
<div class="row">
<div class="col-lg-12">
<div class="card bg-light">
<div class="card-body">
<form class="form-horizontal" method="post" action="<?=self::getUrl("MaintenanceNotification", "save")?>" enctype="multipart/form-data">
<div class="card">
<div class="card-body">
<input type="hidden" name="id" value="<?=$notification->id?>" />
<label class="form-label" for="from">Wartungsfenster Von *:</label>
<div class="form-group row">
<div class="col-2">
<input type="text" class="form-control datepicker" name="from_date" id="from_date" placeholder="Datum" value="<?=($notification && $notification->from) ? date("d.m.Y", $notification->from) : ""?>" />
</div>
<div class="col-2">
<input type="text" class="form-control timepicker" name="from_time" id="from_time" placeholder="Uhrzeit" value="<?=($notification && $notification->from) ? date("H:i", $notification->from) : ""?>" />
</div>
</div>
<label class="form-label" for="from">Wartungsfenster Bis *:</label>
<div class="form-group row">
<div class="col-2">
<input type="text" class="form-control datepicker" name="to_date" id="to_date" placeholder="Datum" value="<?=($notification && $notification->to) ? date("d.m.Y", $notification->to) : ""?>"/>
</div>
<div class="col-2">
<input type="text" class="form-control timepicker" name="to_time" id="to_time" placeholder="Uhrzeit" value="<?=($notification && $notification->to) ? date("H:i", $notification->to) : ""?>"/>
</div>
</div>
<div class="form-group row col-12">
<label class="form-label" for="from">PLZ-Bereich *:</label>
<textarea class="form-control" name="plz" id="from" style="height: 86px;"><?=($notification && $notification->plzs) ? implode(", ", $notification->plzs) : ""?></textarea>
<small class="text-primary">PLZ' mit Leerzeichen oder Komma getrennt</small>
</div>
<hr />
<div class="form-group row mt-3 col-12">
<label class="form-label" for="subject_id">Betreff *:</label>
<select name="subject_id" id="subject_id" class="form-control" onchange="changeText(this)">
<option></option>
<?php foreach(MaintenanceNotificationTemplate::getAll() as $tpl): ?>
<option value="<?=$tpl->id?>" <?=($notification && $notification->subject_id == $tpl->id) ? "selected='selected'" : ""?> data-text-template="<?=htmlentities($tpl->text)?>"><?=htmlentities($tpl->subject)?></option>
<?php endforeach; ?>
</select>
</div>
<label class="form-label" for="text">Wartungsmeldung Text *:</label>
<textarea id="text" name="text" placeholder="Bitte geben Sie hier den Emailinhalt ein..." rows="12" style="width: 100%; height: 600px; padding: 10px;"><?= htmlentities($notification->text)?></textarea>
</div>
</div>
<div class="form-group row">
<label class="col-lg-2"></label>
<div class="col-lg-10">
<button type="submit" name="return" value="form" class="btn btn-primary mr-1">Speichern</button>
<button type="submit" name="return" value="index" class="btn btn-primary">Speichern und zur Übersicht</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function() {
$('.datepicker').datepicker({
language: 'de',
format: "dd.mm.yyyy",
showWeekDays: true,
todayBtn: 'linked',
orientation: "bottom",
autoclose: true
});
$('.timepicker').timepicker({
format: "HH:mm"
});
});
function changeText(elem) {
var text = $(elem).find(":selected").data("text-template");
$("#text").text(text);
}
</script>
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/footer.php"); ?>

View File

@@ -0,0 +1,107 @@
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/header.php"); ?>
<!-- start page title -->
<div class="row">
<div class="col-12">
<div class="page-title-box">
<div class="page-title-right">
<ol class="breadcrumb m-0">
<li class="breadcrumb-item"><a href="<?=self::getUrl("Dashboard")?>"><?=MFAPPNAME_SLUG?></a></li>
<li class="breadcrumb-item active">Wartungsmeldungen</li>
</ol>
</div>
<h4 class="page-title">Wartungsmeldungen</h4>
</div>
</div>
</div>
<!-- end page title -->
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body mb-3">
<h4 class="header-title mb-3">Filter</h4>
<form method="get" action="<?=self::getUrl("Mailtemplate")?>">
<div class="row">
<div class="col-1">
<label class="form-label" for="filter_name">Name</label>
<input type="text" class="form-control" name="filter[name]" id="filter_name" value="<?=$filter['name']?>" />
</div>
<div class="col-1">
<label class="form-label" for="filter_description">Beschreibung</label>
<input type="text" class="form-control" name="filter[description]" id="filter_description" value="<?=$filter['description']?>" />
</div>
<div class="col-1">
<label class="form-label" for="filter_subject">Betreff</label>
<input type="text" class="form-control" name="filter[subject]" id="filter_betreff" value="<?=$filter['subject']?>" />
</div>
<div class="col-1">
<label class="form-label" for="filter_is_include">Betreff</label>
<select class="form-control" name="filter[is_include]" id="filter_is_include">
<option></option>
<option value="0" <?=(array_key_exists("is_include", $filter) && $filter["is_include"] == 0) ? "selected='selected'" : ""?>>Dokumente</option>
<option value="1" <?=(array_key_exists("is_include", $filter) && $filter["is_include"] == 1) ? "selected='selected'" : ""?>>Templates</option>
</select>
</div>
</div>
<div class="row mt-2">
<div class="col">
<button type="submit" class="btn btn-primary">Filter anwenden</button>
<a class="btn btn-secondary" href="<?=self::getUrl("MaintenanceNotification")?>?resetFilter=1">Filter zurücksetzen</a>
</div>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-body mb-3">
<div class="float-left">
<h4 class="header-title">Liste offener Wartungsmeldungen</h4>
</div>
<div class="float-right">
<a class="btn btn-primary mb-2" href="<?=self::getUrl("MaintenanceNotification", "add")?>"><i class="fas fa-plus"></i> Neue Wartungsmeldungen erstellen</a>
</div>
<table class="table table-striped table-hover">
<tr>
<th>Betreff</th>
<th>PLZ-Bereich</th>
<th>Versandtermin</th>
<th>Aussendung abgeschlossen</th>
<th>Erstellt</th>
<th>Bearbeitet</th>
<th></th>
</tr>
<?php foreach($notifications as $notification): ?>
<tr>
<td><?=$notification->subject?></td>
<td><?=($notification->plz) ? implode(", ", $notification->plzs) : ""?></td>
<td><?=($notification->send_ts) ? date("d.m.Y H:i", $notification->send_ts) : ""?></td>
<td><?=($notification->sent) ? "Ja" : "nein"?></td>
<td><?=date("d.m.Y H:i",$notification->create)?> (<?=$notification->editor->name?>)</td>
<td><?=date("d.m.Y H:i",$notification->edit)?> (<?=$notification->creator->name?>)</td>
<td style="text-align: left; letter-spacing: 4px; font-size: 1.1em;">
<a href="<?=self::getUrl("MaintenanceNotification", "edit", ["id" => $notification->id])?>"><i class="far fa-edit" title="Wartungsmeldung bearbeiten"></i></a>
<a href="<?=self::getUrl("MaintenanceNotification", "delete", ["id" => $notification->id])?>" class="text-danger" onclick="if(!confirm('Wartungsmeldung wirklich löschen?')) return false;" title="Wartungsmeldung Löschen"><i class="fas fa-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
</div>
</div>
<?php include(realpath(dirname(__FILE__)."/../../$mfLayoutPackage")."/footer.php"); ?>

View File

@@ -41,6 +41,8 @@
<script type="text/javascript" src="<?=self::getResourcePath()?>assets/libs/select2/select2.full.min.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>js/bootstrap-datepicker.min.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>js/bootstrap-datepicker.de.min.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>js/dayjs.min.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>js/timepicker-bs4.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>assets/js/geo/geo.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>assets/js/leaflet.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>assets/js/Leaflet.fullscreen.min.js"></script>

View File

@@ -133,6 +133,7 @@
<?php if($me->isAdmin()) : ?><li><a href="<?=self::getUrl("Domain")?>"><i class="fad fa-fw fa-globe text-info"></i> Domains</a></li><?php endif; ?>
<?php if($me->isAdmin()) : ?><li><a href="<?=self::getUrl("IpNetwork")?>"><i class="fa-solid fa-network-wired text-info"></i> IPAM</a></li><?php endif; ?>
<?php if($me->isAdmin()) : ?><li><a href="<?=self::getUrl("DeviceMonitoring/congestion")?>"><i class="fa-solid fa-block-brick-fire text-info"></i> Device Congestion</a></li><?php endif; ?>
<?php if($me->isAdmin()) : ?><li><a href="<?=self::getUrl("MaintenanceNotification")?>"><i class="fa-solid fa-envelope-badge text-info"></i> Wartungsmeldungen</a></li><?php endif; ?>
</ul>
</li>
<?php endif; ?>

View File

@@ -0,0 +1,200 @@
<?php
class MaintenanceNotification extends mfBaseModel {
private $plzs;
public function getProperty($name) {
if($this->$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;
}
}

View File

@@ -0,0 +1,162 @@
<?php
class MaintenanceNotificationController extends mfBaseController {
protected function init() {
$this->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]);
}
}

View File

@@ -0,0 +1,192 @@
<?php
class MaintenanceNotificationTemplate extends mfBaseModel {
public function getProperty($name) {
if($this->$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;
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateMaintanenceNotification extends AbstractMigration
{
public function up(): void
{
if($this->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") {
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class CreateMaintenancenotificationtemplate extends AbstractMigration
{
public function up(): void
{
if($this->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") {
}
}
}

1
public/js/dayjs.min.js vendored Normal file

File diff suppressed because one or more lines are too long

964
public/js/timepicker-bs4.js Normal file
View File

@@ -0,0 +1,964 @@
/**
* Time picker for Bootstrap 4
*
* https://github.com/lesilent/timepicker-bs4
*/
(function () {
//-------------------------------------
'use strict';
/**
* Array of dayjs format substrings,
*
* 0 = used for regex to determine whether format contains unit
* 1 = format used for buttons and inputs
* 2 = the unit/view name
*
* @type {array}
*/
let FORMATS = [
['h', 'h', 'hour'],
['m', 'mm', 'minute'],
['s', 'ss', 'second'],
['a', 'A', 'meridiem'],
];
/**
* Unit lengths
*
* @type {object}
*/
const UNIT_LENGTHS = { hour: 24, minute: 60, second: 60, meridiem: 2 };
/**
*
* @type {string}
*/
const ACTIVE_CLASS = 'active btn-info';
/**
*
* @type {string}
*/
const INACTIVE_CLASS = 'btn-outline-dark border-white';
/**
* Flag for whether plugin has been initialized
*
* @type {boolean}
*/
let initialized = false;
/**
* Parse a time string and return a dayjs object
*
* @param {string} str
* @param {object} options
* @return {object|boolean} either a dayjs object or false on error
*/
function parseTime(str, options)
{
let input_time = false, matches;
if (typeof str == 'string')
{
str = str.replace(/^\s+|\s+$/g, '');
if ((matches = str.match(/^([0-2]?\d)(?:s*:\s*([0-5]\d))?(?:\s*:\s*([0-5]\d))?(?:\s*([AP])\.?(?:M\.?)?)?$/i))
&& parseInt(matches[1]) > (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 = '<div class="clock-input' + (clock_enabled ? '' : ' d-none') + '">'
+ '<div class="d-flex justify-content-center align-items-center timepicker-btns">'
+ '<div class="btn-group">'
+ '<button type="button" class="btn px-2 font-weight-bold hour-btn" data-unit="hour">' + viewTime.format('hh') + '</button>'
+ '<a class="btn px-0 disabled" href="javascript:void(0)" role="button" aria-disabled="true">:</a>'
+ '<button type="button" class="btn px-2 minute-btn" data-unit="minute"' + + ((step % 3600 > 0) ? '' : ' disabled="disabled"') + '>' + viewTime.format('mm') + '</button>'
+ (has_second ? '<a class="btn px-0 disabled" href="javascript:void(0)" role="button" aria-disabled="true">:</a><button type="button" id="' + input_id + '-picker-second-btn" class="btn px-2 second-btn" data-unit="second">' + viewTime.format('ss') + '</button>' : '')
+ (clock_24 ? '<button type="button" class="btn px-2 meridiem-btn" data-unit="meridiem">' + viewTime.format('A') + '</button>' : '')
+ '</div></div>'
+ ('<table class="clock-input-table table table-sm table-borderless timepicker-table mx-auto w-auto mb-1">'
+ '<thead class="thead-light"><tr><th class="text-center py-1" colspan="5"><span class="hour">Hour</span><span class="minute">Minute</span><span class="second">Second</span><span class="meridiem">Meridiem</span></th></tr></thead>'
+ '<tbody>'
+ '<tr>'
+ '<td></td>'
+ '<td class="text-right">{{11}}</td>'
+ '<td class="text-center">{{12}}</td>'
+ '<td class="text-left">{{1}}</td>'
+ '<td></td>'
+ '</tr><tr>'
+ '<td class="text-right">{{10}}</td>'
+ '<td class="text-center align-bottom" colspan="3"><a id="' + input_id + '-picker-add-link" class="btn btn-link px-1 mx-0 chevron-btn" data-step="1" href="javascript:void(0)"><i class="fas fa-chevron-up fa-lg"></i></a></td>'
+ '<td class="text-left">{{2}}</td>'
+ '</tr><tr>'
+ '<td class="text-right">{{9}}</td>'
+ '<td class="text-center" colspan="3">{{0}}</td>'
+ '<td class="text-left">{{3}}</td>'
+ '</tr><tr>'
+ '<td class="text-right">{{8}}</td>'
+ '<td class="text-center" colspan="3"><a id="' + input_id + '-picker-sub-link" class="btn btn-link px-1 mx-0 chevron-btn" data-step="-1" href="javascript:void(0)"><i class="fas fa-chevron-down fa-lg"></i></a></td>'
+ '<td class="text-left">{{4}}</td>'
+ '</tr><tr>'
+ '<td></td>'
+ '<td class="text-right">{{7}}</td>'
+ '<td class="text-center">{{6}}</td>'
+ '<td class="text-left">{{5}}</td>'
+ '<td></td>'
+ '</tr></tbody></table>').replace(/{{(\w+)}}/g, function (match, position) {
if (position == 0)
{
return '<button type="button" id="'+ input_id + '-picker-center-btn" class="btn btn-outline-dark border-white font-weight-bold pos-0" data-hour="' + viewHour + '"'
+ (viewHour in validSteps.hour ? '' : ' disabled="disabled"')
+ '>' + ((viewHour % 12 > 0) ? viewHour % 12 : 12) + '</button>';
}
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') : '&nbsp;';
return '<button type="button" class="btn px-1 '
+ ((viewHour % 12 == position % 12) ? 'active btn-info' : 'btn-outline-dark border-white')
+ ' pos-' + position + '" '
+ (pos_hour in validSteps.hour ? '' : ' disabled="disabled"')
+ '><span class="hour">' + position + '</span><span class="minute second">' + (pos_minute > 9 ? '' : '0') + pos_minute + '</span><span class="meridiem' + meridiem_class + '">' + meridiem_text
+ '</span></button>';
})
+ '</div><div class="keyboard-input' + (clock_enabled ? ' d-none' : '') + '"><table class="mx-auto text-center"><tr>'
+ '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="hour" data-step="1" href="javascript:void(0)"><i class="fas fa-chevron-up fa-lg"></i></a></td>'
+ '<td></td>'
+ '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="minute" data-step="1" href="javascript:void(0)"><i class="fas fa-chevron-up fa-lg"></i></a></td>'
+ (has_second ? '<td></td><td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="second" data-step="1" href="javascript:void(0)"><i class="fas fa-chevron-up fa-lg"></i></a></td>' : '')
+ (clock_24 ? '' : '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="meridiem" data-step="1" href="javascript:void(0)"><i class="fas fa-chevron-up fa-lg"></i></a></td>')
+ '</tr><tr>'
+ '<td><input type="text" class="form-control text-center border-light hour-input" minlength="1" maxlength="2" inputmode="numeric" /></td><td>:</td><td><input type="text" class="form-control text-center border-light minute-input" minlength="1" maxlength="2" inputmode="numeric" /></td>'
+ (has_second ? '<td>:</td><td><input type="text" class="form-control text-center border-light second-input" minlength="1" maxlength="2" inputmode="numeric" /></td>' : '')
+ (clock_24 ? '' : '<td><button type="button" class="btn meridiem-btn border-light"></button></td>')
+ '</tr><tr>'
+ '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="hour" data-step="-1" href="javascript:void(0)"><i class="fas fa-chevron-down fa-lg"></i></a></td>'
+ '<td></td>'
+ '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="minute" data-step="-1" href="javascript:void(0)"><i class="fas fa-chevron-down fa-lg"></i></a></td>'
+ (has_second ? '<td></td><td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="second" data-step="-1" href="javascript:void(0)"><i class="fas fa-chevron-down fa-lg"></i></a></td>' : '')
+ (clock_24 ? '' : '<td><a class="btn btn-link px-1 mx-0 chevron-btn" data-unit="meridiem" data-step="-1" href="javascript:void(0)"><i class="fas fa-chevron-down fa-lg"></i></a></td>')
+ '</tr></table></div>'
+ '<div class="d-flex justify-content-between">'
+ '<div class="invisible"><button type="button" class="btn btn-link input-toggle-btn' + (clock_enabled ? ' d-none' : '') + '" data-input="clock"><i class="far fa-clock fa-fw"></i></button><button type="button" class="btn btn-link input-toggle-btn' + (clock_enabled ? '' : ' d-none') + '" data-input="keyboard"><i class="far fa-keyboard fa-fw"></i></button></div>'
+ '<div><button type="button" class="btn btn-secondary mx-1 cancel-btn" data-dismiss="popover" title="Cancel"><i class="fas fa-times fa-fw"></i></button><button type="button" class="btn btn-primary mx-1 submit-btn" title="OK"><i class="fas fa-check fa-fw"></i></button></div>'
+ '</div>';
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('<style id="timepicker-style">'
+ '.timepicker-popover { font-size: inherit; }'
+ '.timepicker-popover .btn-link:hover { background-color: #e2e6ea; }'
+ '.timepicker-popover .meridiem-btn { min-width: 3.2rem; }'
+ '.timepicker-popover .meridiem-btn:hover { box-shadow: 0 0 0 .2rem rgba(0,123,255,.25); }'
+ '.timepicker-popover input { width: 3rem; }'
+ '.timepicker-btns .btn:hover { background-color: #e2e6ea; color: #000; }'
+ '.timepicker-content span.hour, .timepicker-content span.minute, .timepicker-content span.second, .timepicker-content span.meridiem { display: none; }'
+ '.timepicker-content[data-view="hour"] span.hour,'
+ '.timepicker-content[data-view="minute"] span.minute,'
+ '.timepicker-content[data-view="second"] span.second,'
+ '.timepicker-content[data-view="meridiem"] span.meridiem { display: inline; }'
+ table_class + 'td button:focus { box-shadow: none !important; }'
+ table_class + 'td button:not(:disabled):hover { background-color: #6c757d !important; border-color: #6c757d !important; color: #fff; }'
+ table_class + 'td button:disabled { cursor: not-allowed; }'
+ table_class + 'td button.today { background-color: #fcf8e3; }'
+ table_class + 'td, .timepicker-table th { padding: 0; }'
+ table_class + 'button.btn { min-width: 2.3rem; }'
+ table_class + 'button:focus { box-shadow: none !important; }'
+ table_class + 'button:not(:disabled):hover { background-color: #6c757d !important; border-color: #6c757d !important; color: #fff; }'
+ table_class + 'td button.pos-1 { margin: .6rem 0 0 .4rem; }'
+ table_class + 'td button.pos-2 { margin: 0 .7rem .3rem 0; }'
+ table_class + 'td button.pos-3 { margin: 0 0 0 .6rem; }'
+ table_class + 'td button.pos-4 { margin: .3rem .7rem 0 0; }'
+ table_class + 'td button.pos-5 { margin: 0 0 .6rem .4rem; }'
+ table_class + 'td button.pos-6 { margin: .8rem 0 0 0; }'
+ table_class + 'td button.pos-7 { margin: 0 .4rem .6rem 0; }'
+ table_class + 'td button.pos-8 { margin: .3rem 0 0 .7rem; }'
+ table_class + 'td button.pos-9 { margin: 0 .6rem 0 0; }'
+ table_class + 'td button.pos-10 { margin: 0 0 .3rem .7rem; }'
+ table_class + 'td button.pos-11 { margin: .6rem .4rem 0 0; }'
+ table_class + 'td button.pos-12 { margin: 0 0 .6rem 0; }'
+ '.timepicker-popover[data-scheme="dark"] { background-color: #000000; border-color: #ffffff; color: #dee2e6; }'
+ '.timepicker-popover[data-scheme="dark"] .popover-header { background-color: #343a40; color: #ffffff; }'
+ '.timepicker-popover[data-scheme="dark"] .popover-header .close { filter: invert(1) grayscale(1) brightness(2); }'
+ '.timepicker-popover[data-scheme="dark"] .border-light { background-color: #000000; border-color: #6c757d !important; color: #ffffff; }'
+ '.timepicker-popover[data-scheme="dark"] input.border-light:focus { background-color: inherit; border-color: #86b7fe !important; color: #ffffff; }'
+ '.timepicker-popover[data-scheme="dark"] .meridiem-btn { color: #ffffff; }'
+ '.timepicker-popover[data-scheme="dark"] .meridiem-btn:hover { border-color: #86b7fe !important; }'
+ '</style>');
// 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: '<button class="close mt-n1" data-dismiss="popover">&times;</button>' + (($label.length > 0) ? $label.html() : 'Time'),
template: '<div id="' + input_id + '-picker-popover" class="popover timepicker-popover bs-popover-"' + placement + '" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div id="' + input_id + '-popover-body" class="popover-body border-bottom"></div><div class="popover-footer bg-light text-right px-3 py-2 rounded-lg" hidden="hidden"><button type="button" class="btn btn-secondary btn-sm" title="Close the picker" data-dismiss="popover"><i class="fas fa-times"></i> Close</button></div></div>',
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 '<div id="' + input_id + '-picker-content" class="timepicker-content" data-view="hour"></div>';
}
});
$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();
});
});
//-------------------------------------
}());