// refund.jsx — Refund Management Page for Hibank QRIS BackOffice function RefundPage({ merchants, primary, secondary }) { const TEAL = primary || '#0056D2'; const ORANGE = secondary || '#cf5a27'; const GREEN = '#74b50c'; const RED = '#ed3151'; const AMBER = '#d9920b'; // State const [refunds, setRefunds] = React.useState([ { id: 'REFUND-001', txId: 'TX-260624-8831', mid: 'MRC-00244', name: 'Batik Lestari Pekalongan', customer: 'Budi Santoso', amount: 45000, time: '2026-06-24T09:12:00', reason: 'Double payment by customer', status: 'pending', refNo: '—' }, { id: 'REFUND-002', txId: 'TX-260624-8832', mid: 'MRC-00245', name: 'Toko Bangunan Maju Jaya', customer: 'Siti Rahayu', amount: 125000, time: '2026-06-24T10:15:00', reason: 'Wrong nominal input', status: 'success', refNo: 'REF-RFD-8821921' }, { id: 'REFUND-003', txId: 'TX-260624-8833', mid: 'MRC-00246', name: 'Kopi Senja Nusantara', customer: 'Andi Wijaya', amount: 35000, time: '2026-06-24T10:20:00', reason: 'Product out of stock', status: 'rejected', refNo: '—', rejectReason: 'Transactions settled & items delivered' } ]); const [selectedRefund, setSelectedRefund] = React.useState(null); const [showAddModal, setShowAddModal] = React.useState(false); const [rejectingRefund, setRejectingRefund] = React.useState(null); const [rejectReasonInput, setRejectReasonInput] = React.useState(''); const [toast, setToast] = React.useState(null); // Form states const [formMerchantId, setFormMerchantId] = React.useState(''); const [formTxId, setFormTxId] = React.useState(''); const [formCustomer, setFormCustomer] = React.useState(''); const [formAmount, setFormAmount] = React.useState(''); const [formReason, setFormReason] = React.useState(''); const activeMerchants = merchants ? merchants.filter(m => m.status === 'approved' || m.status === 'active' || m.status === 'pending_l2') : []; 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, kind = 'success') => { setToast({ msg, kind }); setTimeout(() => setToast(null), 3000); }; // Action: Approve refund const handleApproveRefund = (refundId) => { setRefunds(prev => prev.map(r => { if (r.id !== refundId) return r; const randRef = 'REF-RFD-' + Math.floor(1000000 + Math.random() * 9000000); return { ...r, status: 'success', refNo: randRef }; })); triggerToast(`Pengajuan refund ${refundId} berhasil disetujui.`, 'success'); }; // Action: Reject refund const handleRejectRefundSubmit = (e) => { e.preventDefault(); if (!rejectingRefund || !rejectReasonInput.trim()) return; setRefunds(prev => prev.map(r => { if (r.id !== rejectingRefund.id) return r; return { ...r, status: 'rejected', rejectReason: rejectReasonInput.trim() }; })); triggerToast(`Pengajuan refund ${rejectingRefund.id} ditolak.`, 'success'); // Close modal setRejectingRefund(null); setRejectReasonInput(''); }; // Action: Add new refund request const handleAddRefundSubmit = (e) => { e.preventDefault(); if (!formMerchantId || !formTxId || !formCustomer || !formAmount.trim()) return; const m = activeMerchants.find(merchant => merchant.id === formMerchantId); if (!m) return; const newId = 'REFUND-' + Math.floor(100 + Math.random() * 900); const newRefund = { id: newId, txId: formTxId, mid: m.id, name: m.merchantName, customer: formCustomer, amount: Number(formAmount), time: new Date().toISOString(), reason: formReason || 'Permintaan refund merchant', status: 'pending', refNo: '—' }; setRefunds(prev => [newRefund, ...prev]); setShowAddModal(false); triggerToast(`Pengajuan refund ${newId} berhasil ditambahkan.`, 'success'); // Reset Form setFormMerchantId(''); setFormTxId(''); setFormCustomer(''); setFormAmount(''); setFormReason(''); }; // Totals calculations const totalVolume = refunds.filter(r => r.status === 'success').reduce((acc, curr) => acc + curr.amount, 0); const pendingCount = refunds.filter(r => r.status === 'pending').length; const successCount = refunds.filter(r => r.status === 'success').length; const rejectedCount = refunds.filter(r => r.status === 'rejected').length; return (
{/* Header */}
Refund Operations

Manajemen Refund QRIS

Kelola, validasi, dan setujui pengembalian dana transaksi QRIS nasabah berdasarkan permohonan merchant.

{/* Stats Summary Cards */}
{[ { label: 'Total Volume Refund', value: fmtRp(totalVolume), sub: 'Dana sukses dikembalikan', icon: 'bx-wallet', color: TEAL }, { label: 'Pengajuan Baru (Pending)', value: pendingCount, sub: 'Butuh pemeriksaan checker', icon: 'bx-time-five', color: pendingCount > 0 ? ORANGE : '#a7a8a8' }, { label: 'Refund Sukses', value: successCount, sub: 'Pengajuan sukses diotorisasi', icon: 'bx-check-double', color: GREEN }, { label: 'Refund Ditolak', value: rejectedCount, sub: 'Pengajuan refund ditolak', icon: 'bx-block', color: RED } ].map((card, i) => (
{card.label}
{card.value}
{card.sub}
))}
{/* Refund Queue Table */}

Daftar Pengajuan Pengembalian Dana (Refund)

{refunds.map(r => { const isPending = r.status === 'pending'; const isSuccess = r.status === 'success'; const isRejected = r.status === 'rejected'; return ( e.currentTarget.style.background = '#fafbfc'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> ); })}
ID Refund ID Transaksi Nama Merchant Nama Pelanggan Nominal Refund Waktu Diajukan Status Aksi
{r.id} {r.txId}
{r.name}
{r.mid}
{r.customer} {fmtRp(r.amount)} {fmtDate(r.time)} {isPending ? 'MENUNGGU' : r.status.toUpperCase()}
{/* Modal: Refund Detail Breakdown */} {selectedRefund && (
setSelectedRefund(null)} style={{ position: 'absolute', inset: 0, background: 'rgba(23,25,25,0.4)', backdropFilter: 'blur(2px)' }} />

Detail Pengajuan Refund

ID Refund
{selectedRefund.id}
{selectedRefund.status.toUpperCase()}
Nama Merchant {selectedRefund.name}
ID Transaksi (Original) {selectedRefund.txId}
Nama Pelanggan {selectedRefund.customer}
Waktu Pengajuan {fmtDate(selectedRefund.time)}
Nominal Refund {fmtRp(selectedRefund.amount)}
Alasan Refund:
"{selectedRefund.reason}"
{selectedRefund.status === 'success' && (
No. Referensi Refund Bank {selectedRefund.refNo}
)} {selectedRefund.status === 'rejected' && (
Alasan Penolakan: {selectedRefund.rejectReason || 'Dokumen penunjang kurang lengkap.'}
)}
)} {/* Modal: Tambah Refund Modal Form */} {showAddModal && (
setShowAddModal(false)} style={{ position: 'absolute', inset: 0, background: 'rgba(23,25,25,0.4)', backdropFilter: 'blur(2px)' }} />

Tambah Pengajuan Refund QRIS

setFormTxId(e.target.value)} placeholder="Contoh: TX-260624-9981..." style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#171919', outline: 'none' }} onFocus={e => e.target.style.borderColor = TEAL} onBlur={e => e.target.style.borderColor = '#efefef'} />
setFormCustomer(e.target.value)} placeholder="Nama pemegang instrumen bayar..." style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#171919', outline: 'none' }} onFocus={e => e.target.style.borderColor = TEAL} onBlur={e => e.target.style.borderColor = '#efefef'} />
Rp setFormAmount(e.target.value)} placeholder="Masukkan nominal pengembalian..." style={{ width: '100%', padding: '10px 12px 10px 32px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#171919', outline: 'none' }} onFocus={e => e.target.style.borderColor = TEAL} onBlur={e => e.target.style.borderColor = '#efefef'} />