Production readiness hardening and ops tooling
This commit is contained in:
@ -123,38 +123,38 @@
|
||||
<p class="font-label-md text-label-md text-slate-500 uppercase tracking-wider">Admin Console</p>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="/ui/admin-dashboard-overview">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
||||
<span>Overview</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="/ui/merchant-detail-view">
|
||||
<span class="material-symbols-outlined" data-icon="storefront">storefront</span>
|
||||
<span>Merchant Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="/ui/device-technical-detail">
|
||||
<span class="material-symbols-outlined" data-icon="speaker_group">speaker_group</span>
|
||||
<span>Device Registry</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="/ui/transaction-history-monitoring">
|
||||
<span class="material-symbols-outlined" data-icon="receipt_long">receipt_long</span>
|
||||
<span>Transactions</span>
|
||||
</a>
|
||||
<!-- Active Tab -->
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 bg-secondary-container text-on-secondary-container font-bold rounded-lg" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 bg-secondary-container text-on-secondary-container font-bold rounded-lg" href="/ui/settlement-batch-management">
|
||||
<span class="material-symbols-outlined" data-icon="account_balance" style="font-variation-settings: 'FILL' 1;">account_balance</span>
|
||||
<span>Ledger & Settlement</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="/ui/admin-reconciliation-management">
|
||||
<span class="material-symbols-outlined" data-icon="history_edu">history_edu</span>
|
||||
<span>Audit Control</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto border-t border-slate-100 pt-4 space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="/ui/hub">
|
||||
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="#">
|
||||
<a class="flex items-center gap-3 px-3 py-2.5 font-body-md text-on-surface-variant hover:bg-slate-100 transition-colors rounded-lg" href="/ui/hub">
|
||||
<span class="material-symbols-outlined" data-icon="help">help</span>
|
||||
<span>Support</span>
|
||||
</a>
|
||||
@ -170,8 +170,8 @@
|
||||
<input class="w-full bg-slate-50 border-none rounded-lg pl-10 pr-4 py-2 text-body-md focus:ring-2 focus:ring-primary/20 focus:bg-white transition-all" placeholder="Search batch or merchant..." type="text"/>
|
||||
</div>
|
||||
<nav class="hidden md:flex gap-6 items-center">
|
||||
<a class="font-body-md font-bold text-primary border-b-2 border-primary h-[72px] flex items-center" href="#">Dashboard</a>
|
||||
<a class="font-body-md text-on-surface-variant hover:text-primary transition-colors h-[72px] flex items-center" href="#">System Health</a>
|
||||
<a class="font-body-md font-bold text-primary border-b-2 border-primary h-[72px] flex items-center" href="/ui/admin-dashboard-overview">Dashboard</a>
|
||||
<a class="font-body-md text-on-surface-variant hover:text-primary transition-colors h-[72px] flex items-center" href="/ui/admin-dashboard-overview">System Health</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
@ -193,7 +193,7 @@
|
||||
<h2 class="font-display-lg text-display-lg text-on-surface">Disbursement Batches</h2>
|
||||
<p class="font-body-lg text-slate-500 mt-1">Manage and track bulk merchant payouts across all bank partners.</p>
|
||||
</div>
|
||||
<button class="inline-flex items-center gap-2 bg-primary text-white px-6 py-3 rounded-xl font-bold hover:opacity-90 active:scale-95 transition-all shadow-lg shadow-primary/20">
|
||||
<button id="generate-settlement-batch" data-admin-permission="settlement:write" class="inline-flex items-center gap-2 bg-primary text-white px-6 py-3 rounded-xl font-bold hover:opacity-90 active:scale-95 transition-all shadow-lg shadow-primary/20 disabled:opacity-60">
|
||||
<span class="material-symbols-outlined" data-icon="add_circle">add_circle</span>
|
||||
Generate New Batch
|
||||
</button>
|
||||
@ -203,35 +203,35 @@
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase tracking-wide">Pending Payouts</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">₹ 14.2M</h3>
|
||||
<h3 id="kpi-pending-payouts" class="font-metric-lg text-metric-lg">Rp 0</h3>
|
||||
<span class="font-metric-sm text-success flex items-center gap-0.5">
|
||||
<span class="material-symbols-outlined !text-[18px]" data-icon="trending_up">trending_up</span> 12%
|
||||
<span class="material-symbols-outlined !text-[18px]" data-icon="trending_up">trending_up</span> Created
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase tracking-wide">Processing</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">24</h3>
|
||||
<h3 id="kpi-created-batches" class="font-metric-lg text-metric-lg">0</h3>
|
||||
<span class="font-metric-sm text-warning flex items-center gap-0.5">
|
||||
<span class="material-symbols-outlined !text-[18px]" data-icon="sync">sync</span> Active
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase tracking-wide">Avg. Success Rate</p>
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase tracking-wide">Paid Batches</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">99.4%</h3>
|
||||
<h3 id="kpi-paid-batches" class="font-metric-lg text-metric-lg">0</h3>
|
||||
<span class="font-metric-sm text-success flex items-center gap-0.5">
|
||||
<span class="material-symbols-outlined !text-[18px]" data-icon="check_circle">check_circle</span> Stable
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white p-card-padding border border-slate-200 rounded-xl">
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase tracking-wide">Total Fees (MTD)</p>
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase tracking-wide">Total Fees</p>
|
||||
<div class="flex items-end justify-between">
|
||||
<h3 class="font-metric-lg text-metric-lg">₹ 420K</h3>
|
||||
<span class="font-metric-sm text-slate-500">vs ₹ 380K</span>
|
||||
<h3 id="kpi-total-fees" class="font-metric-lg text-metric-lg">Rp 0</h3>
|
||||
<span id="kpi-total-adjustments" class="font-metric-sm text-slate-500">Adj Rp 0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -240,11 +240,12 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2 px-3 py-2 border border-slate-200 rounded-lg hover:border-primary cursor-pointer transition-colors">
|
||||
<span class="material-symbols-outlined text-slate-400" data-icon="account_balance">account_balance</span>
|
||||
<select class="bg-transparent border-none p-0 text-body-md focus:ring-0 cursor-pointer">
|
||||
<option>All Bank Partners</option>
|
||||
<option>HDFC Bank</option>
|
||||
<option>ICICI Bank</option>
|
||||
<option>Yes Bank</option>
|
||||
<select id="settlement-status-filter" class="bg-transparent border-none p-0 text-body-md focus:ring-0 cursor-pointer">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="created">Created</option>
|
||||
<option value="paid">Paid</option>
|
||||
<option value="failed">Failed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-2 border border-slate-200 rounded-lg hover:border-primary cursor-pointer transition-colors">
|
||||
@ -278,97 +279,15 @@
|
||||
<th class="px-6"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<!-- Row 1: Completed -->
|
||||
<tr class="h-[52px] hover:bg-slate-50 transition-colors group cursor-pointer">
|
||||
<td class="px-6 font-body-md font-bold text-primary tabular-nums">#BAT-20231015-01</td>
|
||||
<td class="px-6 font-body-md text-on-surface-variant">Oct 15, 08:00 - 12:00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">1,240</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 4,24,500.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 1,240.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums font-bold">₹ 4,23,260.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-success/10 text-success font-label-md">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span> Completed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors" data-icon="chevron_right">chevron_right</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2: Processing -->
|
||||
<tr class="h-[52px] hover:bg-slate-50 transition-colors group cursor-pointer">
|
||||
<td class="px-6 font-body-md font-bold text-primary tabular-nums">#BAT-20231015-02</td>
|
||||
<td class="px-6 font-body-md text-on-surface-variant">Oct 15, 12:00 - 16:00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">850</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 2,12,000.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 850.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums font-bold">₹ 2,11,150.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-warning/10 text-warning font-label-md">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-warning animate-pulse"></span> Processing
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors" data-icon="chevron_right">chevron_right</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3: Failed -->
|
||||
<tr class="h-[52px] hover:bg-slate-50 transition-colors group cursor-pointer">
|
||||
<td class="px-6 font-body-md font-bold text-primary tabular-nums">#BAT-20231014-42</td>
|
||||
<td class="px-6 font-body-md text-on-surface-variant">Oct 14, 20:00 - 23:59</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">412</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 95,400.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 412.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums font-bold">₹ 94,988.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-danger/10 text-danger font-label-md">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-danger"></span> Failed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors" data-icon="chevron_right">chevron_right</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4: Completed -->
|
||||
<tr class="h-[52px] hover:bg-slate-50 transition-colors group cursor-pointer">
|
||||
<td class="px-6 font-body-md font-bold text-primary tabular-nums">#BAT-20231014-41</td>
|
||||
<td class="px-6 font-body-md text-on-surface-variant">Oct 14, 16:00 - 20:00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">2,104</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 12,45,200.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 2,104.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums font-bold">₹ 12,43,096.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-success/10 text-success font-label-md">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span> Completed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors" data-icon="chevron_right">chevron_right</span>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 5: Completed -->
|
||||
<tr class="h-[52px] hover:bg-slate-50 transition-colors group cursor-pointer">
|
||||
<td class="px-6 font-body-md font-bold text-primary tabular-nums">#BAT-20231014-40</td>
|
||||
<td class="px-6 font-body-md text-on-surface-variant">Oct 14, 12:00 - 16:00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">1,892</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 8,12,050.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">₹ 1,892.00</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums font-bold">₹ 8,10,158.00</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-success/10 text-success font-label-md">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-success"></span> Completed
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors" data-icon="chevron_right">chevron_right</span>
|
||||
</td>
|
||||
<tbody id="settlement-batch-rows" class="divide-y divide-slate-100">
|
||||
<tr>
|
||||
<td colspan="8" class="px-6 py-10 text-center text-slate-500">Loading settlement batches...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Pagination -->
|
||||
<div class="bg-white px-6 py-4 border-t border-slate-100 flex items-center justify-between">
|
||||
<p class="text-body-md text-slate-500">Showing <span class="font-bold text-on-surface">1 - 5</span> of 248 batches</p>
|
||||
<p id="settlement-pagination-summary" class="text-body-md text-slate-500">Showing 0 batches</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50" disabled="">
|
||||
<span class="material-symbols-outlined" data-icon="chevron_left">chevron_left</span>
|
||||
@ -399,19 +318,34 @@
|
||||
<span class="font-body-md font-bold" id="drawerBatchId">#BAT-20231015-01</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="font-label-md text-slate-500 uppercase">Settlement Bank</span>
|
||||
<span class="flex items-center gap-2 font-body-md">
|
||||
<span class="w-6 h-6 bg-blue-100 rounded flex items-center justify-center text-[10px] font-bold text-blue-700">HDFC</span>
|
||||
HDFC Bank Ltd.
|
||||
</span>
|
||||
<span class="font-label-md text-slate-500 uppercase">Merchant ID</span>
|
||||
<span id="drawerMerchantId" class="font-body-md font-bold text-right break-all">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-slate-50 rounded-xl p-4 mb-8">
|
||||
<p class="font-label-md text-slate-500 mb-3 uppercase">Verification Progress</p>
|
||||
<p class="font-label-md text-slate-500 mb-3 uppercase">Settlement Status</p>
|
||||
<div class="relative h-2 w-full bg-slate-200 rounded-full overflow-hidden mb-2">
|
||||
<div class="absolute inset-y-0 left-0 bg-success w-[100%] transition-all duration-1000"></div>
|
||||
<div id="drawerProgressBar" class="absolute inset-y-0 left-0 bg-warning w-[40%] transition-all duration-1000"></div>
|
||||
</div>
|
||||
<p id="drawerStatusText" class="text-label-md text-warning font-bold">CREATED</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mb-8">
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p class="font-label-md text-slate-500 uppercase mb-1">Gross</p>
|
||||
<p id="drawerGrossAmount" class="font-body-md font-bold tabular-nums">Rp 0</p>
|
||||
</div>
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p class="font-label-md text-slate-500 uppercase mb-1">Fee</p>
|
||||
<p id="drawerFeeAmount" class="font-body-md font-bold tabular-nums">Rp 0</p>
|
||||
</div>
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p class="font-label-md text-slate-500 uppercase mb-1">Net Payable</p>
|
||||
<p id="drawerNetAmount" class="font-body-md font-bold tabular-nums">Rp 0</p>
|
||||
</div>
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<p class="font-label-md text-slate-500 uppercase mb-1">Entries</p>
|
||||
<p id="drawerEntryCount" class="font-body-md font-bold tabular-nums">0</p>
|
||||
</div>
|
||||
<p class="text-label-md text-success font-bold">100% KYC Verified & Cleaned</p>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<h4 class="font-body-md font-bold text-on-surface">Timeline</h4>
|
||||
@ -419,67 +353,536 @@
|
||||
<div class="relative">
|
||||
<span class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-success border-4 border-white shadow-sm"></span>
|
||||
<p class="text-body-md font-bold">Batch Initialized</p>
|
||||
<p class="text-label-md text-slate-500">Oct 15, 2023 • 08:00 AM</p>
|
||||
<p id="drawerCreatedAt" class="text-label-md text-slate-500">-</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-success border-4 border-white shadow-sm"></span>
|
||||
<p class="text-body-md font-bold">Merchant Ledger Locked</p>
|
||||
<p class="text-label-md text-slate-500">Oct 15, 2023 • 08:15 AM</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-success border-4 border-white shadow-sm"></span>
|
||||
<p class="text-body-md font-bold">Bank File Uploaded (SFTP)</p>
|
||||
<p class="text-label-md text-slate-500">Oct 15, 2023 • 09:30 AM</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-success border-4 border-white shadow-sm"></span>
|
||||
<span id="drawerPaidDot" class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-slate-300 border-4 border-white shadow-sm"></span>
|
||||
<p class="text-body-md font-bold">Batch Settled</p>
|
||||
<p class="text-label-md text-slate-500">Oct 15, 2023 • 11:45 AM</p>
|
||||
<p id="drawerPaidAt" class="text-label-md text-slate-500">Not paid yet</p>
|
||||
<p id="drawerPaidReference" class="text-label-md text-slate-500">Reference: -</p>
|
||||
<p id="drawerPaidNote" class="text-label-md text-slate-500">Note: -</p>
|
||||
<p id="drawerAdjustmentSummary" class="text-label-md text-slate-500">Adjustments: Rp 0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 space-y-4">
|
||||
<h4 class="font-body-md font-bold text-on-surface">Payout Event History</h4>
|
||||
<div id="drawerSettlementEvents" class="relative pl-6 border-l-2 border-slate-100 space-y-5">
|
||||
<div class="relative">
|
||||
<span class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-slate-300 border-4 border-white shadow-sm"></span>
|
||||
<p class="text-body-md font-bold">Loading events</p>
|
||||
<p class="text-label-md text-slate-500">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form id="settlement-paid-form" class="mt-8 bg-slate-50 border border-slate-200 rounded-xl p-4 space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-body-md font-bold text-on-surface">Confirm Payout</p>
|
||||
<p class="text-label-md text-slate-500">Record manual payout proof before closing this batch.</p>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-warning" data-icon="receipt_long">receipt_long</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-md font-bold text-slate-500 uppercase mb-1" for="settlement-paid-at">Paid At</label>
|
||||
<input id="settlement-paid-at" type="datetime-local" class="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-primary/30" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-md font-bold text-slate-500 uppercase mb-1" for="settlement-paid-reference">Reference No</label>
|
||||
<input id="settlement-paid-reference" type="text" maxlength="120" placeholder="Bank transfer / payout reference" class="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-primary/30" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-md font-bold text-slate-500 uppercase mb-1" for="settlement-paid-note">Note</label>
|
||||
<textarea id="settlement-paid-note" maxlength="500" rows="3" placeholder="Optional reconciliation note" class="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-primary/30"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
<form id="settlement-reference-form" class="mt-8 bg-primary/5 border border-primary/10 rounded-xl p-4 space-y-4 hidden">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-body-md font-bold text-on-surface">Update Payout Reference</p>
|
||||
<p class="text-label-md text-slate-500">Correct bank transfer reference or reconciliation note for a paid batch.</p>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-primary" data-icon="edit_note">edit_note</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-md font-bold text-slate-500 uppercase mb-1" for="settlement-reference-update">Reference No</label>
|
||||
<input id="settlement-reference-update" type="text" maxlength="120" placeholder="Bank transfer / payout reference" class="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-primary/30" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-md font-bold text-slate-500 uppercase mb-1" for="settlement-note-update">Note</label>
|
||||
<textarea id="settlement-note-update" maxlength="500" rows="3" placeholder="Optional reconciliation note" class="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-primary/30"></textarea>
|
||||
</div>
|
||||
<button id="update-settlement-reference" data-admin-permission="settlement:pay" type="button" class="w-full px-4 py-3 bg-primary text-white rounded-xl font-bold hover:opacity-90 transition-colors disabled:opacity-50">Update Reference</button>
|
||||
</form>
|
||||
<form id="settlement-adjustment-form" class="mt-8 bg-warning/5 border border-warning/20 rounded-xl p-4 space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-body-md font-bold text-on-surface">Record Adjustment</p>
|
||||
<p class="text-label-md text-slate-500">Track payout dispute or correction without changing ledger entries.</p>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-warning" data-icon="balance">balance</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-label-md font-bold text-slate-500 uppercase mb-1" for="settlement-adjustment-type">Type</label>
|
||||
<select id="settlement-adjustment-type" class="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-warning/30">
|
||||
<option value="credit">Credit Merchant</option>
|
||||
<option value="debit">Debit Merchant</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-md font-bold text-slate-500 uppercase mb-1" for="settlement-adjustment-amount">Amount</label>
|
||||
<input id="settlement-adjustment-amount" type="number" min="1" step="0.01" placeholder="0" class="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-warning/30" required />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-md font-bold text-slate-500 uppercase mb-1" for="settlement-adjustment-reason">Reason</label>
|
||||
<input id="settlement-adjustment-reason" type="text" maxlength="300" placeholder="Required adjustment reason" class="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-warning/30" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-md font-bold text-slate-500 uppercase mb-1" for="settlement-adjustment-note">Note</label>
|
||||
<textarea id="settlement-adjustment-note" maxlength="500" rows="3" placeholder="Optional internal note" class="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-warning/30"></textarea>
|
||||
</div>
|
||||
<button id="record-settlement-adjustment" data-admin-permission="settlement:adjust" type="button" class="w-full px-4 py-3 bg-warning text-slate-900 rounded-xl font-bold hover:opacity-90 transition-colors disabled:opacity-50">Record Adjustment</button>
|
||||
</form>
|
||||
<form id="settlement-resolution-form" class="mt-8 bg-danger/5 border border-danger/10 rounded-xl p-4 space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-body-md font-bold text-on-surface">Exception Resolution</p>
|
||||
<p class="text-label-md text-slate-500">Mark payout as failed or cancel the batch before it is paid.</p>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-danger" data-icon="report">report</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-md font-bold text-slate-500 uppercase mb-1" for="settlement-resolution-reason">Reason</label>
|
||||
<input id="settlement-resolution-reason" type="text" maxlength="300" placeholder="Required reason" class="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-danger/30" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-md font-bold text-slate-500 uppercase mb-1" for="settlement-resolution-note">Note</label>
|
||||
<textarea id="settlement-resolution-note" maxlength="500" rows="3" placeholder="Optional internal note" class="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:ring-2 focus:ring-danger/30"></textarea>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button id="mark-settlement-failed" data-admin-permission="settlement:write" type="button" class="px-4 py-3 bg-white border border-danger/20 text-danger rounded-xl font-bold hover:bg-danger/5 transition-colors disabled:opacity-50">Mark Failed</button>
|
||||
<button id="cancel-settlement-batch" data-admin-permission="settlement:write" type="button" class="px-4 py-3 bg-white border border-slate-200 text-slate-700 rounded-xl font-bold hover:bg-slate-100 transition-colors disabled:opacity-50">Cancel Batch</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="settlement-reprocess-panel" class="mt-8 bg-primary/5 border border-primary/10 rounded-xl p-4 space-y-3 hidden">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="font-body-md font-bold text-on-surface">Reprocess Payable</p>
|
||||
<p id="settlement-reprocess-detail" class="text-label-md text-slate-500">Move entries into a new created batch.</p>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-primary" data-icon="sync_alt">sync_alt</span>
|
||||
</div>
|
||||
<button id="reprocess-settlement-batch" data-admin-permission="settlement:write" type="button" class="w-full px-4 py-3 bg-primary text-white rounded-xl font-bold hover:opacity-90 transition-colors disabled:opacity-50">Reprocess Batch</button>
|
||||
</div>
|
||||
<div class="mt-12">
|
||||
<p class="font-label-md text-slate-500 mb-2 uppercase">Raw API Response</p>
|
||||
<div class="bg-slate-900 rounded-lg p-4 font-mono text-[12px] text-primary-fixed-dim relative group">
|
||||
<button class="absolute top-2 right-2 text-slate-500 hover:text-white transition-colors opacity-0 group-hover:opacity-100">
|
||||
<span class="material-symbols-outlined !text-[18px]" data-icon="content_copy">content_copy</span>
|
||||
</button>
|
||||
<pre>{
|
||||
"batch_id": "BAT-20231015-01",
|
||||
"status": "COMPLETED",
|
||||
"merchant_count": 1240,
|
||||
"net_payout": 423260.00,
|
||||
"currency": "INR",
|
||||
"bank_ref": "HDFC_91230491_SET"
|
||||
}</pre>
|
||||
<pre id="drawerRawJson">{}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t border-slate-100 bg-slate-50 flex gap-3">
|
||||
<button class="flex-1 px-4 py-3 bg-white border border-slate-200 rounded-xl font-bold hover:bg-slate-100 transition-colors">Download CSV</button>
|
||||
<button class="flex-1 px-4 py-3 bg-primary text-white rounded-xl font-bold hover:opacity-90 transition-colors">Re-run Reconciliation</button>
|
||||
<select id="settlement-export-format" class="w-[180px] px-3 py-3 bg-white border border-slate-200 rounded-xl font-body-md focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||
<option value="standard">General CSV</option>
|
||||
<option value="bank_generic">Bank Upload</option>
|
||||
</select>
|
||||
<button id="download-settlement-csv" data-admin-permission="settlement:export" class="flex-1 px-4 py-3 bg-white border border-slate-200 rounded-xl font-bold hover:bg-slate-100 transition-colors disabled:opacity-50">Download CSV</button>
|
||||
<button id="mark-settlement-paid" data-admin-permission="settlement:pay" class="flex-1 px-4 py-3 bg-primary text-white rounded-xl font-bold hover:opacity-90 transition-colors disabled:opacity-50">Confirm Paid</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/ui/shared/admin-api.js"></script>
|
||||
<script>
|
||||
function toggleDrawer() {
|
||||
const drawer = document.getElementById('detailDrawer');
|
||||
if (drawer.classList.contains('translate-x-full')) {
|
||||
drawer.classList.remove('translate-x-full');
|
||||
} else {
|
||||
drawer.classList.add('translate-x-full');
|
||||
}
|
||||
}
|
||||
const SettlementUI = (() => {
|
||||
const api = window.AdminUIAPI;
|
||||
if (!api) return;
|
||||
|
||||
// Simulating row click to open drawer
|
||||
document.querySelectorAll('tbody tr').forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
const batchId = row.querySelector('td:first-child').textContent;
|
||||
document.getElementById('drawerBatchId').textContent = batchId;
|
||||
toggleDrawer();
|
||||
const rowsEl = document.getElementById('settlement-batch-rows');
|
||||
const generateBtn = document.getElementById('generate-settlement-batch');
|
||||
const statusFilter = document.getElementById('settlement-status-filter');
|
||||
const summaryEl = document.getElementById('settlement-pagination-summary');
|
||||
const drawer = document.getElementById('detailDrawer');
|
||||
const markPaidBtn = document.getElementById('mark-settlement-paid');
|
||||
const downloadCsvBtn = document.getElementById('download-settlement-csv');
|
||||
const exportFormatSelect = document.getElementById('settlement-export-format');
|
||||
const paidForm = document.getElementById('settlement-paid-form');
|
||||
const paidAtInput = document.getElementById('settlement-paid-at');
|
||||
const paidReferenceInput = document.getElementById('settlement-paid-reference');
|
||||
const paidNoteInput = document.getElementById('settlement-paid-note');
|
||||
const referenceForm = document.getElementById('settlement-reference-form');
|
||||
const referenceUpdateInput = document.getElementById('settlement-reference-update');
|
||||
const noteUpdateInput = document.getElementById('settlement-note-update');
|
||||
const updateReferenceBtn = document.getElementById('update-settlement-reference');
|
||||
const adjustmentForm = document.getElementById('settlement-adjustment-form');
|
||||
const adjustmentTypeInput = document.getElementById('settlement-adjustment-type');
|
||||
const adjustmentAmountInput = document.getElementById('settlement-adjustment-amount');
|
||||
const adjustmentReasonInput = document.getElementById('settlement-adjustment-reason');
|
||||
const adjustmentNoteInput = document.getElementById('settlement-adjustment-note');
|
||||
const recordAdjustmentBtn = document.getElementById('record-settlement-adjustment');
|
||||
const resolutionForm = document.getElementById('settlement-resolution-form');
|
||||
const resolutionReasonInput = document.getElementById('settlement-resolution-reason');
|
||||
const resolutionNoteInput = document.getElementById('settlement-resolution-note');
|
||||
const markFailedBtn = document.getElementById('mark-settlement-failed');
|
||||
const cancelBatchBtn = document.getElementById('cancel-settlement-batch');
|
||||
const reprocessPanel = document.getElementById('settlement-reprocess-panel');
|
||||
const reprocessDetail = document.getElementById('settlement-reprocess-detail');
|
||||
const reprocessBtn = document.getElementById('reprocess-settlement-batch');
|
||||
const settlementEventsEl = document.getElementById('drawerSettlementEvents');
|
||||
let batches = [];
|
||||
let activeBatchId = null;
|
||||
let activeBatchCode = '';
|
||||
let adminProfile = null;
|
||||
|
||||
const money = (value) => api.formatMoney ? api.formatMoney(value) : `Rp ${Number(value || 0).toLocaleString('id-ID')}`;
|
||||
const dt = (value) => api.formatDateTime ? api.formatDateTime(value) : (value || '-');
|
||||
const setText = (id, value) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = value || '-';
|
||||
};
|
||||
const toDateTimeLocalValue = (value) => {
|
||||
const date = value ? new Date(value) : new Date();
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
const offsetMs = date.getTimezoneOffset() * 60 * 1000;
|
||||
return new Date(date.getTime() - offsetMs).toISOString().slice(0, 16);
|
||||
};
|
||||
const can = (permission) => api.hasPermission ? api.hasPermission(permission, adminProfile) : true;
|
||||
|
||||
const statusClass = (status) => {
|
||||
if (status === 'paid') return 'bg-success/10 text-success';
|
||||
if (status === 'failed' || status === 'cancelled') return 'bg-danger/10 text-danger';
|
||||
return 'bg-warning/10 text-warning';
|
||||
};
|
||||
|
||||
const statusDot = (status) => {
|
||||
if (status === 'paid') return 'bg-success';
|
||||
if (status === 'failed' || status === 'cancelled') return 'bg-danger';
|
||||
return 'bg-warning animate-pulse';
|
||||
};
|
||||
|
||||
const renderSettlementEvents = (events) => {
|
||||
if (!settlementEventsEl) return;
|
||||
const rows = Array.isArray(events) ? events : [];
|
||||
if (!rows.length) {
|
||||
settlementEventsEl.innerHTML = '<div class="relative"><span class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-slate-300 border-4 border-white shadow-sm"></span><p class="text-body-md font-bold">No payout events yet</p><p class="text-label-md text-slate-500">-</p></div>';
|
||||
return;
|
||||
}
|
||||
settlementEventsEl.innerHTML = rows.map((event) => {
|
||||
const label = String(event.event_type || '').replace(/_/g, ' ').toUpperCase();
|
||||
const actor = `${event.actor_type || 'system'}${event.actor_id ? ` · ${event.actor_id}` : ''}`;
|
||||
return `
|
||||
<div class="relative">
|
||||
<span class="absolute -left-[33px] top-0 w-4 h-4 rounded-full bg-success border-4 border-white shadow-sm"></span>
|
||||
<p class="text-body-md font-bold">${label}</p>
|
||||
<p class="text-label-md text-slate-500">${dt(event.created_at)} · ${actor}</p>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
};
|
||||
|
||||
const renderKpis = () => {
|
||||
const created = batches.filter((batch) => batch.status === 'created');
|
||||
const paid = batches.filter((batch) => batch.status === 'paid');
|
||||
const pendingAmount = created.reduce((sum, batch) => sum + Number(batch.net_payable_amount || 0), 0);
|
||||
const totalFees = batches.reduce((sum, batch) => sum + Number(batch.platform_fee_amount || 0), 0);
|
||||
const totalAdjustments = batches.reduce((sum, batch) => sum + Number(batch.metadata_json?.total_adjustment_amount || 0), 0);
|
||||
setText('kpi-pending-payouts', money(pendingAmount));
|
||||
setText('kpi-created-batches', String(created.length));
|
||||
setText('kpi-paid-batches', String(paid.length));
|
||||
setText('kpi-total-fees', money(totalFees));
|
||||
setText('kpi-total-adjustments', `Adj ${money(totalAdjustments)}`);
|
||||
};
|
||||
|
||||
const renderRows = () => {
|
||||
if (!rowsEl) return;
|
||||
if (!batches.length) {
|
||||
rowsEl.innerHTML = '<tr><td colspan="8" class="px-6 py-10 text-center text-slate-500">No settlement batches yet.</td></tr>';
|
||||
summaryEl.textContent = 'Showing 0 batches';
|
||||
renderKpis();
|
||||
return;
|
||||
}
|
||||
|
||||
rowsEl.innerHTML = batches.map((batch) => `
|
||||
<tr class="h-[52px] hover:bg-slate-50 transition-colors group cursor-pointer" data-batch-id="${batch.id}">
|
||||
<td class="px-6 font-body-md font-bold text-primary tabular-nums">${batch.batch_code}</td>
|
||||
<td class="px-6 font-body-md text-on-surface-variant">${dt(batch.cutoff_at)}</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">${batch.entry_count}</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">${money(batch.gross_amount)}</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums">${money(batch.platform_fee_amount)}</td>
|
||||
<td class="px-6 font-body-md text-right tabular-nums font-bold">${money(batch.net_payable_amount)}</td>
|
||||
<td class="px-6">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full ${statusClass(batch.status)} font-label-md">
|
||||
<span class="w-1.5 h-1.5 rounded-full ${statusDot(batch.status)}"></span> ${String(batch.status).toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 text-right">
|
||||
<span class="material-symbols-outlined text-slate-400 group-hover:text-primary transition-colors" data-icon="chevron_right">chevron_right</span>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
rowsEl.querySelectorAll('tr[data-batch-id]').forEach((row) => {
|
||||
row.addEventListener('click', () => openDrawer(row.dataset.batchId));
|
||||
});
|
||||
summaryEl.textContent = `Showing ${batches.length} batches`;
|
||||
renderKpis();
|
||||
};
|
||||
|
||||
const loadBatches = async () => {
|
||||
try {
|
||||
api.requireToken();
|
||||
const query = { limit: 100 };
|
||||
if (statusFilter.value) query.status = statusFilter.value;
|
||||
batches = await api.listSettlementBatches(query);
|
||||
renderRows();
|
||||
} catch (error) {
|
||||
rowsEl.innerHTML = '<tr><td colspan="8" class="px-6 py-10 text-center text-danger">Unable to load settlement batches.</td></tr>';
|
||||
}
|
||||
};
|
||||
|
||||
window.toggleDrawer = function toggleDrawer(forceOpen) {
|
||||
if (!drawer) return;
|
||||
const shouldOpen = typeof forceOpen === 'boolean' ? forceOpen : drawer.classList.contains('translate-x-full');
|
||||
drawer.classList.toggle('translate-x-full', !shouldOpen);
|
||||
};
|
||||
|
||||
const openDrawer = async (batchId) => {
|
||||
activeBatchId = batchId;
|
||||
const payload = await api.getSettlementBatch(batchId);
|
||||
const batch = payload.batch;
|
||||
activeBatchCode = batch.batch_code;
|
||||
const metadata = batch.metadata_json || {};
|
||||
const formalAdjustmentAmount = Array.isArray(payload.adjustments)
|
||||
? payload.adjustments
|
||||
.filter((item) => (item.approval_status || 'approved') === 'approved')
|
||||
.reduce((sum, item) => sum + Number(item.signed_amount || 0), 0)
|
||||
: Number(metadata.total_adjustment_amount || 0);
|
||||
setText('drawerBatchId', batch.batch_code);
|
||||
setText('drawerMerchantId', batch.merchant_id);
|
||||
setText('drawerStatusText', String(batch.status).toUpperCase());
|
||||
setText('drawerGrossAmount', money(batch.gross_amount));
|
||||
setText('drawerFeeAmount', money(batch.platform_fee_amount));
|
||||
setText('drawerNetAmount', money(batch.net_payable_amount));
|
||||
setText('drawerEntryCount', String(batch.entry_count));
|
||||
setText('drawerCreatedAt', dt(batch.created_at));
|
||||
setText('drawerPaidAt', batch.paid_at ? dt(batch.paid_at) : 'Not paid yet');
|
||||
setText('drawerPaidReference', `Reference: ${metadata.paid_reference || '-'}`);
|
||||
setText('drawerPaidNote', `Note: ${metadata.paid_note || '-'}`);
|
||||
setText('drawerAdjustmentSummary', `Adjustments: ${money(formalAdjustmentAmount)}`);
|
||||
setText('drawerStatusText', batch.failure_reason ? `${String(batch.status).toUpperCase()} · ${batch.failure_reason}` : String(batch.status).toUpperCase());
|
||||
renderSettlementEvents(payload.events || []);
|
||||
document.getElementById('drawerRawJson').textContent = JSON.stringify(payload, null, 2);
|
||||
const finalStatus = ['paid', 'failed', 'cancelled'].includes(batch.status);
|
||||
const progressColor = batch.status === 'paid' ? 'bg-success' : batch.status === 'created' ? 'bg-warning' : 'bg-danger';
|
||||
document.getElementById('drawerProgressBar').className = `absolute inset-y-0 left-0 ${progressColor} ${finalStatus ? 'w-[100%]' : 'w-[40%]'} transition-all duration-1000`;
|
||||
document.getElementById('drawerPaidDot').className = `absolute -left-[33px] top-0 w-4 h-4 rounded-full ${finalStatus ? progressColor : 'bg-slate-300'} border-4 border-white shadow-sm`;
|
||||
markPaidBtn.disabled = batch.status !== 'created' || !can('settlement:pay');
|
||||
markPaidBtn.textContent = batch.status === 'created' ? 'Confirm Paid' : 'Paid';
|
||||
if (paidForm) paidForm.classList.toggle('hidden', batch.status !== 'created');
|
||||
if (referenceForm) referenceForm.classList.toggle('hidden', batch.status !== 'paid');
|
||||
if (resolutionForm) resolutionForm.classList.toggle('hidden', batch.status !== 'created');
|
||||
const canAdjust = !metadata.reprocessed_to_batch_id;
|
||||
if (adjustmentForm) adjustmentForm.classList.toggle('hidden', !canAdjust);
|
||||
const canReprocess = ['failed', 'cancelled'].includes(batch.status) && !metadata.reprocessed_to_batch_id;
|
||||
if (reprocessPanel) reprocessPanel.classList.toggle('hidden', !canReprocess);
|
||||
if (reprocessDetail) {
|
||||
reprocessDetail.textContent = metadata.reprocessed_to_batch_id
|
||||
? `Already reprocessed to ${metadata.reprocessed_to_batch_id}.`
|
||||
: 'Move payable entries into a new created batch.';
|
||||
}
|
||||
if (paidAtInput) paidAtInput.value = toDateTimeLocalValue(new Date());
|
||||
if (paidReferenceInput) paidReferenceInput.value = '';
|
||||
if (paidNoteInput) paidNoteInput.value = '';
|
||||
if (referenceUpdateInput) referenceUpdateInput.value = metadata.paid_reference || '';
|
||||
if (noteUpdateInput) noteUpdateInput.value = metadata.paid_note || '';
|
||||
if (updateReferenceBtn) {
|
||||
updateReferenceBtn.disabled = batch.status !== 'paid' || !can('settlement:pay');
|
||||
updateReferenceBtn.textContent = 'Update Reference';
|
||||
}
|
||||
if (adjustmentTypeInput) adjustmentTypeInput.value = 'credit';
|
||||
if (adjustmentAmountInput) adjustmentAmountInput.value = '';
|
||||
if (adjustmentReasonInput) adjustmentReasonInput.value = '';
|
||||
if (adjustmentNoteInput) adjustmentNoteInput.value = '';
|
||||
if (recordAdjustmentBtn) {
|
||||
recordAdjustmentBtn.disabled = !canAdjust || !can('settlement:adjust');
|
||||
recordAdjustmentBtn.textContent = 'Record Adjustment';
|
||||
}
|
||||
if (resolutionReasonInput) resolutionReasonInput.value = '';
|
||||
if (resolutionNoteInput) resolutionNoteInput.value = '';
|
||||
if (markFailedBtn) {
|
||||
markFailedBtn.disabled = batch.status !== 'created' || !can('settlement:write');
|
||||
markFailedBtn.textContent = 'Mark Failed';
|
||||
}
|
||||
if (cancelBatchBtn) {
|
||||
cancelBatchBtn.disabled = batch.status !== 'created' || !can('settlement:write');
|
||||
cancelBatchBtn.textContent = 'Cancel Batch';
|
||||
}
|
||||
if (reprocessBtn) {
|
||||
reprocessBtn.disabled = !canReprocess || !can('settlement:write');
|
||||
reprocessBtn.textContent = 'Reprocess Batch';
|
||||
}
|
||||
downloadCsvBtn.disabled = !can('settlement:export');
|
||||
window.toggleDrawer(true);
|
||||
};
|
||||
|
||||
generateBtn?.addEventListener('click', async () => {
|
||||
generateBtn.disabled = true;
|
||||
generateBtn.innerHTML = '<span class="material-symbols-outlined">sync</span>Generating...';
|
||||
try {
|
||||
await api.createSettlementBatches({ cutoff_at: new Date().toISOString() });
|
||||
await loadBatches();
|
||||
} finally {
|
||||
generateBtn.disabled = false;
|
||||
generateBtn.innerHTML = '<span class="material-symbols-outlined" data-icon="add_circle">add_circle</span>Generate New Batch';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
statusFilter?.addEventListener('change', loadBatches);
|
||||
markPaidBtn?.addEventListener('click', async () => {
|
||||
if (!activeBatchId) return;
|
||||
if (paidForm && !paidForm.reportValidity()) return;
|
||||
const paidAt = paidAtInput?.value ? new Date(paidAtInput.value).toISOString() : new Date().toISOString();
|
||||
const paidReference = paidReferenceInput?.value.trim();
|
||||
const paidNote = paidNoteInput?.value.trim();
|
||||
markPaidBtn.disabled = true;
|
||||
markPaidBtn.textContent = 'Saving...';
|
||||
try {
|
||||
await api.markSettlementBatchPaid(activeBatchId, {
|
||||
paid_at: paidAt,
|
||||
paid_reference: paidReference,
|
||||
paid_note: paidNote
|
||||
});
|
||||
await loadBatches();
|
||||
await openDrawer(activeBatchId);
|
||||
} catch (error) {
|
||||
markPaidBtn.disabled = false;
|
||||
markPaidBtn.textContent = 'Confirm Paid';
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
const resolveBatch = async (status) => {
|
||||
if (!activeBatchId) return;
|
||||
if (resolutionForm && !resolutionForm.reportValidity()) return;
|
||||
const reason = resolutionReasonInput?.value.trim();
|
||||
const note = resolutionNoteInput?.value.trim();
|
||||
const button = status === 'failed' ? markFailedBtn : cancelBatchBtn;
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
button.textContent = status === 'failed' ? 'Marking...' : 'Cancelling...';
|
||||
}
|
||||
try {
|
||||
if (status === 'failed') {
|
||||
await api.markSettlementBatchFailed(activeBatchId, { reason, note });
|
||||
} else {
|
||||
await api.cancelSettlementBatch(activeBatchId, { reason, note });
|
||||
}
|
||||
await loadBatches();
|
||||
await openDrawer(activeBatchId);
|
||||
} catch (error) {
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.textContent = status === 'failed' ? 'Mark Failed' : 'Cancel Batch';
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
markFailedBtn?.addEventListener('click', () => resolveBatch('failed'));
|
||||
cancelBatchBtn?.addEventListener('click', () => resolveBatch('cancelled'));
|
||||
updateReferenceBtn?.addEventListener('click', async () => {
|
||||
if (!activeBatchId) return;
|
||||
if (referenceForm && !referenceForm.reportValidity()) return;
|
||||
const paidReference = referenceUpdateInput?.value.trim();
|
||||
const paidNote = noteUpdateInput?.value.trim();
|
||||
updateReferenceBtn.disabled = true;
|
||||
updateReferenceBtn.textContent = 'Updating...';
|
||||
try {
|
||||
await api.updateSettlementBatchReference(activeBatchId, {
|
||||
paid_reference: paidReference,
|
||||
paid_note: paidNote
|
||||
});
|
||||
await loadBatches();
|
||||
await openDrawer(activeBatchId);
|
||||
} catch (error) {
|
||||
updateReferenceBtn.disabled = false;
|
||||
updateReferenceBtn.textContent = 'Update Reference';
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
recordAdjustmentBtn?.addEventListener('click', async () => {
|
||||
if (!activeBatchId) return;
|
||||
if (adjustmentForm && !adjustmentForm.reportValidity()) return;
|
||||
const amount = Number(adjustmentAmountInput?.value || 0);
|
||||
recordAdjustmentBtn.disabled = true;
|
||||
recordAdjustmentBtn.textContent = 'Recording...';
|
||||
try {
|
||||
await api.recordSettlementBatchAdjustment(activeBatchId, {
|
||||
adjustment_type: adjustmentTypeInput?.value || 'credit',
|
||||
amount,
|
||||
reason: adjustmentReasonInput?.value.trim(),
|
||||
note: adjustmentNoteInput?.value.trim()
|
||||
});
|
||||
await loadBatches();
|
||||
await openDrawer(activeBatchId);
|
||||
} catch (error) {
|
||||
recordAdjustmentBtn.disabled = false;
|
||||
recordAdjustmentBtn.textContent = 'Record Adjustment';
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
reprocessBtn?.addEventListener('click', async () => {
|
||||
if (!activeBatchId) return;
|
||||
reprocessBtn.disabled = true;
|
||||
reprocessBtn.textContent = 'Reprocessing...';
|
||||
try {
|
||||
const result = await api.reprocessSettlementBatch(activeBatchId);
|
||||
await loadBatches();
|
||||
await openDrawer(result.new_batch?.id || activeBatchId);
|
||||
} catch (error) {
|
||||
reprocessBtn.disabled = false;
|
||||
reprocessBtn.textContent = 'Reprocess Batch';
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
downloadCsvBtn?.addEventListener('click', async () => {
|
||||
if (!activeBatchId) return;
|
||||
downloadCsvBtn.disabled = true;
|
||||
downloadCsvBtn.textContent = 'Downloading...';
|
||||
try {
|
||||
const token = api.getToken();
|
||||
const format = exportFormatSelect?.value || 'standard';
|
||||
const response = await fetch(`/admin/settlement-batches/${activeBatchId}/export.csv?format=${encodeURIComponent(format)}`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`CSV export failed: ${response.status}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = format === 'bank_generic'
|
||||
? `${activeBatchCode || activeBatchId}-bank-generic-payout.csv`
|
||||
: `${activeBatchCode || activeBatchId}-payout-report.csv`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
} finally {
|
||||
downloadCsvBtn.disabled = false;
|
||||
downloadCsvBtn.textContent = 'Download CSV';
|
||||
}
|
||||
});
|
||||
|
||||
(async () => {
|
||||
adminProfile = await api.applyPermissions();
|
||||
await loadBatches();
|
||||
})();
|
||||
})();
|
||||
</script>
|
||||
<!-- ui-nav -->
|
||||
<div id="__sb_nav" style="position:fixed;left:16px;bottom:16px;z-index:9999;background:#fff;border:1px solid #e2e8f0;padding:8px 10px;border-radius:8px;box-shadow:0 6px 24px rgba(15,23,42,0.12);font-family:Inter,Arial,sans-serif;font-size:12px;line-height:1.4">
|
||||
@ -490,4 +893,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