Production readiness hardening and ops tooling
This commit is contained in:
@ -130,27 +130,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-3 bg-secondary-container dark:bg-on-secondary-fixed-variant text-on-secondary-container dark:text-on-secondary-fixed rounded-xl font-bold transition-all scale-98 active:opacity-80" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-3 bg-secondary-container dark:bg-on-secondary-fixed-variant text-on-secondary-container dark:text-on-secondary-fixed rounded-xl font-bold transition-all scale-98 active:opacity-80" href="/ui/admin-reconciliation-management">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance_wallet">account_balance_wallet</span>
|
||||
<span class="font-label-md text-label-md">Reconciliation</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="/ui/admin-dashboard-overview">
|
||||
<span class="material-symbols-outlined" data-icon="security">security</span>
|
||||
<span class="font-label-md text-label-md">Audit Logs</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="/ui/admin-dashboard-overview">
|
||||
<span class="material-symbols-outlined" data-icon="payments">payments</span>
|
||||
<span class="font-label-md text-label-md">Fee Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="/ui/settlement-batch-management">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
<span class="font-label-md text-label-md">Settlements</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="/ui/device-technical-detail">
|
||||
<span class="material-symbols-outlined" data-icon="router">router</span>
|
||||
<span class="font-label-md text-label-md">Device Health</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-secondary dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-xl transition-all scale-98 active:opacity-80" href="/ui/hub">
|
||||
<span class="material-symbols-outlined" data-icon="contact_support">contact_support</span>
|
||||
<span class="font-label-md text-label-md">Support</span>
|
||||
</a>
|
||||
@ -161,7 +161,7 @@
|
||||
<span class="font-label-md text-label-md">Generate Report</span>
|
||||
</button>
|
||||
<div class="h-px bg-slate-200"></div>
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-danger hover:bg-red-50 rounded-xl transition-all" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-3 text-danger hover:bg-red-50 rounded-xl transition-all" href="/ui/admin-login">
|
||||
<span class="material-symbols-outlined" data-icon="logout">logout</span>
|
||||
<span class="font-label-md text-label-md">Logout</span>
|
||||
</a>
|
||||
@ -174,10 +174,10 @@
|
||||
<div class="flex items-center gap-8">
|
||||
<h2 class="font-headline-md text-headline-md font-bold text-primary dark:text-inverse-primary">Reconciliation</h2>
|
||||
<nav class="hidden lg:flex items-center gap-6">
|
||||
<a class="text-primary dark:text-inverse-primary border-b-2 border-primary font-bold pb-1 transition-colors" href="#">Dashboard</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 pb-1 hover:text-primary transition-colors" href="#">Merchants</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 pb-1 hover:text-primary transition-colors" href="#">Operations</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 pb-1 hover:text-primary transition-colors" href="#">Audit</a>
|
||||
<a class="text-primary dark:text-inverse-primary border-b-2 border-primary font-bold pb-1 transition-colors" href="/ui/admin-dashboard-overview">Dashboard</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 pb-1 hover:text-primary transition-colors" href="/ui/merchant-detail-view">Merchants</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 pb-1 hover:text-primary transition-colors" href="/ui/settlement-batch-management">Operations</a>
|
||||
<a class="text-on-surface-variant dark:text-slate-400 pb-1 hover:text-primary transition-colors" href="/ui/admin-reconciliation-management">Audit</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
@ -211,7 +211,7 @@
|
||||
<span class="material-symbols-outlined text-success" data-icon="check_circle">check_circle</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-metric-lg text-metric-lg tabular-nums">42,892</div>
|
||||
<div id="recon-total-matched" class="font-metric-lg text-metric-lg tabular-nums">-</div>
|
||||
<div class="mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-success text-[16px]" data-icon="trending_up">trending_up</span>
|
||||
<span class="font-metric-sm text-metric-sm text-success">+12.4%</span>
|
||||
@ -225,7 +225,7 @@
|
||||
<span class="material-symbols-outlined text-danger" data-icon="error_outline">error_outline</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-metric-lg text-metric-lg tabular-nums">148</div>
|
||||
<div id="recon-discrepancies" class="font-metric-lg text-metric-lg tabular-nums">-</div>
|
||||
<div class="mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-danger text-[16px]" data-icon="trending_down">trending_down</span>
|
||||
<span class="font-metric-sm text-metric-sm text-danger">-2.1%</span>
|
||||
@ -239,7 +239,7 @@
|
||||
<span class="material-symbols-outlined text-warning" data-icon="hourglass_empty">hourglass_empty</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-metric-lg text-metric-lg tabular-nums">1,024</div>
|
||||
<div id="recon-total-batches" class="font-metric-lg text-metric-lg tabular-nums">-</div>
|
||||
<div class="mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-slate-400 text-[16px]" data-icon="history">history</span>
|
||||
<span class="font-metric-sm text-metric-sm text-slate-600">Avg 4h processing</span>
|
||||
@ -252,7 +252,7 @@
|
||||
<span class="material-symbols-outlined text-info" data-icon="account_balance">account_balance</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-metric-lg text-metric-lg tabular-nums">842</div>
|
||||
<div id="recon-issue-count" class="font-metric-lg text-metric-lg tabular-nums">-</div>
|
||||
<div class="mt-2 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-success text-[16px]" data-icon="cloud_done">cloud_done</span>
|
||||
<span class="font-metric-sm text-metric-sm text-slate-600">12 API Connections</span>
|
||||
@ -294,111 +294,24 @@
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 border-b border-slate-200 sticky top-0 z-10">
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Transaction Details</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider bg-primary/5">System Record (Internal)</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider bg-info/5">Bank Record (External)</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Variance</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Batch Details</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider bg-primary/5">Batch Aggregate</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider bg-info/5">Ledger Computed</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Issues</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-slate-500 uppercase tracking-wider text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<!-- Row 1: Matched -->
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-bold text-on-surface">TXN-90283471</p>
|
||||
<p class="text-[12px] text-slate-400">Oct 24, 2023 • 14:22:10</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-primary/5 tabular-nums font-medium">₹ 14,500.00</td>
|
||||
<td class="px-6 py-4 bg-info/5 tabular-nums font-medium">₹ 14,500.00</td>
|
||||
<td class="px-6 py-4 tabular-nums text-slate-400">0.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-success/10 text-success text-[12px] font-bold rounded-full">MATCHED</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 hover:bg-slate-100 rounded-lg text-slate-400">
|
||||
<span class="material-symbols-outlined" data-icon="more_vert">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2: Discrepancy -->
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-bold text-on-surface">TXN-88273412</p>
|
||||
<p class="text-[12px] text-slate-400">Oct 24, 2023 • 11:05:45</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-primary/5 tabular-nums font-medium">₹ 8,240.50</td>
|
||||
<td class="px-6 py-4 bg-info/5 tabular-nums font-medium">₹ 8,245.50</td>
|
||||
<td class="px-6 py-4 tabular-nums text-danger font-bold">- 5.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-danger/10 text-danger text-[12px] font-bold rounded-full">EXCEPTION</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="px-3 py-1 bg-primary text-white text-[12px] font-bold rounded hover:bg-primary/90 transition-colors">
|
||||
Resolve
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3: Pending -->
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-bold text-on-surface">TXN-90112456</p>
|
||||
<p class="text-[12px] text-slate-400">Oct 23, 2023 • 23:18:02</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-primary/5 tabular-nums font-medium">₹ 1,20,000.00</td>
|
||||
<td class="px-6 py-4 bg-info/5 tabular-nums font-medium italic text-slate-400">Not Found</td>
|
||||
<td class="px-6 py-4 tabular-nums text-slate-400">Pending</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-warning/10 text-warning text-[12px] font-bold rounded-full">PENDING</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 hover:bg-slate-100 rounded-lg text-slate-400">
|
||||
<span class="material-symbols-outlined" data-icon="refresh">refresh</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4: Matched -->
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-bold text-on-surface">TXN-90283472</p>
|
||||
<p class="text-[12px] text-slate-400">Oct 23, 2023 • 18:45:30</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-primary/5 tabular-nums font-medium">₹ 450.00</td>
|
||||
<td class="px-6 py-4 bg-info/5 tabular-nums font-medium">₹ 450.00</td>
|
||||
<td class="px-6 py-4 tabular-nums text-slate-400">0.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-success/10 text-success text-[12px] font-bold rounded-full">MATCHED</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="p-2 hover:bg-slate-100 rounded-lg text-slate-400">
|
||||
<span class="material-symbols-outlined" data-icon="more_vert">more_vert</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 5: Exception (Fee Miscalc) -->
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-bold text-on-surface">TXN-90283478</p>
|
||||
<p class="text-[12px] text-slate-400">Oct 23, 2023 • 16:12:11</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-primary/5 tabular-nums font-medium">₹ 22,000.00</td>
|
||||
<td class="px-6 py-4 bg-info/5 tabular-nums font-medium">₹ 21,560.00</td>
|
||||
<td class="px-6 py-4 tabular-nums text-danger font-bold">- 440.00</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 bg-danger/10 text-danger text-[12px] font-bold rounded-full">EXCEPTION</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<button class="px-3 py-1 bg-primary text-white text-[12px] font-bold rounded hover:bg-primary/90 transition-colors">
|
||||
Resolve
|
||||
</button>
|
||||
</td>
|
||||
<tbody id="recon-mismatch-rows" class="divide-y divide-slate-100">
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-8 text-center text-slate-500">Loading settlement reconciliation...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Table Pagination/Footer -->
|
||||
<div class="px-6 py-4 bg-slate-50 border-t border-slate-200 flex items-center justify-between">
|
||||
<p class="text-label-md text-slate-500">Showing 1 to 5 of 42,892 entries</p>
|
||||
<p id="recon-footer" class="text-label-md text-slate-500">Loading reconciliation report...</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-slate-200 bg-white text-slate-400 hover:bg-slate-50 disabled:opacity-50" disabled="">
|
||||
<span class="material-symbols-outlined text-[18px]" data-icon="chevron_left">chevron_left</span>
|
||||
@ -427,7 +340,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="font-mono text-[13px] text-success leading-relaxed h-[200px] overflow-y-auto hide-scrollbar">
|
||||
<pre>{
|
||||
<pre id="recon-raw-payload">{
|
||||
"reconciliation_id": "RECON-00492-AX",
|
||||
"timestamp": "2023-10-24T14:22:10.452Z",
|
||||
"system_ledger": {
|
||||
@ -450,27 +363,47 @@
|
||||
</div>
|
||||
<!-- Verification Timeline -->
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-6">
|
||||
<h3 class="font-headline-md text-[16px] mb-6">Recent Resolution Activity</h3>
|
||||
<div class="space-y-6">
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-4 h-4 rounded-full bg-success ring-4 ring-success/10 z-10"></div>
|
||||
<div class="absolute left-1.5 top-5 w-[2px] h-full bg-slate-100"></div>
|
||||
<p class="font-bold text-on-surface leading-none mb-1">TXN-88273412 Resolved</p>
|
||||
<p class="text-[12px] text-slate-500 mb-1">Manual match confirmed by Admin A12</p>
|
||||
<p class="text-[11px] text-slate-400">12 minutes ago</p>
|
||||
<div class="flex items-start justify-between gap-3 mb-6">
|
||||
<div>
|
||||
<h3 class="font-headline-md text-[16px]">Recent Adjustment Activity</h3>
|
||||
<p id="recon-adjustment-summary" class="text-[12px] text-slate-500 mt-1">Loading finance adjustment report...</p>
|
||||
</div>
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-4 h-4 rounded-full bg-primary ring-4 ring-primary/10 z-10"></div>
|
||||
<div class="absolute left-1.5 top-5 w-[2px] h-full bg-slate-100"></div>
|
||||
<p class="font-bold text-on-surface leading-none mb-1">Report Exported</p>
|
||||
<p class="text-[12px] text-slate-500 mb-1">Full Oct report generated (PDF)</p>
|
||||
<p class="text-[11px] text-slate-400">45 minutes ago</p>
|
||||
<button id="download-adjustment-report" data-admin-permission="settlement:export" class="w-9 h-9 flex items-center justify-center rounded-lg border border-slate-200 text-warning hover:bg-warning/10 transition-colors" title="Download adjustment CSV">
|
||||
<span class="material-symbols-outlined" data-icon="download">download</span>
|
||||
</button>
|
||||
</div>
|
||||
<p id="adjustment-export-status" class="hidden text-[11px] text-slate-500 mb-4"></p>
|
||||
<div id="adjustment-export-history" class="hidden mb-6 border border-slate-100 rounded-lg divide-y divide-slate-100 overflow-hidden"></div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-2 mb-6">
|
||||
<div>
|
||||
<input id="adjustment-merchant-filter" class="w-full border border-slate-200 rounded-lg px-3 py-2 text-[12px] focus:ring-primary focus:border-primary" list="adjustment-merchant-options" placeholder="Search merchant" type="text"/>
|
||||
<datalist id="adjustment-merchant-options"></datalist>
|
||||
<p id="adjustment-merchant-hint" class="mt-1 text-[11px] text-slate-400">Loading merchants...</p>
|
||||
</div>
|
||||
<select id="adjustment-type-filter" class="border border-slate-200 rounded-lg px-3 py-2 text-[12px] focus:ring-primary focus:border-primary bg-white">
|
||||
<option value="">All types</option>
|
||||
<option value="credit">Credit</option>
|
||||
<option value="debit">Debit</option>
|
||||
</select>
|
||||
<select id="adjustment-approval-filter" class="border border-slate-200 rounded-lg px-3 py-2 text-[12px] focus:ring-primary focus:border-primary bg-white">
|
||||
<option value="">All approval</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
<input id="adjustment-from-filter" class="border border-slate-200 rounded-lg px-3 py-2 text-[12px] focus:ring-primary focus:border-primary" type="date"/>
|
||||
<input id="adjustment-to-filter" class="border border-slate-200 rounded-lg px-3 py-2 text-[12px] focus:ring-primary focus:border-primary" type="date"/>
|
||||
</div>
|
||||
<div class="flex gap-2 mb-6">
|
||||
<button id="apply-adjustment-filter" class="flex-1 px-3 py-2 bg-primary text-white rounded-lg text-[12px] font-bold hover:bg-primary/90 transition-colors">Apply Filter</button>
|
||||
<button id="clear-adjustment-filter" class="px-3 py-2 border border-slate-200 rounded-lg text-[12px] font-bold hover:bg-slate-50 transition-colors">Clear</button>
|
||||
</div>
|
||||
<div id="recon-adjustment-activity" class="space-y-6">
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-4 h-4 rounded-full bg-slate-300 z-10"></div>
|
||||
<p class="font-bold text-on-surface leading-none mb-1">HSBC Sync Completed</p>
|
||||
<p class="text-[12px] text-slate-500 mb-1">2,400 statements fetched via API</p>
|
||||
<p class="text-[11px] text-slate-400">1 hour ago</p>
|
||||
<p class="font-bold text-on-surface leading-none mb-1">Loading adjustments</p>
|
||||
<p class="text-[12px] text-slate-500 mb-1">Reading settlement adjustment ledger</p>
|
||||
<p class="text-[11px] text-slate-400">Please wait</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -523,7 +456,425 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/ui/shared/admin-api.js"></script>
|
||||
<script>
|
||||
const api = window.AdminUIAPI;
|
||||
const numberFormatter = new Intl.NumberFormat('id-ID');
|
||||
let currentAdjustmentQuery = { limit: 5 };
|
||||
let merchants = [];
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[char]));
|
||||
}
|
||||
|
||||
function formatMoney(value) {
|
||||
return api.formatMoney(value);
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
return api.formatDateTime(value);
|
||||
}
|
||||
|
||||
function setText(id, value) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setExportStatus(message, tone = 'muted') {
|
||||
const el = document.getElementById('adjustment-export-status');
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
el.textContent = message || '';
|
||||
el.classList.toggle('hidden', !message);
|
||||
el.classList.toggle('text-danger', tone === 'danger');
|
||||
el.classList.toggle('text-success', tone === 'success');
|
||||
el.classList.toggle('text-primary', tone === 'active');
|
||||
el.classList.toggle('text-slate-500', tone === 'muted');
|
||||
}
|
||||
|
||||
function dateToIsoStart(value) {
|
||||
return value ? `${value}T00:00:00.000Z` : undefined;
|
||||
}
|
||||
|
||||
function dateToIsoEnd(value) {
|
||||
return value ? `${value}T23:59:59.999Z` : undefined;
|
||||
}
|
||||
|
||||
function buildAdjustmentQuery(limit = 5) {
|
||||
const merchantInput = document.getElementById('adjustment-merchant-filter')?.value.trim();
|
||||
const matchedMerchant = merchants.find((merchant) =>
|
||||
merchantInput &&
|
||||
[merchant.id, merchant.merchant_code, merchant.brand_name, merchant.legal_name]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase() === merchantInput.toLowerCase())
|
||||
);
|
||||
const merchantId = matchedMerchant?.id || merchantInput;
|
||||
const adjustmentType = document.getElementById('adjustment-type-filter')?.value;
|
||||
const approvalStatus = document.getElementById('adjustment-approval-filter')?.value;
|
||||
const from = document.getElementById('adjustment-from-filter')?.value;
|
||||
const to = document.getElementById('adjustment-to-filter')?.value;
|
||||
return {
|
||||
limit,
|
||||
merchant_id: merchantId || undefined,
|
||||
adjustment_type: adjustmentType || undefined,
|
||||
approval_status: approvalStatus || undefined,
|
||||
from: dateToIsoStart(from),
|
||||
to: dateToIsoEnd(to)
|
||||
};
|
||||
}
|
||||
|
||||
function merchantLabel(merchant) {
|
||||
return merchant?.brand_name || merchant?.legal_name || merchant?.merchant_code || merchant?.id || 'Merchant';
|
||||
}
|
||||
|
||||
function exportStatusClass(status) {
|
||||
if (status === 'completed') return 'bg-success/10 text-success';
|
||||
if (status === 'failed') return 'bg-danger/10 text-danger';
|
||||
if (status === 'running') return 'bg-primary/10 text-primary';
|
||||
return 'bg-slate-100 text-slate-500';
|
||||
}
|
||||
|
||||
function renderExportHistory(jobs) {
|
||||
const container = document.getElementById('adjustment-export-history');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const rows = jobs || [];
|
||||
container.classList.toggle('hidden', rows.length === 0);
|
||||
container.innerHTML = rows.map((job) => `
|
||||
<div class="flex items-center justify-between gap-3 px-3 py-2 bg-white">
|
||||
<div class="min-w-0">
|
||||
<p class="text-[12px] font-bold text-on-surface truncate">${escapeHtml(job.result_filename || job.id)}</p>
|
||||
<p class="text-[11px] text-slate-400">${formatDateTime(job.created_at)} • ${job.result_size_bytes ? numberFormatter.format(job.result_size_bytes) + ' bytes' : 'waiting'}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<span class="px-2 py-1 rounded text-[10px] font-bold uppercase ${exportStatusClass(job.status)}">${escapeHtml(job.status)}</span>
|
||||
${job.download_url ? `<button data-export-download-id="${escapeHtml(job.id)}" class="w-8 h-8 flex items-center justify-center rounded border border-slate-200 text-primary hover:bg-primary/5" title="Download export"><span class="material-symbols-outlined text-[18px]">download</span></button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function loadExportHistory() {
|
||||
try {
|
||||
const jobs = await api.listExportJobs({ job_type: 'settlement_adjustments_csv', limit: 5 });
|
||||
renderExportHistory(jobs);
|
||||
} catch (_error) {
|
||||
renderExportHistory([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadExportById(jobId) {
|
||||
const { blob, filename } = await api.downloadExportJob(jobId);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename || 'settlement-adjustment-report.csv';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function renderMerchantOptions(items) {
|
||||
const options = document.getElementById('adjustment-merchant-options');
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
options.innerHTML = (items || [])
|
||||
.map((merchant) => `<option value="${escapeHtml(merchant.id)}" label="${escapeHtml(merchantLabel(merchant))}"></option>`)
|
||||
.join('');
|
||||
setText('adjustment-merchant-hint', `${numberFormatter.format((items || []).length)} merchants available`);
|
||||
}
|
||||
|
||||
async function loadMerchantOptions() {
|
||||
try {
|
||||
merchants = await api.listMerchants();
|
||||
renderMerchantOptions(merchants);
|
||||
} catch (error) {
|
||||
setText('adjustment-merchant-hint', 'Merchant list unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
function renderIssueSummary(row) {
|
||||
if (!row.issues?.length) {
|
||||
if (row.computed?.archived_reprocessed) {
|
||||
return '<span class="text-slate-500">Archived after reprocess</span>';
|
||||
}
|
||||
return '<span class="text-slate-400">No variance</span>';
|
||||
}
|
||||
return row.issues
|
||||
.slice(0, 2)
|
||||
.map((issue) => `<p class="text-[12px] text-danger font-semibold">${escapeHtml(issue.message)}</p>`)
|
||||
.join('') + (row.issues.length > 2 ? `<p class="text-[12px] text-slate-500">+${row.issues.length - 2} more issue(s)</p>` : '');
|
||||
}
|
||||
|
||||
function renderRows(rows) {
|
||||
const tbody = document.getElementById('recon-mismatch-rows');
|
||||
if (!tbody) {
|
||||
return;
|
||||
}
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-8 text-center text-slate-500">No settlement batches available for reconciliation.</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = rows.map((row) => {
|
||||
const batch = row.batch || {};
|
||||
const computed = row.computed || {};
|
||||
const isMismatch = row.status === 'mismatch';
|
||||
const badgeClass = isMismatch ? 'bg-danger/10 text-danger' : 'bg-success/10 text-success';
|
||||
const variance = Number(batch.net_payable_amount || 0) - Number(computed.net_payable_amount || 0);
|
||||
return `
|
||||
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<p class="font-bold text-on-surface">${escapeHtml(batch.batch_code || batch.id)}</p>
|
||||
<p class="text-[12px] text-slate-400">${escapeHtml(batch.merchant_id || '-')} • ${formatDateTime(batch.created_at)}</p>
|
||||
<p class="text-[12px] text-slate-500">${numberFormatter.format(Number(batch.entry_count || 0))} entry • ${escapeHtml(batch.status || '-')}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-primary/5 tabular-nums font-medium">
|
||||
<p>${formatMoney(batch.net_payable_amount)}</p>
|
||||
<p class="text-[12px] text-slate-500">gross ${formatMoney(batch.gross_amount)} / fee ${formatMoney(batch.platform_fee_amount)}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 bg-info/5 tabular-nums font-medium">
|
||||
<p>${formatMoney(computed.net_payable_amount)}</p>
|
||||
<p class="text-[12px] text-slate-500">gross ${formatMoney(computed.gross_amount)} / fee ${formatMoney(computed.platform_fee_amount)}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<p class="tabular-nums ${isMismatch ? 'text-danger font-bold' : 'text-slate-400'}">${formatMoney(variance)}</p>
|
||||
<div class="mt-1 space-y-1">${renderIssueSummary(row)}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-3 py-1 ${badgeClass} text-[12px] font-bold rounded-full">${isMismatch ? 'MISMATCH' : 'MATCHED'}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<a href="/ui/settlement-batch-management" class="inline-flex px-3 py-1 bg-primary text-white text-[12px] font-bold rounded hover:bg-primary/90 transition-colors">Open</a>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderAdjustmentActivity(report) {
|
||||
const container = document.getElementById('recon-adjustment-activity');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const rows = report?.rows || [];
|
||||
setText(
|
||||
'recon-adjustment-summary',
|
||||
`${numberFormatter.format(Number(report?.total_count || 0))} adjustment • net ${formatMoney(report?.signed_amount || 0)}`
|
||||
);
|
||||
if (!rows.length) {
|
||||
container.innerHTML = `
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-4 h-4 rounded-full bg-slate-300 z-10"></div>
|
||||
<p class="font-bold text-on-surface leading-none mb-1">No adjustment yet</p>
|
||||
<p class="text-[12px] text-slate-500 mb-1">Settlement corrections will appear here after admin records them.</p>
|
||||
<p class="text-[11px] text-slate-400">-</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = rows.map((item, index) => {
|
||||
const isCredit = item.adjustment_type === 'credit';
|
||||
const approvalStatus = item.approval_status || 'approved';
|
||||
const dotClass = approvalStatus === 'pending'
|
||||
? 'bg-primary ring-primary/10'
|
||||
: approvalStatus === 'rejected'
|
||||
? 'bg-danger ring-danger/10'
|
||||
: isCredit ? 'bg-success ring-success/10' : 'bg-warning ring-warning/10';
|
||||
const actionButtons = approvalStatus === 'pending'
|
||||
? `
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button data-admin-permission="settlement:adjust" data-adjustment-action="approve" data-adjustment-id="${escapeHtml(item.id)}" class="px-3 py-1.5 bg-success text-white text-[12px] font-bold rounded hover:bg-success/90 transition-colors">Approve</button>
|
||||
<button data-admin-permission="settlement:adjust" data-adjustment-action="reject" data-adjustment-id="${escapeHtml(item.id)}" class="px-3 py-1.5 border border-danger/20 text-danger text-[12px] font-bold rounded hover:bg-danger/5 transition-colors">Reject</button>
|
||||
</div>
|
||||
`
|
||||
: '';
|
||||
return `
|
||||
<div class="relative pl-8">
|
||||
<div class="absolute left-0 top-1 w-4 h-4 rounded-full ${dotClass} ring-4 z-10"></div>
|
||||
${index < rows.length - 1 ? '<div class="absolute left-1.5 top-5 w-[2px] h-full bg-slate-100"></div>' : ''}
|
||||
<p class="font-bold text-on-surface leading-none mb-1">${escapeHtml(item.batch_code || item.batch_id)}</p>
|
||||
<p class="text-[12px] text-slate-500 mb-1">${escapeHtml(item.adjustment_type || '-').toUpperCase()} ${formatMoney(item.signed_amount)} • ${escapeHtml(item.reason || '-')}</p>
|
||||
<p class="text-[11px] font-bold uppercase tracking-wide ${approvalStatus === 'approved' ? 'text-success' : approvalStatus === 'rejected' ? 'text-danger' : 'text-primary'}">${escapeHtml(approvalStatus)}</p>
|
||||
<p class="text-[11px] text-slate-400">${formatDateTime(item.created_at)}</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
api.applyPermissions?.();
|
||||
}
|
||||
|
||||
async function handleAdjustmentApproval(event) {
|
||||
const button = event.target.closest('[data-adjustment-action]');
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
const action = button.dataset.adjustmentAction;
|
||||
const adjustmentId = button.dataset.adjustmentId;
|
||||
if (!adjustmentId || !['approve', 'reject'].includes(action)) {
|
||||
return;
|
||||
}
|
||||
const note = window.prompt(action === 'approve' ? 'Approval note (optional)' : 'Rejection note (optional)') || '';
|
||||
button.disabled = true;
|
||||
button.classList.add('opacity-50');
|
||||
try {
|
||||
if (action === 'approve') {
|
||||
await api.approveSettlementAdjustment(adjustmentId, { note });
|
||||
} else {
|
||||
await api.rejectSettlementAdjustment(adjustmentId, { note });
|
||||
}
|
||||
await loadAdjustmentActivity();
|
||||
} catch (error) {
|
||||
alert(error.message || 'Failed to update adjustment approval');
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.classList.remove('opacity-50');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAdjustmentActivity() {
|
||||
currentAdjustmentQuery = buildAdjustmentQuery(5);
|
||||
const adjustmentReport = await api.listSettlementAdjustments(currentAdjustmentQuery);
|
||||
renderAdjustmentActivity(adjustmentReport);
|
||||
}
|
||||
|
||||
async function loadReconciliation() {
|
||||
try {
|
||||
api.requireToken();
|
||||
currentAdjustmentQuery = buildAdjustmentQuery(5);
|
||||
const [report, adjustmentReport] = await Promise.all([
|
||||
api.getSettlementReconciliationReport({ limit: 100 }),
|
||||
api.listSettlementAdjustments(currentAdjustmentQuery)
|
||||
]);
|
||||
setText('recon-total-matched', numberFormatter.format(Number(report.matched_batches || 0)));
|
||||
setText('recon-discrepancies', numberFormatter.format(Number(report.mismatch_batches || 0)));
|
||||
setText('recon-total-batches', numberFormatter.format(Number(report.total_batches || 0)));
|
||||
setText('recon-issue-count', numberFormatter.format(Number(report.issue_count || 0)));
|
||||
setText('recon-footer', `Showing ${numberFormatter.format((report.rows || []).length)} of ${numberFormatter.format(Number(report.total_batches || 0))} settlement batches`);
|
||||
const raw = document.getElementById('recon-raw-payload');
|
||||
if (raw) {
|
||||
raw.textContent = JSON.stringify(report, null, 2);
|
||||
}
|
||||
renderRows(report.rows || []);
|
||||
renderAdjustmentActivity(adjustmentReport);
|
||||
} catch (error) {
|
||||
const tbody = document.getElementById('recon-mismatch-rows');
|
||||
if (tbody) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-8 text-center text-danger">${escapeHtml(error.message || 'Failed to load reconciliation report')}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
setText('recon-footer', 'Failed to load reconciliation report');
|
||||
setText('recon-adjustment-summary', 'Failed to load finance adjustment report');
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadAdjustmentReport() {
|
||||
const button = document.getElementById('download-adjustment-report');
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
button.classList.add('opacity-50');
|
||||
}
|
||||
try {
|
||||
const exportQuery = { ...currentAdjustmentQuery, limit: 5000 };
|
||||
setExportStatus('Creating export job...', 'active');
|
||||
let job = await api.createSettlementAdjustmentExportJob(exportQuery);
|
||||
setExportStatus(`Export ${job.status}: ${job.id}`, 'active');
|
||||
|
||||
for (let attempt = 0; attempt < 30 && !['completed', 'failed'].includes(job.status); attempt += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
job = await api.getExportJob(job.id);
|
||||
setExportStatus(`Export ${job.status}: ${job.id}`, job.status === 'failed' ? 'danger' : 'active');
|
||||
}
|
||||
|
||||
if (job.status !== 'completed') {
|
||||
throw new Error(job.error_message || `Export job ${job.status}`);
|
||||
}
|
||||
|
||||
const { blob, filename } = await api.downloadExportJob(job.id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename || 'settlement-adjustment-report.csv';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
setExportStatus(`Export completed: ${filename || job.id}`, 'success');
|
||||
await loadExportHistory();
|
||||
} catch (error) {
|
||||
setExportStatus(error.message || 'Export failed', 'danger');
|
||||
alert(error.message || 'Export failed');
|
||||
} finally {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.classList.remove('opacity-50');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExportHistoryClick(event) {
|
||||
const button = event.target.closest('[data-export-download-id]');
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
button.disabled = true;
|
||||
button.classList.add('opacity-50');
|
||||
try {
|
||||
await downloadExportById(button.dataset.exportDownloadId);
|
||||
} catch (error) {
|
||||
alert(error.message || 'Download failed');
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.classList.remove('opacity-50');
|
||||
}
|
||||
}
|
||||
|
||||
async function applyAdjustmentFilter() {
|
||||
const button = document.getElementById('apply-adjustment-filter');
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
button.textContent = 'Loading...';
|
||||
}
|
||||
try {
|
||||
await loadAdjustmentActivity();
|
||||
} finally {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.textContent = 'Apply Filter';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAdjustmentFilter() {
|
||||
['adjustment-merchant-filter', 'adjustment-type-filter', 'adjustment-approval-filter', 'adjustment-from-filter', 'adjustment-to-filter'].forEach((id) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.value = '';
|
||||
}
|
||||
});
|
||||
await applyAdjustmentFilter();
|
||||
}
|
||||
|
||||
// Micro-interactions
|
||||
function toggleDrawer() {
|
||||
const drawer = document.getElementById('detailDrawer');
|
||||
@ -552,6 +903,15 @@
|
||||
card.style.transform = 'translateY(0)';
|
||||
}, 50);
|
||||
});
|
||||
api.applyPermissions?.();
|
||||
loadReconciliation();
|
||||
loadMerchantOptions();
|
||||
loadExportHistory();
|
||||
document.getElementById('download-adjustment-report')?.addEventListener('click', downloadAdjustmentReport);
|
||||
document.getElementById('apply-adjustment-filter')?.addEventListener('click', applyAdjustmentFilter);
|
||||
document.getElementById('clear-adjustment-filter')?.addEventListener('click', clearAdjustmentFilter);
|
||||
document.getElementById('recon-adjustment-activity')?.addEventListener('click', handleAdjustmentApproval);
|
||||
document.getElementById('adjustment-export-history')?.addEventListener('click', handleExportHistoryClick);
|
||||
});
|
||||
</script>
|
||||
<!-- ui-nav -->
|
||||
@ -563,4 +923,4 @@
|
||||
<a href="/ui/admin-dashboard-overview" style="margin-right:0;color:#2563eb;text-decoration:none;font-weight:600">Dashboard</a>
|
||||
</div>
|
||||
'
|
||||
</body></html>
|
||||
</body></html>
|
||||
|
||||
Reference in New Issue
Block a user