diff --git a/cp/app/Controllers/DapiController.php b/cp/app/Controllers/DapiController.php
index 09802ea..4de3dbc 100644
--- a/cp/app/Controllers/DapiController.php
+++ b/cp/app/Controllers/DapiController.php
@@ -186,4 +186,184 @@ class DapiController extends Controller
$response->getBody()->write(json_encode($payload, JSON_UNESCAPED_UNICODE));
return $response;
}
+
+ public function listApplications(Request $request, Response $response): Response
+ {
+ $params = $request->getQueryParams();
+ $db = $this->container->get('db');
+
+ // Map fields to fully qualified columns
+ $allowedFieldsMap = [
+ 'name' => 'd.name',
+ 'crdate' => 'd.crdate',
+ 'exdate' => 'd.exdate',
+ 'registrant_identifier' => 'c.identifier'
+ ];
+
+ // --- SORTING ---
+ $sortField = 'd.crdate'; // default
+ $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':
+ // If searching in 'name' and user might enter Cyrillic
+ if ($fField === 'name') {
+ // Convert to punycode
+ $punyVal = idn_to_ascii($fVal, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
+ if ($punyVal !== false && $punyVal !== $fVal) {
+ // Search for both punycode and original term
+ // (d.name LIKE '%cyrillic%' OR d.name LIKE '%punycode%')
+ $whereClauses[] = "($column LIKE :f_{$key}_original OR $column LIKE :f_{$key}_puny)";
+ $bindParams["f_{$key}_original"] = "%$fVal%";
+ $bindParams["f_{$key}_puny"] = "%$punyVal%";
+ } else {
+ // Just search normally
+ $whereClauses[] = "$column LIKE :f_{$key}";
+ $bindParams["f_{$key}"] = "%$fVal%";
+ }
+ } else {
+ // Non-name field, just search as usual
+ $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 application d
+ LEFT JOIN contact c ON d.registrant = c.id
+ LEFT JOIN application_status ds ON d.id = ds.domain_id
+ ";
+
+ $sqlWhere = '';
+ if (!empty($whereClauses)) {
+ $sqlWhere = "WHERE " . implode(" OR ", $whereClauses);
+ }
+
+ // Count total results
+ $totalSql = "SELECT COUNT(DISTINCT d.id) AS total $sqlBase $sqlWhere";
+ $totalCount = $db->selectValue($totalSql, $bindParams);
+
+ // Data query
+ $selectFields = "
+ d.id,
+ d.name,
+ d.crdate,
+ d.exdate,
+ d.phase_type,
+ c.identifier AS registrant_identifier,
+ GROUP_CONCAT(ds.status) AS application_status
+ ";
+
+ $dataSql = "
+ SELECT $selectFields
+ $sqlBase
+ $sqlWhere
+ GROUP BY d.id
+ ORDER BY $sortField $sortDir
+ LIMIT $offset, $size
+ ";
+
+ $records = $db->select($dataSql, $bindParams);
+
+ // Ensure records is always an array
+ if (!$records) {
+ $records = [];
+ }
+
+ // Format API results
+ foreach ($records as &$row) {
+ // Check if name is punycode by checking if it starts with 'xn--'
+ if (stripos($row['name'], 'xn--') === 0) {
+ // Convert punycode to Unicode and store it in 'name'
+ $unicode_name = idn_to_utf8($row['name'], IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
+ $row['name_o'] = $row['name']; // Keep the original punycode in 'name_o'
+ $row['name'] = $unicode_name; // Store the Unicode version in 'name'
+ } else {
+ // For regular names, both 'name' and 'name_o' are the same
+ $row['name_o'] = $row['name'];
+ }
+
+ // Format application_status as array of {status: '...'} objects
+ if (!empty($row['application_status'])) {
+ $statuses = explode(',', $row['application_status']);
+ $row['application_status'] = array_map(function($status) {
+ return ['status' => $status];
+ }, $statuses);
+ } else {
+ $row['application_status'] = [];
+ }
+ }
+
+ $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-applications.twig b/cp/resources/views/partials/js-applications.twig
index 9305014..afc0b67 100644
--- a/cp/resources/views/partials/js-applications.twig
+++ b/cp/resources/views/partials/js-applications.twig
@@ -6,13 +6,14 @@