diff --git a/cp/app/Controllers/DapiController.php b/cp/app/Controllers/DapiController.php index 4de3dbc..31b215c 100644 --- a/cp/app/Controllers/DapiController.php +++ b/cp/app/Controllers/DapiController.php @@ -366,4 +366,290 @@ class DapiController extends Controller $response->getBody()->write(json_encode($payload, JSON_UNESCAPED_UNICODE)); return $response; } + + public function listPayments(Request $request, Response $response): Response + { + $params = $request->getQueryParams(); + $db = $this->container->get('db'); + + // Map fields to fully qualified columns for filtering/sorting + // Adjust field names if needed + $allowedFieldsMap = [ + 'date' => 'ph.date', + 'registrar_id' => 'ph.registrar_id', + 'description' => 'ph.description', + 'amount' => 'ph.amount', + 'registrar_name' => 'r.name' + ]; + + // --- SORTING --- + $sortField = 'ph.date'; // default sort by date + $sortDir = 'desc'; + if (!empty($params['order'])) { + $orderParts = explode(',', $params['order']); + if (count($orderParts) === 2) { + $fieldCandidate = preg_replace('/[^a-zA-Z0-9_]/', '', $orderParts[0]); + if (array_key_exists($fieldCandidate, $allowedFieldsMap)) { + $sortField = $allowedFieldsMap[$fieldCandidate]; + } + $sortDir = strtolower($orderParts[1]) === 'asc' ? 'asc' : 'desc'; + } + } + + // --- PAGINATION --- + $page = 1; + $size = 10; + if (!empty($params['page'])) { + $pageParts = explode(',', $params['page']); + if (count($pageParts) === 2) { + $pageNum = (int)$pageParts[0]; + $pageSize = (int)$pageParts[1]; + if ($pageNum > 0) { + $page = $pageNum; + } + if ($pageSize > 0) { + $size = $pageSize; + } + } + } + $offset = ($page - 1) * $size; + + // --- FILTERING --- + $whereClauses = []; + $bindParams = []; + foreach ($params as $key => $value) { + if (preg_match('/^filter\d+$/', $key)) { + $fParts = explode(',', $value); + if (count($fParts) === 3) { + list($fField, $fOp, $fVal) = $fParts; + $fField = preg_replace('/[^a-zA-Z0-9_]/', '', $fField); + + // Ensure the field is allowed and fully qualify it + if (!array_key_exists($fField, $allowedFieldsMap)) { + // Skip unknown fields + continue; + } + $column = $allowedFieldsMap[$fField]; + + switch ($fOp) { + case 'eq': + $whereClauses[] = "$column = :f_{$key}"; + $bindParams["f_{$key}"] = $fVal; + break; + case 'cs': + $whereClauses[] = "$column LIKE :f_{$key}"; + $bindParams["f_{$key}"] = "%$fVal%"; + break; + case 'sw': + $whereClauses[] = "$column LIKE :f_{$key}"; + $bindParams["f_{$key}"] = "$fVal%"; + break; + case 'ew': + $whereClauses[] = "$column LIKE :f_{$key}"; + $bindParams["f_{$key}"] = "%$fVal"; + break; + // Add other cases if needed + } + } + } + } + + // Base SQL + $sqlBase = " + FROM payment_history ph + LEFT JOIN registrar r ON ph.registrar_id = r.id + "; + + // If you want all filters combined with OR, keep " OR ". + // If you want AND logic for multiple filters, change to "AND". + $sqlWhere = ''; + if (!empty($whereClauses)) { + $sqlWhere = "WHERE " . implode(" OR ", $whereClauses); + } + + // Count total results + $totalSql = "SELECT COUNT(DISTINCT ph.id) AS total $sqlBase $sqlWhere"; + $totalCount = $db->selectValue($totalSql, $bindParams); + + // Data query + $selectFields = " + ph.id, + ph.registrar_id, + ph.date, + ph.description, + ph.amount, + r.name AS registrar_name + "; + + $dataSql = " + SELECT $selectFields + $sqlBase + $sqlWhere + ORDER BY $sortField $sortDir + LIMIT $offset, $size + "; + + $records = $db->select($dataSql, $bindParams); + + // Ensure records is always an array + if (!$records) { + $records = []; + } + + $payload = [ + 'records' => $records, + 'results' => $totalCount + ]; + + $response = $response->withHeader('Content-Type', 'application/json; charset=UTF-8'); + $response->getBody()->write(json_encode($payload, JSON_UNESCAPED_UNICODE)); + return $response; + } + + public function listStatements(Request $request, Response $response): Response + { + $params = $request->getQueryParams(); + $db = $this->container->get('db'); + + // Map fields to fully qualified columns for filtering/sorting + $allowedFieldsMap = [ + 'date' => 'st.date', + 'registrar_id' => 'st.registrar_id', + 'command' => 'st.command', + 'domain_name' => 'st.domain_name', + 'length_in_months' => 'st.length_in_months', + 'fromS' => 'st.fromS', + 'toS' => 'st.toS', + 'amount' => 'st.amount', + 'registrar_name' => 'r.name' + ]; + + // --- SORTING --- + $sortField = 'st.date'; // default sort by date + $sortDir = 'desc'; + if (!empty($params['order'])) { + $orderParts = explode(',', $params['order']); + if (count($orderParts) === 2) { + $fieldCandidate = preg_replace('/[^a-zA-Z0-9_]/', '', $orderParts[0]); + if (array_key_exists($fieldCandidate, $allowedFieldsMap)) { + $sortField = $allowedFieldsMap[$fieldCandidate]; + } + $sortDir = strtolower($orderParts[1]) === 'asc' ? 'asc' : 'desc'; + } + } + + // --- PAGINATION --- + $page = 1; + $size = 10; + if (!empty($params['page'])) { + $pageParts = explode(',', $params['page']); + if (count($pageParts) === 2) { + $pageNum = (int)$pageParts[0]; + $pageSize = (int)$pageParts[1]; + if ($pageNum > 0) { + $page = $pageNum; + } + if ($pageSize > 0) { + $size = $pageSize; + } + } + } + $offset = ($page - 1) * $size; + + // --- FILTERING --- + $whereClauses = []; + $bindParams = []; + foreach ($params as $key => $value) { + if (preg_match('/^filter\d+$/', $key)) { + $fParts = explode(',', $value); + if (count($fParts) === 3) { + list($fField, $fOp, $fVal) = $fParts; + $fField = preg_replace('/[^a-zA-Z0-9_]/', '', $fField); + + // Ensure the field is allowed and fully qualify it + if (!array_key_exists($fField, $allowedFieldsMap)) { + // Skip unknown fields + continue; + } + $column = $allowedFieldsMap[$fField]; + + switch ($fOp) { + case 'eq': + $whereClauses[] = "$column = :f_{$key}"; + $bindParams["f_{$key}"] = $fVal; + break; + case 'cs': + $whereClauses[] = "$column LIKE :f_{$key}"; + $bindParams["f_{$key}"] = "%$fVal%"; + break; + case 'sw': + $whereClauses[] = "$column LIKE :f_{$key}"; + $bindParams["f_{$key}"] = "$fVal%"; + break; + case 'ew': + $whereClauses[] = "$column LIKE :f_{$key}"; + $bindParams["f_{$key}"] = "%$fVal"; + break; + // Add other cases if needed + } + } + } + } + + // Base SQL + $sqlBase = " + FROM statement st + LEFT JOIN registrar r ON st.registrar_id = r.id + "; + + // Combine filters with OR (common approach) + $sqlWhere = ''; + if (!empty($whereClauses)) { + $sqlWhere = "WHERE " . implode(" OR ", $whereClauses); + } + + // Count total results + $totalSql = "SELECT COUNT(DISTINCT st.id) AS total $sqlBase $sqlWhere"; + $totalCount = $db->selectValue($totalSql, $bindParams); + + // Data query + $selectFields = " + st.id, + st.registrar_id, + st.date, + st.command, + st.domain_name, + st.length_in_months, + st.fromS, + st.toS, + st.amount, + r.name AS registrar_name + "; + + $dataSql = " + SELECT $selectFields + $sqlBase + $sqlWhere + ORDER BY $sortField $sortDir + LIMIT $offset, $size + "; + + $records = $db->select($dataSql, $bindParams); + + // Ensure records is always an array + if (!$records) { + $records = []; + } + + $payload = [ + 'records' => $records, + 'results' => $totalCount + ]; + + $response = $response->withHeader('Content-Type', 'application/json; charset=UTF-8'); + $response->getBody()->write(json_encode($payload, JSON_UNESCAPED_UNICODE)); + return $response; + } + + } \ No newline at end of file diff --git a/cp/resources/views/partials/js-overview.twig b/cp/resources/views/partials/js-overview.twig index 903dfe1..2581c4f 100644 --- a/cp/resources/views/partials/js-overview.twig +++ b/cp/resources/views/partials/js-overview.twig @@ -8,6 +8,7 @@ document.addEventListener("DOMContentLoaded", function(){ currency = "{{ currency }} "; + var searchTerm = ""; // global variable to hold the search term function updateSearchTerm(term) { @@ -19,20 +20,29 @@ pagination: true, paginationMode: "remote", paginationSize: 10, - ajaxURL: "/api/records/payment_history", - ajaxParams: { - join: "registrar" - }, + sortMode: "remote", + ajaxURL: "/dapi/payments", ajaxURLGenerator: function(url, config, params) { - var queryParts = ["join=registrar"]; + var queryParts = []; // Handle search term if (searchTerm) { - queryParts.push("filter1=date,cs," + encodeURIComponent(searchTerm)); - queryParts.push("filter2=description,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter1=registrar_name,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter2=date,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter3=description,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter4=amount,cs," + encodeURIComponent(searchTerm)); } - queryParts.push("order=date,desc"); + // Handle sorting from Tabulator + if (params.sort && params.sort.length > 0) { + var sorter = params.sort[0]; // single-column sorting + var sortField = encodeURIComponent(sorter.field); + var sortDir = (sorter.dir === "asc" ? "asc" : "desc"); + queryParts.push("order=" + sortField + "," + sortDir); + } else { + // fallback default order if no sorters + queryParts.push("order=date,desc"); + } // Include pagination parameters if (params.page) { @@ -53,9 +63,6 @@ return { last_page: 1, data: [] }; } }, - dataReceiveParams: { - "last_page": "results", // Mapping 'results' to 'last_page' - }, layout:"fitDataFill", responsiveLayout: "collapse", responsiveLayoutCollapseStartOpen:false, @@ -63,10 +70,10 @@ placeholder: "{{ __('No Data') }}", columns:[ {formatter:"responsiveCollapse", width:30, minWidth:30, hozAlign:"center", resizable:false, headerSort:false, responsive:0}, - {title:"{{ __('Registrar') }}", field:"registrar_id.name", resizable:false, headerSort:false, responsive:0}, - {title:"{{ __('Date') }}", field:"date", resizable:false, headerSort:false, responsive:2}, - {title:"{{ __('Description') }}", field:"description", resizable:false, headerSort:false, responsive:2}, - {title:"{{ __('Amount') }}", field:"amount", resizable:false, headerSort:false, download:false, responsive:0, formatter:"money", formatterParams:{ + {title:"{{ __('Registrar') }}", field:"registrar_name", resizable:false, headerSort:true, responsive:0}, + {title:"{{ __('Date') }}", field:"date", resizable:false, headerSort:true, responsive:2}, + {title:"{{ __('Description') }}", field:"description", resizable:false, headerSort:true, responsive:2, formatter:cell => (cell.getElement().setAttribute("title", cell.getValue() || ""), cell.getValue()?.length > 80 ? cell.getValue().substring(0, 80) + "..." : cell.getValue())}, + {title:"{{ __('Amount') }}", field:"amount", resizable:false, headerSort:true, width:200, minWidth:100, responsive:0, formatter:"money", formatterParams:{ decimal:".", thousand:" ", symbol:currency, @@ -75,8 +82,13 @@ ] }); var searchInput = document.getElementById("search-input"); + let searchTimeout; + searchInput.addEventListener("input", function () { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { updateSearchTerm(searchInput.value); + }, 300); // 300ms delay }); }); diff --git a/cp/resources/views/partials/js-transactions.twig b/cp/resources/views/partials/js-transactions.twig index 8ba6f14..9576987 100644 --- a/cp/resources/views/partials/js-transactions.twig +++ b/cp/resources/views/partials/js-transactions.twig @@ -19,22 +19,33 @@ pagination: true, paginationMode: "remote", paginationSize: 10, - ajaxURL: "/api/records/statement", - ajaxParams: { - join: "registrar" - }, + sortMode: "remote", + ajaxURL: "/dapi/statements", ajaxURLGenerator: function(url, config, params) { - var queryParts = ["join=registrar"]; + var queryParts = []; // Handle search term if (searchTerm) { - queryParts.push("filter1=command,cs," + encodeURIComponent(searchTerm)); - queryParts.push("filter2=domain_name,cs," + encodeURIComponent(searchTerm)); - queryParts.push("filter3=fromS,cs," + encodeURIComponent(searchTerm)); - queryParts.push("filter4=toS,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter1=registrar_name,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter2=date,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter3=command,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter4=domain_name,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter5=length_in_months,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter6=fromS,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter7=toS,cs," + encodeURIComponent(searchTerm)); + queryParts.push("filter8=amount,cs," + encodeURIComponent(searchTerm)); } - queryParts.push("order=date,desc"); + // Handle sorting from Tabulator + if (params.sort && params.sort.length > 0) { + var sorter = params.sort[0]; // single-column sorting + var sortField = encodeURIComponent(sorter.field); + var sortDir = (sorter.dir === "asc" ? "asc" : "desc"); + queryParts.push("order=" + sortField + "," + sortDir); + } else { + // fallback default order if no sorters + queryParts.push("order=date,desc"); + } // Include pagination parameters if (params.page) { @@ -55,9 +66,6 @@ return { last_page: 1, data: [] }; } }, - dataReceiveParams: { - "last_page": "results", // Mapping 'results' to 'last_page' - }, layout:"fitDataFill", responsiveLayout: "collapse", responsiveLayoutCollapseStartOpen:false, @@ -65,14 +73,14 @@ placeholder: "{{ __('No Data') }}", columns:[ {formatter:"responsiveCollapse", width:30, minWidth:30, hozAlign:"center", resizable:false, headerSort:false, responsive:0}, - {title:"{{ __('Registrar') }}", field:"registrar_id.name", resizable:false, headerSort:false, responsive:0}, - {title:"{{ __('Date') }}", field:"date", resizable:false, headerSort:false, responsive:2}, - {title:"{{ __('Command') }}", field:"command", resizable:false, headerSort:false, responsive:2}, - {title:"{{ __('Domain') }}", field:"domain_name", resizable:false, headerSort:false, download:false, responsive:0}, - {title:"{{ __('Length') }}", field:"length_in_months", resizable:false, headerSort:false, responsive:2}, - {title:"{{ __('From') }}", field:"fromS", resizable:false, headerSort:false, responsive:2}, - {title:"{{ __('To') }}", field:"toS", resizable:false, headerSort:false, responsive:2}, - {title:"{{ __('Amount') }}", field:"amount", resizable:false, headerSort:false, download:false, responsive:2, formatter:"money", formatterParams:{ + {title:"{{ __('Registrar') }}", field:"registrar_name", resizable:false, headerSort:true, responsive:0}, + {title:"{{ __('Date') }}", field:"date", resizable:false, headerSort:true, responsive:2}, + {title:"{{ __('Command') }}", field:"command", resizable:false, headerSort:true, responsive:2}, + {title:"{{ __('Domain') }}", field:"domain_name", resizable:false, headerSort:true, download:false, responsive:0}, + {title:"{{ __('Length') }}", field:"length_in_months", resizable:false, download:false, headerSort:true, responsive:2}, + {title:"{{ __('From') }}", field:"fromS", resizable:false, headerSort:true, download:false, responsive:2}, + {title:"{{ __('To') }}", field:"toS", resizable:false, headerSort:true, download:false, responsive:2}, + {title:"{{ __('Amount') }}", field:"amount", resizable:false, headerSort:true, responsive:0, formatter:"money", formatterParams:{ decimal:".", thousand:" ", symbol:currency, @@ -81,8 +89,13 @@ ] }); var searchInput = document.getElementById("search-input"); + let searchTimeout; + searchInput.addEventListener("input", function () { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { updateSearchTerm(searchInput.value); + }, 300); // 300ms delay }); }); diff --git a/cp/routes/web.php b/cp/routes/web.php index fda5102..b25e476 100644 --- a/cp/routes/web.php +++ b/cp/routes/web.php @@ -151,6 +151,8 @@ $app->group('', function ($route) { $route->get('/dapi/domains', [DapiController::class, 'listDomains']); $route->get('/dapi/applications', [DapiController::class, 'listApplications']); + $route->get('/dapi/payments', [DapiController::class, 'listPayments']); + $route->get('/dapi/statements', [DapiController::class, 'listStatements']); })->add(new AuthMiddleware($container)); $app->any('/api[/{params:.*}]', function (