// 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 */}
| ID Request |
ID Transaksi |
Merchant |
Nominal Refund |
Waktu Diajukan |
Petugas Checker |
Status |
Aksi |
{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'}>
| {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
)}
{/* 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 });