// merchant-management.jsx — Merchant Management page function MerchantManagementPage({ onLogout, onNavigate, merchants, setMerchants, primary, secondary, sidebarColor, pendingApproval = 0, role = 'super', user }) { const TEAL = primary || '#0056D2'; const ORANGE = secondary || '#cf5a27'; const GREEN = '#74b50c', RED = '#ed3151', AMBER = '#d9920b'; const [sidebarOpen, setSidebarOpen] = React.useState(true); const [filter, setFilter] = React.useState('all'); // all | active | suspended | pending const [search, setSearch] = React.useState(''); const [selectedMerchant, setSelectedMerchant] = React.useState(null); // for detail drawer const [qrisModal, setQrisModal] = React.useState(null); // for QRIS modal popup const [view, setView] = React.useState('list'); // 'list' | 'biz_config' | 'integration' const [activeMerchantId, setActiveMerchantId] = React.useState(null); // ID of merchant being configured const [sortCol, setSortCol] = React.useState('submittedAt'); const [sortAsc, setSortAsc] = React.useState(false); // Formatting helpers const fmtRp = (v) => { const n = String(v).replace(/[^0-9]/g, ''); return n ? 'Rp ' + Number(n).toLocaleString('id-ID') : '—'; }; const fmtDate = (iso) => new Date(iso).toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' }); // Status handler: toggle between approved (Active) and suspended const toggleStatus = (id) => { setMerchants(prev => prev.map(m => { if (m.id !== id) return m; const newStatus = m.status === 'approved' ? 'suspended' : 'approved'; return { ...m, status: newStatus }; })); }; const handleSaveBizConfig = (merchantId, updatedConfig) => { setMerchants(prev => prev.map(m => { if (m.id !== merchantId) return m; return { ...m, configList: updatedConfig.configList }; })); }; const handleSaveIntegration = (merchantId, updatedConfig) => { setMerchants(prev => prev.map(m => { if (m.id !== merchantId) return m; return { ...m, clientId: updatedConfig.clientId, clientSecret: updatedConfig.clientSecret, integrationMode: updatedConfig.integrationMode, webhookUrl: updatedConfig.webhookUrl, posList: updatedConfig.posList }; })); }; // Status details const getStatusInfo = (status) => { switch (status) { case 'approved': return { label: 'Aktif', color: GREEN, bg: 'rgba(116,181,12,0.12)', icon: 'bx-check-circle' }; case 'suspended': return { label: 'Ditangguhkan', color: RED, bg: 'rgba(237,49,81,0.10)', icon: 'bx-block' }; case 'pending_l1': case 'pending_l2': return { label: 'Pending Approval', color: AMBER, bg: 'rgba(217,146,11,0.12)', icon: 'bx-time-five' }; case 'rejected': return { label: 'Ditolak', color: '#777', bg: 'rgba(161,168,168,0.15)', icon: 'bx-x-circle' }; default: return { label: status, color: '#777', bg: '#efefef', icon: 'bx-help-circle' }; } }; // Stats calculation const stats = [ { label: 'Total Merchant', value: merchants.length, icon: 'bx-store', color: TEAL }, { label: 'Merchant Aktif', value: merchants.filter(m => m.status === 'approved').length, icon: 'bx-check-circle', color: GREEN }, { label: 'Ditangguhkan', value: merchants.filter(m => m.status === 'suspended').length, icon: 'bx-block', color: RED }, { label: 'Menunggu Approval', value: merchants.filter(m => m.status === 'pending_l1' || m.status === 'pending_l2').length, icon: 'bx-time', color: AMBER }, ]; // Filtering and searching logic const filteredMerchants = merchants .filter(m => { if (filter === 'active') return m.status === 'approved'; if (filter === 'suspended') return m.status === 'suspended'; if (filter === 'pending') return m.status === 'pending_l1' || m.status === 'pending_l2'; return true; // all }) .filter(m => { if (!search) return true; const q = search.toLowerCase(); return ( m.id.toLowerCase().includes(q) || m.merchantName.toLowerCase().includes(q) || m.picName.toLowerCase().includes(q) || (m.mKabupaten && m.mKabupaten.toLowerCase().includes(q)) ); }) .sort((a, b) => { const av = a[sortCol] || '', bv = b[sortCol] || ''; if (av < bv) return sortAsc ? -1 : 1; if (av > bv) return sortAsc ? 1 : -1; return 0; }); const handleSort = (col) => { if (sortCol === col) setSortAsc(!sortAsc); else { setSortCol(col); setSortAsc(true); } }; const SortIcon = ({ col }) => { if (sortCol !== col) return ; return ; }; const activeMerchant = merchants.find(m => m.id === activeMerchantId); return (
Aktifkan, tangguhkan, atau tinjau QRIS & profil keuangan merchant terdaftar.
| canSort && handleSort(col.key)}
style={{
padding: '14px 20px', textAlign: 'left', fontSize: 11, fontWeight: 700,
color: '#a7a8a8', letterSpacing: '0.04em', textTransform: 'uppercase',
cursor: canSort ? 'pointer' : 'default', userSelect: 'none'
}}
>
{col.label} {canSort && |
);
})}
||||||
|---|---|---|---|---|---|---|
| Tidak ada merchant terdaftar dalam filter ini | ||||||
| {m.id} | {/* Name */}
{m.merchantName}
PIC: {m.picName}
|
{/* Business Type */}
{m.businessType || '—'} | {/* Monthly Omset */}{fmtRp(m.monthlyOmset)} | {/* City */}{m.mKabupaten || m.mProvinsi || '—'} | {/* Status */}{st.label} | {/* Actions */}
{m.status === 'approved' || m.status === 'suspended' ? (
<>
>
) : (
{isRejected ? 'Tinjauan Ditolak' : 'Proses Approval'}
)}
|
Pengaturan parameter operasional & biaya merchant terstruktur dalam tabel.
| Parameter ID | Nama Parameter | Nilai Konfigurasi | Deskripsi | Aksi |
|---|---|---|---|---|
| Belum ada parameter konfigurasi yang terdaftar. | ||||
| {cfg.id} | {/* Name */}{cfg.name} | {/* Value Badge */}{cfg.value} | {/* Description */}{cfg.desc} | {/* Actions */}
{isCustom && (
)}
|
Konfigurasi terminal kasir (POS) merchant. Setiap POS terdaftar memiliki Client ID dan Public Key khusus untuk verifikasi tanda tangan digital.
| Terminal ID | Client ID | Nama POS / Outlet | Status | Public Key POS | Aksi |
|---|---|---|---|---|---|
| Belum ada terminal POS yang terdaftar untuk merchant ini. | |||||
| {pos.id} | {/* Client ID */}
{pos.clientId}
{copiedKey === `${pos.id}_client_id` && Tersalin!}
|
{/* Name */}
{pos.name} | {/* Status */}{pos.status === 'active' ? 'Aktif' : 'Non-aktif'} | {/* Public Key Display */}
Yakin regenerasi Public Key terminal ini?
) : null}
|
{/* Actions */}
|