// recon-settlement.jsx — Recon & Settlement Page for Hibank QRIS BackOffice function ReconSettlementPage({ merchants, primary, secondary, merchantView = false }) { const TEAL = primary || '#0056D2'; const ORANGE = secondary || '#cf5a27'; const GREEN = '#74b50c'; const RED = '#ed3151'; const AMBER = '#d9920b'; // State Utama const [view, setView] = React.useState('list'); // 'list' | 'detail' | 'investigate' const [selectedDay, setSelectedDay] = React.useState(null); const [toast, setToast] = React.useState(null); // State Form Kelengkapan Dokumen / Berita Acara (BARU) const [investigateNote, setInvestigateNote] = React.useState(''); const [uploadedFile, setUploadedFile] = React.useState(null); // Custom adjustment modal state const [adjustingTx, setAdjustingTx] = React.useState(null); const [adjustAmount, setAdjustAmount] = React.useState(''); const [adjustNote, setAdjustNote] = React.useState(''); // Seed progress data const [progress, setProgress] = React.useState([ { date: '2026-06-24', total: 1250, matched: 1247, suspect: 3, status: 'investigate', volume: 85240000 }, { date: '2026-06-23', total: 1100, matched: 1100, status: 'success', volume: 74120000 }, { date: '2026-06-22', total: 950, matched: 948, suspect: 2, status: 'success', volume: 61500000 }, { date: '2026-06-21', total: 1320, matched: 1320, status: 'success', volume: 92800000 }, { date: '2026-06-20', total: 1050, matched: 1050, status: 'success', volume: 71050000 } ]); // Seed suspect data const [suspects, setSuspects] = React.useState([ { id: 'TX-260624-9912', date: '2026-06-24', mid: 'MRC-00244', merchantName: 'Batik Lestari Pekalongan', coreAmount: 150000, switchAmount: 155000, type: 'selisih_nominal', desc: 'Selisih nominal core QRIS vs switching', resolution: null }, { id: 'TX-260624-9913', date: '2026-06-24', mid: 'MRC-00245', merchantName: 'Toko Bangunan Maju Jaya', coreAmount: 75000, switchAmount: null, type: 'gantung_switch', desc: 'Transaksi tercatat di Core tetapi tidak ditemukan di switching', resolution: null }, { id: 'TX-260624-9914', date: '2026-06-24', mid: 'MRC-00246', merchantName: 'Kopi Senja Nusantara', coreAmount: null, switchAmount: 45000, type: 'gantung_core', desc: 'Transaksi tercatat di switching tetapi belum ada di Core', resolution: null }, { id: 'TX-260622-8141', date: '2026-06-22', mid: 'MRC-00244', merchantName: 'Batik Lestari Pekalongan', coreAmount: 320000, switchAmount: 320000, type: 'gantung_switch', desc: 'Settle manual — resolved', resolution: 'resolved_success', resolutionNote: 'Dilunaskan manual via rekon' }, { id: 'TX-260622-8142', date: '2026-06-22', mid: 'MRC-00245', merchantName: 'Toko Bangunan Maju Jaya', coreAmount: 50000, switchAmount: null, type: 'gantung_switch', desc: 'Settle manual — resolved', resolution: 'resolved_failed', resolutionNote: 'Dibatalkan manual via rekon' } ]); // Seed matched transaction detail data for 2026-06-24 const matchedTxSeed = [ { id: 'TX-260624-8831', mid: 'MRC-00244', name: 'Batik Lestari', time: '2026-06-24T10:15:00', amount: 45000, issuer: 'ShopeePay', status: 'Matched' }, { id: 'TX-260624-8832', mid: 'MRC-00245', name: 'Maju Jaya', time: '2026-06-24T10:20:00', amount: 125000, issuer: 'OVO', status: 'Matched' }, { id: 'TX-260624-8833', mid: 'MRC-00246', name: 'Kopi Senja', time: '2026-06-24T10:24:00', amount: 35000, issuer: 'Gopay', status: 'Matched' }, { id: 'TX-260624-8834', mid: 'MRC-00244', name: 'Batik Lestari', time: '2026-06-24T10:30:00', amount: 500000, issuer: 'Dana', status: 'Matched' }, { id: 'TX-260624-8835', mid: 'MRC-00246', name: 'Kopi Senja', time: '2026-06-24T10:45:00', amount: 18000, issuer: 'LinkAja', status: 'Matched' }, { id: 'TX-260624-8836', mid: 'MRC-00245', name: 'Maju Jaya', time: '2026-06-24T10:50:00', amount: 24000, issuer: 'QRIS BCA', status: 'Matched' }, { id: 'TX-260624-8837', mid: 'MRC-00244', name: 'Batik Lestari', time: '2026-06-24T11:02:00', amount: 120000, issuer: 'QRIS Hibank', status: 'Matched' } ]; const triggerToast = (msg, kind = 'success') => { setToast({ msg, kind }); setTimeout(() => setToast(null), 3000); }; const fmtRp = (v) => { if (v === null || v === undefined) return '—'; return 'Rp ' + Number(v).toLocaleString('id-ID'); }; const fmtDate = (iso) => { const d = new Date(iso); return d.toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' }) + ' ' + d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); }; // Action: Resolve suspect transaction const handleResolveSuspect = (txId, resolutionType, customNote = '') => { let noteText = ''; if (resolutionType === 'resolved_success') { noteText = 'Dilunaskan manual via rekon'; } else if (resolutionType === 'resolved_failed') { noteText = 'Dibatalkan manual via rekon'; } else if (resolutionType === 'resolved_adjusted') { noteText = customNote || 'Penyesuaian nominal selisih'; } else { noteText = null; } setSuspects(prev => prev.map(s => { if (s.id !== txId) return s; return { ...s, resolution: resolutionType, resolutionNote: noteText }; })); if (resolutionType === null) { triggerToast(`Status transaksi ${txId} dikembalikan ke Belum Selesai.`, 'amber'); } else { triggerToast(`Transaksi ${txId} berhasil diperbarui.`, 'success'); } setTimeout(() => { setSuspects(currentSuspects => { const targetTx = currentSuspects.find(s => s.id === txId); if (!targetTx) return currentSuspects; const targetDay = targetTx.date; const remainingSuspects = currentSuspects.filter(s => s.date === targetDay && s.resolution === null).length; setProgress(prevProg => prevProg.map(p => { if (p.date !== targetDay) return p; if (remainingSuspects === 0) { return { ...p, status: 'success', suspect: 0 }; } else { return { ...p, status: 'investigate', suspect: remainingSuspects }; } })); return currentSuspects; }); }, 50); }; // Action: Sesuaikan Dana const handleAdjustFunds = (e) => { e.preventDefault(); if (!adjustingTx || !adjustAmount.trim()) return; const formattedNote = `Nominal disesuaikan sebesar Rp ${Number(adjustAmount).toLocaleString('id-ID')}. ${adjustNote}`.trim(); handleResolveSuspect(adjustingTx.id, 'resolved_adjusted', formattedNote); setAdjustingTx(null); setAdjustAmount(''); setAdjustNote(''); }; // Action: Simpan Berita Acara & Catatan Akhir (BARU) const handleSaveInvestigationForm = (e) => { e.preventDefault(); // Validasi file berita acara if (!uploadedFile) { triggerToast('Gagal simpan! Dokumen Berita Acara (BAR) wajib diupload.', 'error'); return; } triggerToast(`Berita Acara & Berkas Investigasi untuk tanggal ${selectedDay.date} sukses disimpan ke log sistem.`, 'success'); // Reset state form & kembali ke daftar utama setInvestigateNote(''); setUploadedFile(null); setView('list'); setSelectedDay(null); }; // Calculations const stats = { volume: progress.reduce((acc, curr) => acc + curr.volume, 0), successRate: 99.88, totalSuspects: suspects.filter(s => s.resolution === null).length, settledDays: progress.filter(p => p.status === 'success').length }; // ── MERCHANT VIEW: SETTLEMENT-ONLY (dana yang telah settled ke merchant) ── if (merchantView) { const MDR_RATE = 0.007; const settledDays = progress.filter(p => p.status === 'success'); const totalNet = settledDays.reduce((a, p) => a + Math.round(p.volume * (1 - MDR_RATE)), 0); const totalGross = settledDays.reduce((a, p) => a + p.volume, 0); const totalMdr = totalGross - totalNet; const acctName = 'Bank Hibank · 8820145566'; return (
{/* Header */}
Settlement

Dana Settlement Anda

Rincian dana transaksi QRIS yang telah berhasil di-settle dan dikreditkan ke rekening settlement Anda.

{/* Summary cards */}
{[ { label: 'Total Dana Settled', value: fmtRp(totalNet), sub: 'Bersih setelah MDR', icon: 'bx-wallet', color: GREEN }, { label: 'Total Volume Bruto', value: fmtRp(totalGross), sub: 'Sebelum potongan', icon: 'bx-line-chart', color: TEAL }, { label: 'Total Potongan MDR', value: fmtRp(totalMdr), sub: '0,7% per transaksi', icon: 'bx-receipt', color: ORANGE }, { label: 'Batch Settled', value: `${settledDays.length} Hari`, sub: 'Periode aktif', icon: 'bx-calendar-check', color: AMBER } ].map((card, i) => (
{card.label}
{card.value}
{card.sub}
))}
{/* Settled table */}

Riwayat Settlement Harian

Rekening tujuan: {acctName}
{settledDays.map(row => { const net = Math.round(row.volume * (1 - MDR_RATE)); const mdr = row.volume - net; return ( ); })}
Tanggal Settlement No. Referensi Total Trx Volume Bruto Potongan MDR Dana Settled (Net) Status
{row.date} STL-{row.date.replace(/-/g, '').slice(2)} {row.total} Trx {fmtRp(row.volume)} -{fmtRp(mdr)} {fmtRp(net)} Settled
); } // ── SUB-PAGE: DETAIL TRANSAKSI YANG DIREKON ── if (view === 'detail' && selectedDay) { return (
{/* Breadcrumb */}
Recon & Settlement / Detail Progress Recon {selectedDay.date}
{/* Title */}

Detail Transaksi Reconciled

Daftar transaksi yang sukses dipadankan dengan pembukuan core bank pada {selectedDay.date}.

{/* Stats card */}
Tanggal Rekon
{selectedDay.date}
Total Transaksi
{selectedDay.total} Transaksi
Volume Recon
{fmtRp(selectedDay.volume)}
Status
{selectedDay.status === 'success' ? 'Selesai' : 'Perlu Investigasi'}
{/* Reconciled table */}
{matchedTxSeed.map(tx => ( ))}
ID Transaksi MID Merchant Nama Outlet Waktu Transaksi Issuer QRIS Nominal Status Recon
{tx.id} {tx.mid} {tx.name} {fmtDate(tx.time)} {tx.issuer} {fmtRp(tx.amount)} {tx.status}
); } // ── SUB-PAGE: INVESTIGASI SUSPECT WORKBENCH ── if (view === 'investigate' && selectedDay) { const daySuspects = suspects.filter(s => s.date === selectedDay.date); const unresolvedCount = daySuspects.filter(s => s.resolution === null).length; return (
{/* Breadcrumb */}
Recon & Settlement / Investigasi Suspect {selectedDay.date}
{/* Title */}

Investigasi Transaksi Suspect

Penyelesaian perbedaan data nominal atau status gantung antara Core Bank dan Switch.

0 ? 'rgba(237,49,81,0.1)' : 'rgba(116,181,12,0.12)', color: unresolvedCount > 0 ? '#ed3151' : '#74b50c', fontSize: 12, fontWeight: 700, borderRadius: 6, padding: '6px 12px' }}> 0 ? 'bx-error' : 'bx-check-circle'}`} /> {unresolvedCount} Suspect Belum Selesai
{/* Card Ringkasan Harian Investigasi (BARU) */}
Tanggal Log Buku
{selectedDay.date}
Volume Kliring Harian
{fmtRp(selectedDay.volume)}
Total Antrean Suspect
{daySuspects.length} Kasus
{/* Suspect list table */}
{daySuspects.map(s => { let selectColor = '#515252'; let selectBorder = '#efefef'; if (s.resolution === 'resolved_success') { selectColor = GREEN; selectBorder = GREEN; } else if (s.resolution === 'resolved_failed') { selectColor = RED; selectBorder = RED; } else if (s.resolution === 'resolved_adjusted') { selectColor = TEAL; selectBorder = TEAL; } return ( ); })}
ID Transaksi Nama Merchant Penyebab Suspect Nominal Core Nominal Switch Aksi Rekon
{s.id}
{s.mid}
{s.merchantName} {s.type === 'selisih_nominal' ? 'Nominal Selisih' : s.type === 'gantung_switch' ? 'Gantung di Switch' : 'Gantung di Core'} {s.desc} {fmtRp(s.coreAmount)} {fmtRp(s.switchAmount)}
{s.resolution && ( "{s.resolutionNote}" )}
{/* ── SECTION FORM DOCUMENT KELENGKAPAN & BERITA ACARA (BARU) ── */}

Otorisasi Dokumen & Catatan Audit

{/* Kolom Catatan */}