// refund-management.jsx — Refund Request & Monitor Pages for Hibank QRIS BackOffice // Share state wrapper using localStorage or window global to sync data between Request and Monitor const getRefundRequests = () => { const defaultReqs = [ { id: 'REQ-RFD-7011', txId: 'TX-260624-9981', mid: 'MRC-00244', name: 'Batik Lestari Pekalongan', customer: 'Reza Firmansyah', amount: 120000, time: '2026-06-24T09:30:00', reason: 'Salah nominal transfer', status: 'pending', checker: '—', refNo: '—' }, { id: 'REQ-RFD-7012', txId: 'TX-260624-9982', mid: 'MRC-00245', name: 'Toko Bangunan Maju Jaya', customer: 'Rina Marlina', amount: 250000, time: '2026-06-24T10:12:00', reason: 'Barang kosong', status: 'assigned', checker: 'Chandra Wijaya (Checker)', refNo: '—' }, { id: 'REQ-RFD-7013', txId: 'TX-260624-9983', mid: 'MRC-00246', name: 'Kopi Senja Nusantara', customer: 'Hendra Wijaya', amount: 85000, time: '2026-06-24T10:15:00', reason: 'Double payment by customer', status: 'approved', checker: 'Arief Budiman (Checker)', refNo: 'REF-RFD-8812903' }, { id: 'REQ-RFD-7010', txId: 'TX-260623-8109', mid: 'MRC-00244', name: 'Batik Lestari Pekalongan', customer: 'Dian Sastro', amount: 150000, time: '2026-06-23T14:45:00', reason: 'Double payment by customer', status: 'rejected', checker: 'Arief Budiman (Checker)', refNo: '—', rejectReason: 'Transaksi sah dan sudah di-settle' } ]; if (!window.__refundRequests) { window.__refundRequests = defaultReqs; } return window.__refundRequests; }; const saveRefundRequests = (data) => { window.__refundRequests = data; }; // ── 1. REFUND REQUEST PAGE ── function RefundRequestPage({ merchants, primary, secondary }) { const TEAL = primary || '#0056D2'; const ORANGE = secondary || '#cf5a27'; const GREEN = '#74b50c'; const RED = '#ed3151'; const AMBER = '#d9920b'; const [requests, setRequests] = React.useState(getRefundRequests()); const [view, setView] = React.useState('list'); // 'list' | 'detail' const [selectedReq, setSelectedReq] = React.useState(null); const [assigningReq, setAssigningReq] = React.useState(null); const [selectedChecker, setSelectedChecker] = React.useState(''); const [toast, setToast] = React.useState(null); const checkersList = [ 'Arief Budiman (Checker)', 'Chandra Wijaya (Checker)', 'Bambang Pamungkas (Checker)' ]; 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' }); }; const triggerToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), 3000); }; // Action: Assign Checker const handleAssignSubmit = (e) => { e.preventDefault(); if (!assigningReq || !selectedChecker) return; const updated = requests.map(r => { if (r.id !== assigningReq.id) return r; return { ...r, status: 'assigned', checker: selectedChecker }; }); setRequests(updated); saveRefundRequests(updated); triggerToast(`Request ${assigningReq.id} berhasil ditugaskan ke ${selectedChecker}.`); // Close modal setAssigningReq(null); setSelectedChecker(''); }; const counts = { total: requests.length, pending: requests.filter(r => r.status === 'pending').length, assigned: requests.filter(r => r.status === 'assigned').length, completed: requests.filter(r => r.status === 'approved' || r.status === 'rejected').length }; // ── SUB-PAGE: DETAIL REQUEST ── if (view === 'detail' && selectedReq) { const freshReq = requests.find(r => r.id === selectedReq.id) || selectedReq; const isPending = freshReq.status === 'pending'; const isAssigned = freshReq.status === 'assigned'; const isApproved = freshReq.status === 'approved'; const isRejected = freshReq.status === 'rejected'; return (
{/* Breadcrumb */}
Refund Management / Detail Request {freshReq.id}
{/* Layout Grid */}
{/* Left: Info Card */}
Permohonan Pengembalian Dana Merchant

Request ID: {freshReq.id}

{freshReq.status.toUpperCase()}
Nama Merchant
{freshReq.name}
{freshReq.mid}
Waktu Pengajuan
{fmtDate(freshReq.time)}
ID Transaksi (Original)
{freshReq.txId}
Nama Pelanggan (Customer)
{freshReq.customer}
Alasan Refund & Detail:
Nominal yang diajukan: {fmtRp(freshReq.amount)}
Keterangan: "{freshReq.reason}"
{/* Checker logs */} {!isPending && (

Status Otorisasi Checker

Petugas Pemeriksa (Checker)
{freshReq.checker}
Status Pemeriksaan
{isApproved ? 'DISETUJUI (APPROVED)' : (isRejected ? 'DITOLAK (REJECTED)' : 'SEDANG DIPERIKSA')}
{isApproved && (
Nomor Referensi Refund
{freshReq.refNo}
)} {isRejected && (
Alasan Penolakan Audit
"{freshReq.rejectReason || '—'}"
)}
)}
{/* Right: Actions Sidebar */}

Tindakan Pengurus

{isPending ? (

Permohonan ini belum ditugaskan ke petugas pemeriksa (Checker).

) : isAssigned ? (
Menunggu Review Checker

Sedang diperiksa oleh {freshReq.checker}.

) : (
Pemeriksaan Selesai

Otorisasi oleh {freshReq.checker} selesai.

)}
); } // ── MAIN LIST VIEW ── return (
{/* Header */}
Refund Management

Refund Requests Queue

Kelola alur penugasan verifikator dan checker untuk pengembalian dana QRIS merchant.

{/* Stats Summary Cards */}
{[ { label: 'Total Permohonan', value: counts.total, sub: 'Total pengajuan masuk', icon: 'bx-git-pull-request', color: TEAL }, { label: 'Belum Ditugaskan', value: counts.pending, sub: 'Perlu penetapan checker', icon: 'bx-user-plus', color: counts.pending > 0 ? ORANGE : '#a7a8a8' }, { label: 'Dalam Pemeriksaan', value: counts.assigned, sub: 'Oleh checker aktif', icon: 'bx-loader-circle', color: counts.assigned > 0 ? AMBER : '#a7a8a8' }, { label: 'Otorisasi Selesai', value: counts.completed, sub: 'Approved & Rejected', icon: 'bx-badge-check', color: GREEN } ].map((card, i) => (
{card.label}
{card.value}
{card.sub}
))}
{/* Request Table Queue */}
{requests.map(req => { const isPending = req.status === 'pending'; const isAssigned = req.status === 'assigned'; const isApproved = req.status === 'approved'; const isRejected = req.status === 'rejected'; return ( e.currentTarget.style.background = '#fafbfc'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> ); })}
ID Request ID Transaksi Merchant Nominal Refund Waktu Diajukan Petugas Checker Status Aksi
{req.id} {req.txId}
{req.name}
{req.mid}
{fmtRp(req.amount)} {fmtDate(req.time)} {isPending ? ( Belum Ditugaskan ) : ( {req.checker} )} {req.status === 'pending' ? 'Unassigned' : req.status.toUpperCase()}
{isPending ? ( ) : ( )}
{/* Modal: Assign Checker */} {assigningReq && (
setAssigningReq(null)} style={{ position: 'absolute', inset: 0, background: 'rgba(23,25,25,0.4)', backdropFilter: 'blur(2px)' }} />

Tugaskan Checker Refund

Request ID: {assigningReq.id}
Merchant: {assigningReq.name}
Nominal Refund: {fmtRp(assigningReq.amount)}
)} {/* Toast Alert */} {toast && (
{toast}
)}
); } // ── 2. REFUND MONITOR PAGE ── function RefundMonitorPage({ merchants, primary, secondary }) { const TEAL = primary || '#0056D2'; const ORANGE = secondary || '#cf5a27'; const GREEN = '#74b50c'; const RED = '#ed3151'; const AMBER = '#d9920b'; const [requests] = React.useState(getRefundRequests()); const fmtRp = (v) => { if (v === null || v === undefined) return '—'; return 'Rp ' + Number(v).toLocaleString('id-ID'); }; const getWorkflowStep = (status) => { if (status === 'pending') return 1; if (status === 'assigned') return 2; if (status === 'approved' || status === 'rejected') return 3; return 1; }; // Workflow steps definitions const steps = [ { n: 1, label: 'Pengajuan Merchant' }, { n: 2, label: 'Pemeriksaan Checker' }, { n: 3, label: 'Otorisasi Akhir (Selesai)' } ]; return (
{/* Header */}
Refund Management

Refund Monitoring Dashboard

Lacak progress persetujuan workflow pengembalian dana QRIS merchant secara real-time.

{/* Monitor list cards */}
{requests.map(req => { const currentStep = getWorkflowStep(req.status); const isApproved = req.status === 'approved'; const isRejected = req.status === 'rejected'; return (
{/* Card top */}
{req.id}

{req.name}

Tx ID: {req.txId} · Nominal: {fmtRp(req.amount)}
{req.status.toUpperCase()}
Pemeriksa: {req.checker}
{/* Stepper Progress bar */}
{/* Stepper line */}
{steps.map(step => { const isPast = currentStep > step.n; const isCurrent = currentStep === step.n; const isFinal = step.n === 3; let dotBg = '#fff'; let borderCol = '#efefef'; let textCol = '#a7a8a8'; let checkIcon = null; if (isPast) { dotBg = GREEN; borderCol = GREEN; textCol = '#171919'; checkIcon = ; } else if (isCurrent) { dotBg = '#fff'; borderCol = isRejected ? RED : (isApproved ? GREEN : TEAL); textCol = isRejected ? RED : (isApproved ? GREEN : TEAL); checkIcon =
; } if (isFinal && isCurrent) { dotBg = isRejected ? RED : GREEN; borderCol = isRejected ? RED : GREEN; textCol = isRejected ? RED : GREEN; checkIcon = ; } return (
{checkIcon || {step.n}}
{step.label}
); })}
); })}
); } // Register components globally Object.assign(window, { RefundRequestPage, RefundMonitorPage });