// 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 (
Rincian dana transaksi QRIS yang telah berhasil di-settle dan dikreditkan ke rekening settlement Anda.
| 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 |
Daftar transaksi yang sukses dipadankan dengan pembukuan core bank pada {selectedDay.date}.
| 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} |
Penyelesaian perbedaan data nominal atau status gantung antara Core Bank dan Switch.
| 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}"
)}
|
Pantau proses rekonsiliasi harian antara Core Banking System (CBS) dengan QRIS Switch log.
| Tanggal Kerja | Total Trx | Volume Transaksi | Cocok (Match) | Selisih (Suspect) | Status Rekon | Aksi |
|---|---|---|---|---|---|---|
| {row.date} | {row.total} Trx | {fmtRp(row.volume)} | {row.matched} | 0 ? RED : '#515252', fontWeight: 600 }}>{row.suspect} | {isInvestigate ? 'Perlu Investigasi' : 'Selesai'} |
{row.total - row.matched > 0 || isInvestigate ? (
) : (
)}
|