927 lines
46 KiB
HTML
927 lines
46 KiB
HTML
<!DOCTYPE html>
|
|
|
|
<html class="light" lang="en"><head>
|
|
<meta charset="utf-8"/>
|
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
|
<title>Reconciliation | Soundbox Ops</title>
|
|
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@600;700;800&family=JetBrains+Mono&display=swap" rel="stylesheet"/>
|
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
|
<script id="tailwind-config">
|
|
tailwind.config = {
|
|
darkMode: "class",
|
|
theme: {
|
|
extend: {
|
|
"colors": {
|
|
"success": "#16A34A",
|
|
"on-tertiary-fixed": "#360f00",
|
|
"danger": "#DC2626",
|
|
"slate-900": "#0F172A",
|
|
"on-surface-variant": "#434655",
|
|
"slate-100": "#F1F5F9",
|
|
"tertiary-container": "#bc4800",
|
|
"on-secondary-fixed": "#0b1c30",
|
|
"error-container": "#ffdad6",
|
|
"outline": "#737686",
|
|
"primary-fixed": "#dbe1ff",
|
|
"on-tertiary": "#ffffff",
|
|
"info": "#0EA5E9",
|
|
"on-secondary-fixed-variant": "#38485d",
|
|
"surface-container": "#ededf9",
|
|
"primary-container": "#2563eb",
|
|
"warning": "#F59E0B",
|
|
"inverse-on-surface": "#f0f0fb",
|
|
"error": "#ba1a1a",
|
|
"surface": "#faf8ff",
|
|
"slate-700": "#334155",
|
|
"inverse-primary": "#b4c5ff",
|
|
"on-error": "#ffffff",
|
|
"secondary-container": "#d0e1fb",
|
|
"tertiary": "#943700",
|
|
"surface-container-low": "#f3f3fe",
|
|
"on-surface": "#191b23",
|
|
"tertiary-fixed": "#ffdbcd",
|
|
"slate-200": "#E2E8F0",
|
|
"inverse-surface": "#2e3039",
|
|
"surface-tint": "#0053db",
|
|
"on-error-container": "#93000a",
|
|
"tertiary-fixed-dim": "#ffb596",
|
|
"on-tertiary-fixed-variant": "#7d2d00",
|
|
"on-secondary": "#ffffff",
|
|
"background": "#F8FAFC",
|
|
"on-background": "#191b23",
|
|
"slate-500": "#64748B",
|
|
"primary": "#004ac6",
|
|
"surface-bright": "#faf8ff",
|
|
"primary-fixed-dim": "#b4c5ff",
|
|
"on-primary": "#ffffff",
|
|
"outline-variant": "#c3c6d7",
|
|
"surface-container-high": "#e7e7f3",
|
|
"on-primary-fixed": "#00174b",
|
|
"surface-dim": "#d9d9e5",
|
|
"secondary-fixed-dim": "#b7c8e1",
|
|
"on-primary-container": "#eeefff",
|
|
"surface-variant": "#e1e2ed",
|
|
"surface-container-highest": "#e1e2ed",
|
|
"secondary-fixed": "#d3e4fe",
|
|
"surface-container-lowest": "#ffffff",
|
|
"on-tertiary-container": "#ffede6",
|
|
"on-primary-fixed-variant": "#003ea8",
|
|
"secondary": "#505f76",
|
|
"on-secondary-container": "#54647a"
|
|
},
|
|
"borderRadius": {
|
|
"DEFAULT": "0.125rem",
|
|
"lg": "0.25rem",
|
|
"xl": "0.5rem",
|
|
"full": "0.75rem"
|
|
},
|
|
"spacing": {
|
|
"topbar-height": "72px",
|
|
"row-height": "52px",
|
|
"page-padding": "24px",
|
|
"card-padding": "20px",
|
|
"gutter": "24px"
|
|
},
|
|
"fontFamily": {
|
|
"label-md": ["Inter"],
|
|
"body-md": ["Inter"],
|
|
"metric-sm": ["Inter"],
|
|
"metric-lg": ["Inter"],
|
|
"headline-lg": ["Plus Jakarta Sans"],
|
|
"display-lg": ["Plus Jakarta Sans"],
|
|
"headline-md": ["Plus Jakarta Sans"],
|
|
"body-lg": ["Inter"]
|
|
},
|
|
"fontSize": {
|
|
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.01em", "fontWeight": "500"}],
|
|
"body-md": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
|
"metric-sm": ["14px", {"lineHeight": "20px", "fontWeight": "600"}],
|
|
"metric-lg": ["32px", {"lineHeight": "40px", "fontWeight": "600"}],
|
|
"headline-lg": ["28px", {"lineHeight": "36px", "fontWeight": "600"}],
|
|
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "600"}],
|
|
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
|
|
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}]
|
|
}
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
<style>
|
|
body { background-color: #F8FAFC; color: #191B23; -webkit-font-smoothing: antialiased; }
|
|
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; display: inline-block; }
|
|
.tabular-nums { font-variant-numeric: tabular-nums; }
|
|
.hide-scrollbar::-webkit-scrollbar { display: none; }
|
|
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
.glass-effect { background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(12px); }
|
|
</style>
|
|
</head>
|
|
<body class="font-body-md text-body-md overflow-x-hidden">
|
|
<!-- SideNavBar -->
|
|
<aside class="flex flex-col fixed left-0 top-0 h-full p-4 gap-2 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-700 w-64 z-50">
|
|
<div class="mb-8 px-2 flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-primary rounded-xl flex items-center justify-center">
|
|
<span class="material-symbols-outlined text-white" data-icon="account_balance">account_balance</span>
|
|
</div>
|
|
<div>
|
|
<h1 class="font-headline-md text-headline-md text-primary leading-tight">Soundbox Admin</h1>
|
|
<p class="font-label-md text-label-md text-slate-500">Fintech Ops Suite</p>
|
|
</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="/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="/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="/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="/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="/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="/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>
|
|
</nav>
|
|
<div class="mt-auto space-y-4">
|
|
<button class="w-full bg-primary text-white py-3 rounded-xl font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-opacity">
|
|
<span class="material-symbols-outlined text-[20px]" data-icon="description">description</span>
|
|
<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="/ui/admin-login">
|
|
<span class="material-symbols-outlined" data-icon="logout">logout</span>
|
|
<span class="font-label-md text-label-md">Logout</span>
|
|
</a>
|
|
</div>
|
|
</aside>
|
|
<!-- Main Content -->
|
|
<main class="ml-64 min-h-screen">
|
|
<!-- TopNavBar -->
|
|
<header class="flex justify-between items-center h-[72px] px-page-padding w-full sticky top-0 z-40 bg-surface dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700">
|
|
<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="/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">
|
|
<div class="relative group">
|
|
<input class="pl-10 pr-4 py-2 rounded-full border border-slate-200 focus:outline-none focus:ring-2 focus:ring-primary/20 w-64 bg-slate-50 transition-all focus:bg-white text-body-md" placeholder="Search transactions..." type="text"/>
|
|
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" data-icon="search">search</span>
|
|
</div>
|
|
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 transition-colors text-slate-600">
|
|
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
|
|
</button>
|
|
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-100 transition-colors text-slate-600">
|
|
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
|
</button>
|
|
<div class="h-8 w-px bg-slate-200 mx-2"></div>
|
|
<div class="flex items-center gap-3">
|
|
<img alt="Administrator Avatar" class="w-10 h-10 rounded-full border border-slate-200" data-alt="A professional headshot of a senior financial operations manager in a minimalist, bright office setting. The person exudes competence and calm, dressed in corporate business attire. Soft, high-key lighting illuminates the scene, consistent with a modern fintech dashboard aesthetic using a clean white and blue color palette." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDhfFggnhz_VMmYf0ijSR2I_RiQAO21qPqG33SGjrEM2wAQ79xWw-6u77CyKPUuRePmDC7vc-pftCEMyfLbsNUg8xnKqZ_gy0ei1QmvnyDRu8Hj1ytkFZLGH0awGL-1pJj1epvjRyJpVPdORX_03QJwW9ZzpXdv_WDhH_wvr5dWzrEPQm0IYyls85ZJbKM0jq73rP2IRY4IlwS-nTc2aBF4bKfgSM7v40-NDHULJgRYLxpa56nDBrXDs76c2_zmj8CtP7vOa3TNnwo"/>
|
|
<div class="hidden xl:block">
|
|
<p class="font-label-md text-label-md font-bold text-on-surface">Admin Ops</p>
|
|
<p class="text-[11px] text-slate-500 uppercase tracking-wider">Level 4</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<div class="p-page-padding space-y-6">
|
|
<!-- KPI Cards -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-gutter">
|
|
<div class="bg-white p-card-padding border border-slate-200 rounded-xl transition-shadow hover:shadow-md">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<span class="font-label-md text-label-md text-slate-500">Total Matched</span>
|
|
<div class="p-2 bg-success/10 rounded-lg">
|
|
<span class="material-symbols-outlined text-success" data-icon="check_circle">check_circle</span>
|
|
</div>
|
|
</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>
|
|
<span class="text-[12px] text-slate-400 ml-1">vs last period</span>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white p-card-padding border border-slate-200 rounded-xl transition-shadow hover:shadow-md">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<span class="font-label-md text-label-md text-slate-500">Discrepancies</span>
|
|
<div class="p-2 bg-danger/10 rounded-lg">
|
|
<span class="material-symbols-outlined text-danger" data-icon="error_outline">error_outline</span>
|
|
</div>
|
|
</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>
|
|
<span class="text-[12px] text-slate-400 ml-1">Requires attention</span>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white p-card-padding border border-slate-200 rounded-xl transition-shadow hover:shadow-md">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<span class="font-label-md text-label-md text-slate-500">Pending Verification</span>
|
|
<div class="p-2 bg-warning/10 rounded-lg">
|
|
<span class="material-symbols-outlined text-warning" data-icon="hourglass_empty">hourglass_empty</span>
|
|
</div>
|
|
</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>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white p-card-padding border border-slate-200 rounded-xl transition-shadow hover:shadow-md">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<span class="font-label-md text-label-md text-slate-500">Bank Statements</span>
|
|
<div class="p-2 bg-info/10 rounded-lg">
|
|
<span class="material-symbols-outlined text-info" data-icon="account_balance">account_balance</span>
|
|
</div>
|
|
</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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Filter Bar -->
|
|
<div class="bg-white border border-slate-200 rounded-xl p-4 flex flex-wrap items-center gap-4">
|
|
<div class="flex items-center gap-2 px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg cursor-pointer hover:bg-slate-100 transition-colors">
|
|
<span class="material-symbols-outlined text-slate-500 text-[20px]" data-icon="calendar_today">calendar_today</span>
|
|
<span class="font-body-md text-body-md">Oct 01, 2023 - Oct 31, 2023</span>
|
|
</div>
|
|
<select class="px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg font-body-md text-body-md focus:ring-primary/20 focus:border-primary">
|
|
<option>All Bank Partners</option>
|
|
<option>HSBC Corporate</option>
|
|
<option>JPMorgan Chase</option>
|
|
<option>Citibank Enterprise</option>
|
|
</select>
|
|
<select class="px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg font-body-md text-body-md focus:ring-primary/20 focus:border-primary">
|
|
<option>All Statuses</option>
|
|
<option>Matched</option>
|
|
<option>Exception</option>
|
|
<option>Pending</option>
|
|
</select>
|
|
<div class="ml-auto flex items-center gap-3">
|
|
<button class="px-4 py-2 text-primary font-bold border border-primary/20 hover:bg-primary/5 transition-colors rounded-lg flex items-center gap-2">
|
|
<span class="material-symbols-outlined text-[20px]" data-icon="merge">merge</span>
|
|
Manual Match
|
|
</button>
|
|
<button class="px-4 py-2 bg-slate-900 text-white font-bold hover:bg-slate-800 transition-colors rounded-lg flex items-center gap-2">
|
|
<span class="material-symbols-outlined text-[20px]" data-icon="file_download">file_download</span>
|
|
Export Report
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- Main Comparison Table -->
|
|
<div class="bg-white border border-slate-200 rounded-xl overflow-hidden">
|
|
<div class="overflow-x-auto">
|
|
<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">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 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 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>
|
|
</button>
|
|
<button class="w-8 h-8 flex items-center justify-center rounded border border-primary bg-primary text-white text-label-md font-bold">1</button>
|
|
<button class="w-8 h-8 flex items-center justify-center rounded border border-slate-200 bg-white text-slate-600 hover:bg-slate-50 text-label-md">2</button>
|
|
<button class="w-8 h-8 flex items-center justify-center rounded border border-slate-200 bg-white text-slate-600 hover:bg-slate-50 text-label-md">3</button>
|
|
<button class="w-8 h-8 flex items-center justify-center rounded border border-slate-200 bg-white text-slate-600 hover:bg-slate-50">
|
|
<span class="material-symbols-outlined text-[18px]" data-icon="chevron_right">chevron_right</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Bento Layout Footer Widgets -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-gutter pb-8">
|
|
<!-- System Logs / Audit Blocks -->
|
|
<div class="lg:col-span-2 bg-slate-900 rounded-xl overflow-hidden p-6 relative">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-white font-headline-md text-[16px] flex items-center gap-2">
|
|
<span class="material-symbols-outlined text-info" data-icon="terminal">terminal</span>
|
|
Raw Reconciliation Payload (Last Match)
|
|
</h3>
|
|
<button class="text-slate-400 hover:text-white transition-colors flex items-center gap-1 text-[12px]">
|
|
<span class="material-symbols-outlined text-[16px]" data-icon="content_copy">content_copy</span>
|
|
Copy JSON
|
|
</button>
|
|
</div>
|
|
<div class="font-mono text-[13px] text-success leading-relaxed h-[200px] overflow-y-auto hide-scrollbar">
|
|
<pre id="recon-raw-payload">{
|
|
"reconciliation_id": "RECON-00492-AX",
|
|
"timestamp": "2023-10-24T14:22:10.452Z",
|
|
"system_ledger": {
|
|
"entry_id": "SL-90283471",
|
|
"amount": 14500.00,
|
|
"currency": "INR",
|
|
"hash": "8f3e22...a1"
|
|
},
|
|
"bank_statement": {
|
|
"ref_num": "BANK-TX-5512",
|
|
"posted_date": "2023-10-24",
|
|
"amount": 14500.00,
|
|
"match_confidence": 0.998
|
|
},
|
|
"result": "AUTO_MATCH_SUCCESS",
|
|
"strategy": "EXACT_AMOUNT_REF_MATCH"
|
|
}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
<!-- Verification Timeline -->
|
|
<div class="bg-white border border-slate-200 rounded-xl p-6">
|
|
<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>
|
|
<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">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>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<!-- Interactive Detail Drawer (Hidden by default) -->
|
|
<div class="fixed inset-y-0 right-0 w-[450px] bg-white shadow-2xl translate-x-full transition-transform duration-300 z-[100] border-l border-slate-200 hidden" id="detailDrawer">
|
|
<div class="h-[72px] px-6 border-b border-slate-200 flex items-center justify-between">
|
|
<h3 class="font-headline-md text-[18px]">Transaction Details</h3>
|
|
<button class="p-2 hover:bg-slate-100 rounded-full" onclick="toggleDrawer()">
|
|
<span class="material-symbols-outlined" data-icon="close">close</span>
|
|
</button>
|
|
</div>
|
|
<div class="p-6 space-y-6">
|
|
<div class="bg-slate-50 rounded-xl p-4 border border-slate-200">
|
|
<p class="text-label-md text-slate-500 uppercase tracking-wider mb-2">Reconciliation Status</p>
|
|
<div class="flex items-center gap-3">
|
|
<span class="px-4 py-1.5 bg-danger/10 text-danger text-[14px] font-bold rounded-full">EXCEPTION FOUND</span>
|
|
<span class="text-slate-400 tabular-nums text-[12px]">Variance: ₹ 5.00</span>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<h4 class="font-label-md font-bold text-slate-700 mb-2">Audit Information</h4>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="p-3 bg-white border border-slate-100 rounded-lg">
|
|
<p class="text-[11px] text-slate-400 uppercase">Merchant ID</p>
|
|
<p class="font-bold text-on-surface">M-92834</p>
|
|
</div>
|
|
<div class="p-3 bg-white border border-slate-100 rounded-lg">
|
|
<p class="text-[11px] text-slate-400 uppercase">Gateway</p>
|
|
<p class="font-bold text-on-surface">Razorpay</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h4 class="font-label-md font-bold text-slate-700 mb-2">Internal Payload</h4>
|
|
<pre class="bg-slate-900 text-info p-4 rounded-xl text-[12px] font-mono">{
|
|
"amount": 8240.50,
|
|
"fee_deducted": 4.50,
|
|
"net_settled": 8236.00
|
|
}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
<div class="pt-6 border-t border-slate-200 space-y-3">
|
|
<button class="w-full bg-primary text-white py-3 rounded-xl font-bold hover:opacity-90 transition-opacity">Resolve with Manual Match</button>
|
|
<button class="w-full bg-white text-danger border border-danger/20 py-3 rounded-xl font-bold hover:bg-red-50 transition-colors">Mark as Fraudulent</button>
|
|
</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');
|
|
drawer.classList.toggle('hidden');
|
|
setTimeout(() => {
|
|
drawer.classList.toggle('translate-x-full');
|
|
}, 10);
|
|
}
|
|
|
|
// Add click events to 'Resolve' buttons for demo
|
|
document.querySelectorAll('button').forEach(btn => {
|
|
if (btn.innerText === 'Resolve') {
|
|
btn.addEventListener('click', toggleDrawer);
|
|
}
|
|
});
|
|
|
|
// Simple fade in for cards on load
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const cards = document.querySelectorAll('.bg-white.p-card-padding');
|
|
cards.forEach((card, index) => {
|
|
card.style.opacity = '0';
|
|
card.style.transform = 'translateY(10px)';
|
|
card.style.transition = 'all 0.4s ease-out ' + (index * 0.1) + 's';
|
|
setTimeout(() => {
|
|
card.style.opacity = '1';
|
|
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 -->
|
|
<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">
|
|
<a href="/ui" style="margin-right:8px;color:#2563eb;text-decoration:none;font-weight:600">UI Catalog</a>
|
|
<a href="/ui/hub" style="margin-right:8px;color:#2563eb;text-decoration:none;font-weight:600">Hub</a>
|
|
<a href="/ui/admin-login" style="margin-right:8px;color:#2563eb;text-decoration:none;font-weight:600">Admin Login</a>
|
|
<a href="/ui/merchant-login" style="margin-right:8px;color:#2563eb;text-decoration:none;font-weight:600">Merchant Login</a>
|
|
<a href="/ui/admin-dashboard-overview" style="margin-right:0;color:#2563eb;text-decoration:none;font-weight:600">Dashboard</a>
|
|
</div>
|
|
'
|
|
</body></html>
|