| <!DOCTYPE html>
|
| <html lang="en" class="dark">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| <title>AI Financial Reconciliation Engine 🧠</title>
|
| <script src="https://cdn.tailwindcss.com"></script>
|
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
| <script src="https://unpkg.com/lucide@latest"></script>
|
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| <script>
|
| tailwind.config = {
|
| darkMode: 'class',
|
| theme: {
|
| extend: {
|
| fontFamily: {
|
| sans: ['Inter', 'sans-serif'],
|
| mono: ['JetBrains Mono', 'monospace'],
|
| },
|
| colors: {
|
| brand: {
|
| 50: '#eef2ff',
|
| 100: '#e0e7ff',
|
| 200: '#c7d2fe',
|
| 300: '#a5b4fc',
|
| 400: '#818cf8',
|
| 500: '#6366f1',
|
| 600: '#4f46e5',
|
| 700: '#4338ca',
|
| 800: '#3730a3',
|
| 900: '#312e81',
|
| },
|
| surface: {
|
| 50: '#f8fafc',
|
| 100: '#f1f5f9',
|
| 200: '#e2e8f0',
|
| 300: '#cbd5e1',
|
| 400: '#94a3b8',
|
| 500: '#64748b',
|
| 600: '#475569',
|
| 700: '#334155',
|
| 800: '#1e293b',
|
| 900: '#0f172a',
|
| 950: '#020617',
|
| }
|
| }
|
| }
|
| }
|
| }
|
| </script>
|
| <style>
|
| * { scrollbar-width: thin; scrollbar-color: #475569 transparent; }
|
| *::-webkit-scrollbar { width: 6px; }
|
| *::-webkit-scrollbar-track { background: transparent; }
|
| *::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; }
|
|
|
| @keyframes pulse-glow {
|
| 0%, 100% { box-shadow: 0 0 8px rgba(99,102,241,0.4); }
|
| 50% { box-shadow: 0 0 20px rgba(99,102,241,0.8); }
|
| }
|
| @keyframes slide-in { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
|
| @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
|
| @keyframes shimmer {
|
| 0% { background-position: -200% 0; }
|
| 100% { background-position: 200% 0; }
|
| }
|
| @keyframes float {
|
| 0%, 100% { transform: translateY(0px); }
|
| 50% { transform: translateY(-6px); }
|
| }
|
| @keyframes count-up { from { opacity: 0.5; } to { opacity: 1; } }
|
|
|
| .animate-slide-in { animation: slide-in 0.4s ease-out forwards; }
|
| .animate-fade-in { animation: fade-in 0.3s ease-out forwards; }
|
| .animate-pulse-glow { animation: pulse-glow 2s ease-in-out infinite; }
|
| .animate-float { animation: float 3s ease-in-out infinite; }
|
| .shimmer-bg {
|
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.05), transparent);
|
| background-size: 200% 100%;
|
| animation: shimmer 2s infinite;
|
| }
|
|
|
| .glass-card {
|
| background: rgba(15, 23, 42, 0.6);
|
| backdrop-filter: blur(16px);
|
| border: 1px solid rgba(99, 102, 241, 0.15);
|
| }
|
| .glass-card-light {
|
| background: rgba(255, 255, 255, 0.7);
|
| backdrop-filter: blur(16px);
|
| border: 1px solid rgba(99, 102, 241, 0.1);
|
| }
|
| .risk-critical { border-left: 4px solid #ef4444; }
|
| .risk-high { border-left: 4px solid #f97316; }
|
| .risk-medium { border-left: 4px solid #eab308; }
|
| .risk-low { border-left: 4px solid #22c55e; }
|
|
|
| .tab-active {
|
| background: linear-gradient(135deg, #4f46e5, #6366f1);
|
| color: white;
|
| box-shadow: 0 4px 12px rgba(99,102,241,0.4);
|
| }
|
|
|
| .network-node { transition: all 0.3s ease; cursor: pointer; }
|
| .network-node:hover { filter: brightness(1.3); transform: scale(1.1); }
|
|
|
| .data-table tr { transition: background-color 0.15s ease; }
|
|
|
| .progress-bar {
|
| background: linear-gradient(90deg, #4f46e5, #818cf8, #4f46e5);
|
| background-size: 200% 100%;
|
| animation: shimmer 1.5s infinite;
|
| }
|
|
|
| .stat-card::before {
|
| content: '';
|
| position: absolute;
|
| top: 0; left: 0; right: 0;
|
| height: 3px;
|
| border-radius: 9999px 9999px 0 0;
|
| }
|
| .stat-card-purple::before { background: linear-gradient(90deg, #6366f1, #a78bfa); }
|
| .stat-card-green::before { background: linear-gradient(90deg, #22c55e, #4ade80); }
|
| .stat-card-orange::before { background: linear-gradient(90deg, #f97316, #fb923c); }
|
| .stat-card-red::before { background: linear-gradient(90deg, #ef4444, #f87171); }
|
| .stat-card-cyan::before { background: linear-gradient(90deg, #06b6d4, #22d3ee); }
|
| .stat-card-pink::before { background: linear-gradient(90deg, #ec4899, #f472b6); }
|
|
|
| .glow-text {
|
| text-shadow: 0 0 20px rgba(99,102,241,0.5);
|
| }
|
|
|
| .sidebar-link {
|
| transition: all 0.2s ease;
|
| position: relative;
|
| }
|
| .sidebar-link::before {
|
| content: '';
|
| position: absolute;
|
| left: 0; top: 0; bottom: 0;
|
| width: 3px;
|
| background: #6366f1;
|
| border-radius: 0 4px 4px 0;
|
| transform: scaleY(0);
|
| transition: transform 0.2s ease;
|
| }
|
| .sidebar-link.active::before,
|
| .sidebar-link:hover::before {
|
| transform: scaleY(1);
|
| }
|
| .sidebar-link.active {
|
| background: rgba(99, 102, 241, 0.15);
|
| color: #818cf8;
|
| }
|
|
|
| .tooltip-container { position: relative; }
|
| .tooltip-container .tooltip {
|
| position: absolute;
|
| bottom: 100%;
|
| left: 50%;
|
| transform: translateX(-50%) translateY(4px);
|
| opacity: 0;
|
| pointer-events: none;
|
| transition: all 0.2s ease;
|
| z-index: 50;
|
| }
|
| .tooltip-container:hover .tooltip {
|
| opacity: 1;
|
| transform: translateX(-50%) translateY(-4px);
|
| }
|
| </style>
|
| </head>
|
| <body class="bg-surface-950 text-surface-100 font-sans min-h-screen">
|
|
|
| <div class="fixed inset-0 pointer-events-none overflow-hidden z-0">
|
| <div class="absolute top-0 left-1/4 w-96 h-96 bg-brand-600/10 rounded-full blur-3xl"></div>
|
| <div class="absolute bottom-0 right-1/4 w-80 h-80 bg-purple-600/8 rounded-full blur-3xl"></div>
|
| <div class="absolute top-1/2 left-1/2 w-64 h-64 bg-cyan-600/5 rounded-full blur-3xl"></div>
|
| </div>
|
|
|
| <div class="flex min-h-screen relative z-10">
|
|
|
| <aside id="sidebar" class="w-64 border-r border-surface-800/50 bg-surface-950/80 backdrop-blur-xl flex flex-col transition-all duration-300 fixed lg:relative z-40 -translate-x-full lg:translate-x-0 h-screen">
|
|
|
| <div class="p-5 border-b border-surface-800/50">
|
| <div class="flex items-center gap-3">
|
| <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-purple-600 flex items-center justify-center animate-pulse-glow">
|
| <i data-lucide="brain" class="w-5 h-5 text-white"></i>
|
| </div>
|
| <div>
|
| <h1 class="text-sm font-bold text-white tracking-tight">ReconAI</h1>
|
| <p class="text-[10px] text-surface-400 font-mono uppercase tracking-widest">Financial Engine</p>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <nav class="flex-1 p-3 space-y-1 overflow-y-auto">
|
| <p class="text-[10px] uppercase tracking-widest text-surface-500 font-semibold px-3 py-2">Main</p>
|
| <button onclick="switchTab('dashboard')" class="sidebar-link active w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="dashboard">
|
| <i data-lucide="layout-dashboard" class="w-4 h-4"></i> Dashboard
|
| </button>
|
| <button onclick="switchTab('reconciliation')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="reconciliation">
|
| <i data-lucide="git-merge" class="w-4 h-4"></i> Reconciliation
|
| </button>
|
| <button onclick="switchTab('anomaly')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="anomaly">
|
| <i data-lucide="alert-triangle" class="w-4 h-4"></i> Anomaly Detection
|
| </button>
|
|
|
| <p class="text-[10px] uppercase tracking-widest text-surface-500 font-semibold px-3 py-2 mt-4">Analysis</p>
|
| <button onclick="switchTab('fraud')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="fraud">
|
| <i data-lucide="network" class="w-4 h-4"></i> Fraud Network
|
| </button>
|
| <button onclick="switchTab('ai-explain')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="ai-explain">
|
| <i data-lucide="message-square-text" class="w-4 h-4"></i> ReconAI
|
| </button>
|
| <button onclick="switchTab('vector')" class="sidebar-link w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-surface-300 hover:text-white" data-nav="vector">
|
| <i data-lucide="database" class="w-4 h-4"></i> Vector Memory
|
| </button>
|
| </nav>
|
|
|
|
|
| <div class="p-4 border-t border-surface-800/50">
|
| <div class="glass-card rounded-lg p-3">
|
| <div class="flex items-center gap-2 mb-2">
|
| <span class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
|
| <span class="text-xs text-surface-300">Engine Online</span>
|
| </div>
|
| <div class="flex items-center gap-2 text-[10px] text-surface-500">
|
| <span>FAISS: Active</span>
|
| <span>•</span>
|
| <span>LLM: Connected</span>
|
| </div>
|
| </div>
|
| </div>
|
| </aside>
|
|
|
|
|
| <div id="sidebar-overlay" class="fixed inset-0 bg-black/50 z-30 hidden lg:hidden" onclick="toggleSidebar()"></div>
|
|
|
|
|
| <main class="flex-1 flex flex-col min-h-screen overflow-x-hidden">
|
|
|
| <header class="sticky top-0 z-20 border-b border-surface-800/50 bg-surface-950/80 backdrop-blur-xl">
|
| <div class="flex items-center justify-between px-4 lg:px-6 py-3">
|
| <div class="flex items-center gap-3">
|
| <button onclick="toggleSidebar()" class="lg:hidden p-2 rounded-lg hover:bg-surface-800 transition">
|
| <i data-lucide="menu" class="w-5 h-5"></i>
|
| </button>
|
| <div>
|
| <h2 id="page-title" class="text-lg font-semibold text-white">Dashboard</h2>
|
| <p id="page-subtitle" class="text-xs text-surface-400">Financial reconciliation overview</p>
|
| </div>
|
| </div>
|
| <div class="flex items-center gap-2">
|
| <div class="hidden sm:flex items-center gap-2 bg-surface-900 border border-surface-700/50 rounded-lg px-3 py-2">
|
| <i data-lucide="search" class="w-4 h-4 text-surface-400"></i>
|
| <input type="text" placeholder="Search transactions..." class="bg-transparent text-sm text-surface-200 placeholder-surface-500 outline-none w-40 lg:w-56">
|
| </div>
|
| <input type="file" id="books-file" class="hidden" accept=".csv" onchange="updateFileLabel('books')">
|
| <input type="file" id="gst-file" class="hidden" accept=".csv" onchange="updateFileLabel('gst')">
|
|
|
| <button onclick="document.getElementById('books-file').click()" id="btn-books" class="flex items-center gap-2 bg-surface-800 hover:bg-surface-700 text-surface-300 text-sm font-medium px-3 py-2 rounded-lg transition-all border border-surface-700/50">
|
| <i data-lucide="upload-cloud" class="w-4 h-4"></i>
|
| <span class="hidden sm:inline" id="lbl-books">Books CSV</span>
|
| </button>
|
| <button onclick="document.getElementById('gst-file').click()" id="btn-gst" class="flex items-center gap-2 bg-surface-800 hover:bg-surface-700 text-surface-300 text-sm font-medium px-3 py-2 rounded-lg transition-all border border-surface-700/50">
|
| <i data-lucide="upload-cloud" class="w-4 h-4"></i>
|
| <span class="hidden sm:inline" id="lbl-gst">GST CSV</span>
|
| </button>
|
|
|
| <button onclick="runReconciliation()" id="btn-run" class="flex items-center gap-2 bg-gradient-to-r from-brand-600 to-purple-600 hover:from-brand-500 hover:to-purple-500 text-white text-sm font-medium px-4 py-2 rounded-lg transition-all shadow-lg shadow-brand-600/25">
|
| <i data-lucide="play" class="w-4 h-4"></i>
|
| <span class="hidden sm:inline">Run Engine</span>
|
| </button>
|
|
|
| <button onclick="exportCSV()" class="p-2 rounded-lg hover:bg-surface-800 transition tooltip-container relative">
|
| <i data-lucide="download" class="w-4 h-4 text-surface-400"></i>
|
| <div class="tooltip bg-surface-800 text-xs px-2 py-1 rounded whitespace-nowrap">Export CSV</div>
|
| </button>
|
|
|
| </div>
|
| </div>
|
| </header>
|
|
|
|
|
| <div class="flex-1 p-4 lg:p-6 space-y-6">
|
|
|
|
|
| <section id="tab-dashboard" class="tab-content space-y-6">
|
|
|
| <div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3 lg:gap-4">
|
| <div class="stat-card stat-card-purple glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:0ms">
|
| <div class="flex items-center justify-between mb-2">
|
| <span class="text-xs text-surface-400 font-medium">Total Records</span>
|
| <div class="w-8 h-8 rounded-lg bg-brand-600/20 flex items-center justify-center">
|
| <i data-lucide="file-text" class="w-4 h-4 text-brand-400"></i>
|
| </div>
|
| </div>
|
| <p id="stat-total-records" class="text-2xl font-bold text-white">0</p>
|
| <p id="stat-total-records-sub" class="text-[10px] text-green-400 mt-1 flex items-center gap-1">Awaiting data</p>
|
| </div>
|
| <div class="stat-card stat-card-green glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:80ms">
|
| <div class="flex items-center justify-between mb-2">
|
| <span class="text-xs text-surface-400 font-medium">Matched</span>
|
| <div class="w-8 h-8 rounded-lg bg-green-600/20 flex items-center justify-center">
|
| <i data-lucide="check-circle" class="w-4 h-4 text-green-400"></i>
|
| </div>
|
| </div>
|
| <p id="stat-matched" class="text-2xl font-bold text-white">0</p>
|
| <p id="stat-matched-sub" class="text-[10px] text-green-400 mt-1">Awaiting data</p>
|
| </div>
|
| <div class="stat-card stat-card-orange glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:160ms">
|
| <div class="flex items-center justify-between mb-2">
|
| <span class="text-xs text-surface-400 font-medium">Unmatched</span>
|
| <div class="w-8 h-8 rounded-lg bg-orange-600/20 flex items-center justify-center">
|
| <i data-lucide="x-circle" class="w-4 h-4 text-orange-400"></i>
|
| </div>
|
| </div>
|
| <p id="stat-unmatched" class="text-2xl font-bold text-white">0</p>
|
| <p id="stat-unmatched-sub" class="text-[10px] text-orange-400 mt-1">Awaiting data</p>
|
| </div>
|
| <div class="stat-card stat-card-red glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:240ms">
|
| <div class="flex items-center justify-between mb-2">
|
| <span class="text-xs text-surface-400 font-medium">Anomalies</span>
|
| <div class="w-8 h-8 rounded-lg bg-red-600/20 flex items-center justify-center">
|
| <i data-lucide="alert-triangle" class="w-4 h-4 text-red-400"></i>
|
| </div>
|
| </div>
|
| <p id="stat-anomalies" class="text-2xl font-bold text-white">0</p>
|
| <p id="stat-anomalies-sub" class="text-[10px] text-red-400 mt-1">Awaiting data</p>
|
| </div>
|
| <div class="stat-card stat-card-cyan glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:320ms">
|
| <div class="flex items-center justify-between mb-2">
|
| <span class="text-xs text-surface-400 font-medium">Fraud Rings</span>
|
| <div class="w-8 h-8 rounded-lg bg-cyan-600/20 flex items-center justify-center">
|
| <i data-lucide="network" class="w-4 h-4 text-cyan-400"></i>
|
| </div>
|
| </div>
|
| <p id="stat-fraud-rings" class="text-2xl font-bold text-white">0</p>
|
| <p id="stat-fraud-rings-sub" class="text-[10px] text-cyan-400 mt-1">Awaiting data</p>
|
| </div>
|
| <div class="stat-card stat-card-pink glass-card rounded-xl p-4 relative overflow-hidden animate-slide-in" style="animation-delay:400ms">
|
| <div class="flex items-center justify-between mb-2">
|
| <span class="text-xs text-surface-400 font-medium">Risk Score</span>
|
| <div class="w-8 h-8 rounded-lg bg-pink-600/20 flex items-center justify-center">
|
| <i data-lucide="gauge" class="w-4 h-4 text-pink-400"></i>
|
| </div>
|
| </div>
|
| <p id="stat-risk-score" class="text-2xl font-bold text-white">0.0<span class="text-sm text-surface-400">/10</span></p>
|
| <p id="stat-risk-score-sub" class="text-[10px] text-pink-400 mt-1">Awaiting data</p>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
|
| <div class="glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:100ms">
|
| <div class="flex items-center justify-between mb-4">
|
| <h3 class="font-semibold text-white">Reconciliation Trend</h3>
|
| </div>
|
| <div class="relative w-full h-[250px]">
|
| <canvas id="chart-recon-trend" class="w-full"></canvas>
|
| </div>
|
| </div>
|
| <div class="glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:200ms">
|
| <div class="flex items-center justify-between mb-4">
|
| <h3 class="font-semibold text-white">Anomaly Distribution</h3>
|
| </div>
|
| <div class="relative w-full h-[250px]">
|
| <canvas id="chart-anomaly-dist" class="w-full"></canvas>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-6">
|
| <div class="glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:150ms">
|
| <h3 class="font-semibold text-white mb-4">Match Confidence</h3>
|
| <div class="relative w-full h-[250px]">
|
| <canvas id="chart-confidence"></canvas>
|
| </div>
|
| </div>
|
| <div class="lg:col-span-2 glass-card rounded-xl p-5 animate-slide-in" style="animation-delay:250ms">
|
| <div class="flex items-center justify-between mb-4">
|
| <h3 class="font-semibold text-white">Recent Alerts</h3>
|
| <span class="text-xs bg-red-500/20 text-red-400 px-2 py-0.5 rounded-full font-medium"><span id="alerts-count">0</span> active</span>
|
| </div>
|
| <div id="recent-alerts" class="space-y-3 max-h-[280px] overflow-y-auto pr-2">
|
| <p class="text-sm text-surface-400">No alerts yet.</p>
|
| </div>
|
| </div>
|
| </div>
|
| </section>
|
|
|
|
|
| <section id="tab-reconciliation" class="tab-content hidden space-y-6">
|
|
|
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
| <div class="glass-card rounded-xl p-4 flex items-center gap-4">
|
| <div class="w-12 h-12 rounded-xl bg-green-600/20 flex items-center justify-center">
|
| <i data-lucide="check-circle-2" class="w-6 h-6 text-green-400"></i>
|
| </div>
|
| <div>
|
| <p id="recon-stat-exact" class="text-2xl font-bold text-white">0</p>
|
| <p class="text-xs text-surface-400">Exact Matches</p>
|
| </div>
|
| </div>
|
| <div class="glass-card rounded-xl p-4 flex items-center gap-4">
|
| <div class="w-12 h-12 rounded-xl bg-brand-600/20 flex items-center justify-center">
|
| <i data-lucide="fuzzy" class="w-6 h-6 text-brand-400"></i>
|
| </div>
|
| <div>
|
| <p id="recon-stat-fuzzy" class="text-2xl font-bold text-white">0</p>
|
| <p class="text-xs text-surface-400">Fuzzy Matches</p>
|
| </div>
|
| </div>
|
| <div class="glass-card rounded-xl p-4 flex items-center gap-4">
|
| <div class="w-12 h-12 rounded-xl bg-purple-600/20 flex items-center justify-center">
|
| <i data-lucide="sparkles" class="w-6 h-6 text-purple-400"></i>
|
| </div>
|
| <div>
|
| <p id="recon-stat-semantic" class="text-2xl font-bold text-white">0</p>
|
| <p class="text-xs text-surface-400">AI Semantic</p>
|
| </div>
|
| </div>
|
| <div class="glass-card rounded-xl p-4 flex items-center gap-4">
|
| <div class="w-12 h-12 rounded-xl bg-red-600/20 flex items-center justify-center">
|
| <i data-lucide="x-circle" class="w-6 h-6 text-red-400"></i>
|
| </div>
|
| <div>
|
| <p id="recon-stat-unmatched" class="text-2xl font-bold text-white">0</p>
|
| <p class="text-xs text-surface-400">Unmatched</p>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="glass-card rounded-xl overflow-hidden">
|
| <div class="flex items-center justify-between p-4 border-b border-surface-800/50">
|
| <h3 class="font-semibold text-white">Reconciliation Results</h3>
|
| <div class="flex items-center gap-2">
|
| <select id="match-filter" onchange="filterReconTable()" class="bg-surface-800/50 border border-surface-700/50 rounded-lg text-xs px-3 py-1.5 text-surface-300 outline-none">
|
| <option value="all">All Results</option>
|
| <option value="matched">Matched</option>
|
| <option value="fuzzy">Fuzzy Match</option>
|
| <option value="unmatched">Unmatched</option>
|
| </select>
|
| <button class="flex items-center gap-1.5 bg-brand-600/20 text-brand-300 text-xs font-medium px-3 py-1.5 rounded-lg hover:bg-brand-600/30 transition">
|
| <i data-lucide="refresh-cw" class="w-3 h-3"></i> Refresh
|
| </button>
|
| </div>
|
| </div>
|
| <div class="overflow-x-auto">
|
| <table class="w-full text-sm data-table">
|
| <thead>
|
| <tr class="bg-surface-900/50 text-surface-400 text-xs uppercase tracking-wider">
|
| <th class="px-4 py-3 text-left font-semibold">Books Entry</th>
|
| <th class="px-4 py-3 text-left font-semibold">GST Entry</th>
|
| <th class="px-4 py-3 text-left font-semibold">Amount</th>
|
| <th class="px-4 py-3 text-left font-semibold">Match Type</th>
|
| <th class="px-4 py-3 text-left font-semibold">Confidence</th>
|
| <th class="px-4 py-3 text-left font-semibold">Status</th>
|
| </tr>
|
| </thead>
|
| <tbody id="recon-table-body">
|
| </tbody>
|
| </table>
|
| </div>
|
| <div class="flex items-center justify-between p-4 border-t border-surface-800/50">
|
| <p class="text-xs text-surface-400">Showing <span id="showing-count" id="showing-count">--</span> of <span id="total-count" id="total-count">--</span> results</p>
|
| <div class="flex items-center gap-1">
|
| <button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">Prev</button>
|
| <button class="px-3 py-1 rounded-md bg-brand-600 text-white text-xs">1</button>
|
| <button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">2</button>
|
| <button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">3</button>
|
| <button class="px-3 py-1 rounded-md bg-surface-800/50 text-surface-400 text-xs hover:bg-surface-700 transition">Next</button>
|
| </div>
|
| </div>
|
| </div>
|
| </section>
|
|
|
|
|
| <section id="tab-anomaly" class="tab-content hidden space-y-6">
|
|
|
| <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
| <div class="glass-card rounded-xl p-5">
|
| <div class="flex items-center gap-3 mb-3">
|
| <div class="w-10 h-10 rounded-lg bg-red-600/20 flex items-center justify-center">
|
| <i data-lucide="shield-alert" class="w-5 h-5 text-red-400"></i>
|
| </div>
|
| <div>
|
| <p class="text-lg font-bold text-white">Isolation Forest</p>
|
| <p class="text-[10px] text-surface-400 font-mono">contamination=0.05</p>
|
| </div>
|
| </div>
|
| <p id="anomaly-trained-sub" class="text-xs text-surface-400 mb-2">Anomaly detection model pending execution.</p>
|
| <div class="flex items-center gap-2">
|
| <span class="px-2 py-0.5 bg-green-500/20 text-green-400 text-[10px] font-semibold rounded-full">ACTIVE</span>
|
| <span class="px-2 py-0.5 bg-brand-500/20 text-brand-400 text-[10px] font-semibold rounded-full">v2.4</span>
|
| </div>
|
| </div>
|
| <div class="glass-card rounded-xl p-5">
|
| <div class="flex items-center gap-3 mb-3">
|
| <div class="w-10 h-10 rounded-lg bg-orange-600/20 flex items-center justify-center">
|
| <i data-lucide="radar" class="w-5 h-5 text-orange-400"></i>
|
| </div>
|
| <div>
|
| <p class="text-lg font-bold text-white">Score Range</p>
|
| <p id="anomaly-score-range" class="text-[10px] text-surface-400 font-mono">--</p>
|
| </div>
|
| </div>
|
| <div class="space-y-2">
|
| <div class="flex items-center justify-between text-xs">
|
| <span class="text-surface-400">Critical (> 0.3)</span>
|
| <span id="anomaly-crit-count" class="text-red-400 font-mono">0</span>
|
| </div>
|
| <div class="w-full bg-surface-800 rounded-full h-1.5"><div id="anomaly-crit-bar" class="bg-red-500 h-1.5 rounded-full" style="width:0%"></div></div>
|
| <div class="flex items-center justify-between text-xs">
|
| <span class="text-surface-400">High (0.1 to 0.3)</span>
|
| <span id="anomaly-high-count" class="text-orange-400 font-mono">0</span>
|
| </div>
|
| <div class="w-full bg-surface-800 rounded-full h-1.5"><div id="anomaly-high-bar" class="bg-orange-500 h-1.5 rounded-full" style="width:0%"></div></div>
|
| <div class="flex items-center justify-between text-xs">
|
| <span class="text-surface-400">Medium (< 0.1)</span>
|
| <span id="anomaly-med-count" class="text-yellow-400 font-mono">0</span>
|
| </div>
|
| <div class="w-full bg-surface-800 rounded-full h-1.5"><div id="anomaly-med-bar" class="bg-yellow-500 h-1.5 rounded-full" style="width:0%"></div></div>
|
| </div>
|
| </div>
|
| <div class="glass-card rounded-xl p-5">
|
| <div class="flex items-center gap-3 mb-3">
|
| <div class="w-10 h-10 rounded-lg bg-brand-600/20 flex items-center justify-center">
|
| <i data-lucide="brain" class="w-5 h-5 text-brand-400"></i>
|
| </div>
|
| <div>
|
| <p class="text-lg font-bold text-white">AI LLM Analysis</p>
|
| <p class="text-[10px] text-surface-400 font-mono">ReconAI</p>
|
| </div>
|
| </div>
|
| <p class="text-xs text-surface-400 mb-3">Natural language explanations generated for each anomaly.</p>
|
| <p class="text-xs text-brand-300 bg-brand-900/20 p-2 rounded border border-brand-500/20">Click "View" on any flagged transaction below to generate a real-time audit explanation.</p>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="glass-card rounded-xl overflow-hidden">
|
| <div class="flex items-center justify-between p-4 border-b border-surface-800/50">
|
| <h3 class="font-semibold text-white flex items-center gap-2">
|
| <i data-lucide="alert-triangle" class="w-4 h-4 text-red-400"></i>
|
| Flagged Transactions
|
| </h3>
|
| <div class="flex items-center gap-2">
|
| <button class="text-xs px-3 py-1.5 rounded-lg bg-red-600/20 text-red-400 hover:bg-red-600/30 transition font-medium">Critical (<span id="filter-crit">0</span>)</button>
|
| <button class="text-xs px-3 py-1.5 rounded-lg bg-surface-800/50 text-surface-300 hover:bg-surface-700/50 transition">All (<span id="filter-all">0</span>)</button>
|
| </div>
|
| </div>
|
| <div class="overflow-x-auto">
|
| <table class="w-full text-sm data-table">
|
| <thead>
|
| <tr class="bg-surface-900/50 text-surface-400 text-xs uppercase tracking-wider">
|
| <th class="px-4 py-3 text-left font-semibold">Invoice</th>
|
| <th class="px-4 py-3 text-left font-semibold">Vendor</th>
|
| <th class="px-4 py-3 text-left font-semibold">Amount</th>
|
| <th class="px-4 py-3 text-left font-semibold">Anomaly Score</th>
|
| <th class="px-4 py-3 text-left font-semibold">Risk Level</th>
|
| <th class="px-4 py-3 text-left font-semibold">AI Explanation</th>
|
| </tr>
|
| </thead>
|
| <tbody id="anomaly-table-body">
|
| </tbody>
|
| </table>
|
| </div>
|
| </div>
|
| </section>
|
|
|
|
|
| <section id="tab-fraud" class="tab-content hidden space-y-6">
|
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
| <div class="lg:col-span-2 glass-card rounded-xl overflow-hidden">
|
| <div class="flex items-center justify-between p-4 border-b border-surface-800/50">
|
| <h3 class="font-semibold text-white flex items-center gap-2">
|
| <i data-lucide="network" class="w-4 h-4 text-cyan-400"></i>
|
| Circular Trading Network
|
| </h3>
|
| <div class="flex items-center gap-2">
|
| </div>
|
| </div>
|
| <div class="relative">
|
| <canvas id="fraud-canvas" width="800" height="500" class="w-full bg-surface-950/50"></canvas>
|
| <div class="absolute top-3 left-3 bg-surface-900/80 backdrop-blur-sm border border-surface-700/30 rounded-lg p-3 text-xs space-y-2">
|
| <div class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-red-500"></span> Critical Node</div>
|
| <div class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-surface-500"></span> Linked</div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="space-y-4">
|
| <div class="glass-card rounded-xl p-5">
|
| <h3 class="font-semibold text-white mb-3 flex items-center gap-2">
|
| <i data-lucide="shield-alert" class="w-4 h-4 text-red-400"></i>
|
| Detected Rings
|
| </h3>
|
| <div class="space-y-3" id="fraud-rings-list">
|
| <p class="text-sm text-surface-400">No fraud rings detected yet. Run Engine to analyze.</p>
|
| </div>
|
| </div>
|
| <div class="glass-card rounded-xl p-5">
|
| <h3 class="font-semibold text-white mb-3 flex items-center gap-2">
|
| <i data-lucide="bar-chart-3" class="w-4 h-4 text-brand-400"></i>
|
| Network Stats
|
| </h3>
|
| <div class="space-y-3">
|
| <div class="flex items-center justify-between">
|
| <span class="text-xs text-surface-400">Total Nodes</span>
|
| <span id="fraud-nodes" class="text-sm font-mono text-white">0</span>
|
| </div>
|
| <div class="flex items-center justify-between">
|
| <span class="text-xs text-surface-400">Total Edges</span>
|
| <span id="fraud-edges" class="text-sm font-mono text-white">0</span>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </section>
|
|
|
|
|
| <section id="tab-ai-explain" class="tab-content hidden space-y-6">
|
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
| <div class="space-y-4">
|
| <div class="glass-card rounded-xl p-5">
|
| <h3 class="font-semibold text-white mb-4 flex items-center gap-2">
|
| <i data-lucide="message-square-text" class="w-4 h-4 text-brand-400"></i>
|
| Ask the AI Auditor
|
| </h3>
|
| <div class="space-y-3 mb-4">
|
| <div id="chat-messages" class="space-y-3 max-h-[400px] overflow-y-auto pr-1">
|
| <div class="flex gap-3">
|
| <div class="w-7 h-7 rounded-full bg-brand-600/30 flex items-center justify-center shrink-0">
|
| <i data-lucide="bot" class="w-3.5 h-3.5 text-brand-400"></i>
|
| </div>
|
| <div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
|
| <p class="text-sm text-surface-200">Welcome! I'm your AI audit assistant powered by ReconAI. Ask me about any discrepancy, anomaly, or vendor relationship and I'll provide a detailed explanation.</p>
|
| <p class="text-[10px] text-surface-500 mt-2 font-mono">model: ReconAI-instruct</p>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| <div class="flex items-center gap-2">
|
| <input id="chat-input" type="text" placeholder="Ask about a discrepancy..." class="flex-1 bg-surface-900/50 border border-surface-700/50 rounded-lg px-3 py-2 text-sm text-white placeholder-surface-500 outline-none focus:border-brand-500/50 focus:ring-1 focus:ring-brand-500/25 transition" onkeydown="if(event.key==='Enter')sendChatMessage()">
|
| <button onclick="sendChatMessage()" class="p-2 bg-brand-600 hover:bg-brand-500 rounded-lg transition">
|
| <i data-lucide="send" class="w-4 h-4 text-white"></i>
|
| </button>
|
| </div>
|
| <div class="flex items-center gap-2 mt-3 flex-wrap">
|
| <button onclick="askSuggested('Explain the highest risk anomaly detected in this batch')" class="text-[10px] px-2 py-1 rounded-md bg-surface-800/50 text-surface-400 hover:text-white hover:bg-surface-700 transition">Explain highest risk anomaly</button>
|
| <button onclick="askSuggested('Summarize the top reasons for discrepancies')" class="text-[10px] px-2 py-1 rounded-md bg-surface-800/50 text-surface-400 hover:text-white hover:bg-surface-700 transition">Summarize discrepancies</button>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="glass-card rounded-xl p-5">
|
| <h3 class="font-semibold text-white mb-4 flex items-center gap-2">
|
| <i data-lucide="sparkles" class="w-4 h-4 text-purple-400"></i>
|
| Generated Explanations
|
| </h3>
|
| <div class="space-y-3 max-h-[530px] overflow-y-auto pr-1" id="explanations-list">
|
| </div>
|
| </div>
|
| </div>
|
| </section>
|
|
|
|
|
| <section id="tab-vector" class="tab-content hidden space-y-6">
|
| <div class="flex justify-center">
|
| <div class="glass-card rounded-xl p-5 w-full max-w-md">
|
| <div class="flex items-center gap-3 mb-4">
|
| <div class="w-10 h-10 rounded-lg bg-brand-600/20 flex items-center justify-center">
|
| <i data-lucide="database" class="w-5 h-5 text-brand-400"></i>
|
| </div>
|
| <div>
|
| <p class="font-semibold text-white">FAISS Index</p>
|
| <p class="text-[10px] text-surface-400 font-mono">L2 • 384-dim • FlatL2</p>
|
| </div>
|
| </div>
|
| <div class="space-y-3">
|
| <div>
|
| <div class="flex items-center justify-between text-xs mb-1">
|
| <span class="text-surface-400">Index Size</span>
|
| <span id="faiss-vectors" class="text-white font-mono">-- vectors</span>
|
| </div>
|
| </div>
|
| <div>
|
| <div class="flex items-center justify-between text-xs mb-1">
|
| <span class="text-surface-400">Memory Used</span>
|
| <span id="faiss-memory" class="text-white font-mono">-- MB</span>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </section>
|
|
|
| </div>
|
|
|
|
|
| <div id="process-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
| <div class="glass-card rounded-2xl p-8 max-w-md w-full mx-4 text-center">
|
| <div class="w-16 h-16 rounded-full bg-brand-600/20 flex items-center justify-center mx-auto mb-4 animate-pulse-glow">
|
| <i data-lucide="brain" class="w-8 h-8 text-brand-400"></i>
|
| </div>
|
| <h3 class="text-lg font-semibold text-white mb-2">Running Reconciliation</h3>
|
| <p class="text-sm text-surface-400 mb-4" id="process-step">Initializing engine...</p>
|
| <div class="w-full bg-surface-800 rounded-full h-2 overflow-hidden">
|
| <div id="process-bar" class="progress-bar h-2 rounded-full transition-all duration-500" style="width: 0%"></div>
|
| </div>
|
| <p class="text-xs text-surface-500 mt-3 font-mono" id="process-pct">0%</p>
|
| </div>
|
| </div>
|
|
|
|
|
| <div id="toast-container" class="fixed bottom-4 right-4 z-50 space-y-2"></div>
|
| </main>
|
| </div>
|
|
|
| <script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| const titles = {
|
| dashboard: ['Dashboard', 'Financial reconciliation overview'],
|
| reconciliation: ['Reconciliation', 'Intelligent matching with Fuzzy + AI semantic analysis'],
|
| anomaly: ['Anomaly Detection', 'IsolationForest-powered anomaly detection'],
|
| fraud: ['Fraud Network', 'NetworkX circular trading visualization'],
|
| 'ai-explain': ['ReconAI', 'ReconAI LLM-powered audit commentary'],
|
| vector: ['Vector Memory', 'FAISS persistent vector index management']
|
| };
|
|
|
| function switchTab(tab) {
|
| document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
| document.getElementById('tab-' + tab).classList.remove('hidden');
|
| document.querySelectorAll('.sidebar-link').forEach(el => {
|
| el.classList.toggle('active', el.dataset.nav === tab);
|
| });
|
| if (titles[tab]) {
|
| document.getElementById('page-title').textContent = titles[tab][0];
|
| document.getElementById('page-subtitle').textContent = titles[tab][1];
|
| }
|
|
|
| const sidebar = document.getElementById('sidebar');
|
| const overlay = document.getElementById('sidebar-overlay');
|
| sidebar.classList.add('-translate-x-full');
|
| overlay.classList.add('hidden');
|
|
|
|
|
| if (tab === 'fraud') {
|
| setTimeout(drawFraudNetwork, 100);
|
| }
|
| }
|
|
|
| function toggleSidebar() {
|
| const sidebar = document.getElementById('sidebar');
|
| const overlay = document.getElementById('sidebar-overlay');
|
| sidebar.classList.toggle('-translate-x-full');
|
| overlay.classList.toggle('hidden');
|
| }
|
|
|
| let isDark = true;
|
| function toggleTheme() {
|
| isDark = !isDark;
|
| document.documentElement.classList.toggle('dark', isDark);
|
| }
|
|
|
|
|
|
|
| async function showExplanation(invoiceId) {
|
| const anomaly = window.anomalyDataMap ? window.anomalyDataMap[invoiceId] : null;
|
| if (!anomaly) return;
|
|
|
| switchTab('ai-explain');
|
|
|
|
|
| addChatMessage(`Explain the anomaly for invoice ${invoiceId} and vendor ${anomaly.VendorName_books}`, false);
|
|
|
|
|
| const chatMessages = document.getElementById('chat-messages');
|
| const loadingDiv = document.createElement('div');
|
| loadingDiv.id = 'chat-loading';
|
| loadingDiv.className = 'flex gap-3 animate-slide-in';
|
| loadingDiv.innerHTML = `
|
| <div class="w-7 h-7 rounded-full bg-brand-600/30 flex items-center justify-center shrink-0">
|
| <i data-lucide="bot" class="w-3.5 h-3.5 text-brand-400"></i>
|
| </div>
|
| <div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
|
| <p class="text-sm text-surface-400 flex items-center gap-2"><i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i> Analyzing with ReconAI...</p>
|
| </div>
|
| `;
|
| chatMessages.appendChild(loadingDiv);
|
| lucide.createIcons();
|
|
|
| try {
|
| const res = await fetch('/api/explain', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({
|
| row: anomaly,
|
| match_status: anomaly.MatchStatus,
|
| b_vendor: anomaly.VendorName_books,
|
| g_vendor: anomaly.VendorName_gst,
|
| b_amount: anomaly.Amount_books,
|
| g_amount: anomaly.Amount_gst
|
| })
|
| });
|
|
|
| const result = await res.json();
|
| document.getElementById('chat-loading')?.remove();
|
|
|
| if (result.explanation) {
|
| addChatMessage(result.explanation, true);
|
|
|
| const expList = document.getElementById('explanations-list');
|
| if (expList) {
|
| const div = document.createElement('div');
|
| div.className = 'p-3 rounded-lg border border-surface-700/50 bg-surface-900/50 text-sm text-surface-200 animate-slide-in';
|
| div.innerHTML = `
|
| <div class="flex items-center justify-between mb-2 pb-2 border-b border-surface-800">
|
| <span class="text-xs font-semibold text-brand-400">Invoice ${invoiceId}</span>
|
| <span class="text-[10px] text-surface-500 font-mono">ReconAI</span>
|
| </div>
|
| <p class="text-xs">${result.explanation}</p>
|
| `;
|
| expList.prepend(div);
|
| }
|
| } else {
|
| addChatMessage('Error generating explanation.', true);
|
| }
|
| } catch (err) {
|
| document.getElementById('chat-loading')?.remove();
|
| addChatMessage('API Error: ' + err.message, true);
|
| }
|
| }
|
| function addChatMessage(text, isAI = false) {
|
| const chatMessages = document.getElementById('chat-messages');
|
| const msgDiv = document.createElement('div');
|
| msgDiv.className = 'flex gap-3 animate-slide-in';
|
| if (isAI) {
|
| msgDiv.innerHTML = `
|
| <div class="w-7 h-7 rounded-full bg-brand-600/30 flex items-center justify-center shrink-0">
|
| <i data-lucide="bot" class="w-3.5 h-3.5 text-brand-400"></i>
|
| </div>
|
| <div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
|
| <p class="text-sm text-surface-200">${text}</p>
|
| <p class="text-[10px] text-surface-500 mt-2 font-mono">model: ReconAI-instruct</p>
|
| </div>
|
| `;
|
| } else {
|
| msgDiv.innerHTML = `
|
| <div class="w-7 h-7 rounded-full bg-purple-600/30 flex items-center justify-center shrink-0">
|
| <i data-lucide="user" class="w-3.5 h-3.5 text-purple-400"></i>
|
| </div>
|
| <div class="glass-card rounded-lg rounded-tl-none p-3 max-w-[90%]">
|
| <p class="text-sm text-surface-200">${text}</p>
|
| </div>
|
| `;
|
| }
|
| chatMessages.appendChild(msgDiv);
|
| chatMessages.scrollTop = chatMessages.scrollHeight;
|
| lucide.createIcons();
|
| }
|
|
|
| async function sendChatMessage() {
|
| const input = document.getElementById('chat-input');
|
| const text = input.value.trim();
|
| if (!text) return;
|
| addChatMessage(text, false);
|
| input.value = '';
|
|
|
|
|
| try {
|
| const res = await fetch('/api/explain', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ row: {}, match_status: text, b_vendor: 'N/A', g_vendor: 'N/A', b_amount: 0, g_amount: 0 })
|
| });
|
| const result = await res.json();
|
| addChatMessage(result.explanation || 'No response from AI.', true);
|
| } catch (err) {
|
| addChatMessage('API Error: ' + err.message, true);
|
| }
|
| }
|
|
|
| function askSuggested(text) {
|
| document.getElementById('chat-input').value = text;
|
| sendChatMessage();
|
| }
|
|
|
|
|
|
|
|
|
| function drawFraudNetwork() {
|
| const canvas = document.getElementById('fraud-canvas');
|
| if (!canvas) return;
|
| const ctx = canvas.getContext('2d');
|
| const dpr = window.devicePixelRatio || 1;
|
| const rect = canvas.getBoundingClientRect();
|
| canvas.width = rect.width * dpr;
|
| canvas.height = rect.height * dpr;
|
| ctx.scale(dpr, dpr);
|
| const W = rect.width;
|
| const H = rect.height;
|
| ctx.clearRect(0, 0, W, H);
|
|
|
| if (!window.fraudNetworkData || !window.fraudNetworkData.nodes || window.fraudNetworkData.nodes.length === 0) {
|
| ctx.font = '14px Inter';
|
| ctx.fillStyle = '#64748b';
|
| ctx.textAlign = 'center';
|
| ctx.fillText('No fraud network data yet.', W/2, H/2 - 10);
|
| ctx.font = '12px Inter';
|
| ctx.fillText('Upload CSVs and Run Engine to analyze transaction networks.', W/2, H/2 + 15);
|
| return;
|
| }
|
|
|
| const nodes = window.fraudNetworkData.nodes;
|
| const edges = window.fraudNetworkData.edges;
|
| const cycles = window.fraudNetworkData.cycles || [];
|
|
|
|
|
| const centerX = W / 2;
|
| const centerY = H / 2;
|
| const radius = Math.min(W, H) / 2 - 40;
|
|
|
| nodes.forEach((node, i) => {
|
| if (!node.x) {
|
| const angle = (i / nodes.length) * 2 * Math.PI;
|
| node.x = centerX + radius * Math.cos(angle);
|
| node.y = centerY + radius * Math.sin(angle);
|
| }
|
| });
|
|
|
|
|
| edges.forEach(edge => {
|
| const from = nodes[edge.from];
|
| const to = nodes[edge.to];
|
| if (!from || !to) return;
|
|
|
|
|
| let inCycle = false;
|
| for (let c of cycles) {
|
| const idx1 = c.indexOf(from.id);
|
| const idx2 = c.indexOf(to.id);
|
| if (idx1 !== -1 && idx2 !== -1) {
|
| if ((idx1 + 1) % c.length === idx2) {
|
| inCycle = true;
|
| break;
|
| }
|
| }
|
| }
|
|
|
| ctx.beginPath();
|
| ctx.moveTo(from.x, from.y);
|
| ctx.lineTo(to.x, to.y);
|
| ctx.strokeStyle = inCycle ? '#ef4444' : '#64748b66';
|
| ctx.lineWidth = inCycle ? 2 : 1;
|
| ctx.stroke();
|
|
|
|
|
| const angle = Math.atan2(to.y - from.y, to.x - from.x);
|
| const midX = from.x + (to.x - from.x) * 0.7;
|
| const midY = from.y + (to.y - from.y) * 0.7;
|
| ctx.beginPath();
|
| ctx.moveTo(midX + 6 * Math.cos(angle), midY + 6 * Math.sin(angle));
|
| ctx.lineTo(midX - 6 * Math.cos(angle) + 4 * Math.cos(angle + Math.PI/2), midY - 6 * Math.sin(angle) + 4 * Math.sin(angle + Math.PI/2));
|
| ctx.lineTo(midX - 6 * Math.cos(angle) - 4 * Math.cos(angle + Math.PI/2), midY - 6 * Math.sin(angle) - 4 * Math.sin(angle + Math.PI/2));
|
| ctx.fillStyle = inCycle ? '#ef4444' : '#64748b66';
|
| ctx.fill();
|
| });
|
|
|
|
|
| nodes.forEach(node => {
|
| let inCycle = cycles.some(c => c.includes(node.id));
|
| const color = inCycle ? '#ef4444' : '#64748b';
|
|
|
| ctx.beginPath();
|
| ctx.arc(node.x, node.y, 8, 0, Math.PI * 2);
|
| ctx.fillStyle = color;
|
| ctx.fill();
|
|
|
| ctx.font = '11px Inter';
|
| ctx.fillStyle = '#e2e8f0';
|
| ctx.textAlign = 'center';
|
| ctx.fillText(node.label, node.x, node.y + 20);
|
| });
|
| }
|
|
|
|
|
|
|
|
|
| function initCharts() {
|
|
|
| const reconCtx = document.getElementById('chart-recon-trend');
|
| if (reconCtx) {
|
| new Chart(reconCtx, {
|
| type: 'line',
|
| data: {
|
| labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
| datasets: [
|
| {
|
| label: 'Matched',
|
| data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
| borderColor: '#22c55e',
|
| backgroundColor: 'rgba(34,197,94,0.1)',
|
| fill: true,
|
| tension: 0.4,
|
| },
|
| {
|
| label: 'Discrepancies',
|
| data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
| borderColor: '#ef4444',
|
| backgroundColor: 'rgba(239,68,68,0.1)',
|
| fill: true,
|
| tension: 0.4,
|
| }
|
| ]
|
| },
|
| options: {
|
| responsive: true,
|
| maintainAspectRatio: false,
|
| plugins: {
|
| legend: { labels: { color: '#94a3b8', font: { size: 11 } } }
|
| },
|
| scales: {
|
| x: { grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } },
|
| y: { grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } }
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| const anomalyCtx = document.getElementById('chart-anomaly-dist');
|
| if (anomalyCtx) {
|
| new Chart(anomalyCtx, {
|
| type: 'bar',
|
| data: {
|
| labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
| datasets: [
|
| {
|
| label: 'Critical',
|
| data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
| backgroundColor: '#ef4444',
|
| borderRadius: 4,
|
| },
|
| {
|
| label: 'High',
|
| data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
| backgroundColor: '#f97316',
|
| borderRadius: 4,
|
| },
|
| {
|
| label: 'Medium',
|
| data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
| backgroundColor: '#eab308',
|
| borderRadius: 4,
|
| }
|
| ]
|
| },
|
| options: {
|
| responsive: true,
|
| maintainAspectRatio: false,
|
| plugins: {
|
| legend: { labels: { color: '#94a3b8', font: { size: 11 } } }
|
| },
|
| scales: {
|
| x: { stacked: true, grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } },
|
| y: { stacked: true, grid: { color: 'rgba(148,163,184,0.08)' }, ticks: { color: '#64748b', font: { size: 10 } } }
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| const confCtx = document.getElementById('chart-confidence');
|
| if (confCtx) {
|
| new Chart(confCtx, {
|
| type: 'doughnut',
|
| data: {
|
| labels: ['Exact Match', 'Fuzzy Match', 'AI Semantic', 'Unmatched'],
|
| datasets: [{
|
| data: [0, 0, 0, 0],
|
| backgroundColor: ['#22c55e', '#eab308', '#6366f1', '#ef4444'],
|
| borderWidth: 0,
|
| hoverOffset: 8,
|
| }]
|
| },
|
| options: {
|
| responsive: true,
|
| maintainAspectRatio: false,
|
| cutout: '65%',
|
| plugins: {
|
| legend: {
|
| display: false
|
| }
|
| }
|
| }
|
| });
|
| }
|
| }
|
|
|
| function updateFileLabel(type) {
|
| const fileInput = document.getElementById(`${type}-file`);
|
| const label = document.getElementById(`lbl-${type}`);
|
| if (fileInput.files.length > 0) {
|
| label.textContent = fileInput.files[0].name;
|
| label.classList.remove('hidden');
|
| label.classList.add('inline');
|
| }
|
| }
|
|
|
| async function runReconciliation() {
|
| const booksFile = document.getElementById('books-file').files[0];
|
| const gstFile = document.getElementById('gst-file').files[0];
|
|
|
| if (!booksFile || !gstFile) {
|
| showToast('Please select both Books and GST CSV files first.', 'error');
|
| return;
|
| }
|
|
|
| const formData = new FormData();
|
| formData.append('books', booksFile);
|
| formData.append('gst', gstFile);
|
|
|
| await executeBackendCall('/api/reconcile', formData, 'Reconciling data...');
|
| }
|
|
|
| async function fetchLiveGst() {
|
| const booksFile = document.getElementById('books-file').files[0];
|
|
|
| if (!booksFile) {
|
| showToast('Please select Books CSV file first.', 'error');
|
| return;
|
| }
|
|
|
| const formData = new FormData();
|
| formData.append('books', booksFile);
|
|
|
| await executeBackendCall('/api/fetch_live', formData, 'Fetching live GST and reconciling...');
|
| }
|
|
|
| async function executeBackendCall(endpoint, formData, startMsg) {
|
| const modal = document.getElementById('process-modal');
|
| const bar = document.getElementById('process-bar');
|
| const step = document.getElementById('process-step');
|
| const pct = document.getElementById('process-pct');
|
|
|
| modal.classList.remove('hidden');
|
| bar.style.width = '30%';
|
| step.textContent = startMsg;
|
| pct.textContent = '30%';
|
|
|
| try {
|
| const response = await fetch(endpoint, {
|
| method: 'POST',
|
| body: formData
|
| });
|
|
|
| if (!response.ok) throw new Error('API request failed');
|
| const data = await response.json();
|
|
|
| if (data.error) throw new Error(data.error);
|
|
|
| bar.style.width = '100%';
|
| step.textContent = 'Complete!';
|
| pct.textContent = '100%';
|
|
|
| updateDashboardWithRealData(data);
|
|
|
| setTimeout(() => {
|
| modal.classList.add('hidden');
|
| showToast('Process complete! ' + data.summary.exact + ' matched.', 'success');
|
| }, 600);
|
| } catch (err) {
|
| modal.classList.add('hidden');
|
| showToast('Error: ' + err.message, 'error');
|
| console.error(err);
|
| }
|
| }
|
|
|
| function updateDashboardWithRealData(data) {
|
|
|
| document.getElementById('stat-total-records').textContent = data.summary.total_books;
|
| document.getElementById('stat-matched').textContent = data.summary.exact;
|
| document.getElementById('stat-unmatched').textContent = data.summary.unmatched;
|
| document.getElementById('stat-anomalies').textContent = data.summary.anomalies;
|
|
|
|
|
| const total = data.summary.total_books || 1;
|
| const matchRate = ((data.summary.exact / total) * 100).toFixed(1);
|
| const unmatchedRate = ((data.summary.unmatched / total) * 100).toFixed(1);
|
| const anomalyRate = ((data.summary.anomalies / total) * 100).toFixed(1);
|
|
|
| document.getElementById('stat-total-records-sub').textContent = 'Live data loaded';
|
| document.getElementById('stat-matched-sub').textContent = `${matchRate}% match rate`;
|
| document.getElementById('stat-unmatched-sub').textContent = `${unmatchedRate}% discrepancy`;
|
| document.getElementById('stat-anomalies-sub').textContent = `${anomalyRate}% contamination`;
|
|
|
|
|
| document.getElementById('stat-fraud-rings').textContent = '0';
|
| document.getElementById('stat-fraud-rings-sub').textContent = 'No rings detected';
|
| document.getElementById('stat-risk-score').innerHTML = '0.0<span class="text-sm text-surface-400">/10</span>';
|
| document.getElementById('stat-risk-score-sub').textContent = 'Low risk level';
|
|
|
|
|
| if(document.getElementById('anomaly-trained-sub')) document.getElementById('anomaly-trained-sub').textContent = `Anomaly detection model trained on ${data.summary.total_books} records.`;
|
| if(document.getElementById('anomaly-analyzed')) document.getElementById('anomaly-analyzed').textContent = `${data.anomalies.length}/${data.summary.anomalies}`;
|
|
|
| let crit = 0, high = 0, med = 0;
|
| data.anomalies.forEach(a => {
|
| const s = a.AnomalyScore || 0;
|
| if (s > 0.3) crit++;
|
| else if (s > 0.1) high++;
|
| else med++;
|
| });
|
| const totAnom = data.summary.anomalies || 1;
|
| document.getElementById('anomaly-crit-count').textContent = crit;
|
| document.getElementById('anomaly-crit-bar').style.width = `${(crit/totAnom)*100}%`;
|
| document.getElementById('anomaly-high-count').textContent = high;
|
| document.getElementById('anomaly-high-bar').style.width = `${(high/totAnom)*100}%`;
|
| document.getElementById('anomaly-med-count').textContent = med;
|
| document.getElementById('anomaly-med-bar').style.width = `${(med/totAnom)*100}%`;
|
|
|
|
|
| if(document.getElementById('recon-stat-exact')) document.getElementById('recon-stat-exact').textContent = data.summary.exact || 0;
|
| if(document.getElementById('recon-stat-fuzzy')) document.getElementById('recon-stat-fuzzy').textContent = data.summary.fuzzy || 0;
|
| if(document.getElementById('recon-stat-semantic')) document.getElementById('recon-stat-semantic').textContent = data.summary.semantic || 0;
|
| if(document.getElementById('recon-stat-unmatched')) document.getElementById('recon-stat-unmatched').textContent = data.summary.unmatched || 0;
|
|
|
| if(document.getElementById('total-count')) document.getElementById('total-count').textContent = data.summary.total_books || 0;
|
| if(document.getElementById('showing-count')) document.getElementById('showing-count').textContent = data.reconciliation.length || 0;
|
|
|
| if(document.getElementById('alerts-count')) document.getElementById('alerts-count').textContent = data.summary.anomalies || 0;
|
| if(document.getElementById('filter-crit')) document.getElementById('filter-crit').textContent = crit;
|
| if(document.getElementById('filter-all')) document.getElementById('filter-all').textContent = totAnom;
|
|
|
|
|
|
|
|
|
| const recentAlertsDiv = document.getElementById('recent-alerts');
|
| if (recentAlertsDiv) {
|
| if (data.anomalies && data.anomalies.length > 0) {
|
|
|
| const sortedAnomalies = [...data.anomalies].sort((a, b) => (b.AnomalyScore || 0) - (a.AnomalyScore || 0)).slice(0, 5);
|
|
|
| recentAlertsDiv.innerHTML = sortedAnomalies.map(a => {
|
| const score = a.AnomalyScore || 0;
|
| const risk = score > 0.3 ? 'Critical' : score > 0.1 ? 'High' : 'Medium';
|
| const riskClass = risk === 'Critical' ? 'bg-red-500/10 border-red-500/20 text-red-400' :
|
| risk === 'High' ? 'bg-orange-500/10 border-orange-500/20 text-orange-400' : 'bg-yellow-500/10 border-yellow-500/20 text-yellow-400';
|
| const icon = risk === 'Critical' ? 'alert-octagon' : risk === 'High' ? 'alert-triangle' : 'info';
|
|
|
| return `
|
| <div class="p-3 rounded-lg border ${riskClass} flex gap-3 items-start">
|
| <div class="mt-0.5"><i data-lucide="${icon}" class="w-4 h-4"></i></div>
|
| <div>
|
| <div class="flex items-center gap-2 mb-1">
|
| <span class="text-xs font-bold uppercase tracking-wider">${risk}</span>
|
| <span class="text-[10px] font-mono opacity-80">Score: ${score.toFixed(3)}</span>
|
| </div>
|
| <p class="text-xs text-surface-200">Invoice <span class="font-mono text-white">${a.InvoiceID || '-'}</span> from <span class="text-white">${a.VendorName_books || 'Unknown'}</span></p>
|
| </div>
|
| </div>
|
| `;
|
| }).join('');
|
|
|
| setTimeout(() => lucide.createIcons(), 50);
|
| } else {
|
| recentAlertsDiv.innerHTML = '<p class="text-sm text-surface-400">No active alerts.</p>';
|
| }
|
| }
|
|
|
|
|
| if (data.faiss_stats) {
|
| if(document.getElementById('faiss-vectors')) document.getElementById('faiss-vectors').textContent = `${data.faiss_stats.ntotal} vectors`;
|
| if(document.getElementById('faiss-memory')) document.getElementById('faiss-memory').textContent = `${data.faiss_stats.memory_mb} MB`;
|
| if(document.getElementById('faiss-latency')) document.getElementById('faiss-latency').textContent = `0.8ms avg`;
|
| }
|
|
|
| if (data.fraud_network) {
|
| const fraudCount = data.summary.fraud_rings || 0;
|
| if(document.getElementById('stat-fraud-rings')) document.getElementById('stat-fraud-rings').textContent = fraudCount;
|
| if(document.getElementById('stat-fraud-rings-sub')) document.getElementById('stat-fraud-rings-sub').textContent = fraudCount > 0 ? `${fraudCount} rings detected` : 'No rings detected';
|
|
|
| if (document.getElementById('fraud-nodes')) document.getElementById('fraud-nodes').textContent = data.fraud_network.nodes.length;
|
| if (document.getElementById('fraud-edges')) document.getElementById('fraud-edges').textContent = data.fraud_network.edges.length;
|
|
|
| const riskScore = (data.summary.overall_risk_score || 0).toFixed(1);
|
| if(document.getElementById('stat-risk-score')) document.getElementById('stat-risk-score').innerHTML = `${riskScore}<span class="text-sm text-surface-400">/10</span>`;
|
|
|
| if (fraudCount > 0) {
|
| if(document.getElementById('stat-risk-score-sub')) document.getElementById('stat-risk-score-sub').textContent = riskScore > 5 ? 'High risk level' : 'Moderate risk level';
|
|
|
| const ringsList = document.getElementById('fraud-rings-list');
|
| if(ringsList) {
|
| ringsList.innerHTML = data.fraud_network.cycles.map((cycle, i) => `
|
| <div class="p-3 rounded-lg bg-red-500/10 border border-red-500/20">
|
| <p class="text-xs text-red-400 font-semibold mb-1">Ring #${i+1} Detected</p>
|
| <p class="text-xs text-surface-300 font-mono">${cycle.join(' → ')}</p>
|
| </div>
|
| `).join('');
|
| }
|
| } else {
|
| const ringsList = document.getElementById('fraud-rings-list');
|
| if (ringsList) ringsList.innerHTML = '<p class="text-sm text-surface-400">No fraud rings detected in current dataset.</p>';
|
| }
|
|
|
|
|
| window.fraudNetworkData = data.fraud_network;
|
| if (!document.getElementById('tab-fraud').classList.contains('hidden')) {
|
| drawFraudNetwork();
|
| }
|
| }
|
|
|
|
|
| const reconBody = document.getElementById('recon-table-body');
|
| reconBody.innerHTML = data.reconciliation.map(row => {
|
| const matchType = row.MatchStatus;
|
| const statusClass = matchType === 'Exact Match' ? 'text-green-400' :
|
| matchType.includes('Fuzzy') ? 'text-yellow-400' :
|
| matchType.includes('Semantic') ? 'text-brand-400' : 'text-red-400';
|
|
|
| const confidence = matchType.includes('(') ? matchType.split('(')[1].replace(')', '') : (matchType === 'Exact Match' ? '100%' : '0%');
|
| const matchLabel = matchType.split(' ')[0];
|
|
|
| return `
|
| <tr class="border-b border-surface-800/30 hover:bg-surface-800/30 transition">
|
| <td class="px-4 py-3 font-mono text-xs">${row.InvoiceID || '-'}</td>
|
| <td class="px-4 py-3 font-mono text-xs">${row.VendorName_gst || row.VendorName_books || '-'}</td>
|
| <td class="px-4 py-3 text-xs">₹${row.Amount_books || row.Amount_gst || 0}</td>
|
| <td class="px-4 py-3"><span class="px-2 py-0.5 rounded-full text-[10px] font-semibold text-white bg-surface-700">${matchLabel}</span></td>
|
| <td class="px-4 py-3"><span class="${statusClass} text-xs font-mono">${confidence}</span></td>
|
| <td class="px-4 py-3"><span class="w-2 h-2 rounded-full inline-block ${matchType === 'Exact Match' ? 'bg-green-400' : 'bg-red-400'}"></span></td>
|
| </tr>
|
| `;
|
| }).join('');
|
|
|
|
|
| const confCtx = document.getElementById('chart-confidence');
|
| if (confCtx) {
|
| const chart = Chart.getChart(confCtx);
|
| if (chart) {
|
| chart.data.datasets[0].data = [
|
| data.summary.exact || 0,
|
| data.summary.fuzzy || 0,
|
| data.summary.semantic || 0,
|
| data.summary.unmatched || 0
|
| ];
|
| chart.update();
|
| }
|
| }
|
|
|
|
|
| const trendCtx = document.getElementById('chart-recon-trend');
|
| if (trendCtx && data.charts && data.charts.recon_trend) {
|
| const chart = Chart.getChart(trendCtx);
|
| if (chart) {
|
| chart.data.datasets[0].data = data.charts.recon_trend;
|
| if (data.charts.discrep_trend && chart.data.datasets[1]) {
|
| chart.data.datasets[1].data = data.charts.discrep_trend;
|
| }
|
| chart.update();
|
| }
|
| }
|
|
|
|
|
| const distCtx = document.getElementById('chart-anomaly-dist');
|
| if (distCtx && data.charts && data.charts.anomaly_dist) {
|
| const chart = Chart.getChart(distCtx);
|
| if (chart) {
|
| chart.data.datasets[0].data = data.charts.anomaly_dist.critical;
|
| chart.data.datasets[1].data = data.charts.anomaly_dist.high;
|
| chart.data.datasets[2].data = data.charts.anomaly_dist.medium;
|
| chart.update();
|
| }
|
| }
|
|
|
|
|
| window.anomalyDataMap = {};
|
|
|
|
|
| let critCount = 0, highCount = 0, medCount = 0;
|
| let maxScore = 0, minScore = 1;
|
| data.anomalies.forEach(a => {
|
| const s = a.AnomalyScore || 0;
|
| if (s > maxScore) maxScore = s;
|
| if (s < minScore && s > 0) minScore = s;
|
| if (s > 0.3) critCount++;
|
| else if (s > 0.1) highCount++;
|
| else medCount++;
|
| });
|
| if (minScore === 1 && maxScore === 0) minScore = 0;
|
| const totalAnomalies = data.anomalies.length;
|
|
|
| if (document.getElementById('anomaly-score-range')) {
|
| document.getElementById('anomaly-score-range').textContent = totalAnomalies > 0 ? `${minScore.toFixed(2)} - ${maxScore.toFixed(2)}` : '--';
|
| }
|
| if (document.getElementById('anomaly-crit-count')) document.getElementById('anomaly-crit-count').textContent = critCount;
|
| if (document.getElementById('anomaly-high-count')) document.getElementById('anomaly-high-count').textContent = highCount;
|
| if (document.getElementById('anomaly-med-count')) document.getElementById('anomaly-med-count').textContent = medCount;
|
|
|
| if (document.getElementById('filter-crit')) document.getElementById('filter-crit').textContent = critCount;
|
| if (document.getElementById('filter-all')) document.getElementById('filter-all').textContent = totalAnomalies;
|
|
|
| if (totalAnomalies > 0) {
|
| if (document.getElementById('anomaly-crit-bar')) document.getElementById('anomaly-crit-bar').style.width = `${(critCount/totalAnomalies)*100}%`;
|
| if (document.getElementById('anomaly-high-bar')) document.getElementById('anomaly-high-bar').style.width = `${(highCount/totalAnomalies)*100}%`;
|
| if (document.getElementById('anomaly-med-bar')) document.getElementById('anomaly-med-bar').style.width = `${(medCount/totalAnomalies)*100}%`;
|
| } else {
|
| if (document.getElementById('anomaly-crit-bar')) document.getElementById('anomaly-crit-bar').style.width = '0%';
|
| if (document.getElementById('anomaly-high-bar')) document.getElementById('anomaly-high-bar').style.width = '0%';
|
| if (document.getElementById('anomaly-med-bar')) document.getElementById('anomaly-med-bar').style.width = '0%';
|
| }
|
|
|
|
|
|
|
| const anomalyBody = document.getElementById('anomaly-table-body');
|
| anomalyBody.innerHTML = data.anomalies.map(a => {
|
| window.anomalyDataMap[a.InvoiceID] = a;
|
| const score = a.AnomalyScore || 0;
|
| const risk = score > 0.3 ? 'Critical' : score > 0.1 ? 'High' : 'Medium';
|
| const riskClass = risk === 'Critical' ? 'bg-red-500/20 text-red-400' :
|
| risk === 'High' ? 'bg-orange-500/20 text-orange-400' : 'bg-yellow-500/20 text-yellow-400';
|
|
|
| return `
|
| <tr class="border-b border-surface-800/30 hover:bg-surface-800/30 transition">
|
| <td class="px-4 py-3 font-mono text-xs">${a.InvoiceID || '-'}</td>
|
| <td class="px-4 py-3 text-xs">${a.VendorName_books || '-'}</td>
|
| <td class="px-4 py-3 text-xs font-mono">₹${a.Amount_books || 0}</td>
|
| <td class="px-4 py-3"><span class="text-xs font-mono text-surface-300">${score.toFixed(3)}</span></td>
|
| <td class="px-4 py-3"><span class="px-2 py-0.5 rounded-full text-[10px] font-semibold ${riskClass}">${risk}</span></td>
|
| <td class="px-4 py-3"><button class="text-[10px] px-2 py-1 rounded bg-brand-600/20 text-brand-300" onclick="showExplanation('${a.InvoiceID}')">View</button></td>
|
| </tr>
|
| `;
|
| }).join('');
|
| }
|
|
|
|
|
|
|
|
|
| function showToast(message, type = 'info') {
|
| const container = document.getElementById('toast-container');
|
| const toast = document.createElement('div');
|
| const colors = {
|
| success: 'border-green-500/30 bg-green-500/10',
|
| error: 'border-red-500/30 bg-red-500/10',
|
| info: 'border-brand-500/30 bg-brand-500/10'
|
| };
|
| const icons = {
|
| success: 'check-circle',
|
| error: 'alert-circle',
|
| info: 'info'
|
| };
|
| toast.className = `flex items-start gap-3 p-4 rounded-xl border ${colors[type]} backdrop-blur-xl animate-slide-in max-w-sm`;
|
| toast.innerHTML = `
|
| <i data-lucide="${icons[type]}" class="w-5 h-5 ${type === 'success' ? 'text-green-400' : type === 'error' ? 'text-red-400' : 'text-brand-400'} shrink-0 mt-0.5"></i>
|
| <p class="text-sm text-surface-200">${message}</p>
|
| `;
|
| container.appendChild(toast);
|
| lucide.createIcons();
|
| setTimeout(() => {
|
| toast.style.opacity = '0';
|
| toast.style.transform = 'translateX(100%)';
|
| toast.style.transition = 'all 0.3s ease';
|
| setTimeout(() => toast.remove(), 300);
|
| }, 5000);
|
| }
|
|
|
|
|
|
|
|
|
| function exportCSV() {
|
| const reconBody = document.getElementById('recon-table-body');
|
| if (!reconBody || reconBody.rows.length === 0) {
|
| showToast('No data to export. Run Engine first.', 'error');
|
| return;
|
| }
|
| showToast('Generating CSV export...', 'info');
|
| setTimeout(() => {
|
| const rows = Array.from(reconBody.querySelectorAll('tr'));
|
| const headers = ['Invoice ID', 'Vendor', 'Amount', 'Match Type', 'Confidence', 'Status'];
|
| const csvRows = rows.map(tr => Array.from(tr.querySelectorAll('td')).map(td => td.textContent.trim()).join(','));
|
| const csv = [headers.join(','), ...csvRows].join('\n');
|
| const blob = new Blob([csv], { type: 'text/csv' });
|
| const url = URL.createObjectURL(blob);
|
| const a = document.createElement('a');
|
| a.href = url;
|
| a.download = 'reconciliation_results.csv';
|
| a.click();
|
| URL.revokeObjectURL(url);
|
| showToast('CSV exported successfully!', 'success');
|
| }, 500);
|
| }
|
|
|
| function filterReconTable() {
|
|
|
| showToast('Filtering is coming soon!', 'info');
|
| }
|
|
|
|
|
|
|
|
|
| document.addEventListener('DOMContentLoaded', () => {
|
| lucide.createIcons();
|
| initCharts();
|
|
|
|
|
| document.getElementById('recon-table-body').innerHTML = `<tr><td colspan="6" class="text-center py-8 text-surface-400">Please upload CSVs and Run Engine to view data.</td></tr>`;
|
| document.getElementById('anomaly-table-body').innerHTML = `<tr><td colspan="6" class="text-center py-8 text-surface-400">No anomalies detected yet. Upload data and Run Engine to begin analysis.</td></tr>`;
|
| });
|
|
|
|
|
| let resizeTimeout;
|
| window.addEventListener('resize', () => {
|
| clearTimeout(resizeTimeout);
|
| resizeTimeout = setTimeout(() => {
|
| if (!document.getElementById('tab-fraud').classList.contains('hidden')) {
|
| drawFraudNetwork();
|
| }
|
| }, 200);
|
| });
|
| </script>
|
| </body>
|
| </html> |