From b0dc3aa1f9dd1eb51c8adab7cfd15c0aa334bda0 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 12 Mar 2024 16:17:23 +0100 Subject: [PATCH 01/54] Add HistoricTickets to thetool --- application/HistoricTicket/HistoricTicket.php | 9 + .../HistoricTicketController.php | 103 +++++++ .../HistoricTicket/HistoricTicketModel.php | 174 ++++++++++++ .../20240312154600_add_historic_ticket.php | 82 ++++++ .../vue/tt-components/css/tt-table.css | 20 ++ public/plugins/vue/tt-components/tt-table.js | 259 ++++++++++++++++++ 6 files changed, 647 insertions(+) create mode 100644 application/HistoricTicket/HistoricTicket.php create mode 100644 application/HistoricTicket/HistoricTicketController.php create mode 100644 application/HistoricTicket/HistoricTicketModel.php create mode 100644 db/migrations/20240312154600_add_historic_ticket.php create mode 100644 public/plugins/vue/tt-components/css/tt-table.css create mode 100644 public/plugins/vue/tt-components/tt-table.js diff --git a/application/HistoricTicket/HistoricTicket.php b/application/HistoricTicket/HistoricTicket.php new file mode 100644 index 000000000..ad3fb26f3 --- /dev/null +++ b/application/HistoricTicket/HistoricTicket.php @@ -0,0 +1,9 @@ +loadMe(); + $this->layout()->set("me", $me); + $this->me = $me; + } + + protected function indexAction(): void { + if (!$this->me->is("employee")) { + $this->redirect("dashboard"); + } + $this->layout()->setTemplate("HistoricTicket/Index"); + } + + protected function apiAction() { + $do = $this->request->do; + + if ($do !== "getConfig" && !$this->me->is("employee")) { + $this->redirect("dashboard"); + } + + switch ($do) { + case "getHistoricTickets": + $return = $this->getHistoricTickets(); + break; + case "getHistoricTicketMessages": + $return = $this->getHistoricTicketMessages(); + break; + case "findHistoricTicket": + $return = $this->findHistoricTicket(); + break; + default: + $return = false; + break; + } + + if (!$return) { + $return = [ + "status" => "error", + "message" => "Invalid request." + ]; + } + + die(json_encode($return)); + } + + private function getHistoricTickets(): array { + $json = json_decode(file_get_contents('php://input'), true); + + $filters = $json['filters'] ?? []; + $page = $json['pagination']['page'] ?? 1; + $perPage = $json['pagination']['per_page'] ?? 10; + + $historicTickets = HistoricTicketModel::getAllHistoricTickets($filters, $perPage, ($page - 1) * $perPage); + $total = HistoricTicketModel::countHistoricTickets($filters); + + return [ + "rows" => $historicTickets, + "pagination" => [ + "page" => $page, + "total_pages" => ceil($total / $perPage), + "per_page" => $perPage, + "total_rows" => intval($total) + ] + ]; + } + + private function getHistoricTicketMessages(): array { + $json = json_decode(file_get_contents('php://input'), true); + $ticketNumber = $json['ticketNumber']; + + return HistoricTicketModel::findHistoricTicket($ticketNumber); + } + + private function findHistoricTicket(): array { + $query = $this->request->query; + + if (empty($query)) { + return [ + "status" => "error", + "message" => "No query provided." + ]; + } + + $rows = HistoricTicketModel::findTicket($query); + + return [ + "rows" => $rows, + "pagination" => [ + "page" => 1, + "total_pages" => 1, + "per_page" => count($rows), + "total_rows" => count($rows) + ] + ]; + } +} \ No newline at end of file diff --git a/application/HistoricTicket/HistoricTicketModel.php b/application/HistoricTicket/HistoricTicketModel.php new file mode 100644 index 000000000..6be6d317f --- /dev/null +++ b/application/HistoricTicket/HistoricTicketModel.php @@ -0,0 +1,174 @@ + $value) { + if (property_exists(get_called_class(), $field)) { + $this->$field = $value; + } + } + } + + 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['subject']) ? self::generateFilterCondition($filters['subject'], "subject") : ""; + $sql .= isset($filters['ticket_number']) ? self::generateFilterCondition($filters['ticket_number'], "ticket_number") : ""; + $sql .= isset($filters['priority']) ? " AND `priority` = " . $filters['priority'] : ""; + $sql .= isset($filters['status']) ? self::generateFilterCondition($filters['status'], "status") : ""; + $sql .= isset($filters['status_id']) ? " AND `status_id` = " . $filters['status_id'] : ""; + $sql .= isset($filters['type']) ? self::generateFilterCondition($filters['type'], "type") : ""; + $sql .= isset($filters['type_id']) ? " AND `type_id` = " . $filters['type_id'] : ""; + $sql .= isset($filters['user_id']) ? " AND `user_id` = " . $filters['user_id'] : ""; + $sql .= isset($filters['agent_id']) ? " AND `agent_id` = " . $filters['agent_id'] : ""; + $sql .= isset($filters['contact_id']) ? " AND `contact_id` = " . $filters['contact_id'] : ""; + $sql .= isset($filters['company']) ? self::generateFilterCondition($filters['company'], "company") : ""; + $sql .= isset($filters['company_id']) ? " AND `company_id` = " . $filters['company_id'] : ""; + $sql .= isset($filters['first_name']) ? self::generateFilterCondition($filters['first_name'], "first_name") : ""; + $sql .= isset($filters['middle_name']) ? self::generateFilterCondition($filters['middle_name'], "middle_name") : ""; + $sql .= isset($filters['last_name']) ? self::generateFilterCondition($filters['last_name'], "last_name") : ""; + $sql .= isset($filters['email']) ? " AND `email` LIKE '%" . $filters['email'] . "%'" : ""; + $sql .= isset($filters['phone']) ? " AND `phone` LIKE '%" . $filters['phone'] . "%'" : ""; + $sql .= isset($filters['ctime']) ? " AND `ctime` = " . $filters['ctime'] : ""; + $sql .= isset($filters['mtime']) ? " AND `mtime` = " . $filters['mtime'] : ""; + $sql .= isset($filters['muser_id']) ? " AND `muser_id` = " . $filters['muser_id'] : ""; + $sql .= isset($filters['files_folder_id']) ? " AND `files_folder_id` = " . $filters['files_folder_id'] : ""; + $sql .= isset($filters['unseen']) ? " AND `unseen` = " . $filters['unseen'] : ""; + $sql .= isset($filters['group_id']) ? " AND `group_id` = " . $filters['group_id'] : ""; + $sql .= isset($filters['order_id']) ? " AND `order_id` = " . $filters['order_id'] : ""; + $sql .= isset($filters['last_response_time']) ? " AND `last_response_time` = " . $filters['last_response_time'] : ""; + $sql .= isset($filters['cc_addresses']) ? self::generateFilterCondition($filters['cc_addresses'], "cc_addresses") : ""; + return $sql; + } + + public static function getAllHistoricTickets($filters, $limit = null, $offset = 0): array { + $db = FronkDB::singleton(); + $sql = "SELECT * FROM `HistoricTicket` WHERE 1 " . self::getSqlFilter($filters) . " ORDER BY `ticket_number` DESC"; + $sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset; + + $result = $db->query($sql); + $rows = []; + while ($row = $result->fetch_assoc()) { + $rows[] = $row; + } + + return $rows; + } + + public static function countHistoricTickets($filters) { + $db = FronkDB::singleton(); + $sql = "SELECT COUNT(*) as `total_rows` FROM `HistoricTicket` WHERE 1 " . self::getSqlFilter($filters); + $result = $db->query($sql); + return $result->fetch_assoc()['total_rows']; + } + + public static function findHistoricTicket($ticketNumber): array { + $db = FronkDB::singleton(); + + $ticketSql = "SELECT * FROM `HistoricTicket` WHERE `ticket_number` = " . $ticketNumber; + $ticketResult = $db->query($ticketSql); + $ticket = $ticketResult->fetch_assoc(); + + $messagesSql = "SELECT * FROM `HistoricTicketMessage` WHERE `ticket_id` = " . $ticket["id"]; + $messagesResult = $db->query($messagesSql); + $messages = []; + while ($message = $messagesResult->fetch_assoc()) { + $messages[] = $message; + } + + return [ + "ticket" => $ticket, + "messages" => $messages + ]; + } + + public static function findTicket($query): array { + $db = FronkDB::singleton(); + $ticketSql = "SELECT * FROM `HistoricTicket` WHERE `ticket_number` LIKE '%" . $query . "%' OR `subject` LIKE '%" . $query . "%' OR `first_name` LIKE '%" . $query . "%' OR `last_name` LIKE '%" . $query . "%'"; + $ticketResult = $db->query($ticketSql); + $tickets = []; + while ($ticket = $ticketResult->fetch_assoc()) { + $tickets[] = $ticket; + } + + //explore $query by space and add each word to the sql query with and + $query = explode(" ", $query); + + $whereStr = ""; + foreach ($query as $word) { + $whereStr .= " AND `content` LIKE '%" . $word . "%'"; + } + + $messagesSql = "SELECT * FROM `HistoricTicketMessage` + LEFT JOIN `HistoricTicket` ON `HistoricTicket`.`id` = `HistoricTicketMessage`.`ticket_id` + WHERE 1 AND `content` " . $whereStr; + $messagesResult = $db->query($messagesSql); + $messages = []; + while ($message = $messagesResult->fetch_assoc()) { + $messages[] = $message; + } + + $return = []; + + foreach ($tickets as $ticket) { + $return[] = [ + "table_entry" => "ticket", + "ticket_id" => $ticket["id"], + "ticket_number" => $ticket["ticket_number"], + "ticket_subject" => $ticket["subject"], + ]; + } + + foreach ($messages as $message) { + $return[] = [ + "table_entry" => "message", + "ticket_id" => $message["ticket_id"], + "ticket_number" => $message["ticket_number"], + "ticket_subject" => $message["subject"], + "ticket_message" => $message["content"], + ]; + } + + return $return; + } +} \ No newline at end of file diff --git a/db/migrations/20240312154600_add_historic_ticket.php b/db/migrations/20240312154600_add_historic_ticket.php new file mode 100644 index 000000000..e718571d3 --- /dev/null +++ b/db/migrations/20240312154600_add_historic_ticket.php @@ -0,0 +1,82 @@ +getEnvironment() == "thetool") { + //HistoricTicket Table + $historicTicket = $this->table("HistoricTicket", ["signed" => true]); + + $historicTicket->addColumn("ticket_number", "", ["null" => true]); + $historicTicket->addColumn("ticket_verifier", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("priority", "", ["null" => false, "default" => "1"]); + $historicTicket->addColumn("status_id", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("status", "string", ["null" => true]); + $historicTicket->addColumn("type_id", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("type", "string", ["null" => true]); + $historicTicket->addColumn("user_id", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("agent_id", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("contact_id", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("company", "", ["null" => false]); + $historicTicket->addColumn("company_id", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("first_name", "", ["null" => false]); + $historicTicket->addColumn("middle_name", "", ["null" => false]); + $historicTicket->addColumn("last_name", "", ["null" => false]); + $historicTicket->addColumn("email", "", ["null" => false]); + $historicTicket->addColumn("phone", "", ["null" => false]); + $historicTicket->addColumn("subject", "", ["null" => false]); + $historicTicket->addColumn("ctime", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("mtime", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("muser_id", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("files_folder_id", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("unseen", "", ["null" => false, "default" => "1"]); + $historicTicket->addColumn("group_id", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("order_id", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("last_response_time", "", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("cc_addresses", "string", ["null" => false]); + $historicTicket->save(); + + //HistoricTicketMessage Table + $historicTicketMessage = $this->table("HistoricTicketMessage", ["signed" => true]); + + $historicTicketMessage->addColumn("ticket_id", "", ["null" => false]); + $historicTicketMessage->addColumn("status_id", "", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("type_id", "", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("has_status", "", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("has_type", "", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("content", "text", ["null" => true]); + $historicTicketMessage->addColumn("attachments", "", ["null" => false]); + $historicTicketMessage->addColumn("is_note", "", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("user_id", "", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("ctime", "", ["null" => false]); + $historicTicketMessage->addColumn("mtime", "", ["null" => false]); + $historicTicketMessage->addColumn("rate_id", "", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("rate_amount", "", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("rate_hours", "", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("rate_name", "", ["null" => false]); + $historicTicketMessage->addColumn("rate_cost_code", "", ["null" => true]); + $historicTicketMessage->save(); + + } + + if($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if($this->getEnvironment() == "thetool") { + $this->table("HistoricTicket")->drop()->save(); + $this->table("HistoricTicketMessage")->drop()->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } +} diff --git a/public/plugins/vue/tt-components/css/tt-table.css b/public/plugins/vue/tt-components/css/tt-table.css new file mode 100644 index 000000000..a7d6e9c3a --- /dev/null +++ b/public/plugins/vue/tt-components/css/tt-table.css @@ -0,0 +1,20 @@ +.tt-table.loading tbody { + position: relative; +} + +.tt-table.loading tbody:after { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.1); + background-image: url(data:image/gif;base64,R0lGODlhgACAAKUAACQmJJSSlMTGxFxeXOTi5ExKTKyurHx6fNTW1DQ2NOzu7Ly6vHRydISGhKSipMzOzFRWVCwuLGRmZOzq7LS2tNze3Dw+PPT29MTCxIyOjCwqLJyenMzKzGRiZOTm5ExOTLSytHx+fNza3Dw6PPTy9Ly+vHR2dIyKjKyqrNTS1FxaXPj4+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCQArACwAAAAAgACAAAAG/sCVcEgsGo/IpHLJbDqf0KhUeVEQRIiH6Cj4qA4Z1IM0LZvP08tE9BBgSu936rgA2O+AkSqDuaD/gGYKFQ9xcIdxD3R2Gox4ABoDARyBlZZGJCJuhpyIikZ1j42QjgAWGVuXqmYXBBwliLGdGJ9FdaOPoqQQGxOrv00kCLOyxXOgucm5GhohBMDQRcLE1LHHtqTK2pAaB6nRqyQpxdWztUShd43rpLjKDN/ggBci5PbU50O32+ql2ZANFMj748HNvYOc8glJ164hLnf9ICUAMbCMOIQYDV1D90+ZO3bLAEh4VvHJhHIZZSlcwdBhNpARRSUoUZIJvZQoEy6CyA9m/kdGDfzUPEKiEE6cKxn67IkHogYIJIcOmSDgaE5PO/kx5SkRg9SpV8OqXBRTq8efGih8FXLSasqN+v5xNRvSEaW1bcWGTUq3708AJ9aCdYsS7sKXfrcCaCCYSF7C5Pg2Taw1cEmBSR7rtWeY5V/KyixX9IAhnhHNkBOR9Qc6l+iBeU0XQb1ZY1bWre28Pu3LEomqcWQ7rh15de5ku2djwEAm0AWjb+BUUEI7I60KBCYINXIhBYoTHZgdB0x9OQYO29HUmyV8cFgBFXpLwRCgQOvkwxdg0N8+yoR7/a1QHScCiIDZHyk0kIBf+IG1nH5vyGcRcPZMl1k1AhCQXiAk/mwwglkNsrXfg8uVIEBzZYyDkoVI0IaBhtCQ4MCH2oQooHkQjlhCZ06QhtAbLB6hGQIbAjPBAVzZeBKE+umHiAdpvBJWgG1xcGBFKdiHh5I4jlhiHAIUyUQFmwV5GpFfkcDAKFwy2SUiGJjJBAmEBdjYCihowCWJTX55CBxXMqGiW3LeSQSVJHo5Sxw8HqEAccEZKsVJI+boJDWBJjEopHZKSsSjXXp5UKOfQhpLoZ4qF0eOOpaTqRHDpIYIiqka8SiccCyQEgJLkGAqJ1HVWsSthlj1hphDkCnrG7wKe0Ssv5aAqhBSyoqes0dcUC2kGAiQ2bKAYosEsZDBIeGh/uA2K+6zv74h3AUGmcoBresS8Vu0YZ4WbZz1JkHAshicu8J6qeXbb7bbulWCbAnXFuzBRSjL7V1DXIAvshDDayoctPrIbaf9ElzbG1AOIbLCr0LsHmTxQEcYxSoj0fBR50Q7bcwrSEyYV0LQ+avAOE+1bHP//YoxzhqnVvK/ppIatMsOC3FymUEroXNtzW5qbMlVCynrMVBblXLXPkP2ycxiHR20xanB3PXbcMct99x012333XjnrXe/aKekNtK/3hW2XvTGTW5tn2i9GdBve0zYMVMfdXPQV1vVLNM7r/S24keRVHRqf2fsZ20llw3p2Dgf7haKNtMduV5EDI6T/ttvc/DrOa/jhDrECiwbj+PEgVxv7mFxvQLbphrcddKmbth3TstNXi8B0dJOPEYmhu6str6fBvDDKmNuqsDMQ6a8yuUTd77JAKurMrSptff5r7vXqjpxjK/wfErXHsx99Vbb18Igdr2w3MxX4MKA8cQ1oKMUrgjw+9UDhdXAq3Tqfm4RnqEqiJD6rYBzetGgpDiIDydgcEpVI2ExPCgEEGJEhEOZoIiO4jRp1GldpEGU7qJQOZQESAEi0J4qLoCAQ9xMhaWRQvoOcsQ3WGkoCrAdInRYjvX1CIVJOBya5JEJclDRHvkT1AuVcL8MCTENBKAQe8rDmTP8holsJAYH/ipwRie0Qkrl+KIhToSG+cnih9AzUCCAGC+MNFEWCyzD6/SIkDmy8DQVkCLVLjRF54QNkEdBRAoq4AHUKcADFUhBu6QVx/4BwleGOOS+OgO8BCKKOZf4HCYF2BnxJRAOP0wkIHJIRgEy6git9CUMAxHGE0LGfUOwpS9xmSpjQoqVy1yjoZx5zCMo85aRagyosHkIZAohmNxM4lqoaSpvruCa0TTXV7aZTmZZs52IxAs8f2kED4RTFmHk4gPu6U4jUI+fJRiDoegBT3OiE1xBTFVB0mlOcLZLl3cqSjRrGU6BimuhCYQmNiFaqy6u0pq3FIEMnXWRX7ESXCkY6boUUxBBGgLzVwhQ6cGmUZuTjiymdiMBIfRi09lVQKZvI+RVDPoeQe5tNju9R0/xIYJ8HlUNhChkP4vwTwJpQTtHTYMVsMAB4XjgARxAgAiyU8es7i0IACH5BAkJACwALAAAAACAAIAAhSQmJJSSlMTGxFxeXOTi5ERCRLS2tHx6fDQ2NNTW1JyenOzu7GxqbExOTCwuLMzOzLy+vISGhJyanOzq7ExKTDw+PNze3KSmpPT29HRydCwqLJSWlMzKzGRiZOTm5ERGRLy6vISChDw6PNza3KSipPTy9GxubFRWVDQyNNTS1MTCxIyKjPj4+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb+QJZwSCwaj8ikcslsOp/QqFSJWRBGicfoOOE8EiPCAjMtm89TzGT0EKgg73fq6IHH4QLthIzu+8sLFg93hHAPXHCJhSoPFgt/kJFGJSNui4qGiJeFAiMlkqBnGAQcEJibmUYTp6wqdw8En6GzTCUJqKhzRh64p29wCbK0w0O2vbl0x4m+wcS0JSmt0ouHqtPXvyopj86QGCPY4YzJ4q13I3zdZx5u5bjVRavKl5gCHupl0O7hukW8+4QCQkghDF+TCfPcwSMiD+A1ARMMMvnm8NjCIf8SzkMnEUmJQRWxXRTSUKO4BwU7spggIKRFTS4fRlRJ0qTGkStjHlNxj+b+Spvl+hHJqFManJk0ERalBhPozgQ+hyh1Kk0oRqrKtkSVirUVzpJLMWnFxw3J1LBWhRDtWmisOl5uVbFV9DVsIQsSp8aNN1cOub6J9hJ5wEFSiZZxBHNdWhfwG8VCHqBAQMAbyGx4k5zVyMgCgT1I1Fy57BIyCw4OAAA4kQ4NuEWmN2MTYAFplAkWStk0jVoDAN8r/IAlFPtYp7JoFozQaFoAAtWqfUNAc/haZrOoBBBo/WcU4mu8UUSHDgBBzzLRel3nskjF9mGjXEnjnfo3ed8ZzKw1B2G9XEUJcPfMLayENx550YGQhm7iFAcBB8ipswCDjykhGYIYAlBASk3+WGCSf/EESBMGBKpA33i+QZfibwFEUUJFpm3FAgEqQJXEhSveh6AGMRaRnkMgykgEh0SgpiOGOebnxAJY9SikEb3Zt6OUK2qQFhI/OuXkk5GJp2KGRwLAQBNMFhUkl0UYKeWUvlUZXY8l6kQkmkJwIF6KOa75JXkHLFFCX5XReYSaYOKpJ4q2GeFhUTYKakQI9uWZJ5jQSaAEhSFxIKCjLJTwAaWRHvplAZotFSGnQ6SgZ5ugZljYEa/F1CiqRhwQZqig+hbCERi0QxUHc9I6gQg5TtqqBiIIKJtNZ9IqxAV7GkupbyoYEatTAmzqrBAlFNDqtwAEl6ZOgW6LhAT+KIK7IwVFYNBVtuYmUYII6oJq236cbUkrpKKC65sBRFzrEASnxkuEqvVmqMGuQ5Bm06sGJ0HBgQn/dgIRXTUbMQsBSOsvCsXolOjGRe5ZMQCBDgeQtiRj4OXJ9ik4Y0xXkjxEBxQnrAEJQgj8oc1JBAAzeQxnCRBPQCMBgqEVazAmCw4DVHDSIwwN3QB1xsSyzSUwrTO7SYct9thkl2322WinrfbabD+JqUlbt4zVq1FrFKzNZTpVjdEmjRw2vu7o4vM+GpO8qFM2EuDSOGbzvU/KOsVtsLtY9fQiVlOTnHdIwmRc9uAaDTZ32RxgtRDo7mRu8AI6uQV4Qvqiivr+POex4G5M8Cbda1fcve2OK4U7S2NMEA8xezgQ5N6y78xZg5V7QCve1ci74y65oNVTpXzAOtVIcpxammVqxJtT5XfWXWkaLwbM71O8onPFHtXx82j851K1o7qsQ3ezAL5L/XvS/mDHhPKFRH5RGaA4VJeq1m1Mge9YUpNsBkFpMPBgB6TV3SpYiJp5BEa0gosSOKiICxbhcOUwjXKuFwoSJUJjJDSRFLIXDhi+AUIdmRBxRmgcFvpjI0oon4i6QYlWOAgV52uC49oSxEVox4dpIMB3YMPDqpyhOqiwoTQ4YAEoOmEUusHFEesRwCWoLDFNxIaJTEimSiREi4TIXxn+QDfGY3CRjarIDVDgCAcEhiZqKjSJIlJgAQ+obgEesEAKntefKqpAfX/40x34iJUrva40PFRBGaHQkECi5QjSs0sfgyhHP4gwCQas5F9EKcOOJJEFqZQVKFmpCD/SIpaq3IVjTmFLUOBSlkYI5S7RuBUmDRMCs7oKLXfok192JZlCEOYyj0ITY04TmjM6Zhx9EsOKWHKacXglER9wTVBqEwIoEdI3jolNaQKGI1xixy6xeUmnIE1QH3HMldypk3SiSp5LsaRjSolP+oljn2HxRMT0kUt/FIUgQFvA/xIiUJc0Q2zGsMk3bVKjTcarBILQyEbdwUWPas6N4WjnPI4t07Z4hHQaI92EHlpKBdy0oRAqPUUeRgAamkKhCldIAAcE4wHCgOEzXvSp2oIAACH5BAkJACoALAAAAACAAIAAhSQmJJSWlMzKzFxeXOTi5ERCRLS2tHx6fNTW1Ozu7DQ2NKSipGxqbExOTMTCxISGhCwuLNTS1GRmZOzq7ExKTLy+vNze3PT29KyqrIyOjCwqLJyanMzOzGRiZOTm5ERGRLy6vHx+fNza3PTy9Dw6PKSmpGxubFRWVMTGxIyKjPj4+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb+QJVwSCwaj8ikcslsOp/QqFR5SRBECI7oOBFwECJC4jItm8/Ty0TEQTkq73fk6IHH4SjthIzu+8sJFhx3hHAcXHCJhQ4cFgl/kJFGIyJui4qGiJeFKCIjkqBnFwQCFZibmUYTp6wOdxwEn6GzTCMIqKhzRh64p29wCLK0w0O2vbl0x4m+wcS0IxGt0ouHqtPXvw4Rj86QFyLY4YzJ4q13InzdZx5u5bjVRavKl5goHupl0O7hukW8+4QCVoggDF+TCfPcwSMiD+A1FBMMMvnm8NjCIf8SzkMnEcmIQRWxXRTSUKM4DgU7qpiAIqRFTS4fRlRJ0qTGkStjHnNwj+b+Spvl+hHJqFManJk0ERalBhPoTgQ+hyh1Kk0oRqrKtkSVirUVzpJLMWnFxw3J1LBWhRDtWmisOl5uVbFV9DVsIQsSp8aNN1cOub6J9hJJgPTPiJZxBHNdWhfwG8VCEjhAkVIUyGx4k5zVyMgCgT1I1Fy57BKyCsmG0qEBt8j0ZmwoLBSGMsFCKZuuCZl+Ala3kteLOpVFk0CERteIFfU0c/haZrOoUBBQ/WdUclSuzVWGEq3Xcy6LHEwfNsqVNNOoC8FJ+2StuQrf5SpCQP3ZLVboe812cuG2uNwVCDCcOgn495gSkvUiQH1MWGBSfPHQR9MF9zmQnzgQLjFCRbv+bUWAA1AlkZ44Ay7RnUMZbkXEdoslxB4SI+Km4hQxllMiEic61eGMgzn1YhE1ApUij3y5dGMRFerEIpGRdRViEiP0RQCTIrLlwJIqOFjUk1QakSRVQ6pgYEgLdhnamEAJoNlSR5p52lxHshYTl256yRadQlzQDlUCYOmmnl2hUB9wAIVZpwoE6OTAfnI6JeihVKC5TwV7SarRlJAqoaVTDqhJxAWBMpjpEICGBIdq7nG2I6SNAvTGciq0alIFbY5aU0xukWaTp7YqYWk5C3VlaK+bVuRAMTrt12s8SpKkk6jL5rlnRT0lStWP0QqhK0CYyrrPsMsWa1OIOboKa7b+1lyrLVa1ZrshVdX8qgy06ILqEq/o5qvvvvz26++/AAcs8MAE/ytvOPS6C8DCDDfs8MMLN7CuS35GKwIAGmCsccYcb+xxxgMIUa5JyuprAMQoo2yCEN4mBG6vAXSc8swpCGGtsTjp28HMPC+8gLNYJdzrBQpk3LDRSC+ctNEgCPGukQY/bLTSVGvsMKYqCPtvAA4nnXLHChCxrUb47kvB1FN/7HHDIQ/RsjvtjsoBxGmj3PEDQ3W16qEPoH201XU3bMCnoe47AgldW90zxlgLcTAurrzsZgmL80yBEW8rU8Gj9VLwd+A818wsVuLli0HlM2twbBGlUsX5sgkgzrD+zKiTsF3m4oCYbQiKo+7wAWaxuWwEvdO+uOpJPB5OmaOO0IDvKReg6Vx7b/VA8b0vvoESUS51rpsoQAC67xqUPMSXFI+KggZLkw/8EkE6VP1W4UPvMLZDjAzQ/CrWb3zPHXBC/DaCLgGML3VlU4L+snIoLBkwezNjABSedpxDwcVX/7Pb/MQVDvRwRCUUSkSYHJDBh4kOCq0LR5iUIqCOFMg3ySshwwpQsb9g40JxkFA3KNEK0zzwgE0rwwLFgqDgjIcW1pmGD0soQeZchxUrRIUALCA0KYziNrhYYvYU0Lgp9EYROJyGheImwEq4TAkkrFsQV9PD35DNEWWojQD+hIRG9jHshGi4wLbCqLk4RMACHqhVAjxggQiQDj51zNgJauiEKN0hikthT6p0skQS8G8JDeFjTNhzM8f4cHWSuGCV7CJJx4BRJeZ7k10qgCdErRKGPBqgS0r5ylPOSJZUaWUna2khFSWolq2cpCejgss5HWGXpoxDKofxy2Sy8pjOJMT38EGovkiSl4pYpjM+gk1dRrMCKJnRN5ypS2x+kEfsMGUwk8kTM33EMZx8ZTjrlM5I2pAt0+wSD9kST+oxkkf6wMo1u0KQfCUAfS66J0CawS9j2GSgrmLov0YgCI1AdB5T/Ge2ijMtVHhTHMIpGBcqOo2LekUE2hSpGgQj0dFnGuFDp8gDSqso0iNU4QoIEIBgPMABAYDhMzStqVCFEAQAIfkECQkALQAsAAAAAIAAgACFJCYklJaUzMrMXF5c5OLkREJEtLK0fHp8NDI01NbU7O7svL68hIaEpKakdHJ0VFZULC4s1NLUZGZk7OrsTEpMvLq8hIKEPDo83N7c9Pb0xMbEjI6MrK6sLCosnJ6czM7MZGJk5ObktLa0fH58NDY03Nrc9PL0xMLEjIqMrKqsdHZ0XFpcTE5M+Pj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABv7AlnBILBqPyKRyyWw6n9CoVJlREEqJT+k4EXwSJYIiMy2bz9PMpPTRnBbvd+QYgsfhGu2EjO77ywoYH3eEcB9ccImFJx8YCn+QkUYmJW6LioaIl4UaJSaSoGcZBAILmJuZRhOnrCd3HwSfobNMJgmoqHNGIbinb3AJsrTDQ7a9uXTHib7BxLQmEa3Si4eq09e/JxGPzpAZJdjhjMnirXclfN1nIW7luNVFq8qXmBoh6mXQ7uG6Rbz7hAIuiCAMX5MJ89zBIyIP4DUNEwwy+ebw2MIh/xLOQycRiYlBFbFdFNJQo7gPBTu2mKAhpEVNLh9GVEnSpMaRK2MeO3GP5v5Km+X6EcmoUxqcmTQRFqUGE+jOBD6HKHUqTShGqsq2RJWKtRXOkksxacXHDcnUsFaFEO1aaKw6Xm5VsVX0NWwhDBKnxo03Vw65von2ElGA9I+JlnEEc11aF/AbxUIUnNCQUhTIbHiTnNXICAOBPUjUXLnsEnILyYbSoQG3yPRmbBowFIYyAUMpm64JmX4CVreS14s6lUWjoIRG14gV9TRz+Fpms6g0EFD9Z1RyVK7NVYYSrddzLotOTB82ypU006gLwUn7ZK25Bd/lKkpA/dktVuh7zXaS4ba43AsIMJw6Cvj3mBKS9SJAfUxgYFJ88dBHUwb3nZCfOBAuYUJFu/5tRcAJUCWRnjgDLtGdQxluRcR2iyXEHhIj4qbiFDGWUyISJzrV4YyDOfViETUClSKPfLl0YxEV6sQikZF1FWISJvRFAJMisnXCki04WNSTVBqRJFVDtmBgSAt2GdqYQAmg2VJHmnnaXEeyFhOXbnrJFp1CZNAOVQJg6aaeXWlQH3AAhVlnCwTodMJ+cjol6KFUoLnPAntJqtGUkCqhpVMnqElEBoEymOkQgIYEh2rucbYjpI0C9MZyLbRq0gJtjlpTTG6RZpOntiphaTkLdWVor5tWdEIxOu3XazxKkqSTqMvmuWdFPSVK1Y/RCqErQJjKus+wyxZrU4g5ugprtv7WXKstVrVmuyFV1fyqDLToguoSr+jmq+++/Pbr778AByzwwAT/K2849GZrr1OebquRn9EGqZAQ5ZqkrL6puqOLtwmBSyxWIVprLE76VrwPpr0BlHCvoGLV07tG/itxQsII+y/H7hDhsDv47isAVgvhPE+7oyqgk1sZJ7TqoUKLA+vCIT2qb6lUUXcwLq547OaHMfUca0wLSK3w1djslTJnmGYrMlX7UR31yl26DZTYRDT9VLZfcmgWm8vOvM/FYhZV5qj9Ca7pXEtvZTeGSkS51LluEmqSn3k7BfGMkpfTod9K95r5MUS3YPJx0X4+DclGcF42uqZfEjrFISXeUf6YrWOC7SR618kBBBv8ptHrQ4gbDnocqWTCAQAk37tmG0kht3O+Bwg8JBFQkHwHykePC93tNV/lfHD7McEIyZePPQDLQ3cN4EyMLhaCwY1HSwYpXADA+effj772rNyu4XWsoB0qBICB8EHBBB4oQPn0t8DrpQ88rKAMGs4GhwthrQTTU0IEUEAC8zXwgx14oHwIAbkpcAxA8yBgBofwgQCw4HoM/CAIRVikxFRnWxYshyIigIEQ1CoCBgiABBCgv/zJEIYxDKH2BueHKN1BgEthDweOSEUkxtB8NGSIKy5Hm8DADy1H4AD+qmhFIxrxflmUCvvMAJcv9kWKZCyjB/7HmD8ldoR9qjMJnlqQgisysI4ejCMamZRHgMCxgWYMpB9heMY0dqSQegxjIpOoSDLW0ZFkMY9jFrDHPh4RkIL8JCadAUmHdBKQkwwlIs3nNQJp0i6J6KQMz6hKECbPAD6pnY8kychaChIBx4rKR2B5B1n6spYPkN0svrHJN+xRjHM85hFRwMW3TGsuxpRmFRFQATN9xDFSpKM2FwiCtJmJHW8M4zhliABcZooSUVSnFaXZAQuskUr6wMoh51lLByiTRwqoXFDkSUtBdsCf/TKGTfa5SCp24ADmTKggNHLIgh6RAh64p76Kc01pnLKSH7wACv6Zr9rsLA4MneUKUCoQzIItQQ2C6OhHY3iBlV7JpWWowhUSIADBVIAFLHAAChogAI3i9KhDCAIAIfkECQkAKwAsAAAAAIAAgACFJCYklJKUXF5cxMbE5OLkREJEfH58tLK01NbUNDI07O7sVFJUpKakdHJ0jIqMzM7MLC4snJ6cZGZk7OrsTEpMhIaExMLE3N7cPDo89Pb0XFpcLCoslJaUZGJkzMrM5ObkhIKEvLq83NrcNDY09PL0VFZUrKqsdHZ0jI6M1NLUTE5M+Pj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABv7AlXBILBqPyKRyyWw6n9CoVJlREESIh+g48TwQIoIiMy2bz9PMRPQYWEKWt+Vx/MjvA+2EjO77ywoXD3FwcHdzXHF3hYoPFwp/kZJGJCJuh4qKcHRGdoaYjBYDIiSTpmcZBB6MhqxyIXl1mm8DhZehHhd8p7xLJAiYs7SwxIidr7XJocIWCKW90ES/hG6uw8rJnEV218TKIbaHztG9JCmhrdje67HH3eoDyq9yKZDkkRkimZns/djaRLj5G5hsH4Jd98x8qDUPHDyCsAAOEfiwYqFQAz4kLGPu0zuI8CQKoQgSopwHzzY6mcDvo0V4xrZRK/ky0wSVTPLNc0mzmP7IFSRfmgx1ECcSEg889hTaTiZPpuzuoDRaZMIlZEsh/gya1aKim1SFsEzXlWDMgDOhQr2jMewKq7bKWtyaVq5Je2GtPpV7dmJdtTSbuZ1YDbDQviP/2lUneDDhvYZD0IVs9wJOvHUKL/Y2eXNFyyrtbEmykDJTxEAVR1YHeuNYC60zmzbZeTVBBEoUgI1E4mqI2J00265V2zMx4EUUiErZJ0PSQ6ORlJbr6MMYJFU+CBJeFjkR5W8eIDyjT9hvJdOhOsIMJVC87rmpNUYzAR0tC9Flg3z0RwEC7hZ5N4RyrcTRlhm9aSLPGwKOBCAsA+hiSgYXPIhNgysQuKAFzP5JkQJWG+YX3DsS9pJBev1gqOE6cfzkBDc8yYEhiiWSkwEwKcY3kIFprAIZg+i5MQB79yiQlDIq7iXKeE1c8IlJcYi4TVFU3UhLkiXBFgUJCg4VpWNLLIRbEivWZAGRSpyj2o4YgonmgGvKs8kT4MW1Fn5gTlGnWnG8aYSasy0oZZ7xBQoOakaAZ6ighDahaFd9MoHjosVE2iiZhFA631Fd2hbCgZcmmuliHRbhZJwgbRpqEcCgKhSGHoDY1QBMripEBhYuNUASLLkKkZ+27smXBaASMeliY9qaBI7DwZEsEbj6WhGtyuaW62nUVrWIbcVWa0SFlDIUApHlLZatt/7YXZvqs0LEGi4sbaILrl2iFJGBnbPWii606gqFEIyV7SuptFERS4Q++PIJrMAZvussEYO86yLDRLy3mhsAEQxPvAxf4PCuQnD5bkYUL1EmpPZwZZi+Jd/a745tEZBwYBO3LETEfIHWasA2J+ExvWN+GK7BPUun8TpjDkLvwi2fbBgn7trFcs8ZHF0QyEVnrfXWXHft9ddghy322GQzHGuBi2Sa9htT21w12nCr3QonEQ/T5d3UMF2yyHbPhPdJQgDa0uDMdKv1Bx6pzYzinCD8RlyPVxP5KxxT/HND4toN+ZgeK8kiLIgWXbdcoI1lWxxtUxztzIe1Jexqu22tAP5Zhp05hNXeVL5v54tB/G69XUdtG0AID2e77LLy+azKPanac7mng3pvs/Gk7i2urNPE3i2rvUGA1gR0mpVIxZcVx7ktr94scszfqXuoAA/XbbTNLum2uMOhPwSzQNvc6sjsGkKvFmMphhEIdwwxnBCEE5nwWC9PGfDRyLB2BN7BAzABVFZ5RnaeJHCJJjVRYKheM7JSsepiLDoeupy2FOeJamRAYhiKIqM3pa2vZTPsSQaP8LqsvK9RCymL3m42nB9eRkcYbI/VdkgoO8zoZfEY4hAsSBMMDQAEJryHleDwRMoYUX1MYeIKHgABAFCgZrxQgLsI0UWtPNAp7NiYEv7ICIA6bsAAUkRDJTDXQelcS4RMsOFt5ljGDdQRABsYgQnemAYCMEQ+ihjUY/whRiYkiCBWLKMdD1nHAkQgi6hwZEsuIqMgvWNIaPDEQ6w4gk1u0pB1HEEFUtAfS1jDGwXpo36UkUeffQRDdOQkLA9pSFiqgANoJNMFzraPGH2JNJoxInYEWQtgarKYnMymKwGQAAlw4AA1U4B2hKaU5H1OS9B0YCQUwD1ravOd2hzmIRtwhPDdcluB0WVw9NcHgWTylYgUJjwBCgB6GiF8zZxFViKZG0AqBJ1ICOZAASpPbRq0CPZsCL0MIcloADKY2JRnRYkJz4sSAaHGYYw0e/4h0YmGNKABrahJhyAzBJpFn27xQAJiylOSTpSgMxWCPanXj/t0dCM6/SlMselTkRa0njatCRcHk1SejlSpAq1jUFdQ05TuqJeReMBOf/pSrM4Tqhz0CtGoUtWrwtSs2dzAVmUGRUg5lBwXKAE8h8lUuD71oOYk6gPAeooMOMCp2/TrWQGb1hQxkhwhSABi36rYudLOMyRbFQE6sE23mnWuUWXHYL11gLF6Fq6WDW0y7pqnCYDgtKhFq2dg81jHiKABlFXsXzF6WcMggLB5SkEDNgDbvYIWWcC9FAEMUNx3pnZRVPIaASKwgOYuFqMac0RtKZYCB2CguHKtZ2/jyDefshXBAgHQAHEHCtqnVMe8vrCAAzSAAYuKNy15eMR2zUsCD5jAAQ2ggAO44AUEXMA6+4Wv2IIAACH5BAkJACoALAAAAACAAIAAhSQmJJSWlMzKzFxeXOTi5ERCRLSytHx6fDQ2NNTW1Ozu7Ly+vISGhExOTKSipCwuLNTS1GxqbOzq7ExKTLy6vISChDw+PNze3PT29MTGxIyOjCwqLJyanMzOzGRiZOTm5ERGRLS2tHx+fDw6PNza3PTy9MTCxIyKjFRWVKyqrPj4+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAb+QJVwSCwaj8ikcslsOp/QqFSJURBIiQ7pKBF0EiSCAjMtm89TjITUyZgW7zfk+IHH4RmthIzu+8sKFx13hHAdXHCJhSYdFwp/kJFGJSRui4qGiJeFGSQlkqBnGAQCC5ibmUYSp6wmdx0En6GzTCUJqKhzRh+4p29wCbK0w0O2vbl0x4m+wcS0JRCt0ouHqtPXvyYQj86QGCTY4YzJ4q13JHzdZx9u5bjVRavKl5gZH+pl0O7hukW8+4QCLoAgDF8TCfPcwSMiD+C1DBIMMvnm8NjCIf8SzkMnEUmJQRWxXRTSUKO4DgU7qpCQIaRFTS4fRlRJ0qTGkStjHjNxj+b+Spvl+hHJqFManJk0ERalBhPozgQ+hyh1Kk0oRqrKtkSVirUVzpJLMWnFxw3J1LBWhRDtWmisOl5uVbFV9DVsoQsSp8aNN1cOub6J9hJRgPRPiZZxBHNdWhfwG8VCFJjIkFIUyGx4k5zVyOgCgT1I1Fy57BKyCsmG0qEBt8j0ZmwZLhSGIuFCKZuuCZl+Ala3kteLOpVFo4CERteIFfU0c/haZrOoMhBQ/WdUclSuzVWGEq3Xcy6LTEwfNsqVNNOoC8FJ+2StuQXf5SpKQP3ZLVboe812guG2uNwLCDCcOgr495gSkvUiQH1MXGBSfPHQRxMG95mQnzgQLlFCRbv+bUWACVAlkZ44Ay7RnUMZbkXEdoslxB4SI+Km4hQxllMiEic61eGMgzn1YhE1ApUij3y5dGMRFerEIpGRdRViEiX0RQCTIrJlwpIqOFjUk1QakSRVQ6pgYEgLdhnamEAJoNlSR5p52lxHshYTl256yRadQmDQDlUCYOmmnl1lUB9wAIVZpwoE6GTCfnI6JeihVKC5zwJ7SarRlJAqoaVTJqhJBAaBMpjpEICGBIdq7nG2I6SNAvTGciq0atICbY5aU0xukWaTp7YqYWk5C3VlaK+bVmRCMTrt12s8SpKkk6jL5rlnRT0lStWP0QqhK0CYyrrPsMsWa1OIOboKa7b+1lyrLVa1ZrshVdX8qgy06ILqEq/o5qvvvvz26++/AAcs8MAE/zsBAAgnrPDCDAOwgZ/R2uuUpwM4bPEGF2eM8cYJr2prkAoJEUHDJC+8MQUBp+qOLgxgXPLLCAcQsHEuhegAzDh7EHC5JmFKgcUIuyx00EQrjAC9y0oc0kwXMDw00EO7DAC++ybokjAIJCw1zkDL7C/NnBJRsdYalz30BP8KgNVCJxTNNdAIY2sroQm59TPZTz+tMMYV9OutRrBKsPXbC48A8aGgdkXdwXAT7rID+34YE9UqtO024Qgbnm9/Ou2VAeZOY5xCvtbGtB8GIzQOOgAjtGvmYaEeUcH+5ZhjfEC2f7sDIhICrN7wBnJTSfc+ygpRAO1vuzzB4TNy3hXlRHCAPOh9j5q7RmEK7rvWLhsw6vDzXLnEAdPXjimk4IeDp5flc43xCb2mj4vrKoy8fcLVx2+eScELAcH9CINftuTHCvqJTHVvE6CbsETAO/RvCCQYXAIP9SEAzYN5Q2ibBF+mQC5wRCUYgIAJKGCh32xECiU43gYb1kG5dMqAkSiQK8yTvWM8Sgp341oL+TLCx2AQDRTpIQnhYEFpFK8JI1thwnbIEFeQMA7SQRoQCXCdUxSxEA9EAgGyxkETOnF/eBiPJKxzjCsmgjJo+NkKmciVHvbQFySA4RL+ijMt75jwFEeEguUYxsaaCBGMd0iEABxRhtqoDUV3TIxhUDC4Pv7kj+GDwAU+UCsFfOACInTJG2oIhzL9YYsKcyRC3PjFcGCCPSrDSm7EFwkTuEyUbiThEx2yvtI5xjXn+oMBNpA/8MzwiW/k31/sQkSVQK+NfxwiUGpJTN8QaZSynCFWUOkYK/IoQV8MZkiYWU1nRsVq0ozDNIfZzWL6JEYUgIMyXYLKZrYij8MApztrWU5C5BIfDaxIO+t5lK18xJ1voCdAF4CS5oGNmNx05wd5xI5qri+Vmrynij7iGPbYkjE/7Agv+kLNsNhjVJRYikXD4oll6WOcuygKQfJYpYAv7aOjTmkGv4xhk5HaBEQZhVQJbKORfbpjkDntVXEAWRVyTkM4BeOCIHBh02noIakTWYMA6jgQo05GD1KE6hFKIIErQEAAgvmAAAQAhs9kVatoVUEQAAAh+QQJCQAuACwAAAAAgACAAIUkJiSUkpRcXlzExsTk4uREQkR8eny0trQ0NjTU1tSkoqRsbmzs7uxMTkyEhoQsLizMzsy8vrycmpxsamzs6uxMSkw8Pjzc3tysqqx0dnT09vSMjowsKiyUlpRkYmTMyszk5uRERkSEgoS8urw8Ojzc2tykpqR0cnT08vRUVlSMiow0MjTU0tTEwsT4+PgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG/kCXcEgsGo/IpHLJbDqf0KhUqWEQSglI6Uj5QBIlAkMzLZvPUw2lBBm0Iu836wiCx+EDLYWM7vvLDBcQd4RwEFxwiYUtEBcMf5CRRiglbouKhoiXhQMlKJKgZxoEHxGYm5lGFKesLXcQBJ+hs0woCaioc0YguKdvcAmytMNDtr25dMeJvsHEtCgsrdKLh6rT178tLI/OkBol2OGMyeKtdyV83WcgbuW41UWrypeYAyDqZdDu4bpFvPuEAkZgIQxfEwrz3MEjIg/gtQEUDDL55vDYwiH/Es5DJxEJikEVsV0U0lCjOAgFO7qgMCCkRU0uH0ZUSdKkxpErYx5rcY/m/kqb5foRyahTGpyZNBEWpQYT6M4EPocodSpNKEaqyrZElYq1Fc6SSzFpxYeTa1g55LpeGqvuAIcNSqYu/XoW0wWJAzgAABAgrtpEVoUQ/fuGrREGSP8QQKB3b98kcrHSJZzIcBEGLQakFJWicePHSCK7Y3SBwB4kaq6AxGqZCGZD6dCo2EsbgF64kDUOuJAYCoULpWy2NhtneJQItmvTfusXWydufRiU0Gh8ZUtCPc2AQKBcOfPc0gYQiP1n1PVr1cHGabEZyonky73jDr2oxfhho1xJq/66EJzAT4zQXXzLzccFIQmQ98wtrPDXS29OoFCAd/BV+B19EXwAHT4M/gRXmRKY9fKBgkx0sFdjFMoXV4I0acBgCw6KcxcUJaAYH4o22mbgVkcQAGNz82zIxAInDgifXjaCxuMRQsZznjIAIsFChUXmOOCFSz4hmjJNIjGBkUV2l6OSWS6xpThRFpFAmMlZKWaBZTJxZpBMGOAmlUbiSECckLXjFFRKgPDAm2CCiQGfGGLFnhISUOlZod6dgCgS0xU1IxIh5AmpcgW0NykKHlL1QRIfWIjnphykOemc+3Tpgghg3jmgAZMuwYJ+VLWmgQUUyjogCRDWSgQKuIY0gIJ51eYrmAoIu4SPLrkC4WyEbroXCZ46KwSoMUVgWQM3Wqtss9oqMR1W/qMydOqytQFbLhXFAhWbgMqKW5sI7y5RKVXZuTCbjewqp2q+rO7ElgD12rtXBfky8QFVESy0QrWoktmwEedW5MoQBJiqMADpXkzfCEDBIQtyKYq7QrYiaxDvaBHMpEDCCnsg8hIPU7WnCw7kGPCJFt9MxL4VafUlgQqPIDR4TukiAMAKc1DdzQWLJEQFSNvLAcs3E+tSyEuHLfbYZJdt9tlop6322my/G6pDJIbt8tdCrFYR1yL3V1E10VAVbNiDAaQLOFRdWvYFWAFKQLRlLd13SDurB1DcN7uMVU8o6OTqzXoDJUxXhotNuEtE2G0T2GI/7NJCozu1ecMM6MRW/uAmTf1u6xX1OzdVx4qtgZ8hkfe2Rq6ELjK0omLcbe9CazA8QJZJTvzONy/eFYS/d8X8xdnHtP3QOrUA6MUvujSc9AC9jmjnIf3twvMmjZiv80WhXgTiRdmeJe5OGT8sYf0SVtWUwbXyYQVvSxogepjAPqDoL0sK3IT6hPA4pzywTBE8ReMuwxqhZfAOExxCBfdxQZWw7IMDgULmhOMsXqRHIyEkAv4Swh+OqMRFifAfcbBRQiF0Txk6rEmGYhiJDhHihc6hXKA2AiIEKTESlGgFElHhPiaMcC1NPIV4nogG80xjiqwY2BJQ8CRWBPEn0vjABbgohVEEBxdgvINm/tCAvh8loYG+KAERlyAd4PXijFsKYBn4B4c4YkONe2QIcIACyCOWx3TeymJCFMGCC4DgdQwAwQVuFa0INBIO8vsDse7wSbUAiHay88uiItGQGP0FQNarix2RkMlQuFCSSzklZRzZkSrikSrjI0Isd1nIOP3SJbqUpSJ6SItjArNHyuRlVEKkzGBeJZrLnCYx72BNIQwTmy2o4jCouc0IdNMFyAOnIPGBQo2cEpxH2cpHsHnOdBITJUv6RjnriU0bZokdxDwnKhW1Th59ZJewlCU+hQXQXKZlKQXlUxRNCc2/eOJi+sDKO7tCkKUxwID7SOafEKgtY9hkowARH0nzQoUCQbiTDgBR40o5Vwll1HMez2mbKlw6DZRuQg86XYIaBOFHc/ZoEXkowWmCGoUqXCEBH2gNCCDwATCYho1MZWoQAAAh+QQJCQAoACwAAAAAgACAAIUkJiSUkpTExsRcXlzk4uR8enxMSkysrqzU1tQ0NjTs7uyEhoSkoqTMzsx0cnS8vrwsLixkZmTs6uyEgoRUVlTc3tw8Pjz09vSMjowsKiycnpzMysxkYmTk5uR8fny0srTc2tw8Ojz08vSMioysqqzU0tTEwsRcWlz4+PgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG/kCUcEgsGo/IpHLJbDqf0KhUKWqQMAXK5CjZNBAgguIyLZvP04sJc0oA3nDOsWN61O+CBkhCRvv/ZRsBAxlwhYcAGXJGdHaOd44NFQqAlZZGIBgWb4WJcJ8Ai0WNkKUmeCAil6tnEhoUnJ+dnZ+iRBKPuXW6Dw0EqqzBTAQeELGetKBxXLylvHYIwMLTQxUFs8rZx7ZDuM7fj6XR1MIgDobaysmhzODuvCXS5H8KC+vpx8hv3ELezf/fTCDoM+/MhxDo1mHDJoudEX/vIj4S0KFgGQIR0GW7l7BWO4Agv8WzCOVBAkTaFupr6LAIxJAwHzwQIIEkkwsjPKXjiC8R/j8UBCQKDXcHBEGbRQhQ4JkPEVOPD2EO3QWpgTykKAS42amxJyhFR4JKHWuqJlYhGyDco5UMpVewUaeSzVXxbFa1Xdl61fYzqNy/dupQsivAGEudT1P+fAlYoh0QdodsyMAw8d6+jedCjiyZo+Wdi+cCrmBzMJLJK/fiwyx67GaLdF4fQa36bejMEmXPk1BUCerPXjHjBqi7iAKzlUQIeFScyO/aij8Od9Z8iAITAq6eudAAkh3SSZ5D9xmAxAbtQi5IIACixPTHSnjbaXD0DIhm1dG67WkgwIYyEoCwQWsm5IeCBMv15gdjzPm2nzIJTFACIAGe4lp839RlhnIg/oGHhAmUaROCBshVcgEBAghlIIK52JHdGSXA5OERIK6TAAPoJYfiOyu+M2EZpABUx4xG1MhJASVOI8J936yY4D9JOnHBgFMZOFkGBvxnUxel9BiSAPU1UcFoSphQQI7kiOAefEkwCA6RTIhAoIGcASWQEteRZVoTMWYGZ51EoHkgYD82kSdudAJ6xKFz7alEnwSyqaihuBWK53tDTsqEfLg5egQCmNoh6KSMZobAEiKEagIBmiZRKoEmCDpmpJCc2ioSoNLqyJ+S6VrHBmHemh6VumopHa2eCmtdqMmiwKSutiqLK6bREnFBisWOKuy174H5EKa8SjsEAb6aEOWz/rh5K24SU+r6QHHE4sbqukrMipsJxqbXbbD0DsFtpHYcFWRjBfa7BLqi1aGhs+42a3A/78nWHa35Pjybrg0Q8V64FqNgL6xDyDldlB134yswbpLFb8n/4lYXucNZWrIRE8srBMJkzozEx43ZCinBC+t8i64/1kygwzOLHGnGKMTb2sozq0Gx0FRXbfXVWGet9dZcd+3115M6DRjULE+npdGNaWvxq38x/XNrJAs9sGg/4jwXxyXzLJqtMMPKNNZvAzZvylOR/bAa09WldKdYsz2XNBtjbfdfRKAtWsVCbzDd3wxPh7TBCvgq29yAJUrv5GQtLPVw6urccqT1iU3W/il4ryvWcJijPtRMhivb7nTFES7Vqjr3PVyUr2fW+uHYsh6s7sNXa3CuwCMh/FSft+o4YHE37esDwBr8e7H1hmo6oNBPxXGq35sQtLKc6joq9Sf3G39miW6v2cP3i5Y9CoETzfkm1b+pcO5StBqgpgoIk/8JIYBDUSBSBMVAH0FhcWSRoE1igyE9RUFvucGTUc5yAQTsqoMqkkLyOtTBDTjQEgrQXIPaJJTlPYF0+EHgHQZSkCXlkIYg6R4TIJgLA7FNAATonRlO9KQmoRAeZ+DQOzhWQfBVQIlQOBGVIuIlZ7yoFcTRoZBA8MIlKAAEzZPRE+/wvilMrotS2cAk/gBUARnmzHpdAgR3qCPG3d2hBBXoANIU0IEKrOleD6DiHcIHiFRBQpHtk5kQcFg9GsbqEv4woqoeIEmgtE8XRmyjHzjoqk2aoJOUFB1WhKg/U4Xlk04klSntgMpZ5hFQrYyU9IRgPFjOMDLXseUDdomCVKpKgzAUZh2I2UtlCnEawfRlrcKizEeIcjfVPOUcpKmLZ6apAdxcJjXD2Qu1keMCIBAmM8k5wkl1II2hIqYxYXXNyFTBlJ1spq+sIq53qqqWsKznpHyIqXx+MhUWU5OvUPm9kcxMAfQj1BymM46qiQBUjWEowSqKNRFUwHIR0ShZ5GjOh6JxKMycigDIMQg2l3w0pNuMiB68+TX1fBSewxxnKfKwByy21F8KYA8CNtCcDjRgA2AgAB9+ylQmBAEAOw==); + background-position: center; + background-repeat: no-repeat; + background-size: 50px 50px; + content: ""; +} +.tt-table-card .page-link { + padding: 5px .75rem !important; +} \ No newline at end of file diff --git a/public/plugins/vue/tt-components/tt-table.js b/public/plugins/vue/tt-components/tt-table.js new file mode 100644 index 000000000..b0f92ab53 --- /dev/null +++ b/public/plugins/vue/tt-components/tt-table.js @@ -0,0 +1,259 @@ + +//TODO: tt-autocomplete , tt-select aswell as tt-input should be used for filtering +//TODO: Add sorting functionality +//TODO: Add export to excel and pdf functionality +//TODO: Add Date-Range filter +//TODO: Add Exact Date filter +//TODO: Add new prop serverSide to disable pagination and filtering on the client side +//TODO: Add filtering function if serverSide is disabled +//TODO: Add JSDoc for various functions and props + +/** + * @typedef {Object} ttTableColumnConfig + * @property {string} text - The display text of the column. + * @property {string} key - The unique key of the column. + * @property {string} filter - Indicates if filtering is enabled for the column. + * @property {boolean} sortEnabled - Indicates if sorting is enabled for the column. + * @property {string} class - The CSS class(es) applied to the column. + */ + +Vue.component('tt-table', { + template: ` +
+
+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
{{ column.text }}
+ + +
Keine Ergebnisse!
Laden...
+
+
+ `, props: { + fetchUrl: String, striped: { + type: Boolean, default: true + }, bordered: { + type: Boolean, default: true + }, hover: { + type: Boolean, default: true + }, small: Boolean, excelExport: Boolean, pdfExport: Boolean, tableConfig: { + type: Object, default: () => ({}) + } + }, data() { + return { + loading: false, rows: null, pagination: null, filters: {}, debounceTimeout: null, latestFetchTimestamp: null + }; + }, + + methods: { + /** + * Creates a debounced function that delays invoking `fn` until after `wait` milliseconds + * have elapsed since the last time the debounced function was invoked. + * + * @param {Function} fn The function to debounce. + * @param {number} wait The number of milliseconds to delay. + * @return {Function} The debounced function. + */ + debounce(fn, wait) { + return function (...args) { + const context = this; + if (this.debounceTimeout) clearTimeout(this.debounceTimeout); + this.debounceTimeout = setTimeout(() => fn.apply(context, args), wait); + } + }, /** + * Fetches and updates data for a specified page. + * + * @param {number} page The page number to fetch data for. + * @async + */ + async fetchData(page) { + try { + const fetchTimestamp = Date.now(); + this.latestFetchTimestamp = fetchTimestamp; + const response = await axios.post(this.fetchUrl, { + pagination: { + page: Math.max(page, 1), per_page: this.pagination?.per_page ? this.pagination.per_page : 10, + }, filters: this.filters, + }); + + if (fetchTimestamp !== this.latestFetchTimestamp) return; + + if (typeof response.data !== 'object') { // if the response is not an object + this.rows = []; + this.pagination = {page: 1, per_page: 10, total_rows: 0, total_pages: 1}; + } else { + this.rows = response.data.rows; + this.pagination = response.data.pagination; + } + this.loading = false; + } catch (error) { + console.error('Error fetching data:', error); + } + }, /** + * Fetches rows for a given page, with an option to debounce the fetch operation. + * + * @param {number} page The page number to fetch. Defaults to 1. + * @param {boolean} debounce Whether to debounce the fetch operation. Defaults to false. + */ + async fetchRows(page = 1, debounce = false) { + this.loading = true + if (debounce) { + this.debounce(this.fetchData.bind(this), 300)(page); + } else { + await this.fetchData(page); // Directly call fetchData without debounce + } + }, + applyFilter(event, key) { + this.$set(this.filters, key, event.target.value); // Ensure reactivity + } + }, watch: { + filters: { + handler: function () { + this.fetchRows(this.pagination.page, true).then(); + }, deep: true + } + }, computed: { + /** + * Returns an object containing the columns' configuration. + * @return {ttTableColumnConfig} The columns configuration. + */ + columns() { + return this.tableConfig.headers.reduce((columns, column) => { + if (!column.key) { + console.warn('WARN: tt-table: Column text or key is not defined:', column); + return columns; // Continue to the next iteration without modifying the accumulator + } + columns[column.key] = { + text: column.text, + key: column.key, + filter: column.filter !== undefined ? column.filter : 'search', + filterOptions: column.filterOptions || undefined, + sortEnabled: column.sortEnabled !== undefined ? column.sortEnabled : true, + class: column.class !== undefined ? column.class : '' + }; + return columns; + }, {}); + + }, pagesToDisplay() { + let range = 2; // Number of pages before and after the current page + let start = (this.pagination.page < 4 ? 1 : this.pagination.page - range) ; + let end = (this.pagination.page + range > this.pagination.total_pages ? this.pagination.total_pages : this.pagination.page + range); + if (end < 5) end = 5; + + // Adjust start and end if they are out of bounds + end = end > this.pagination.total_pages ? this.pagination.total_pages : end; + + // Adjust the start and end if we are at the end of the page range + if (this.pagination.page > this.pagination.total_pages - 2) { + start = this.pagination.total_pages - 4 < 1 ? 1 : this.pagination.total_pages - 4; + } + + // Create an array of page numbers to display + let pagesArray = []; + for (let i = start; i <= end; i++) { + pagesArray.push(i); + } + + return pagesArray; + } + + }, mounted() { + if(this.tableConfig.defaultPageSize) { + this.pagination = {page: 1, per_page: this.tableConfig.defaultPageSize, total_rows: null, total_pages: 1}; + } + this.fetchRows().then(); + }, +}) \ No newline at end of file From 0f5fdfa559b79a5bbf25beb86347b7ebe7bf4710 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 12 Mar 2024 16:17:34 +0100 Subject: [PATCH 02/54] Add HistoricTickets to thetool --- Layout/default/HistoricTicket/Index.php | 163 ++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 Layout/default/HistoricTicket/Index.php diff --git a/Layout/default/HistoricTicket/Index.php b/Layout/default/HistoricTicket/Index.php new file mode 100644 index 000000000..5fa77b806 --- /dev/null +++ b/Layout/default/HistoricTicket/Index.php @@ -0,0 +1,163 @@ + 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/vue/tt-components/tt-page-title.js", + "plugins/vue/tt-components/tt-table.js", +]; +$additionalCSS = [ + 'plugins/vue/tt-components/css/tt-table.css', +]; + +include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?> + +
+ + + + + + + + + + + + + + + + +
+ + + + From 2c5bf4abbce2e847f927893c98e73574cbbb75c8 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 12 Mar 2024 16:01:51 +0000 Subject: [PATCH 03/54] Update AddHistoricTicket Migration --- .../20240312154600_add_historic_ticket.php | 109 +++++++++--------- 1 file changed, 53 insertions(+), 56 deletions(-) diff --git a/db/migrations/20240312154600_add_historic_ticket.php b/db/migrations/20240312154600_add_historic_ticket.php index e718571d3..bb97275ff 100644 --- a/db/migrations/20240312154600_add_historic_ticket.php +++ b/db/migrations/20240312154600_add_historic_ticket.php @@ -3,80 +3,77 @@ declare(strict_types=1); use Phinx\Migration\AbstractMigration; -final class AddHistoricTicket extends AbstractMigration -{ - public function up(): void - { - if($this->getEnvironment() == "thetool") { +final class AddHistoricTicket extends AbstractMigration { + public function up(): void { + if ($this->getEnvironment() == "thetool") { //HistoricTicket Table $historicTicket = $this->table("HistoricTicket", ["signed" => true]); - $historicTicket->addColumn("ticket_number", "", ["null" => true]); - $historicTicket->addColumn("ticket_verifier", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("priority", "", ["null" => false, "default" => "1"]); - $historicTicket->addColumn("status_id", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("status", "string", ["null" => true]); - $historicTicket->addColumn("type_id", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("type", "string", ["null" => true]); - $historicTicket->addColumn("user_id", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("agent_id", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("contact_id", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("company", "", ["null" => false]); - $historicTicket->addColumn("company_id", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("first_name", "", ["null" => false]); - $historicTicket->addColumn("middle_name", "", ["null" => false]); - $historicTicket->addColumn("last_name", "", ["null" => false]); - $historicTicket->addColumn("email", "", ["null" => false]); - $historicTicket->addColumn("phone", "", ["null" => false]); - $historicTicket->addColumn("subject", "", ["null" => false]); - $historicTicket->addColumn("ctime", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("mtime", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("muser_id", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("files_folder_id", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("unseen", "", ["null" => false, "default" => "1"]); - $historicTicket->addColumn("group_id", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("order_id", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("last_response_time", "", ["null" => false, "default" => "0"]); - $historicTicket->addColumn("cc_addresses", "string", ["null" => false]); + $historicTicket->addColumn("ticket_number", "integer", ["null" => true]); + $historicTicket->addColumn("ticket_verifier", "string", ["null" => false, "default" => "0", "limit" => 255]); + $historicTicket->addColumn("priority", "integer", ["null" => false, "default" => "1"]); + $historicTicket->addColumn("status_id", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("status", "string", ["null" => true, "limit" => 255]); + $historicTicket->addColumn("type_id", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("type", "string", ["null" => true, "limit" => 255]); + $historicTicket->addColumn("user_id", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("agent_id", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("contact_id", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("company", "string", ["null" => false, "limit" => 255]); + $historicTicket->addColumn("company_id", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("first_name", "string", ["null" => false, "limit" => 255]); + $historicTicket->addColumn("middle_name", "string", ["null" => false, "limit" => 255]); + $historicTicket->addColumn("last_name", "string", ["null" => false, "limit" => 255]); + $historicTicket->addColumn("email", "string", ["null" => false, "limit" => 255]); + $historicTicket->addColumn("phone", "string", ["null" => false, "limit" => 255]); + $historicTicket->addColumn("subject", "string", ["null" => false, "limit" => 255]); + $historicTicket->addColumn("ctime", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("mtime", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("muser_id", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("files_folder_id", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("unseen", "integer", ["null" => false, "default" => "1"]); + $historicTicket->addColumn("group_id", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("order_id", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("last_response_time", "integer", ["null" => false, "default" => "0"]); + $historicTicket->addColumn("cc_addresses", "string", ["null" => false, "limit" => 255]); $historicTicket->save(); //HistoricTicketMessage Table $historicTicketMessage = $this->table("HistoricTicketMessage", ["signed" => true]); - $historicTicketMessage->addColumn("ticket_id", "", ["null" => false]); - $historicTicketMessage->addColumn("status_id", "", ["null" => false, "default" => "0"]); - $historicTicketMessage->addColumn("type_id", "", ["null" => false, "default" => "0"]); - $historicTicketMessage->addColumn("has_status", "", ["null" => false, "default" => "0"]); - $historicTicketMessage->addColumn("has_type", "", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("ticket_id", "integer", ["null" => false]); + $historicTicketMessage->addColumn("status_id", "integer", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("type_id", "integer", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("has_status", "integer", ["null" => false, "default" => "0", "limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY]); + $historicTicketMessage->addColumn("has_type", "integer", ["null" => false, "default" => "0", "limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY]); $historicTicketMessage->addColumn("content", "text", ["null" => true]); - $historicTicketMessage->addColumn("attachments", "", ["null" => false]); - $historicTicketMessage->addColumn("is_note", "", ["null" => false, "default" => "0"]); - $historicTicketMessage->addColumn("user_id", "", ["null" => false, "default" => "0"]); - $historicTicketMessage->addColumn("ctime", "", ["null" => false]); - $historicTicketMessage->addColumn("mtime", "", ["null" => false]); - $historicTicketMessage->addColumn("rate_id", "", ["null" => false, "default" => "0"]); - $historicTicketMessage->addColumn("rate_amount", "", ["null" => false, "default" => "0"]); - $historicTicketMessage->addColumn("rate_hours", "", ["null" => false, "default" => "0"]); - $historicTicketMessage->addColumn("rate_name", "", ["null" => false]); - $historicTicketMessage->addColumn("rate_cost_code", "", ["null" => true]); + $historicTicketMessage->addColumn("attachments", "string", ["null" => false, "limit" => 255]); + $historicTicketMessage->addColumn("is_note", "integer", ["null" => false, "default" => "0", "limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY]); + $historicTicketMessage->addColumn("user_id", "integer", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("ctime", "integer", ["null" => false]); + $historicTicketMessage->addColumn("mtime", "integer", ["null" => false]); + $historicTicketMessage->addColumn("rate_id", "integer", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("rate_amount", "integer", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("rate_hours", "integer", ["null" => false, "default" => "0"]); + $historicTicketMessage->addColumn("rate_name", "string", ["null" => false, "limit" => 255]); + $historicTicketMessage->addColumn("rate_cost_code", "string", ["null" => true, "limit" => 255]); $historicTicketMessage->save(); } - - if($this->getEnvironment() == "addressdb") { - + + if ($this->getEnvironment() == "addressdb") { + } } - public function down(): void - { - if($this->getEnvironment() == "thetool") { + public function down(): void { + if ($this->getEnvironment() == "thetool") { $this->table("HistoricTicket")->drop()->save(); $this->table("HistoricTicketMessage")->drop()->save(); } - - if($this->getEnvironment() == "addressdb") { - + + if ($this->getEnvironment() == "addressdb") { + } } } From 62ee33678835cf4fba9412f092634efeb96b202d Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 12 Mar 2024 16:04:29 +0000 Subject: [PATCH 04/54] Update menu.php to include HistoricTicket --- Layout/default/menu.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Layout/default/menu.php b/Layout/default/menu.php index eb9cd226f..788b1bed2 100644 --- a/Layout/default/menu.php +++ b/Layout/default/menu.php @@ -75,6 +75,7 @@ is(["Admin"])): ?>
  • "> Open Access IDs
  • "> Technologien
  • +
  • "> Historische Tickets
  • can("Fibu")): ?>
  • "> Steuersätze
  • @@ -163,4 +164,4 @@ - \ No newline at end of file + From 0d60d93ab1068bd4c31b6146de779aa6b4c304c6 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 12 Mar 2024 19:24:34 +0100 Subject: [PATCH 05/54] add lastname and filtering to historicticket --- Layout/default/HistoricTicket/Index.php | 4 ++++ application/HistoricTicket/HistoricTicketModel.php | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Layout/default/HistoricTicket/Index.php b/Layout/default/HistoricTicket/Index.php index 5fa77b806..c7c3d19a7 100644 --- a/Layout/default/HistoricTicket/Index.php +++ b/Layout/default/HistoricTicket/Index.php @@ -44,6 +44,10 @@ include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php") + + diff --git a/application/HistoricTicket/HistoricTicketModel.php b/application/HistoricTicket/HistoricTicketModel.php index 6be6d317f..d7f645673 100644 --- a/application/HistoricTicket/HistoricTicketModel.php +++ b/application/HistoricTicket/HistoricTicketModel.php @@ -64,7 +64,6 @@ class HistoricTicketModel { $sql .= isset($filters['contact_id']) ? " AND `contact_id` = " . $filters['contact_id'] : ""; $sql .= isset($filters['company']) ? self::generateFilterCondition($filters['company'], "company") : ""; $sql .= isset($filters['company_id']) ? " AND `company_id` = " . $filters['company_id'] : ""; - $sql .= isset($filters['first_name']) ? self::generateFilterCondition($filters['first_name'], "first_name") : ""; $sql .= isset($filters['middle_name']) ? self::generateFilterCondition($filters['middle_name'], "middle_name") : ""; $sql .= isset($filters['last_name']) ? self::generateFilterCondition($filters['last_name'], "last_name") : ""; $sql .= isset($filters['email']) ? " AND `email` LIKE '%" . $filters['email'] . "%'" : ""; @@ -78,6 +77,14 @@ class HistoricTicketModel { $sql .= isset($filters['order_id']) ? " AND `order_id` = " . $filters['order_id'] : ""; $sql .= isset($filters['last_response_time']) ? " AND `last_response_time` = " . $filters['last_response_time'] : ""; $sql .= isset($filters['cc_addresses']) ? self::generateFilterCondition($filters['cc_addresses'], "cc_addresses") : ""; + + if (isset($filters['first_name'])) { + $filterItems = explode(" ", $filters['first_name']); + foreach ($filterItems as $item) { + $sql .= " AND (`first_name` LIKE '%" . $item . "%' OR `middle_name` LIKE '%" . $item . "%' OR `last_name` LIKE '%" . $item . "%')"; + } + } + return $sql; } From 4054fa46082f48394ca75a714ddd3283d0a998d7 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 12 Mar 2024 20:36:11 +0100 Subject: [PATCH 06/54] Add Domains --- Layout/default/Domain/Index.php | 240 ++++++++++++++++++ application/Domain/Domain.php | 9 + application/Domain/DomainController.php | 196 ++++++++++++++ application/Domain/DomainModel.php | 133 ++++++++++ application/DomainContact/DomainContact.php | 8 + .../DomainContact/DomainContactModel.php | 121 +++++++++ db/migrations/20240312203000_add_domain.php | 64 +++++ lib/inwx/inwx.php | 155 +++++++++++ lib/plesk/plesk.php | 36 +++ 9 files changed, 962 insertions(+) create mode 100644 Layout/default/Domain/Index.php create mode 100644 application/Domain/Domain.php create mode 100644 application/Domain/DomainController.php create mode 100644 application/Domain/DomainModel.php create mode 100644 application/DomainContact/DomainContact.php create mode 100644 application/DomainContact/DomainContactModel.php create mode 100644 db/migrations/20240312203000_add_domain.php create mode 100644 lib/inwx/inwx.php create mode 100644 lib/plesk/plesk.php diff --git a/Layout/default/Domain/Index.php b/Layout/default/Domain/Index.php new file mode 100644 index 000000000..e080c4b5a --- /dev/null +++ b/Layout/default/Domain/Index.php @@ -0,0 +1,240 @@ + 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/vue/tt-components/tt-page-title.js", + "plugins/vue/tt-components/tt-table.js", +]; +$additionalCSS = [ + 'plugins/vue/tt-components/css/tt-table.css', +]; + +include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?> + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + diff --git a/application/Domain/Domain.php b/application/Domain/Domain.php new file mode 100644 index 000000000..bff2e8496 --- /dev/null +++ b/application/Domain/Domain.php @@ -0,0 +1,9 @@ +loadMe(); + $this->layout()->set("me", $me); + $this->me = $me; + + $this->inwx = new Inwx($this->INWX_USER, $this->INWX_PASS); + $this->plesk = new Plesk($this->PLESK_USER, $this->PLESK_AUTH); + } + + protected function indexAction(): void { + $this->layout()->setTemplate("Domain/Index"); + } + + protected function apiAction() { + $do = $this->request->do; + + if ($do !== "getConfig" && !$this->me->is("employee")) { + $this->redirect("dashboard"); + } + switch ($do) { + case "importAllDomains": + $return = $this->importAllDomains(); + break; + case "getDomains": + $return = $this->getAllDomains(); + break; + case "getDomainContacts": + $return = $this->getDomainContacts(); + break; + case "getDnsRecords": + $return = $this->getDnsRecords(); + break; + case "checkDomain": + $return = $this->checkDomain(); + break; + default: + $return = false; + break; + } + + if (!$return) { + $return = [ + "status" => "error", + "message" => "Invalid request." + ]; + } + + die(json_encode($return)); + } + + protected function importDomain(): void { + // use plesk api to get all domains + + + } + + protected function importAllDomains(): array { + + try { + $inwxContact = $this->inwx->contactList(); + $pleskDomains = $this->plesk->getAllDomains(); + $inwxDomains = $this->inwx->domainList(); + + $domains = []; + $pleskDomainsArray = []; + + foreach ($pleskDomains as $pleskDomain) { + $pleskDomainsArray[$pleskDomain['name']] = $pleskDomain; + } + + foreach ($inwxDomains as $inwxDomain) { + if (isset($pleskDomainsArray[$inwxDomain['domain']])) { + $inwxDomain['plesk'] = [ + "id" => $pleskDomainsArray[$inwxDomain['domain']]['id'], + "hosting_type" => $pleskDomainsArray[$inwxDomain['domain']]['hosting_type'], + "created" => strtotime($pleskDomainsArray[$inwxDomain['domain']]['created']) + ]; + } + + $domains[] = $inwxDomain; + } + + $domainsImport = DomainModel::importDomains($domains); + $contactsImport = DomainContactModel::importDomainContacts($inwxContact); + + return [ + "status" => "success", + "importMessages" => [ + $domainsImport['message'], + $contactsImport['message'] + ], + ]; + } catch (Exception $e) { + $this->log->error("Error while importing domains: " . $e->getMessage()); + return [ + "status" => "error", + "message" => "Error while importing domains: " . $e->getMessage() + ]; + } + + } + + private function getAllDomains(): array { + + $json = json_decode(file_get_contents('php://input'), true); + + $filters = $json['filters'] ?? []; + $page = $json['pagination']['page'] ?? 1; + $perPage = $json['pagination']['per_page'] ?? 10; + + $domains = DomainModel::getAllDomains($filters, $perPage, $perPage * $page - $perPage); + $totalRows = DomainModel::countDomains($filters); + + return [ + "rows" => $domains, + "pagination" => [ + "page" => $page, + "total_pages" => ceil($totalRows / $perPage), + "per_page" => $perPage, + "total_rows" => intval($totalRows) + ] + ]; + + } + + private function getDnsRecords() { + if (!isset($this->request->domain)) { + return ["status" => "error", "message" => "No domain specified."]; + } + + $domain = $this->request->domain; + return array_merge( + dns_get_record($domain, DNS_TXT), + dns_get_record($domain, DNS_A), + dns_get_record($domain, DNS_CNAME), + dns_get_record($domain, DNS_MX), + dns_get_record($domain, DNS_NS), + dns_get_record($domain, DNS_SOA), + dns_get_record($domain, DNS_SRV), + dns_get_record($domain, DNS_AAAA), + + ); + } + + private function getDomainContacts(): array { + $domainContacts = []; + $dbDomainContacts = DomainContactModel::getAllDomainContacts(); + + foreach ($dbDomainContacts as $dbDomainContact) { + $domainContacts[$dbDomainContact['inwxRoId']] = $dbDomainContact; + } + + return $domainContacts; + } + + private function checkDomain(): array { + $domain = $this->request->domain; + + if(empty($domain)) { + return ["status" => "error", "message" => "No domain or tld specified."]; + } + + try { + $domainCheck = $this->inwx->domainCheck($domain); + + if($domainCheck['domain'][0]['status'] === "free") { + $domainPrice = $this->inwx->domainGetDomainPrice($domain, "reg"); + } else { + $domainPrice = $this->inwx->domainGetDomainPrice($domain, "transfer"); + } + + $domainCheck['domain'][0]['price'] = $domainPrice; + + return $domainCheck['domain'][0]; + } catch (Exception $e) { + $this->log->error("Error while checking domain: " . $e->getMessage()); + return ["status" => "error", "message" => "Error while checking domain: " . $e->getMessage()]; + } + } + + +} \ No newline at end of file diff --git a/application/Domain/DomainModel.php b/application/Domain/DomainModel.php new file mode 100644 index 000000000..60d13d016 --- /dev/null +++ b/application/Domain/DomainModel.php @@ -0,0 +1,133 @@ + $value) { + if (property_exists(get_called_class(), $field)) { + $this->$field = $value; + } + } + } + + public static function importDomains($domains): array { + $db = FronkDB::singleton(); + + $db->query("TRUNCATE TABLE `Domain`"); + + $sql = /** @lang text */ + "INSERT INTO `Domain` (`inwxRoId`, `domain`, `period`, `crDate`, `exDate`, `reDate`, `upDate`, `transferLock`, `status`, `authCode`, `registrant`, `admin`, `tech`, `billing`, `ns`, `pleskId`, `pleskHostingType`, `pleskCreated`) VALUES "; + $values = []; + foreach ($domains as $domain) { + $valueStr ="(" . + $domain['roId'] . ", '" . + $domain['domain'] . "', '" . + $domain['period'] . "', " . + $domain['crDate']['timestamp'] . ", " . + $domain['exDate']['timestamp'] . ", " . + $domain['reDate']['timestamp'] . ", " . + $domain['upDate']['timestamp'] . ", " . + ($domain['transferLock'] ? 1 : 0) . ", '" . + $domain['status'] . "', '" . + $domain['authCode'] . "', " . + $domain['registrant'] . ", " . + $domain['admin'] . ", " . + $domain['tech'] . ", " . + $domain['billing'] . ", '" . + implode(", ", $domain['ns']) . "', "; + + // Check if 'pleskId' is set + if (isset($domain['plesk']) && is_array($domain['plesk'])) { + $valueStr .= $domain['plesk']['id'] . ", "; + $valueStr .= "'" . $domain['plesk']['hosting_type'] . "', "; + $valueStr .= $domain['plesk']['created']; + } else { + $valueStr .= "NULL, NULL, NULL"; + } + + $values[] = $valueStr . ")"; + } + $sql .= implode(", ", $values); + $db->query($sql); + return [ + "message" => "Imported " . count($domains) . " domains." + ]; + } + + /** + * 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['crDate']) ? self::generateFilterCondition($filters['domain'], "domain") : ""; + $sql .= isset($filters['crDate']) ? " AND `crDate` = " . $filters['crDate'] : ""; + $sql .= isset($filters['exDate']) ? " AND `exDate` = " . $filters['exDate'] : ""; + $sql .= isset($filters['reDate']) ? " AND `reDate` = " . $filters['reDate'] : ""; + $sql .= isset($filters['upDate']) ? " AND `upDate` = " . $filters['upDate'] : ""; + $sql .= isset($filters['status']) ? " AND `status` = '" . $filters['status'] . "'" : ""; + $sql .= isset($filters['transferLock']) && $filters['transferLock'] == 1 ? " AND `transferLock` = true" : ""; + $sql .= isset($filters['authCode']) ? self::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['ns']) ? self::generateFilterCondition($filters['ns'], "ns") : ""; + return $sql; + + } + + public static function getAllDomains($filters, $limit = null, $offset = 0): array { + $db = FronkDB::singleton(); + $sql = "SELECT * FROM `Domain` WHERE 1 " . self::getSqlFilter($filters); + $sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset; + + $result = $db->query($sql); + $rows = []; + while ($row = $result->fetch_assoc()) { + $rows[] = new DomainModel($row); + } + + return $rows; + } + + public static function countDomains($filters) { + $db = FronkDB::singleton(); + $sql = "SELECT COUNT(*) as `total_rows` FROM `Domain` WHERE 1 " . self::getSqlFilter($filters); + $result = $db->query($sql); + return $result->fetch_assoc()['total_rows']; + } +} \ No newline at end of file diff --git a/application/DomainContact/DomainContact.php b/application/DomainContact/DomainContact.php new file mode 100644 index 000000000..19f738f64 --- /dev/null +++ b/application/DomainContact/DomainContact.php @@ -0,0 +1,8 @@ + $value) { + if (property_exists(get_called_class(), $field)) { + $this->$field = $value; + } + } + } + + public static function importDomainContacts($domainContacts): array { + $db = FronkDB::singleton(); + + $db->query("TRUNCATE TABLE `DomainContact`"); + + $sql = /** @lang text */ + "INSERT INTO `DomainContact` (`inwxRoId`, `type`, `name`, `street`, `city`, `pc`, `cc`, `voice`, `email`, `protection`, `verificationStatus`, `usedCount`) VALUES "; + $values = []; + + foreach ($domainContacts as $domainContact) { + $valueStr = "(" . + $domainContact['roId'] . ", '" . + $domainContact['type'] . "', '" . + $domainContact['name'] . "', '" . + $domainContact['street'] . "', '" . + $domainContact['city'] . "', '" . + $domainContact['pc'] . "', '" . + $domainContact['cc'] . "', '" . + $domainContact['voice'] . "', '" . + $domainContact['email'] . "', " . + ($domainContact['protection'] ? 1 : 0) . ", '" . + $domainContact['verificationStatus'] . "', "; + + $valueStr .= $domainContact['usedCount'] ?? "NULL"; + $valueStr .= ")"; + + $values[] = $valueStr; + } + + $sql .= implode(", ", $values); + $db->query($sql); + return [ + "message" => "Imported " . count($domainContacts) . " domain contacts." + ]; + } + + /** + * 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['roId']) ? " AND `inwxRoId` = " . $filters['roId'] : ""; + $sql .= isset($filters['type']) ? " AND `type` = '" . $filters['type'] . "'" : ""; + $sql .= isset($filters['name']) ? self::generateFilterCondition($filters['name'], "name") : ""; + $sql .= isset($filters['street']) ? self::generateFilterCondition($filters['street'], "street") : ""; + $sql .= isset($filters['city']) ? self::generateFilterCondition($filters['city'], "city") : ""; + $sql .= isset($filters['pc']) ? " AND `pc` = " . $filters['pc'] : ""; + $sql .= isset($filters['cc']) ? " AND `cc` = " . $filters['cc'] : ""; + $sql .= isset($filters['voice']) ? " AND `voice` = " . $filters['voice'] : ""; + $sql .= isset($filters['email']) ? self::generateFilterCondition($filters['email'], "email") : ""; + $sql .= isset($filters['protection']) ? " AND `protection` = " . $filters['protection'] : ""; + $sql .= isset($filters['usedCount']) ? " AND `usedCount` = " . $filters['usedCount'] : ""; + $sql .= isset($filters['verificationStatus']) ? " AND `verificationStatus` = '" . $filters['verificationStatus'] . "'" : ""; + return $sql; + + } + + public static function getAllDomainContacts($filters = null, $limit = null, $offset = 0, $raw_array = true): array { + $db = FronkDB::singleton(); + $sql = "SELECT * FROM `DomainContact` WHERE 1 "; + $sql .= $filters === null ? "" : self::getSqlFilter($filters); + $sql .= $limit === null ? "" : " LIMIT " . $limit . " OFFSET " . $offset; + + $result = $db->query($sql); + $rows = []; + while ($row = $result->fetch_assoc()) { + $rows[] = $raw_array ? $row : new DomainContactModel($row); + } + + return $rows; + } + + public static function countDomainContacts($filters) { + $db = FronkDB::singleton(); + $sql = "SELECT COUNT(*) as `total_rows` FROM `DomainContact` WHERE 1 " . self::getSqlFilter($filters); + $result = $db->query($sql); + return $result->fetch_assoc()['total_rows']; + } +} \ No newline at end of file diff --git a/db/migrations/20240312203000_add_domain.php b/db/migrations/20240312203000_add_domain.php new file mode 100644 index 000000000..f3a13c65a --- /dev/null +++ b/db/migrations/20240312203000_add_domain.php @@ -0,0 +1,64 @@ +getEnvironment() == "thetool") { + //Domain Table + $domainTable = $this->table("Domain", ["signed" => true]); + $domainTable->addColumn("inwxRoId", "integer", ["null" => true]); + $domainTable->addColumn("domain", "string", ["null" => true, "limit" => 255]); + $domainTable->addColumn("period", "string", ["null" => true, "limit" => 50]); + $domainTable->addColumn("crDate", "integer", ["null" => true]); + $domainTable->addColumn("exDate", "integer", ["null" => true]); + $domainTable->addColumn("reDate", "integer", ["null" => true]); + $domainTable->addColumn("upDate", "integer", ["null" => true]); + $domainTable->addColumn("transferLock", "integer", ["null" => true, "limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY]); + $domainTable->addColumn("status", "string", ["null" => true, "limit" => 50]); + $domainTable->addColumn("authCode", "string", ["null" => true, "limit" => 50]); + $domainTable->addColumn("registrant", "integer", ["null" => true]); + $domainTable->addColumn("admin", "integer", ["null" => true]); + $domainTable->addColumn("tech", "integer", ["null" => true]); + $domainTable->addColumn("billing", "integer", ["null" => true]); + $domainTable->addColumn("ns", "string", ["null" => true, "limit" => 255]); + $domainTable->addColumn("pleskId", "string", ["null" => true, "limit" => 255]); + $domainTable->addColumn("pleskHostingType", "string", ["null" => true, "limit" => 255]); + $domainTable->addColumn("pleskCreated", "integer", ["null" => true]); + $domainTable->save(); + + //DomainContact Table + + $domainContactTable = $this->table("DomainContact", ["signed" => true]); + $domainContactTable->addColumn("inwxRoId", "integer", ["null" => true]); + $domainContactTable->addColumn("type", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("name", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("street", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("city", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("pc", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("cc", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("voice", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("email", "string", ["null" => true, "limit" => 255]); + $domainContactTable->addColumn("protection", "integer", ["null" => true, "limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY]); + $domainContactTable->addColumn("usedCount", "integer", ["null" => true]); + $domainContactTable->addColumn("verificationStatus", "string", ["null" => true, "limit" => 255]); + $domainContactTable->save(); + } + + if ($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void { + if ($this->getEnvironment() == "thetool") { + $this->table("Domain")->drop()->save(); + $this->table("DomainContact")->drop()->save(); + } + + if ($this->getEnvironment() == "addressdb") { + + } + } +} diff --git a/lib/inwx/inwx.php b/lib/inwx/inwx.php new file mode 100644 index 000000000..6d1c6ed2f --- /dev/null +++ b/lib/inwx/inwx.php @@ -0,0 +1,155 @@ +username = $username; + $this->password = $password; + } + + /** + * Get the list of contacts. + * + * @param int $pageLimit + * @return array + * @throws Exception + */ + public function contactList(int $pageLimit = 300): array { + $requestData = array( + 'jsonrpc' => '2.0', + 'method' => 'contact.list', + 'params' => array( + 'user' => $this->username, + 'pass' => $this->password, + 'pagelimit' => $pageLimit, + ), + 'id' => 1 + ); + + return $this->makeRequest($requestData)['contact']; + } + + /** + * Get the list of domains. + * + * @param int $pageLimit + * @return array + * @throws Exception + */ + public function domainList(int $pageLimit = 300): array { + $requestData = array( + 'jsonrpc' => '2.0', + 'method' => 'domain.list', + 'params' => array( + 'user' => $this->username, + 'pass' => $this->password, + 'pagelimit' => $pageLimit, + ), + 'id' => 1 + ); + + return $this->makeRequest($requestData)['domain']; + } + + /** + * Check if a domain is available. + * + * @param string $domain + * @param string $tld + * @return array + * @throws Exception + */ + public function domainCheck(string $domain): array { + $requestData = array( + 'jsonrpc' => '2.0', + 'method' => 'domain.check', + 'params' => array( + 'user' => $this->username, + 'pass' => $this->password, + 'domain' => $domain + ), + 'id' => 1 + ); + + return $this->makeRequest($requestData); + } + + /** + * Get the price of a domain. + * + * @param string $domain + * @param string $priceType reg | renewal | transfer | update | trade | restore + * @return array + * @throws Exception + */ + public function domainGetDomainPrice(string $domain, string $priceType): array { + $requestData = array( + 'jsonrpc' => '2.0', + 'method' => 'domain.getdomainprice', + 'params' => array( + 'user' => $this->username, + 'pass' => $this->password, + 'domain' => $domain, + 'pricetype' => $priceType, + ), + 'id' => 1 + ); + + return $this->makeRequest($requestData); + } + + /** + * Make a request to the INWX API. + * + * @param array $requestData + * @return array + * @throws Exception + */ + private function makeRequest(array $requestData): array { + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $this->apiUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData)); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Content-Type: application/json', + 'Accept: application/json' + )); + + $response = curl_exec($ch); + + if ($response === false) { + throw new Exception('cURL Error: ' . curl_error($ch)); + } + + curl_close($ch); + + $responseData = json_decode($response, true); + + if (isset($responseData['error'])) { + throw new Exception('JSON-RPC Error: ' . $responseData['error']['message']); + } + + if (!isset($responseData['resData'])) { + throw new Exception('Unexpected response format.'); + } + + return $responseData['resData']; + } +} +?> diff --git a/lib/plesk/plesk.php b/lib/plesk/plesk.php new file mode 100644 index 000000000..d0406d99f --- /dev/null +++ b/lib/plesk/plesk.php @@ -0,0 +1,36 @@ +host = $host; + $this->authorization = $authorization; + } + + public function getAllDomains() { + // Implement code to fetch all configured domains using Plesk API + // You can use cURL or any HTTP client library to make API requests + // Example: + $url = "https://{$this->host}/api/v2/domains"; + $headers = array( + "Authorization: {$this->authorization}", + "Content-Type: application/json" + ); + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($curl); + + $response = json_decode($response, true); + + if (is_array($response)) { + return $response; + } else { + return false; + } + } +} \ No newline at end of file From f9e53b9653a519fe822766125e4d4344777f7a7a Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 12 Mar 2024 21:24:56 +0100 Subject: [PATCH 07/54] Fixed lowercase --- lib/{inwx/inwx.php => Inwx/Inwx.php} | 0 lib/{plesk/plesk.php => Plesk/Plesk.php} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename lib/{inwx/inwx.php => Inwx/Inwx.php} (100%) rename lib/{plesk/plesk.php => Plesk/Plesk.php} (100%) diff --git a/lib/inwx/inwx.php b/lib/Inwx/Inwx.php similarity index 100% rename from lib/inwx/inwx.php rename to lib/Inwx/Inwx.php diff --git a/lib/plesk/plesk.php b/lib/Plesk/Plesk.php similarity index 100% rename from lib/plesk/plesk.php rename to lib/Plesk/Plesk.php From dfd20a346e77d9036f64c2b732e768f406feb5a2 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 12 Mar 2024 21:33:23 +0100 Subject: [PATCH 08/54] [HistoricTickets] fix newlines and escape to close --- Layout/default/HistoricTicket/Index.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Layout/default/HistoricTicket/Index.php b/Layout/default/HistoricTicket/Index.php index c7c3d19a7..1c0f6e188 100644 --- a/Layout/default/HistoricTicket/Index.php +++ b/Layout/default/HistoricTicket/Index.php @@ -56,9 +56,9 @@ include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php") -