// 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 (
onNavigate(id)} sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} onLogout={onLogout} pendingApproval={pendingApproval} primary={TEAL} secondary={ORANGE} sidebarBg={sidebarColor} role={role} user={user} />
{/* TOPBAR */}
{view === 'list' && ( <> Merchant Management Kelola status operasional, parameter, dan kode QRIS merchant )} {view === 'biz_config' && ( <> Konfigurasi Bisnis Pengaturan parameter operasional & biaya merchant )} {view === 'integration' && ( <> Integrasi API & POS Kelola kredensial pengembang dan terminal POS merchant )}
AB
{view === 'list' && ( <> {/* Header */}
Katalog Merchant

Kelola Data Merchant

Aktifkan, tangguhkan, atau tinjau QRIS & profil keuangan merchant terdaftar.

{/* STATS */}
{stats.map((s, i) => (
{s.label}
{s.value}
))}
{/* TABLE CONTAINER */}
{/* Filter and search bar */}
{/* Filter Dropdown */}
Status:
{/* Search input */}
setSearch(e.target.value)} placeholder="Cari ID, nama merchant, kota..." style={{ width: '100%', padding: '9px 12px 9px 36px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 12.5, outline: 'none', fontFamily: 'inherit', transition: 'all 150ms' }} onFocus={e => e.target.style.borderColor = TEAL} onBlur={e => e.target.style.borderColor = '#efefef'} />
{/* Merchant Table */}
{[ { key: 'id', label: 'MID' }, { key: 'merchantName', label: 'Nama Merchant' }, { key: 'businessType', label: 'Jenis Usaha' }, { key: 'monthlyOmset', label: 'Omset/Bulan' }, { key: 'mKabupaten', label: 'Kota/Kabupaten' }, { key: 'status', label: 'Status' }, { key: 'actions', label: 'Aksi' } ].map(col => { const canSort = col.key !== 'actions'; return ( ); })} {filteredMerchants.length === 0 ? ( ) : filteredMerchants.map(m => { const st = getStatusInfo(m.status); const isPending = m.status === 'pending_l1' || m.status === 'pending_l2'; const isRejected = m.status === 'rejected'; return ( e.currentTarget.style.background = '#fafbfc'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'} > {/* MID */} {/* Name */} {/* Business Type */} {/* Monthly Omset */} {/* City */} {/* Status */} {/* Actions */} ); })}
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}
{m.merchantName}
PIC: {m.picName}
{m.businessType || '—'} {fmtRp(m.monthlyOmset)} {m.mKabupaten || m.mProvinsi || '—'} {st.label}
{m.status === 'approved' || m.status === 'suspended' ? ( <> ) : ( {isRejected ? 'Tinjauan Ditolak' : 'Proses Approval'} )}
{/* Pagination Footer */}
Menampilkan {filteredMerchants.length} dari {merchants.length} merchant terdaftar
)} {view === 'biz_config' && activeMerchant && ( setView('list')} onSave={handleSaveBizConfig} primary={TEAL} secondary={ORANGE} /> )} {view === 'integration' && activeMerchant && ( setView('list')} onSave={handleSaveIntegration} primary={TEAL} secondary={ORANGE} /> )}
{/* DETAIL DRAWER */} {selectedMerchant && (
setSelectedMerchant(null)} style={{ position: 'absolute', inset: 0, background: 'rgba(23,25,25,0.4)', backdropFilter: 'blur(3px)', animation: 'fadeIn 200ms ease' }} />
{/* Drawer Header */}
{selectedMerchant.merchantName}
{selectedMerchant.id}
{/* Drawer Body */}
{/* Profile Card */}
Status Merchant {getStatusInfo(selectedMerchant.status).label}
{/* MID Info */}
NMID
NMID{selectedMerchant.id.slice(-8)}
Tanggal Gabung
{fmtDate(selectedMerchant.submittedAt)}
{/* Data Usaha */}
Informasi Usaha
Kategori (MCC) {selectedMerchant.mcc || '—'} (Code: {selectedMerchant.mccCode})
Tipe Bisnis {selectedMerchant.businessType || '—'}
NPWP Usaha {selectedMerchant.npwp || '—'}
Omset Bulanan {fmtRp(selectedMerchant.monthlyOmset)}
Alamat Lengkap {selectedMerchant.mAddress}, {selectedMerchant.mKelurahan}, {selectedMerchant.mKecamatan}, {selectedMerchant.mKabupaten}, {selectedMerchant.mProvinsi} {selectedMerchant.mPostal}
{/* Data PIC */}
Penanggung Jawab (PIC)
Nama PIC {selectedMerchant.picName}
No. Telepon +62 {selectedMerchant.picPhone}
NIK e-KTP {selectedMerchant.nik || '—'}
Alamat Email {selectedMerchant.email}
{/* Data Rekening */}
Rekening Pencairan
Nama Bank {selectedMerchant.bankName}
Nomor Rekening {selectedMerchant.accountNumber}
Nama Pemilik {selectedMerchant.accountHolder}
{/* Drawer Footer */}
)} {/* QRIS MODAL */} {qrisModal && (
setQrisModal(null)}>
e.stopPropagation()}> {/* Modal Header */}
QRIS Code Merchant
{/* QRIS STICKER CARD (Simulated CSS) */}
{/* Yellow Sticker Header */}
QR IS
QR Code Indonesian Standard
{/* Merchant Title Block */}
{qrisModal.merchantName}
NMID: NMID{qrisModal.id.slice(-8)}
{/* QR Code Graphic Box */}
{/* Stylized QR icon representing high-fidelity mockup */} {/* Floating miniature hibank logo in center of QR */}
hi
{/* Acceptance Notice */}
Dapat Menerima Pembayaran GPN Melalui Semua Penyedia Jasa Pembayaran Elektronik
{/* Small Payment Badges */}
{['hi-payment', 'GPN', 'OVO', 'GOPAY', 'DANA'].map(net => ( {net} ))}
{/* Modal Actions */}
)}
); } // ── BUSINESS CONFIGURATION SUB-PAGE ── function BusinessConfigPage({ merchant, onBack, onSave, primary, secondary }) { const TEAL = primary || '#0056D2'; const ORANGE = secondary || '#cf5a27'; // Initialize config list with defaults if not present const defaultConfigs = [ { id: 'CFG-001', name: 'Siklus Settlement', value: merchant.settlementCycle || 'H+1', desc: 'Siklus pemindahan bukuan dana hasil transaksi QRIS ke rekening pencairan merchant.' }, { id: 'CFG-002', name: 'MDR Fee QRIS', value: merchant.mdrFee || '0.7%', desc: 'Persentase potongan biaya per transaksi untuk QRIS Reguler.' }, { id: 'CFG-003', name: 'Limit per Transaksi', value: merchant.trxLimit !== undefined ? 'Rp ' + Number(merchant.trxLimit).toLocaleString('id-ID') : 'Rp 10.000.000', desc: 'Batas maksimal nominal pembayaran dalam satu kali scan QRIS.' }, { id: 'CFG-004', name: 'Limit Akumulasi Harian', value: merchant.dailyLimit !== undefined ? 'Rp ' + Number(merchant.dailyLimit).toLocaleString('id-ID') : 'Rp 100.000.000', desc: 'Batas kumulatif penerimaan dana QRIS dalam satu hari kalender.' }, { id: 'CFG-005', name: 'Metode QRIS Standard', value: 'Aktif', desc: 'Scan e-wallet / mobile banking (Gopay, OVO, Dana, LinkAja).' }, { id: 'CFG-006', name: 'Metode GPN Debit', value: 'Aktif', desc: 'Gerbang Pembayaran Nasional untuk kartu debit kartu fisik.' } ]; const [configList, setConfigList] = React.useState(merchant.configList || defaultConfigs); // Modal / popup states const [showAddModal, setShowAddModal] = React.useState(false); const [editingConfig, setEditingConfig] = React.useState(null); // holds the config item being edited // Form states const [formName, setFormName] = React.useState(''); const [formValue, setFormValue] = React.useState(''); const [formDesc, setFormDesc] = React.useState(''); const [showSuccessToast, setShowSuccessToast] = React.useState(false); // Setup form for editing const startEdit = (cfg) => { setEditingConfig(cfg); setFormName(cfg.name); setFormValue(cfg.value); setFormDesc(cfg.desc); }; const handleSaveEdit = () => { if (!formName.trim() || !formValue.trim()) return; setConfigList(prev => prev.map(cfg => { if (cfg.id !== editingConfig.id) return cfg; return { ...cfg, name: formName.trim(), value: formValue.trim(), desc: formDesc.trim() }; })); setEditingConfig(null); clearForm(); }; // Add new custom parameter const handleAddConfig = () => { if (!formName.trim() || !formValue.trim()) return; const nextSeq = configList.length + 1; const newId = `CFG-${String(nextSeq).padStart(3, '0')}`; const newCfg = { id: newId, name: formName.trim(), value: formValue.trim(), desc: formDesc.trim() }; setConfigList(prev => [...prev, newCfg]); setShowAddModal(false); clearForm(); }; const clearForm = () => { setFormName(''); setFormValue(''); setFormDesc(''); }; const handleDeleteConfig = (id) => { setConfigList(prev => prev.filter(cfg => cfg.id !== id)); }; const handleSaveAll = () => { onSave(merchant.id, { configList }); setShowSuccessToast(true); setTimeout(() => { setShowSuccessToast(false); onBack(); }, 1500); }; return (
{/* Breadcrumb & Navigation */}
Merchant Management / {merchant.merchantName} / Konfigurasi Bisnis
{/* Header Info */}

{merchant.merchantName}

MID: {merchant.id} | PIC: {merchant.picName}
{merchant.status === 'approved' ? 'Merchant Aktif' : 'Merchant Ditangguhkan'}
{/* TABULAR CONFIG LIST CARD */}

Daftar Parameter Konfigurasi Bisnis

Pengaturan parameter operasional & biaya merchant terstruktur dalam tabel.

{/* Configurations Table */}
{configList.length === 0 ? ( ) : configList.map(cfg => { const isCustom = !['CFG-001', 'CFG-002', 'CFG-003', 'CFG-004', 'CFG-005', 'CFG-006'].includes(cfg.id); return ( e.currentTarget.style.background = '#fafbfc'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> {/* ID */} {/* Name */} {/* Value Badge */} {/* Description */} {/* Actions */} ); })}
Parameter ID Nama Parameter Nilai Konfigurasi Deskripsi Aksi
Belum ada parameter konfigurasi yang terdaftar.
{cfg.id} {cfg.name} {cfg.value} {cfg.desc}
{isCustom && ( )}
{/* Action Footer */}
{/* Success Toast */} {showSuccessToast && (
Konfigurasi Bisnis berhasil diperbarui!
)} {/* ADD PARAMETER MODAL */} {showAddModal && (
setShowAddModal(false)}>
e.stopPropagation()}>

Tambah Parameter Konfigurasi

setFormName(e.target.value)} style={{ padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#171919', outline: 'none', fontFamily: 'inherit', marginBottom: 14 }} /> setFormValue(e.target.value)} style={{ padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#171919', outline: 'none', fontFamily: 'inherit', marginBottom: 14 }} />