diff --git a/Layout/default/Domain/Index.php b/Layout/default/Domain/Index.php deleted file mode 100644 index 6c17a8db8..000000000 --- a/Layout/default/Domain/Index.php +++ /dev/null @@ -1,256 +0,0 @@ - self::getUrl("Domain"), - "DASHBOARD_URL" => self::getUrl("Dashboard"), - "MFAPPNAME" => MFAPPNAME_SLUG, - "PAGE_TITLE" => "Domains", - "PATH" => [ - ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], - ["text" => "Domain Management", "href" => self::getUrl("Domain")], - ["text" => "Domains"] - ], - "DOMAIN_API_URL" => self::getUrl("Domain/api"), -]; - -$additionalJS = [ - "plugins/vue/vue.js", - "plugins/axios/axios.min.js", - "plugins/moment/moment.min.js", - "plugins/daterangepicker/daterangepicker.js", - "plugins/xlsx/xlsx.min.js", - "plugins/vue/tt-components/tt-table.js", - "plugins/vue/tt-components/tt-page-title.js", - "plugins/vue/tt-components/tt-select.js", - "plugins/vue/tt-components/tt-datepicker.js", - "plugins/vue/tt-components/tt-input.js", - "plugins/vue/tt-components/tt-autocomplete.js", - "plugins/vue/tt-components/tt-icon-select.js", - "plugins/vue/tt-components/tt-number-range.js", -]; -$additionalCSS = [ - "plugins/daterangepicker/daterangepicker.css", - 'plugins/vue/tt-components/css/tt-table.css', -]; - -include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?> - -
- - - - - - - - - - - - - - - - - - - -
- - - - diff --git a/Layout/default/HistoricTicket/Index.php b/Layout/default/HistoricTicket/Index.php deleted file mode 100644 index 514a9e2d8..000000000 --- a/Layout/default/HistoricTicket/Index.php +++ /dev/null @@ -1,222 +0,0 @@ - self::getUrl("Domain"), - "DASHBOARD_URL" => self::getUrl("Dashboard"), - "MFAPPNAME" => MFAPPNAME_SLUG, - "PAGE_TITLE" => "Historische Tickets", - "PATH" => [ - ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], - ["text" => "Historische Tickets", "href" => self::getUrl("HistoricTicket")] - ], - "HISTORIC_TICKET_API_URL" => self::getUrl("HistoricTicket/api"), -]; - -$additionalJS = [ - "plugins/vue/vue.js", - "plugins/axios/axios.min.js", - "plugins/moment/moment.min.js", - "plugins/daterangepicker/daterangepicker.js", - "plugins/xlsx/xlsx.min.js", - "plugins/vue/tt-components/tt-table.js", - "plugins/vue/tt-components/tt-page-title.js", - "plugins/vue/tt-components/tt-select.js", - "plugins/vue/tt-components/tt-datepicker.js", - "plugins/vue/tt-components/tt-input.js", - "plugins/vue/tt-components/tt-autocomplete.js", - "plugins/vue/tt-components/tt-icon-select.js", - "plugins/vue/tt-components/tt-number-range.js", -]; -$additionalCSS = [ - "plugins/daterangepicker/daterangepicker.css", - 'plugins/vue/tt-components/css/tt-table.css', -]; - -include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?> - -
- - - - - - - - - - - - - - - - - - - - -
- - - - diff --git a/Layout/default/RaspberryDisplay/Index.php b/Layout/default/RaspberryDisplay/Index.php deleted file mode 100644 index 72f92698a..000000000 --- a/Layout/default/RaspberryDisplay/Index.php +++ /dev/null @@ -1,99 +0,0 @@ - self::getUrl("RaspberryDisplay"), - "DASHBOARD_URL" => self::getUrl("Dashboard"), - "MFAPPNAME" => MFAPPNAME_SLUG, - "PAGE_TITLE" => "Raspberry Displays", - "PATH" => [ - ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], - ["text" => "Raspberry Displays", "href" => self::getUrl("RaspberryDisplay")] - ] -]; - -$additionalJS = ["plugins/vue/vue.min.js", - "plugins/axios/axios.min.js", - "plugins/vue/tt-components/tt-page-title.js", - "plugins/vue/tt-components/tt-loader.js"]; -$additionalCSS = ["css/views/RaspberryDisplay.css", "plugins/vue/tt-components/css/tt-loader.css"]; - -include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?> - -
- - - -
- - -
-

8322 Studenzen NOC Displays

- -
-
-
-
- - -
-
-
- {{ display['display_url'] | cleanupURL }} - -
- -
-
- - -
-
- -
- -
- - -
- - - | - - -
- - - -
-
- -
- -
-
-
-
-
- - - \ No newline at end of file diff --git a/Layout/default/VoiceCallActive/Index.php b/Layout/default/VoiceCallActive/Index.php deleted file mode 100644 index 0a3f71d6b..000000000 --- a/Layout/default/VoiceCallActive/Index.php +++ /dev/null @@ -1,163 +0,0 @@ - self::getUrl("VoiceCallActive"), - "DASHBOARD_URL" => self::getUrl("Dashboard"), - "MFAPPNAME" => MFAPPNAME_SLUG, - "PAGE_TITLE" => "Active Voice Calls", - "PATH" => [ - ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], - ["text" => "Active Voice Calls", "href" => self::getUrl("VoiceCallActive")] - ], - "VOICE_CALL_ACTIVE_API_URL" => self::getUrl("VoiceCallActive/api"), -]; - -$additionalJS = [ - "plugins/vue/vue.js", - "plugins/axios/axios.min.js", - "plugins/moment/moment.min.js", - "plugins/daterangepicker/daterangepicker.js", - "plugins/xlsx/xlsx.min.js", - "plugins/vue/tt-components/tt-table.js", - "plugins/vue/tt-components/tt-page-title.js", - "plugins/vue/tt-components/tt-select.js", - "plugins/vue/tt-components/tt-datepicker.js", - "plugins/vue/tt-components/tt-input.js", - "plugins/vue/tt-components/tt-autocomplete.js", - "plugins/vue/tt-components/tt-icon-select.js", - "plugins/vue/tt-components/tt-number-range.js", -]; -$additionalCSS = [ - "plugins/daterangepicker/daterangepicker.css", - 'plugins/vue/tt-components/css/tt-table.css', -]; - -include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?> - -
- - - - - - - - - - - -
- - - - - - diff --git a/Layout/default/VoiceCallHistory/Index.php b/Layout/default/VoiceCallHistory/Index.php deleted file mode 100644 index 42fedac35..000000000 --- a/Layout/default/VoiceCallHistory/Index.php +++ /dev/null @@ -1,97 +0,0 @@ - self::getUrl("Domain"), - "DASHBOARD_URL" => self::getUrl("Dashboard"), - "MFAPPNAME" => MFAPPNAME_SLUG, - "PAGE_TITLE" => "Domains", - "PATH" => [ - ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], - ["text" => "Voice Calls History", "href" => self::getUrl("VoiceCallHistory")] - ], - "VOICE_CALL_HISTORY_API_URL" => self::getUrl("VoiceCallHistory/api"), -]; - -$additionalJS = [ - "plugins/vue/vue.js", - "plugins/axios/axios.min.js", - "plugins/moment/moment.min.js", - "plugins/daterangepicker/daterangepicker.js", - "plugins/xlsx/xlsx.min.js", - "plugins/vue/tt-components/tt-table.js", - "plugins/vue/tt-components/tt-page-title.js", - "plugins/vue/tt-components/tt-select.js", - "plugins/vue/tt-components/tt-datepicker.js", - "plugins/vue/tt-components/tt-input.js", - "plugins/vue/tt-components/tt-autocomplete.js", - "plugins/vue/tt-components/tt-icon-select.js", - "plugins/vue/tt-components/tt-number-range.js", -]; -$additionalCSS = [ - "plugins/daterangepicker/daterangepicker.css", - 'plugins/vue/tt-components/css/tt-table.css', -]; - -include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?> - -
- - - - - -
- - - - diff --git a/Layout/default/VueViews/Vue.php b/Layout/default/VueViews/Vue.php new file mode 100644 index 000000000..7233d5a9d --- /dev/null +++ b/Layout/default/VueViews/Vue.php @@ -0,0 +1,49 @@ + + + +
+ <>> +
+ + + \ No newline at end of file diff --git a/Layout/default/menu.php b/Layout/default/menu.php index 19efe9e0a..2efe0f355 100644 --- a/Layout/default/menu.php +++ b/Layout/default/menu.php @@ -16,14 +16,14 @@ is("Admin")): ?>
  • - "> Dashboard + "> Dashboard
  • is("Admin")): ?>
  • - "> Dashboard
    + "> Dashboard
  • diff --git a/Layout/default/vueHeader.php b/Layout/default/vueHeader.php new file mode 100644 index 000000000..e6a0b46e7 --- /dev/null +++ b/Layout/default/vueHeader.php @@ -0,0 +1,77 @@ + + + + + + <?=MFAPPNAME_FULL?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + + +
    +
    diff --git a/application/DocumentationCheck/DocumentationCheckController.php b/application/DocumentationCheck/DocumentationCheckController.php index 3521c6832..1a2031f62 100644 --- a/application/DocumentationCheck/DocumentationCheckController.php +++ b/application/DocumentationCheck/DocumentationCheckController.php @@ -5,7 +5,7 @@ class DocumentationCheckController extends mfBaseController { private string $ZABBIX_API_URL = ZABBIX_API_URL; private string $ZABBIX_API_KEY = ZABBIX_API_KEY; - private Zabbix $zabbix; +// private Zabbix $zabbix; protected function init(): void { @@ -18,7 +18,7 @@ class DocumentationCheckController extends mfBaseController { $this->redirect("dashboard"); } - $this->zabbix = new Zabbix($this->ZABBIX_API_URL, $this->ZABBIX_API_KEY); +// $this->zabbix = new Zabbix($this->ZABBIX_API_URL, $this->ZABBIX_API_KEY); } protected function indexAction(): void { diff --git a/application/Domain/DomainController.php b/application/Domain/DomainController.php index df84300c1..fb660f364 100644 --- a/application/Domain/DomainController.php +++ b/application/Domain/DomainController.php @@ -26,7 +26,21 @@ class DomainController extends mfBaseController { } protected function indexAction(): void { - $this->layout()->setTemplate("Domain/Index"); + $JSGlobals = ["BASE_URL" => self::getUrl("Domain"), + "DASHBOARD_URL" => self::getUrl("Dashboard"), + "MFAPPNAME" => MFAPPNAME_SLUG, + "PAGE_TITLE" => "Domains", + "PATH" => [ + ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], + ["text" => "Domain Management", "href" => self::getUrl("Domain")], + ["text" => "Domains"] + ], + "DOMAIN_API_URL" => self::getUrl("Domain/api"), + ]; + + $this->layout()->set("vueViewName", "Domain"); + $this->layout()->set("JSGlobals", $JSGlobals); + $this->layout()->setTemplate("VueViews/Vue"); } protected function apiAction() { @@ -128,7 +142,7 @@ class DomainController extends mfBaseController { "rows" => $domains, "pagination" => [ "page" => $page, - "total_pages" => ceil($totalRows / $perPage), + "total_pages" => ceil($filtered_available / $perPage), "per_page" => $perPage, "filtered_available" => intval($filtered_available), "total_rows" => intval($totalRows) diff --git a/application/Domain/DomainModel.php b/application/Domain/DomainModel.php index 686e4e9f9..51fe5bfbe 100644 --- a/application/Domain/DomainModel.php +++ b/application/Domain/DomainModel.php @@ -73,25 +73,6 @@ class DomainModel { ]; } - /** - * Generate SQL Filter condition (space separated) for a given column. - * - * @param string|null $filterValue The filter value to match against. - * @param string $columnName The name of the column in the database table. - * @return string The SQL condition generated based on the filter value and column name. - */ - public static function generateFilterCondition(?string $filterValue, string $columnName): string { - $sql = ""; - if (!empty($filterValue)) { - $filterItems = explode(" ", $filterValue); - foreach ($filterItems as $item) { - $sql .= " AND `$columnName` LIKE '%" . $item . "%'"; - } - } - - return $sql; - } - public static function getSqlFilter($filters): string { $sql = isset($filters['domain']) ? Helper::generateFilterCondition($filters['domain'], "domain") : ""; $sql .= isset($filters['crDate']) ? Helper::generateFilterCondition($filters['crDate'], "crDate") : ""; @@ -101,10 +82,10 @@ class DomainModel { $sql .= isset($filters['status']) ? Helper::generateFilterCondition($filters['status'], "status") : ""; $sql .= isset($filters['transferLock']) ? Helper::generateFilterCondition($filters['transferLock'], "transferLock") : ""; $sql .= isset($filters['authCode']) ? Helper::generateFilterCondition($filters['authCode'], "authCode") : ""; - $sql .= isset($filters['registrant']) && $filters['registrant'] !== 'all' ? " AND `registrant` = " . $filters['registrant'] : ""; - $sql .= isset($filters['admin']) && $filters['admin'] !== 'all' ? " AND `admin` = " . $filters['admin'] : ""; - $sql .= isset($filters['tech']) && $filters['tech'] !== 'all' ? " AND `tech` = " . $filters['tech'] : ""; - $sql .= isset($filters['billing']) && $filters['billing'] !== 'all' ? " AND `billing` = " . $filters['billing'] : ""; + $sql .= isset($filters['registrant']) ? Helper::generateFilterCondition($filters['registrant'], "registrant", true) : ""; + $sql .= isset($filters['admin']) ? Helper::generateFilterCondition($filters['admin'], "admin", true) : ""; + $sql .= isset($filters['tech']) ? Helper::generateFilterCondition($filters['tech'], "tech", true) : ""; + $sql .= isset($filters['billing']) ? Helper::generateFilterCondition($filters['billing'], "billing", true) : ""; $sql .= isset($filters['ns']) ? Helper::generateFilterCondition($filters['ns'], "ns") : ""; $sql .= isset($filters['pleskId']) ? " AND `pleskId` " . ($filters['pleskId'] === "0" ? "IS NULL" : "IS NOT NULL") : ""; return $sql; diff --git a/application/HistoricTicket/HistoricTicketController.php b/application/HistoricTicket/HistoricTicketController.php index ed1dae5ae..39d867815 100644 --- a/application/HistoricTicket/HistoricTicketController.php +++ b/application/HistoricTicket/HistoricTicketController.php @@ -15,7 +15,21 @@ class HistoricTicketController extends mfBaseController { if (!$this->me->is("employee")) { $this->redirect("dashboard"); } - $this->layout()->setTemplate("HistoricTicket/Index"); + + $JSGlobals = ["BASE_URL" => self::getUrl("Domain"), + "DASHBOARD_URL" => self::getUrl("Dashboard"), + "MFAPPNAME" => MFAPPNAME_SLUG, + "PAGE_TITLE" => "Historische Tickets", + "PATH" => [ + ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], + ["text" => "Historische Tickets", "href" => self::getUrl("HistoricTicket")] + ], + "HISTORIC_TICKET_API_URL" => self::getUrl("HistoricTicket/api"), + ]; + + $this->layout()->set("vueViewName", "HistoricTicket"); + $this->layout()->set("JSGlobals", $JSGlobals); + $this->layout()->setTemplate("VueViews/Vue"); } protected function apiAction() { @@ -66,7 +80,7 @@ class HistoricTicketController extends mfBaseController { "rows" => $historicTickets, "pagination" => [ "page" => $page, - "total_pages" => ceil($totalRows / $perPage), + "total_pages" => ceil($filtered_available / $perPage), "per_page" => $perPage, "filtered_available" => intval($filtered_available), "total_rows" => intval($totalRows) diff --git a/application/RaspberryDisplay/RaspberryDisplayController.php b/application/RaspberryDisplay/RaspberryDisplayController.php index 65c11e9c9..ea014e086 100644 --- a/application/RaspberryDisplay/RaspberryDisplayController.php +++ b/application/RaspberryDisplay/RaspberryDisplayController.php @@ -2,82 +2,18 @@ use phpseclib3\Net\SSH2; -class RaspberryDisplayController extends mfBaseController -{ +class RaspberryDisplayController extends mfBaseController { private int $port = 22; private string $username = XINON_RASPBERRY_DISPLAY_SSH_USER; private string $password = XINON_RASPBERRY_DISPLAY_SSH_PASS; - protected function init(): void - { + protected function init(): void { $me = new User(); $me->loadMe(); $this->me = $me; $this->layout()->set("me", $me); } - protected function restartRaspberryPi($id) { - $display = RaspberryDisplayModel::get($id); - - $ssh = new SSH2($display->ip_address, $this->port); - $ssh->login($this->username, $this->password); - $ssh->exec('sudo reboot now'); - return true; - } - - protected function getDisplaysApi(): array - { - $displays = RaspberryDisplayModel::getAll(); - $result = []; - foreach ($displays as $display) { - $result[] = [ - "display_label" => $display->display_label, - "hostname" => $display->hostname, - "ip" => $display->ip_address, - "display_url" => $display->display_url, - "auto_refresh_enabled" => $display->auto_refresh_enabled === "1", - "margin_hot_fix_enabled" => $display->margin_hot_fix_enabled === "1", - "custom_style" => $display->custom_style, - "id" => $display->id, - ]; - } - return $result; - } - - protected function change() - { - $displayID = $this->request->displayID; - $field = $this->request->field; - $value = $this->request->value; - $value = $value === "true" ? 1 : ($value === "false" ? 0 : $value); - $display = RaspberryDisplayModel::get($displayID); - if ($display === null) { - return false; - } - $display->$field = $value; - $display->save(); - return true; - } - - protected function getConfig() { - $hostname = $this->request->hostname; - - $displays = RaspberryDisplayModel::getByHostname($hostname); - - if ($displays === null) { - die("No display found for this hostname and ip:" . $hostname . " X "); - } - - return array_map(function ($display) { - return [ - "display_url" => $display->data->display_url, - "auto_refresh_enabled" => $display->data->auto_refresh_enabled === "1", - "margin_hot_fix_enabled" => $display->data->margin_hot_fix_enabled === "1", - "id" => $display->id, - ]; - } - , $displays); - } protected function apiAction() { $do = $this->request->do; @@ -111,7 +47,7 @@ class RaspberryDisplayController extends mfBaseController $this->returnJson($data); } - if(!is_array($return) || !count($return)) { + if (!is_array($return) || !count($return)) { $data = ["status" => "error"]; $this->returnJson($data); } @@ -120,9 +56,77 @@ class RaspberryDisplayController extends mfBaseController $this->returnJson($data); } - protected function indexAction(): void - { - $this->layout()->setTemplate("RaspberryDisplay/Index"); + protected function getDisplaysApi(): array { + $displays = RaspberryDisplayModel::getAll(); + $result = []; + foreach ($displays as $display) { + $result[] = ["display_label" => $display->display_label, + "hostname" => $display->hostname, + "ip" => $display->ip_address, + "display_url" => $display->display_url, + "auto_refresh_enabled" => $display->auto_refresh_enabled === "1", + "margin_hot_fix_enabled" => $display->margin_hot_fix_enabled === "1", + "custom_style" => $display->custom_style, + "id" => $display->id,]; + } + return $result; + } + + protected function change() { + $displayID = $this->request->displayID; + $field = $this->request->field; + $value = $this->request->value; + $value = $value === "true" ? 1 : ($value === "false" ? 0 : $value); + $display = RaspberryDisplayModel::get($displayID); + if ($display === null) { + return false; + } + $display->$field = $value; + $display->save(); + return true; + } + + protected function restartRaspberryPi($id) { + $display = RaspberryDisplayModel::get($id); + + $ssh = new SSH2($display->ip_address, $this->port); + $ssh->login($this->username, $this->password); + $ssh->exec('sudo reboot now'); + return true; + } + + protected function getConfig() { + $hostname = $this->request->hostname; + + $displays = RaspberryDisplayModel::getByHostname($hostname); + + if ($displays === null) { + die("No display found for this hostname and ip:" . $hostname . " X "); + } + + return array_map(function ($display) { + return ["display_url" => $display->data->display_url, + "auto_refresh_enabled" => $display->data->auto_refresh_enabled === "1", + "margin_hot_fix_enabled" => $display->data->margin_hot_fix_enabled === "1", + "id" => $display->id,]; + }, $displays); + } + + protected function indexAction(): void { + $JSGlobals = ["BASE_URL" => self::getUrl("RaspberryDisplay"), + "DASHBOARD_URL" => self::getUrl("Dashboard"), + "MFAPPNAME" => MFAPPNAME_SLUG, + "PAGE_TITLE" => "Raspberry Displays", + "PATH" => [ + ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], + ["text" => "Raspberry Displays", "href" => self::getUrl("RaspberryDisplay")] + ] + ]; + + $this->layout()->set("vueViewName", "RaspberryDisplay"); + $this->layout()->set("JSGlobals", $JSGlobals); + $this->layout()->set("additionalCSS", ["css/views/RaspberryDisplay.css"]); + $this->layout()->setTemplate("VueViews/Vue"); } diff --git a/application/VoiceCallActive/VoiceCallActiveController.php b/application/VoiceCallActive/VoiceCallActiveController.php index f3aa609a4..f5fb98541 100644 --- a/application/VoiceCallActive/VoiceCallActiveController.php +++ b/application/VoiceCallActive/VoiceCallActiveController.php @@ -24,7 +24,21 @@ class VoiceCallActiveController extends mfBaseController { } protected function indexAction(): void { - $this->layout()->setTemplate("VoiceCallActive/Index"); + $JSGlobals = ["BASE_URL" => self::getUrl("VoiceCallActive"), + "DASHBOARD_URL" => self::getUrl("Dashboard"), + "MFAPPNAME" => MFAPPNAME_SLUG, + "PAGE_TITLE" => "Active Voice Calls", + "PATH" => [ + ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], + ["text" => "Active Voice Calls", "href" => self::getUrl("VoiceCallActive")] + ], + "VOICE_CALL_ACTIVE_API_URL" => self::getUrl("VoiceCallActive/api"), + ]; + + $this->layout()->set("additionalCSS", ["css/views/VoiceCallActive.css"]); + $this->layout()->set("vueViewName", "VoiceCallActive"); + $this->layout()->set("JSGlobals", $JSGlobals); + $this->layout()->setTemplate("VueViews/Vue"); } protected function apiAction() { @@ -54,8 +68,10 @@ class VoiceCallActiveController extends mfBaseController { } private function getActiveCalls(): array { + $activeCalls = $this->kolmisoftMore->getActiveCalls(); + return [ - "rows" => array_reverse($this->kolmisoftMore->getActiveCalls()) + "rows" => is_null($activeCalls) ? [] : (is_object($activeCalls) ? [$activeCalls] : $activeCalls), ]; } } \ No newline at end of file diff --git a/application/VoiceCallHistory/VoiceCallHistoryController.php b/application/VoiceCallHistory/VoiceCallHistoryController.php index 0a4ef5701..1970ee31f 100644 --- a/application/VoiceCallHistory/VoiceCallHistoryController.php +++ b/application/VoiceCallHistory/VoiceCallHistoryController.php @@ -24,7 +24,20 @@ class VoiceCallHistoryController extends mfBaseController { } protected function indexAction(): void { - $this->layout()->setTemplate("VoiceCallHistory/Index"); + $JSGlobals = ["BASE_URL" => self::getUrl("Domain"), + "DASHBOARD_URL" => self::getUrl("Dashboard"), + "MFAPPNAME" => MFAPPNAME_SLUG, + "PAGE_TITLE" => "Voice Call History", + "PATH" => [ + ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")], + ["text" => "Voice Call History", "href" => self::getUrl("VoiceCallHistory")] + ], + "VOICE_CALL_HISTORY_API_URL" => self::getUrl("VoiceCallHistory/api"), + ]; + + $this->layout()->set("vueViewName", "VoiceCallHistory"); + $this->layout()->set("JSGlobals", $JSGlobals); + $this->layout()->setTemplate("VueViews/Vue"); } protected function apiAction() { @@ -73,6 +86,11 @@ class VoiceCallHistoryController extends mfBaseController { $page = $json['pagination']['page'] ?? 1; $perPage = $json['pagination']['per_page'] ?? 10; + if (isset($filters['start']['from']) && isset($filters['start']['to']) && is_numeric($filters['start']['to'])) { + $filters['start']['from'] += 7200; + $filters['start']['to'] += 7200; + } + $calls = VoiceCallHistoryModel::getVoiceCallHistory($filters, $perPage, $perPage * $page - $perPage, $order); $filtered_available = VoiceCallHistoryModel::countVoiceCallHistory($filters); $totalRows = VoiceCallHistoryModel::countVoiceCallHistory([]); @@ -81,7 +99,7 @@ class VoiceCallHistoryController extends mfBaseController { "rows" => $calls, "pagination" => [ "page" => $page, - "total_pages" => ceil($totalRows / $perPage), + "total_pages" => ceil($filtered_available / $perPage), "per_page" => $perPage, "filtered_available" => intval($filtered_available), "total_rows" => intval($totalRows) diff --git a/lib/Helper/Helper.php b/lib/Helper/Helper.php index cfdeb41d4..9f5ba3e01 100644 --- a/lib/Helper/Helper.php +++ b/lib/Helper/Helper.php @@ -9,19 +9,27 @@ class Helper { * @return string The SQL condition generated based on the filter value and column name. * @noinspection PhpMissingParamTypeInspection */ - public static function generateFilterCondition($filterValue, string $columnName): string { + public static function generateFilterCondition($filterValue, string $columnName, bool $exactMatch = false): string { $sql = ""; if (is_array($filterValue)) { if (isset($filterValue['from']) && isset($filterValue['to'])) { $sql = " AND `$columnName` >= " . $filterValue['from'] . " AND `$columnName` <= " . $filterValue['to']; + } else if (isset($filterValue['from'])) { + $sql = " AND `$columnName` >= " . $filterValue['from']; + } else if (isset($filterValue['to'])) { + $sql = " AND `$columnName` <= " . $filterValue['to']; } } else if ($filterValue === "0" || $filterValue === "1") { $sql .= " AND `$columnName` = " . $filterValue; } else if (!empty($filterValue)) { - $filterItems = explode(" ", $filterValue); - foreach ($filterItems as $item) { - $sql .= " AND `$columnName` LIKE '%" . $item . "%'"; + if ($exactMatch) { + $sql .= " AND `$columnName` = '" . $filterValue . "'"; + } else { + $filterItems = explode(" ", $filterValue); + foreach ($filterItems as $item) { + $sql .= " AND `$columnName` LIKE '%" . $item . "%'"; + } } } diff --git a/package.json b/package.json new file mode 100644 index 000000000..8ae0a71b1 --- /dev/null +++ b/package.json @@ -0,0 +1,3 @@ +{ + "dependencies": {"vue": "2.6.11"} +} \ No newline at end of file diff --git a/public/JSShrink.php b/public/JSShrink.php new file mode 100644 index 000000000..ef68499d3 --- /dev/null +++ b/public/JSShrink.php @@ -0,0 +1,721 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * JShrink + * + * + * @package JShrink + * @author Robert Hafner + */ + +namespace JShrink; + +/** + * Minifier + * + * Usage - Minifier::minify($js); + * Usage - Minifier::minify($js, $options); + * Usage - Minifier::minify($js, array('flaggedComments' => false)); + * + * @package JShrink + * @author Robert Hafner + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ +class Minifier +{ + /** + * The input javascript to be minified. + * + * @var string + */ + protected $input; + + /** + * Length of input javascript. + * + * @var int + */ + protected $len = 0; + + /** + * The location of the character (in the input string) that is next to be + * processed. + * + * @var int + */ + protected $index = 0; + + /** + * The first of the characters currently being looked at. + * + * @var string + */ + protected $a = ''; + + /** + * The next character being looked at (after a); + * + * @var string + */ + protected $b = ''; + + /** + * This character is only active when certain look ahead actions take place. + * + * @var string + */ + protected $c; + + /** + * This character is only active when certain look ahead actions take place. + * + * @var string + */ + protected $last_char; + + /** + * This character is only active when certain look ahead actions take place. + * + * @var string + */ + protected $output; + + /** + * Contains the options for the current minification process. + * + * @var array + */ + protected $options; + + /** + * These characters are used to define strings. + */ + protected $stringDelimiters = ['\'' => true, '"' => true, '`' => true]; + + /** + * Contains the default options for minification. This array is merged with + * the one passed in by the user to create the request specific set of + * options (stored in the $options attribute). + * + * @var array + */ + protected static $defaultOptions = ['flaggedComments' => true]; + + + protected static $keywords = ["delete", "do", "for", "in", "instanceof", "return", "typeof", "yield"]; + + protected $max_keyword_len; + + /** + * Contains lock ids which are used to replace certain code patterns and + * prevent them from being minified + * + * @var array + */ + protected $locks = []; + + /** + * Takes a string containing javascript and removes unneeded characters in + * order to shrink the code without altering it's functionality. + * + * @param string $js The raw javascript to be minified + * @param array $options Various runtime options in an associative array + * @throws \Exception + * @return bool|string + */ + public static function minify($js, $options = []) + { + try { + $jshrink = new Minifier(); + $js = $jshrink->lock($js); + $js = ltrim($jshrink->minifyToString($js, $options)); + $js = $jshrink->unlock($js); + unset($jshrink); + return $js; + } catch (\Exception $e) { + if (isset($jshrink)) { + // Since the breakdownScript function probably wasn't finished + // we clean it out before discarding it. + $jshrink->clean(); + unset($jshrink); + } + throw $e; + } + } + + /** + * Processes a javascript string and outputs only the required characters, + * stripping out all unneeded characters. + * + * @param string $js The raw javascript to be minified + * @param array $options Various runtime options in an associative array + */ + protected function minifyToString($js, $options) + { + $this->initialize($js, $options); + $this->loop(); + $this->clean(); + return $this->output; + } + + /** + * Initializes internal variables, normalizes new lines, + * + * @param string $js The raw javascript to be minified + * @param array $options Various runtime options in an associative array + */ + protected function initialize($js, $options) + { + $this->options = array_merge(static::$defaultOptions, $options); + $this->input = $js; + + // We add a newline to the end of the script to make it easier to deal + // with comments at the bottom of the script- this prevents the unclosed + // comment error that can otherwise occur. + $this->input .= PHP_EOL; + + // save input length to skip calculation every time + $this->len = strlen($this->input); + + // Populate "a" with a new line, "b" with the first character, before + // entering the loop + $this->a = "\n"; + $this->b = "\n"; + $this->last_char = "\n"; + $this->output = ""; + + $this->max_keyword_len = max(array_map('strlen', static::$keywords)); + } + + /** + * Characters that can't stand alone preserve the newline. + * + * @var array + */ + protected $noNewLineCharacters = [ + '(' => true, + '-' => true, + '+' => true, + '[' => true, + '#' => true, + '@' => true]; + + + protected function echo($char) { + $this->output .= $char; + $this->last_char = $char[-1]; + } + + + /** + * The primary action occurs here. This function loops through the input string, + * outputting anything that's relevant and discarding anything that is not. + */ + protected function loop() + { + while ($this->a !== false && !is_null($this->a) && $this->a !== '') { + switch ($this->a) { + // new lines + case "\r": + case "\n": + // if the next line is something that can't stand alone preserve the newline + if ($this->b !== false && isset($this->noNewLineCharacters[$this->b])) { + $this->echo($this->a); + $this->saveString(); + break; + } + + // if B is a space we skip the rest of the switch block and go down to the + // string/regex check below, resetting $this->b with getReal + if ($this->b === ' ') { + break; + } + + // otherwise we treat the newline like a space + + // no break + case ' ': + if (static::isAlphaNumeric($this->b)) { + $this->echo($this->a); + } + + $this->saveString(); + break; + + default: + switch ($this->b) { + case "\r": + case "\n": + if (strpos('}])+-"\'', $this->a) !== false) { + $this->echo($this->a); + $this->saveString(); + break; + } else { + if (static::isAlphaNumeric($this->a)) { + $this->echo($this->a); + $this->saveString(); + } + } + break; + + case ' ': + if (!static::isAlphaNumeric($this->a)) { + break; + } + + // no break + default: + // check for some regex that breaks stuff + if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) { + $this->saveRegex(); + continue 3; + } + + $this->echo($this->a); + $this->saveString(); + break; + } + } + + // do reg check of doom + $this->b = $this->getReal(); + + if ($this->b == '/') { + $valid_tokens = "(,=:[!&|?\n"; + + # Find last "real" token, excluding spaces. + $last_token = $this->a; + if ($last_token == " ") { + $last_token = $this->last_char; + } + + if (strpos($valid_tokens, $last_token) !== false) { + // Regex can appear unquoted after these symbols + $this->saveRegex(); + } else if ($this->endsInKeyword()) { + // This block checks for the "return" token before the slash. + $this->saveRegex(); + } + } + + // if (($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) { + // $this->saveRegex(); + // } + } + } + + /** + * Resets attributes that do not need to be stored between requests so that + * the next request is ready to go. Another reason for this is to make sure + * the variables are cleared and are not taking up memory. + */ + protected function clean() + { + unset($this->input); + $this->len = 0; + $this->index = 0; + $this->a = $this->b = ''; + unset($this->c); + unset($this->options); + } + + /** + * Returns the next string for processing based off of the current index. + * + * @return string + */ + protected function getChar() + { + // Check to see if we had anything in the look ahead buffer and use that. + if (isset($this->c)) { + $char = $this->c; + unset($this->c); + } else { + // Otherwise we start pulling from the input. + $char = $this->index < $this->len ? $this->input[$this->index] : false; + + // If the next character doesn't exist return false. + if (isset($char) && $char === false) { + return false; + } + + // Otherwise increment the pointer and use this char. + $this->index++; + } + + # Convert all line endings to unix standard. + # `\r\n` converts to `\n\n` and is minified. + if ($char == "\r") { + $char = "\n"; + } + + // Normalize all whitespace except for the newline character into a + // standard space. + if ($char !== "\n" && $char < "\x20") { + return ' '; + } + + return $char; + } + + /** + * This function returns the next character without moving the index forward. + * + * + * @return string The next character + * @throws \RuntimeException + */ + protected function peek() + { + if ($this->index >= $this->len) { + return false; + } + + $char = $this->input[$this->index]; + # Convert all line endings to unix standard. + # `\r\n` converts to `\n\n` and is minified. + if ($char == "\r") { + $char = "\n"; + } + + // Normalize all whitespace except for the newline character into a + // standard space. + if ($char !== "\n" && $char < "\x20") { + return ' '; + } + + # Return the next character but don't push the index. + return $char; + } + + /** + * This function gets the next "real" character. It is essentially a wrapper + * around the getChar function that skips comments. This has significant + * performance benefits as the skipping is done using native functions (ie, + * c code) rather than in script php. + * + * + * @return string Next 'real' character to be processed. + * @throws \RuntimeException + */ + protected function getReal() + { + $startIndex = $this->index; + $char = $this->getChar(); + + // Check to see if we're potentially in a comment + if ($char !== '/') { + return $char; + } + + $this->c = $this->getChar(); + + if ($this->c === '/') { + $this->processOneLineComments($startIndex); + + return $this->getReal(); + } elseif ($this->c === '*') { + $this->processMultiLineComments($startIndex); + + return $this->getReal(); + } + + return $char; + } + + /** + * Removed one line comments, with the exception of some very specific types of + * conditional comments. + * + * @param int $startIndex The index point where "getReal" function started + * @return void + */ + protected function processOneLineComments($startIndex) + { + $thirdCommentString = $this->index < $this->len ? $this->input[$this->index] : false; + + // kill rest of line + $this->getNext("\n"); + + unset($this->c); + + if ($thirdCommentString == '@') { + $endPoint = $this->index - $startIndex; + $this->c = "\n" . substr($this->input, $startIndex, $endPoint); + } + } + + /** + * Skips multiline comments where appropriate, and includes them where needed. + * Conditional comments and "license" style blocks are preserved. + * + * @param int $startIndex The index point where "getReal" function started + * @return void + * @throws \RuntimeException Unclosed comments will throw an error + */ + protected function processMultiLineComments($startIndex) + { + $this->getChar(); // current C + $thirdCommentString = $this->getChar(); + + // Detect a completely empty comment, ie `/**/` + if ($thirdCommentString == "*") { + $peekChar = $this->peek(); + if ($peekChar == "/") { + $this->index++; + return; + } + } + + // kill everything up to the next */ if it's there + if ($this->getNext('*/')) { + $this->getChar(); // get * + $this->getChar(); // get / + $char = $this->getChar(); // get next real character + + // Now we reinsert conditional comments and YUI-style licensing comments + if (($this->options['flaggedComments'] && $thirdCommentString === '!') + || ($thirdCommentString === '@')) { + + // If conditional comments or flagged comments are not the first thing in the script + // we need to echo a and fill it with a space before moving on. + if ($startIndex > 0) { + $this->echo($this->a); + $this->a = " "; + + // If the comment started on a new line we let it stay on the new line + if ($this->input[($startIndex - 1)] === "\n") { + $this->echo("\n"); + } + } + + $endPoint = ($this->index - 1) - $startIndex; + $this->echo(substr($this->input, $startIndex, $endPoint)); + + $this->c = $char; + + return; + } + } else { + $char = false; + } + + if ($char === false) { + throw new \RuntimeException('Unclosed multiline comment at position: ' . ($this->index - 2)); + } + + // if we're here c is part of the comment and therefore tossed + $this->c = $char; + } + + /** + * Pushes the index ahead to the next instance of the supplied string. If it + * is found the first character of the string is returned and the index is set + * to it's position. + * + * @param string $string + * @return string|false Returns the first character of the string or false. + */ + protected function getNext($string) + { + // Find the next occurrence of "string" after the current position. + $pos = strpos($this->input, $string, $this->index); + + // If it's not there return false. + if ($pos === false) { + return false; + } + + // Adjust position of index to jump ahead to the asked for string + $this->index = $pos; + + // Return the first character of that string. + return $this->index < $this->len ? $this->input[$this->index] : false; + } + + /** + * When a javascript string is detected this function crawls for the end of + * it and saves the whole string. + * + * @throws \RuntimeException Unclosed strings will throw an error + */ + protected function saveString() + { + $startpos = $this->index; + + // saveString is always called after a gets cleared, so we push b into + // that spot. + $this->a = $this->b; + + // If this isn't a string we don't need to do anything. + if (!isset($this->stringDelimiters[$this->a])) { + return; + } + + // String type is the quote used, " or ' + $stringType = $this->a; + + // Echo out that starting quote + $this->echo($this->a); + + // Loop until the string is done + // Grab the very next character and load it into a + while (($this->a = $this->getChar()) !== false) { + switch ($this->a) { + + // If the string opener (single or double quote) is used + // output it and break out of the while loop- + // The string is finished! + case $stringType: + break 2; + + // New lines in strings without line delimiters are bad- actual + // new lines will be represented by the string \n and not the actual + // character, so those will be treated just fine using the switch + // block below. + case "\n": + if ($stringType === '`') { + $this->echo($this->a); + } else { + throw new \RuntimeException('Unclosed string at position: ' . $startpos); + } + break; + + // Escaped characters get picked up here. If it's an escaped new line it's not really needed + case '\\': + + // a is a slash. We want to keep it, and the next character, + // unless it's a new line. New lines as actual strings will be + // preserved, but escaped new lines should be reduced. + $this->b = $this->getChar(); + + // If b is a new line we discard a and b and restart the loop. + if ($this->b === "\n") { + break; + } + + // echo out the escaped character and restart the loop. + $this->echo($this->a . $this->b); + break; + + + // Since we're not dealing with any special cases we simply + // output the character and continue our loop. + default: + $this->echo($this->a); + } + } + } + + /** + * When a regular expression is detected this function crawls for the end of + * it and saves the whole regex. + * + * @throws \RuntimeException Unclosed regex will throw an error + */ + protected function saveRegex() + { + if ($this->a != " ") { + $this->echo($this->a); + } + + $this->echo($this->b); + + while (($this->a = $this->getChar()) !== false) { + if ($this->a === '/') { + break; + } + + if ($this->a === '\\') { + $this->echo($this->a); + $this->a = $this->getChar(); + } + + if ($this->a === "\n") { + throw new \RuntimeException('Unclosed regex pattern at position: ' . $this->index); + } + + $this->echo($this->a); + } + $this->b = $this->getReal(); + } + + /** + * Checks to see if a character is alphanumeric. + * + * @param string $char Just one character + * @return bool + */ + protected static function isAlphaNumeric($char) + { + return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/'; + } + + protected function endsInKeyword() { + + # When this function is called A is not yet assigned to output. + # Regular expression only needs to check final part of output for keyword. + $testOutput = substr($this->output . $this->a, -1 * ($this->max_keyword_len + 10)); + + foreach(static::$keywords as $keyword) { + if (preg_match('/[^\w]'.$keyword.'[ ]?$/i', $testOutput) === 1) { + return true; + } + } + return false; + } + + + + /** + * Replace patterns in the given string and store the replacement + * + * @param string $js The string to lock + * @return bool + */ + protected function lock($js) + { + /* lock things like "asd" + ++x; */ + $lock = '"LOCK---' . crc32(time()) . '"'; + + $matches = []; + preg_match('/([+-])(\s+)([+-])/S', $js, $matches); + if (empty($matches)) { + return $js; + } + + $this->locks[$lock] = $matches[2]; + + $js = preg_replace('/([+-])\s+([+-])/S', "$1{$lock}$2", $js); + /* -- */ + + return $js; + } + + /** + * Replace "locks" with the original characters + * + * @param string $js The string to unlock + * @return bool + */ + protected function unlock($js) + { + if (empty($this->locks)) { + return $js; + } + + foreach ($this->locks as $lock => $replacement) { + $js = str_replace($lock, $replacement, $js); + } + + return $js; + } +} \ No newline at end of file diff --git a/public/bundler.php b/public/bundler.php new file mode 100644 index 000000000..1773a60d1 --- /dev/null +++ b/public/bundler.php @@ -0,0 +1,48 @@ + diff --git a/public/css/views/VoiceCallActive.css b/public/css/views/VoiceCallActive.css new file mode 100644 index 000000000..a39ca668e --- /dev/null +++ b/public/css/views/VoiceCallActive.css @@ -0,0 +1,13 @@ +.voice-green { + background-color: #6CAE75 !important +} +.voice-yellow { + background-color: #E8E288 !important +} +.voice-red { + background-color: #FF8360 !important +} + +.table-sm td { + padding: 4px !important; +} \ No newline at end of file diff --git a/public/js/pages/Domain/Domain.js b/public/js/pages/Domain/Domain.js new file mode 100644 index 000000000..73650f418 --- /dev/null +++ b/public/js/pages/Domain/Domain.js @@ -0,0 +1,225 @@ +Vue.component('Domain', { + //language=Vue + template: ` +
    + + + + + + + + + + + + + + + + + + + + + +
    + `, + data() { + return { + window: window, + domainContacts: {}, + reloadDomainsLoading: false, + dnsRecordsModalLoading: null, + dnsRecordsModal: { + domain: null, records: [] + }, + checkDomainInput: '', + checkDomainResult: null, + checkDomainLoading: false + } + }, created() { + this.fetchDomainContacts().then() + }, computed: { + domainsTableConfig() { + const base = { + headers: [{text: "DNS", key: "inwxRoId", filter: false, sortable: false}, { + text: "Domain", + key: "domain" + }, { + text: "Plesk", + key: "pleskId", + filter: 'iconSelect', + filterOptions: [{value: 1, text: 'Yes', icon: 'fas fa-check text-success'}, { + value: 0, + text: 'No', + icon: 'fas fa-times text-danger' + }], + sortable: false + }, {text: "Created Date", key: "crDate", filter: "date"}, { + text: "Expiration Date", + key: "exDate", + filter: "date" + }, {text: "Renewal Date", key: "reDate", filter: "date"}, { + text: "Updated Date", + key: "upDate", + filter: "date" + }, { + text: "Transfer Lock", + key: "transferLock", + filter: 'iconSelect', + filterOptions: [{value: 1, text: 'Locked', icon: 'fas fa-lock text-danger'}, { + value: 0, + text: 'Unlocked', + icon: 'fas fa-unlock text-success' + }] + }, {text: "Authorization Code", key: "authCode", sortable: false}, { + text: "Registrant ID", + key: "registrant", + sortable: false + }, {text: "Admin ID", key: "admin", sortable: false}, { + text: "Tech ID", + key: "tech", + sortable: false + }, {text: "Billing ID", key: "billing", sortable: false}, {text: "Name Servers", key: "ns"}], + tableHeader: 'Domains', + key: 'Domain' + } + + const domainContactsSorted = Object.entries(this.domainContacts).sort(([, a], [, b]) => a.name.localeCompare(b.name)) + const domainContactsFilterOptions = domainContactsSorted.map(([, contact]) => { + return {text: contact.name, value: contact.inwxRoId} + }) + + // for registrant admin tech billing set filter to select with domainContacts if domainContacts is not empty + if (Object.keys(this.domainContacts).length > 0) { + base.headers = base.headers.map(header => { + if (['registrant', 'admin', 'tech', 'billing'].includes(header.key)) { + header.filter = 'select' + header.filterOptions = domainContactsFilterOptions + } + return header + }) + } + return base + } + }, methods: { + async showDnsRecordsModal(domain) { + this.dnsRecordsModalLoading = domain + this.dnsRecordsModal = { + domain: null, records: [] + } + const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=getDnsRecords&domain=' + domain) + this.dnsRecordsModal.domain = domain + this.dnsRecordsModal.records = response.data.map(record => { + if (typeof record.entries === 'object') { + record.value = record.entries[0] + } else { + record.value = record.target || record.txt || record.ip + } + if (record.type === 'SOA') { + record.value = record.mname + ' ' + record.rname + ' ' + record.serial + ' ' + record.refresh + ' ' + record.retry + ' ' + record.expire + + } + return record + }) + this.dnsRecordsModalLoading = null + this.$nextTick(() => { + this.$refs.dnsRecordsModal.focus() + }) + }, async fetchDomainContacts() { + const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=getDomainContacts') + this.domainContacts = response.data + }, async reloadDomains() { + this.reloadDomainsLoading = true + const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=importAllDomains') + window.notify('success', response.data["importMessages"].join('
    ')) + await Promise.all([this.fetchDomainContacts(), this.$refs.table.fetchData(this.$refs.table.pagination.page)]) + this.reloadDomainsLoading = false + }, //TODO: make this cleaner + async checkDomainAvailability() { + this.checkDomainLoading = true + const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=checkDomain&domain=' + this.checkDomainInput) + const priceInformation = response.data.price.domain[this.checkDomainInput] + window.notify(response.data.status === 'free' ? 'success' : 'error', `Domain ist ${response.data.status === 'free' ? 'verfügbar. Registrieren um' : 'nicht frei. Transfer um'} ${priceInformation.price}${priceInformation.currency}/${priceInformation.period === '1Y' ? 'Jahr' : priceInformation.period}`) + this.checkDomainLoading = false + } + } + +}) diff --git a/public/js/pages/HistoricTicket/HistoricTicket.js b/public/js/pages/HistoricTicket/HistoricTicket.js new file mode 100644 index 000000000..ee07537dd --- /dev/null +++ b/public/js/pages/HistoricTicket/HistoricTicket.js @@ -0,0 +1,185 @@ +Vue.component('HistoricTicket', { + //language=Vue + template: ` +
    + + + + + + + + + + + + + + + + + + + + +
    + `, data() { + return { + window: window, + selectedTicketNumber: null, + selectedTicketData: null, + globalSearch: '', + globalSearchModal: false, + globalSearchModalTableConfig: { + headers: [{text: 'Ticket Number', key: 'ticket_number', filter: false, sortable: false}, + {text: 'Erstellt', key: 'ctime', filter: false, sortable: false}, + {text: 'Subject', key: 'ticket_subject', filter: false, sortable: false}, + {text: 'Message', key: 'ticket_message', filter: false, sortable: false},], + tableHeader: 'Suchergebnisse', + key: 'HistoricTicketGlobalSearch', + }, + historicTicketTableConfig: { + headers: [{text: 'Ticket Number', key: 'ticket_number', filter: 'search'}, + {text: 'Erstellt', key: 'ctime', filter: false}, + {text: 'Subject', key: 'subject', filter: 'search', sortable: false}, + { + text: 'Type', + key: 'type', + filter: 'select', + filterOptions: [{value: 'BACKOFFICE', text: 'BACKOFFICE'}, + {value: 'KUNDENANFRAGEN', text: 'KUNDENANFRAGEN'}, + {value: 'STÖRUNGEN', text: 'STÖRUNGEN'}, + {value: 'ALLGEMEINES', text: 'ALLGEMEINES'}, + {value: 'TERMIN VEREINBART', text: 'TERMIN VEREINBART'}, + {value: 'VERRECHNEN AB DATUM', text: 'VERRECHNEN AB DATUM'}, + {value: 'ONLINE-TICKETS', text: 'ONLINE-TICKETS'}, + {value: 'KÜNDIGUNG', text: 'KÜNDIGUNG'}, + {value: 'BESTELLUNGEN', text: 'BESTELLUNGEN'}, + {value: 'PORTIERUNG', text: 'PORTIERUNG'}, + {value: 'KABEL-TV', text: 'KABEL-TV'}, + {value: 'TIEFBAU', text: 'TIEFBAU'}, + {value: 'ENERGIE STEIERMARK', text: 'ENERGIE STEIERMARK'}, + {value: 'INTERN', text: 'INTERN'}, + {value: 'FELIX', text: 'FELIX'}, + {value: '0', text: '0'}, + {value: 'JETTEN', text: 'JETTEN'},] + }, + { + text: 'Status', + key: 'status', + filter: 'select', + filterOptions: [{value: 'Geschlossen', text: 'Geschlossen'}, + {value: 'In Evidenz', text: 'In Evidenz'}, + {value: 'In Bearbeitung', text: 'In Bearbeitung'}, + {value: 'Business In Bearbeitung', text: 'Business In Bearbeitung'}, + {value: 'Business Angebot gelegt', text: 'Business Angebot gelegt'},] + }, + {text: 'Name', key: 'first_name', filter: 'search', sortable: false}, + {text: 'Email', key: 'email', filter: 'search', sortable: false}, + {text: 'Phone', key: 'phone', filter: 'search', sortable: false},], + defaultPageSize: 25, + tableHeader: 'Historische Tickets', + key: 'HistoricTicket', + } + } + }, methods: { + async doGlobalSearch() { + if (this.globalSearch.length > 0) { + this.globalSearchModal = true; + this.$nextTick(() => { + this.$refs.globalSearchModal.focus(); + }); + } + }, async clickTicketNumber(ticketNumber) { + this.globalSearchModal = false; + this.selectedTicketData = null; + + this.selectedTicketNumber = ticketNumber; + const response = await axios.post(`${window['TT_CONFIG']['HISTORIC_TICKET_API_URL']}?do=getHistoricTicketMessages`, + {ticketNumber}); + this.selectedTicketData = response.data; + this.$nextTick(() => { + this.$refs.selectedTicketModal.focus(); + }); + } + } + +}) diff --git a/public/js/pages/IpNetwork/IpNetwork.js b/public/js/pages/IpNetwork/IpNetwork.js new file mode 100644 index 000000000..9d057823f --- /dev/null +++ b/public/js/pages/IpNetwork/IpNetwork.js @@ -0,0 +1,178 @@ +Vue.component('IpNetwork', { + //language=Vue + template: ` +
    + + + + + + + + + + + + + +
    + `, + data() { + return { + window: window, + apiUrl: window['TT_CONFIG']['IPNETWORK_API_URL'], + IpNetworkTableConfig: { + defaultPageSize: 50, + customRowClass: function (row) { + return row.cidr !== '32' ? 'tt-pointer' : ''; + }, + expandCondition: function (row) { + return !!row.description; + }, + headers: [ + {text: 'Network Address', key: 'network_address_str'}, + {text: 'Name', key: 'name'}, + { + text: 'Status', key: 'status', filter: 'iconSelect', + filterOptions: [{value: 'active', text: 'Active', icon: 'fas fa-check text-success'}, + {value: 'inactive', text: 'Inactive', icon: 'fas fa-times text-danger'}, + {value: 'reserved', text: 'Reserved', icon: 'fas fa-lock text-warning'}] + }, + {text: 'Children', key: 'children', filter: 'numberRange'}, + ], + tableHeader: 'IPAM', + key: 'IpNetwork' + }, + currentNetworkData: null, + addModal: false, + addModalData: { + network_address: '', + cidr: '', + parent_network_id: '', + status: 'active', + name: '', + description: '', + location: '', + }, + } + }, + async mounted() { + function popstateFunction() { + const parentNetworkId = new URLSearchParams(window.location.search).get('parent_network_id'); + this.switchCurrentNetwork(parentNetworkId).then(); + } + + window.onpopstate = popstateFunction.bind(this); + window.onpopstate.call(this) + + }, + methods: { + async switchCurrentNetwork(networkId = null) { + if (!networkId) { + this.$refs.table.$set(this.$refs.table.filters, 'parent_network_id', undefined); + this.currentNetworkData = null; + this.IpNetworkTableConfig.tableHeader = 'IPAM'; + this.$refs.table.disableDebounce = true; + window.history.pushState({}, '', `?`); + } else { + this.$refs.table.disableDebounce = true; + this.$refs.table.$set(this.$refs.table.filters, 'parent_network_id', networkId); + window.history.pushState({}, '', `?parent_network_id=${networkId}`); + + const response = await axios.post(`${this.apiUrl}?do=getById`, {id: networkId}); + this.currentNetworkData = response.data.network; + this.IpNetworkTableConfig.tableHeader = `IPAM - ${this.currentNetworkData.network_address_str}/${this.currentNetworkData.cidr} - ${this.currentNetworkData.name}`; + } + await this.$refs.table.fetchData(); + }, + async addSubmit() { + const response = await axios.post(`${this.apiUrl}?do=create`, + { + ...this.addModalData, + parent_network_id: this.currentNetworkData ? this.currentNetworkData.id : null + }); + if (response.data.status === 'success') { + this.addModal = false; + window.notify('success', 'Network space created successfully'); + await this.$refs.table.fetchData(); + } else { + window.notify('error', response.data.message); + } + }, + }, +}) diff --git a/public/js/pages/RaspberryDisplay/RaspberryDisplay.js b/public/js/pages/RaspberryDisplay/RaspberryDisplay.js new file mode 100644 index 000000000..189d54c0f --- /dev/null +++ b/public/js/pages/RaspberryDisplay/RaspberryDisplay.js @@ -0,0 +1,130 @@ +Vue.filter('cleanupURL', function (value) { + value = value.replace(/^(?:https?:\/\/)?(?:www\.)?/i, "").split('/')[0]; + return value; +}) + +Vue.component('RaspberryDisplay', { + //language=Vue + template: ` +
    + + +
    + + +
    +

    8322 Studenzen NOC Displays

    + +
    +
    +
    +
    + + +
    +
    +
    + {{ display['display_url'] | cleanupURL }} + +
    + +
    +
    + + +
    +
    + +
    + +
    + + +
    + + + | + + +
    + + + +
    +
    + +
    + +
    +
    +
    +
    +
    + `, + + data() { + return { + loading: false, displaysURLEditMode: null, displays: null, window: window + } + }, + mounted() { + this.fetchDisplays().then() + }, methods: { + async rebootRaspberry(displayID) { + this.loading = true; + await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=reboot`, { + params: { + displayID: displayID + } + }); + this.loading = false; + }, async fetchDisplays() { + this.loading = true; + const response = await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=getDisplays`); + this.displays = response.data.result; + this.loading = false; + Vue.nextTick(() => { + $('[data-toggle="tooltip"]').tooltip('dispose'); + $('[data-toggle="tooltip"]').tooltip(); + }); + }, enableDisplayURLEditMode(displayID) { + this.displaysURLEditMode = displayID; + const _this = this; + // wait for the DOM to update + Vue.nextTick(() => { + _this.$refs['displayURLEditInput'][0].focus(); + }); + }, disableDisplayURLEditMode(displayID, displayURL) { + this.displaysURLEditMode = null; + this.submitChanges(displayID, 'display_url', displayURL); + }, async submitChanges(displayID, field, value) { + this.loading = true; + await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=change`, { + params: { + displayID: displayID, field: field, value: value, + } + }); + await this.fetchDisplays(); + this.loading = false; + } + }, + + +}) diff --git a/public/js/pages/VoiceCallActive/VoiceCallActive.js b/public/js/pages/VoiceCallActive/VoiceCallActive.js new file mode 100644 index 000000000..4a2c3e801 --- /dev/null +++ b/public/js/pages/VoiceCallActive/VoiceCallActive.js @@ -0,0 +1,112 @@ +Vue.component('VoiceCallActive', { + //language=Vue + template: ` +
    + + + + + + + + + + + + +
    + `, + data() { + return { + window: window, + VoiceCallActiveTableConfig: { + customRowClass: function (row) { + if (row.status.toLowerCase() === 'ringing') { + return 'voice-yellow'; + } + if (!row.dst_device_extension) { + return 'voice-red'; + } + if (row.status.toLowerCase() === 'answered') { + return 'voice-green'; + } + }, + headers: [ + {text: 'Call ID', key: 'id', filter: false, sortable: false}, + {text: 'Status', key: 'status', filter: false, sortable: false}, + {text: 'Answer Time', key: 'answer_time', filter: false, sortable: false}, + {text: 'Duration', key: 'duration', filter: false, sortable: false}, + {text: 'Source', key: 'src', filter: false, sortable: false}, + {text: 'Device Type', key: 'device_type', filter: false, sortable: false}, + {text: 'Destination', key: 'localized_dst', filter: false, sortable: false}, + {text: 'Destination User', key: 'dst_user', filter: false, sortable: false}, + {text: 'Destination Device Extension', key: 'dst_device_extension', filter: false, sortable: false}, + ], + tableHeader: 'Active Voice Calls', + key: 'VoiceCallActive', + }, + refreshLoading: false, + autoRefresh: null, + } + }, + mounted() { + //TODO: create vue tooltip component + $('[data-toggle="tooltip"]').tooltip(); + + const _this = this; + $('#autoRefresh').change(function () { + console.log(this.checked); + if (this.checked) { + _this.autoRefresh = setInterval(function () { + _this.refresh(); + }, 5000); + } else { + clearInterval(_this.autoRefresh); + } + }) + }, + methods: { + async refresh() { + this.refreshLoading = true; + this.$refs.table.loading = true; + await this.$refs.table.fetchData(); + $('.tooltip').tooltip('hide'); + this.$refs.table.loading = false; + this.refreshLoading = false; + }, + } + +}) diff --git a/public/js/pages/VoiceCallHistory/VoiceCallHistory.js b/public/js/pages/VoiceCallHistory/VoiceCallHistory.js new file mode 100644 index 000000000..52e238f23 --- /dev/null +++ b/public/js/pages/VoiceCallHistory/VoiceCallHistory.js @@ -0,0 +1,56 @@ +Vue.component('VoiceCallHistory', { + //language=Vue + template: ` +
    + + + + + +
    + `, data() { + return { + window: window, + VoiceCallHistoryTableConfig: { + headers: [{text: "Call-ID", key: "uid"}, + {text: "Voice Account", key: "voice_account"}, + {text: "Time Range", key: "start", filter: "date"}, + {text: "Source", key: "source"}, + {text: "Destination", key: "destination"}, + { + text: "Billable", + key: "billable", + filter: "iconSelect", + filterOptions: [{value: 1, text: 'Yes', icon: 'fas fa-check text-success'}, + {value: 0, text: 'No', icon: 'fas fa-times text-danger'}] + }, + {text: "Duration", key: "duration", filter: "numberRange"},], + tableHeader: 'Voice Call History', + key: 'VoiceCallHistory', + }, + importCallsFromTodayLoading: false, + } + }, + methods: { + async importCallsFromToday() { + this.importCallsFromTodayLoading = true; + const response = await axios.get(window['TT_CONFIG']['VOICE_CALL_HISTORY_API_URL'] + '?do=importCallsFromToday'); + window.notify(response.data.status === 'success' ? 'success' : 'error', response.data.message); + await this.$refs.table.fetchData(); + this.importCallsFromTodayLoading = false; + }, + } +}) diff --git a/public/js/pages/raspberryDisplay.js b/public/js/pages/raspberryDisplay.js deleted file mode 100644 index 7c7b3c85a..000000000 --- a/public/js/pages/raspberryDisplay.js +++ /dev/null @@ -1,63 +0,0 @@ -// noinspection JSJQueryEfficiency - -Vue.filter('cleanupURL', function (value) { - value = value.replace(/^(?:https?:\/\/)?(?:www\.)?/i, "").split('/')[0]; - return value; -}) - -new Vue({ - el: '#app', - mounted() { - this.fetchDisplays() - }, - methods: { - async rebootRaspberry(displayID) { - this.loading = true; - await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=reboot`, { - params: { - displayID: displayID - } - }); - this.loading = false; - }, - async fetchDisplays() { - this.loading = true; - const response = await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=getDisplays`); - this.displays = response.data.result; - this.loading = false; - Vue.nextTick(() => { - $('[data-toggle="tooltip"]').tooltip('dispose'); - $('[data-toggle="tooltip"]').tooltip(); - }); - }, - enableDisplayURLEditMode(displayID) { - this.displaysURLEditMode = displayID; - const _this = this; - // wait for the DOM to update - Vue.nextTick(() => { - _this.$refs['displayURLEditInput'][0].focus(); - }); - }, - disableDisplayURLEditMode(displayID, displayURL) { - this.displaysURLEditMode = null; - this.submitChanges(displayID, 'display_url', displayURL); - }, - async submitChanges(displayID, field, value) { - this.loading = true; - await axios.get(`${window['TT_CONFIG']["BASE_URL"]}/api?do=change`, { - params: { - displayID: displayID, - field: field, - value: value, - } - }); - await this.fetchDisplays(); - this.loading = false; - } - }, - data: { - loading: false, - displaysURLEditMode: null, - displays: null, - } -}); \ No newline at end of file diff --git a/public/plugins/vue/tt-components/css/tt-table.css b/public/plugins/vue/tt-components/css/tt-table.css index c62be3e1a..548e95ddf 100644 --- a/public/plugins/vue/tt-components/css/tt-table.css +++ b/public/plugins/vue/tt-components/css/tt-table.css @@ -20,10 +20,56 @@ margin-bottom: 4px !important; } + +.tt-table-card { + overflow: auto; +} + .tt-table-card .page-link { padding: 5px .75rem !important; } .tt-table { margin-bottom: 8px; +} + +.tt-table-pagination-container { + display: grid; + grid-template-columns: 1fr; + padding-bottom: 8px; + align-items: center; + justify-content: end; +} +.tt-table-pagination-wrapper { + display: grid; + grid-template-rows: auto auto; + grid-template-columns: auto auto; + grid-auto-flow: column; + grid-gap: 4px; + justify-content: end; +} +.tt-table-pagination { + margin: 0; +} +.tt-table-page-item { + cursor: pointer; +} +.tt-table-page-item.disabled { + pointer-events: none; + opacity: 0.5; +} +.tt-table-page-item.active { + font-weight: bold; + background-color: #007bff; + color: white; +} +.tt-table-select { + display: inline-block; + width: auto; +} +.tt-table-text-center { + text-align: center; +} +.tt-pointer { + cursor: pointer; } \ No newline at end of file diff --git a/public/plugins/vue/tt-components/tt-datepicker.js b/public/plugins/vue/tt-components/tt-datepicker.js index 2b4cc9826..f1d98fd2a 100644 --- a/public/plugins/vue/tt-components/tt-datepicker.js +++ b/public/plugins/vue/tt-components/tt-datepicker.js @@ -66,6 +66,7 @@ Vue.component('tt-date-picker', { }); function checkIfAppliedElseClear() { + if (this.value && this.value.from && this.value.to) return; $(_this.$refs.input).val(''); } @@ -75,7 +76,7 @@ Vue.component('tt-date-picker', { } $(this.$refs.input).on('cancel.daterangepicker', clearIfCancelled); - $(this.$refs.input).on('hide.daterangepicker', checkIfAppliedElseClear); + $(this.$refs.input).on('hide.daterangepicker', checkIfAppliedElseClear.bind(this)); // if value from or to is undefined then clear the input field if (!this.value || this.value.from === null || this.value.to === null) { @@ -84,11 +85,15 @@ Vue.component('tt-date-picker', { }, watch: { - value: function (newVal, oldVal) { - if (this.isInitialized) { - if (!newVal || newVal.from === null || newVal.to === null) { - $(this.$refs.input).val(''); - } + value: function (newVal) { + if (!this.isInitialized) return; + + const datePicker = $(this.$refs.input).data('daterangepicker'); + if (!newVal || newVal.from === null || newVal.to === null) { + $(this.$refs.input).val(''); + } else { + datePicker.setStartDate(this.moment.unix(newVal.from)); + datePicker.setEndDate(this.moment.unix(newVal.to)); } } }, diff --git a/public/plugins/vue/tt-components/tt-icon-select.js b/public/plugins/vue/tt-components/tt-icon-select.js index 193e884a1..5266ad581 100644 --- a/public/plugins/vue/tt-components/tt-icon-select.js +++ b/public/plugins/vue/tt-components/tt-icon-select.js @@ -23,6 +23,11 @@ Vue.component('tt-icon-select', { document.removeEventListener('click', this.handleClick); this.observer.disconnect(); }, + watch: { + value(val) { + this.selectedOption = this.options.find(option => option.value.toString() === val) || null; + } + }, methods: { selectOption(option) { this.selectedOption = option; @@ -38,16 +43,16 @@ Vue.component('tt-icon-select', { }, }, template: ` -
    +