More work on issue 192

This commit is contained in:
Pinga 2024-12-06 15:17:32 +02:00
parent 79eca91eb1
commit ea88b14234
4 changed files with 349 additions and 36 deletions

View file

@ -366,4 +366,290 @@ class DapiController extends Controller
$response->getBody()->write(json_encode($payload, JSON_UNESCAPED_UNICODE)); $response->getBody()->write(json_encode($payload, JSON_UNESCAPED_UNICODE));
return $response; 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;
}
} }

View file

@ -8,6 +8,7 @@
document.addEventListener("DOMContentLoaded", function(){ document.addEventListener("DOMContentLoaded", function(){
currency = "{{ currency }} "; currency = "{{ currency }} ";
var searchTerm = ""; // global variable to hold the search term var searchTerm = ""; // global variable to hold the search term
function updateSearchTerm(term) { function updateSearchTerm(term) {
@ -19,20 +20,29 @@
pagination: true, pagination: true,
paginationMode: "remote", paginationMode: "remote",
paginationSize: 10, paginationSize: 10,
ajaxURL: "/api/records/payment_history", sortMode: "remote",
ajaxParams: { ajaxURL: "/dapi/payments",
join: "registrar"
},
ajaxURLGenerator: function(url, config, params) { ajaxURLGenerator: function(url, config, params) {
var queryParts = ["join=registrar"]; var queryParts = [];
// Handle search term // Handle search term
if (searchTerm) { if (searchTerm) {
queryParts.push("filter1=date,cs," + encodeURIComponent(searchTerm)); queryParts.push("filter1=registrar_name,cs," + encodeURIComponent(searchTerm));
queryParts.push("filter2=description,cs," + encodeURIComponent(searchTerm)); queryParts.push("filter2=date,cs," + encodeURIComponent(searchTerm));
queryParts.push("filter3=description,cs," + encodeURIComponent(searchTerm));
queryParts.push("filter4=amount,cs," + encodeURIComponent(searchTerm));
} }
// 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"); queryParts.push("order=date,desc");
}
// Include pagination parameters // Include pagination parameters
if (params.page) { if (params.page) {
@ -53,9 +63,6 @@
return { last_page: 1, data: [] }; return { last_page: 1, data: [] };
} }
}, },
dataReceiveParams: {
"last_page": "results", // Mapping 'results' to 'last_page'
},
layout:"fitDataFill", layout:"fitDataFill",
responsiveLayout: "collapse", responsiveLayout: "collapse",
responsiveLayoutCollapseStartOpen:false, responsiveLayoutCollapseStartOpen:false,
@ -63,10 +70,10 @@
placeholder: "{{ __('No Data') }}", placeholder: "{{ __('No Data') }}",
columns:[ columns:[
{formatter:"responsiveCollapse", width:30, minWidth:30, hozAlign:"center", resizable:false, headerSort:false, responsive:0}, {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:"{{ __('Registrar') }}", field:"registrar_name", resizable:false, headerSort:true, responsive:0},
{title:"{{ __('Date') }}", field:"date", resizable:false, headerSort:false, responsive:2}, {title:"{{ __('Date') }}", field:"date", resizable:false, headerSort:true, responsive:2},
{title:"{{ __('Description') }}", field:"description", resizable:false, headerSort:false, 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:false, download:false, responsive:0, formatter:"money", formatterParams:{ {title:"{{ __('Amount') }}", field:"amount", resizable:false, headerSort:true, width:200, minWidth:100, responsive:0, formatter:"money", formatterParams:{
decimal:".", decimal:".",
thousand:" ", thousand:" ",
symbol:currency, symbol:currency,
@ -75,8 +82,13 @@
] ]
}); });
var searchInput = document.getElementById("search-input"); var searchInput = document.getElementById("search-input");
let searchTimeout;
searchInput.addEventListener("input", function () { searchInput.addEventListener("input", function () {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
updateSearchTerm(searchInput.value); updateSearchTerm(searchInput.value);
}, 300); // 300ms delay
}); });
}); });

View file

@ -19,22 +19,33 @@
pagination: true, pagination: true,
paginationMode: "remote", paginationMode: "remote",
paginationSize: 10, paginationSize: 10,
ajaxURL: "/api/records/statement", sortMode: "remote",
ajaxParams: { ajaxURL: "/dapi/statements",
join: "registrar"
},
ajaxURLGenerator: function(url, config, params) { ajaxURLGenerator: function(url, config, params) {
var queryParts = ["join=registrar"]; var queryParts = [];
// Handle search term // Handle search term
if (searchTerm) { if (searchTerm) {
queryParts.push("filter1=command,cs," + encodeURIComponent(searchTerm)); queryParts.push("filter1=registrar_name,cs," + encodeURIComponent(searchTerm));
queryParts.push("filter2=domain_name,cs," + encodeURIComponent(searchTerm)); queryParts.push("filter2=date,cs," + encodeURIComponent(searchTerm));
queryParts.push("filter3=fromS,cs," + encodeURIComponent(searchTerm)); queryParts.push("filter3=command,cs," + encodeURIComponent(searchTerm));
queryParts.push("filter4=toS,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));
} }
// 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"); queryParts.push("order=date,desc");
}
// Include pagination parameters // Include pagination parameters
if (params.page) { if (params.page) {
@ -55,9 +66,6 @@
return { last_page: 1, data: [] }; return { last_page: 1, data: [] };
} }
}, },
dataReceiveParams: {
"last_page": "results", // Mapping 'results' to 'last_page'
},
layout:"fitDataFill", layout:"fitDataFill",
responsiveLayout: "collapse", responsiveLayout: "collapse",
responsiveLayoutCollapseStartOpen:false, responsiveLayoutCollapseStartOpen:false,
@ -65,14 +73,14 @@
placeholder: "{{ __('No Data') }}", placeholder: "{{ __('No Data') }}",
columns:[ columns:[
{formatter:"responsiveCollapse", width:30, minWidth:30, hozAlign:"center", resizable:false, headerSort:false, responsive:0}, {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:"{{ __('Registrar') }}", field:"registrar_name", resizable:false, headerSort:true, responsive:0},
{title:"{{ __('Date') }}", field:"date", resizable:false, headerSort:false, responsive:2}, {title:"{{ __('Date') }}", field:"date", resizable:false, headerSort:true, responsive:2},
{title:"{{ __('Command') }}", field:"command", resizable:false, headerSort:false, responsive:2}, {title:"{{ __('Command') }}", field:"command", resizable:false, headerSort:true, responsive:2},
{title:"{{ __('Domain') }}", field:"domain_name", resizable:false, headerSort:false, download:false, responsive:0}, {title:"{{ __('Domain') }}", field:"domain_name", resizable:false, headerSort:true, download:false, responsive:0},
{title:"{{ __('Length') }}", field:"length_in_months", resizable:false, headerSort:false, responsive:2}, {title:"{{ __('Length') }}", field:"length_in_months", resizable:false, download:false, headerSort:true, responsive:2},
{title:"{{ __('From') }}", field:"fromS", resizable:false, headerSort:false, responsive:2}, {title:"{{ __('From') }}", field:"fromS", resizable:false, headerSort:true, download:false, responsive:2},
{title:"{{ __('To') }}", field:"toS", resizable:false, headerSort:false, responsive:2}, {title:"{{ __('To') }}", field:"toS", resizable:false, headerSort:true, download:false, responsive:2},
{title:"{{ __('Amount') }}", field:"amount", resizable:false, headerSort:false, download:false, responsive:2, formatter:"money", formatterParams:{ {title:"{{ __('Amount') }}", field:"amount", resizable:false, headerSort:true, responsive:0, formatter:"money", formatterParams:{
decimal:".", decimal:".",
thousand:" ", thousand:" ",
symbol:currency, symbol:currency,
@ -81,8 +89,13 @@
] ]
}); });
var searchInput = document.getElementById("search-input"); var searchInput = document.getElementById("search-input");
let searchTimeout;
searchInput.addEventListener("input", function () { searchInput.addEventListener("input", function () {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
updateSearchTerm(searchInput.value); updateSearchTerm(searchInput.value);
}, 300); // 300ms delay
}); });
}); });

View file

@ -151,6 +151,8 @@ $app->group('', function ($route) {
$route->get('/dapi/domains', [DapiController::class, 'listDomains']); $route->get('/dapi/domains', [DapiController::class, 'listDomains']);
$route->get('/dapi/applications', [DapiController::class, 'listApplications']); $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)); })->add(new AuthMiddleware($container));
$app->any('/api[/{params:.*}]', function ( $app->any('/api[/{params:.*}]', function (