// merchant-business.jsx — Merchant Business page for Hibank QRIS BackOffice function MerchantBusinessPage({ merchants, setMerchants, primary, secondary }) { const TEAL = primary || '#0056D2'; const ORANGE = secondary || '#cf5a27'; // ── Shared Style Objects ── const primaryBtn = { background: TEAL, color: '#fff', border: 'none', borderRadius: 6, padding: '6px 12px', fontSize: 11.5, fontWeight: 700, cursor: 'pointer', marginTop: 10, display: 'inline-flex', alignItems: 'center', gap: 4, fontFamily: 'inherit' }; const secondaryBtn = { background: '#f6f6f6', color: '#515252', border: 'none', borderRadius: 6, padding: '6px 12px', fontSize: 11.5, fontWeight: 600, cursor: 'pointer', marginTop: 10, display: 'inline-flex', alignItems: 'center', gap: 4, fontFamily: 'inherit' }; const labelStyle = { fontSize: 12.5, fontWeight: 700, color: '#515252', marginBottom: 6, display: 'block' }; const inputStyle = { padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#171919', outline: 'none', fontFamily: 'inherit', marginBottom: 12, width: '100%', boxSizing: 'border-box' }; const thStyle = { padding: '10px 12px', fontSize: 12, fontWeight: 700, color: '#515252', textAlign: 'left' }; const tdStyle = { padding: '10px 12px', fontSize: 13, color: '#171919' }; const actionBtn = { background: TEAL, color: '#fff', border: 'none', borderRadius: 5, padding: '4px 10px', fontSize: 11, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', marginRight: 6 }; const deleteBtn = { background: '#ed3151', color: '#fff', border: 'none', borderRadius: 5, padding: '4px 10px', fontSize: 11, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }; const modalOverlay = { position: 'fixed', inset: 0, zIndex: 1500, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(23,25,25,0.4)', backdropFilter: 'blur(3px)', animation: 'fadeIn 150ms ease' }; const modalContent = { background: '#fff', borderRadius: 12, padding: 24, width: 380, maxWidth: '90%', boxShadow: '0 20px 40px rgba(0,0,0,0.15)', display: 'flex', flexDirection: 'column', animation: 'slideUp 250ms cubic-bezier(0.22, 0.68, 0, 1.1)' }; // Find approved merchants const approvedMerchants = merchants.filter(m => m.status === 'approved'); // Selected merchant state (default to first approved merchant or empty if none) const [selectedMid, setSelectedMid] = React.useState( approvedMerchants.length > 0 ? approvedMerchants[0].id : '' ); const activeMerchant = merchants.find(m => m.id === selectedMid); // Tab State const [activeTab, setActiveTab] = React.useState('rules'); // 'rules' | 'integration' | 'stores' // Modal States const [showingAddForm, setShowingAddForm] = React.useState(false); const [showingEditForm, setShowingEditForm] = React.useState(false); const [showingAddStoreForm, setShowingAddStoreForm] = React.useState(false); const [showingEditStoreForm, setShowingEditStoreForm] = React.useState(false); const [showEditPosModal, setShowEditPosModal] = React.useState(false); const [showRevokePosModal, setShowRevokePosModal] = React.useState(false); const [showAssignModal, setShowAssignModal] = React.useState(false); const [showingDetailForm, setShowingDetailForm] = React.useState(false); // Store Fields const [storeName, setStoreName] = React.useState(''); const [storeAddress, setStoreAddress] = React.useState(''); const [storeStatus, setStoreStatus] = React.useState('active'); // Form Fields const [posName, setPosName] = React.useState(''); const [merchantPublicKey, setMerchantPublicKey] = React.useState(''); const [merchantCallbackUrl, setMerchantCallbackUrl] = React.useState(''); const [tempClientId, setTempClientId] = React.useState(''); const [tempClientSecret, setTempClientSecret] = React.useState(''); const [tempHibankPublicKey] = React.useState('pk_hibank_qris_mgmt_live_2026_06_25_9f8d7c6b5a4f3e2d1c0b'); const [targetPos, setTargetPos] = React.useState(null); const [targetStore, setTargetStore] = React.useState(null); const [selectedPosIds, setSelectedPosIds] = React.useState([]); // Product Management state const [showAddProductModal, setShowAddProductModal] = React.useState(false); const [showEditProductModal, setShowEditProductModal] = React.useState(false); const [showDeleteProductModal, setShowDeleteProductModal] = React.useState(false); const [targetProduct, setTargetProduct] = React.useState(null); const [prodName, setProdName] = React.useState(''); const [prodSku, setProdSku] = React.useState(''); const [prodCategory, setProdCategory] = React.useState('Minuman'); const [prodPrice, setProdPrice] = React.useState(''); const [prodStock, setProdStock] = React.useState(''); const [prodSearch, setProdSearch] = React.useState(''); // User Management state // User Management state - start with empty list, will be populated by sample effect const [userList, setUserList] = React.useState([]); const [showAddUserForm, setShowAddUserForm] = React.useState(false); const [showEditUserForm, setShowEditUserForm] = React.useState(false); const [targetUser, setTargetUser] = React.useState(null); const [newUsername, setNewUsername] = React.useState(''); const [newRole, setNewRole] = React.useState('operator'); const [newEmail, setNewEmail] = React.useState(''); const [newPhone, setNewPhone] = React.useState(''); // Copy states // Load sample users if none exist React.useEffect(() => { if (userList.length === 0) { const sampleUsers = [ { id: 'USR-001', username: 'john.doe', role: 'admin', email: 'john.doe@example.com', phone: '081234567890' }, { id: 'USR-002', username: 'jane.smith', role: 'operator', email: 'jane.smith@example.com', phone: '082345678901' }, { id: 'USR-003', username: 'alice.wonder', role: 'operator', email: 'alice.wonder@example.com', phone: '083456789012' }, { id: 'USR-004', username: 'bob.builder', role: 'admin', email: 'bob.builder@example.com', phone: '084567890123' } ]; setUserList(sampleUsers); } }, []); const [copiedKey, setCopiedKey] = React.useState(null); // Activation simulation states const [activationCode, setActivationCode] = React.useState(null); const [showActivationMessage, setShowActivationMessage] = React.useState(false); // Helper: Copy to clipboard const handleCopy = (text, fieldId) => { navigator.clipboard.writeText(text); setCopiedKey(fieldId); setTimeout(() => setCopiedKey(null), 1500); }; if (!activeMerchant) { return (

Tidak ada merchant aktif

Aktifkan merchant terlebih dahulu di menu Approval.

); } // Ensure posList is initialized const posList = activeMerchant.posList || [ { id: 'POS-001', name: 'POS WEB LITE', status: 'inactive', clientId: `cid_pos_${activeMerchant.id.toLowerCase().replace(/[^a-z0-9]/g, '')}_002`, publicKey: `pk_live_pos_${activeMerchant.id.toLowerCase().replace(/[^a-z0-9]/g, '')}_002_4b5c2a` }, { id: 'POS-003', name: 'Kasir Utama (Lantai 1)', status: 'inactive', clientId: `cid_pos_${activeMerchant.id.toLowerCase().replace(/[^a-z0-9]/g, '')}_003`, publicKey: `pk_live_pos_${activeMerchant.id.toLowerCase().replace(/[^a-z0-9]/g, '')}_003_1f9e2d` } ]; // Ensure storeList is initialized const storeList = activeMerchant.storeList || [ { id: `STR-${activeMerchant.id.replace('MRC-', '')}-01`, name: 'Cabang Utama Pekalongan', address: 'Jl. Hayam Wuruk No. 21, Pekalongan', assignedPos: ['POS-001'], status: 'active' }, { id: `STR-${activeMerchant.id.replace('MRC-', '')}-02`, name: 'Store Outlet Mall Kota', address: 'Grand Mall Lantai Dasar, Jakarta', assignedPos: ['POS-002'], status: 'active' }, { id: `STR-${activeMerchant.id.replace('MRC-', '')}-03`, name: 'Flagship Store Heritage', address: 'Jl. Pemuda No. 45, Semarang', assignedPos: ['POS-003'], status: 'inactive' } ]; // Ensure productList is initialized const productList = activeMerchant.productList || [ { id: 'PRD-001', name: 'Kopi Susu Gula Aren', sku: 'KSGA-01', category: 'Minuman', price: 22000, stock: 120, status: 'active' }, { id: 'PRD-002', name: 'Americano Hot/Ice', sku: 'AMR-01', category: 'Minuman', price: 18000, stock: 80, status: 'active' }, { id: 'PRD-003', name: 'Croissant Butter', sku: 'CRB-01', category: 'Makanan', price: 25000, stock: 40, status: 'active' }, { id: 'PRD-004', name: 'Nasi Goreng Spesial', sku: 'NGS-01', category: 'Makanan', price: 35000, stock: 0, status: 'inactive' }, { id: 'PRD-005', name: 'Tote Bag Merchandise', sku: 'TTB-01', category: 'Merchandise', price: 75000, stock: 15, status: 'active' } ]; const fmtRpProd = (v) => 'Rp ' + Number(v || 0).toLocaleString('id-ID'); // Business Rules (View Only) const businessRules = [ { id: 'RULE-001', parameter: 'Limit Minimum Transaksi QRIS', value: 'Rp 10.000', category: 'Limit Transaksi', desc: 'Batas minimal per transaksi QRIS' }, { id: 'RULE-002', parameter: 'Limit Maksimum Transaksi QRIS', value: 'Rp 10.000.000', category: 'Limit Transaksi', desc: 'Batas maksimal per transaksi QRIS standar Bank Indonesia' }, { id: 'RULE-003', parameter: 'Merchant Discount Rate (MDR) Fee', value: '0.70% (Regular)', category: 'Biaya Layanan', desc: 'Tarif MDR yang dikenakan ke merchant' }, { id: 'RULE-004', parameter: 'Siklus Settlement Merchant', value: 'H+1 Hari Kerja', category: 'Operasional', desc: 'Siklus transfer dana penjualan ke rekening merchant' }, { id: 'RULE-005', parameter: 'Batas Minimum Settlement Dana', value: 'Rp 50.000', category: 'Operasional', desc: 'Batas minimum saldo mengendap untuk proses pencairan dana' }, { id: 'RULE-006', parameter: 'Toleransi Pembatalan / Refund', value: 'Maksimum 24 Jam', category: 'Refund', desc: 'Jendela waktu pengajuan refund transaksi sukses' } ]; // Generate preview values when showingAddForm is opened React.useEffect(() => { if (showingAddForm && activeMerchant) { const nextSeq = posList.length + 1; const midClean = activeMerchant.id.toLowerCase().replace(/[^a-z0-9]/g, ''); const cid = `cid_pos_${midClean}_${String(nextSeq).padStart(3, '0')}`; const randomHex = Math.random().toString(36).slice(2, 8); const secret = `cs_live_pos_${midClean}_${String(nextSeq).padStart(3, '0')}_${randomHex}${Math.random().toString(36).slice(2, 6)}`; setTempClientId(cid); setTempClientSecret(secret); setMerchantPublicKey(''); setMerchantCallbackUrl(''); } }, [showingAddForm, activeMerchant, posList.length]); // State Updater Helper const updateMerchantData = (updates) => { setMerchants(prev => prev.map(m => { if (m.id !== activeMerchant.id) return m; return { ...m, ...updates }; })); }; // Add POS action const handleAddPos = () => { if (!posName.trim()) { alert('Nama POS / Kasir harus diisi!'); return; } if (!merchantPublicKey.trim()) { alert('Public Key Merchant harus diisi!'); return; } if (!merchantCallbackUrl.trim()) { alert('Callback URL Merchant harus diisi!'); return; } const nextSeq = posList.length + 1; const posId = `POS-${String(nextSeq).padStart(3, '0')}`; const randomHex = Math.random().toString(36).slice(2, 8); const generatedPublicKey = `pk_live_pos_${activeMerchant.id.toLowerCase().replace(/[^a-z0-9]/g, '')}_${String(nextSeq).padStart(3, '0')}_${randomHex}`; const newPos = { id: posId, name: posName.trim(), status: 'active', clientId: tempClientId, clientSecret: tempClientSecret, publicKey: generatedPublicKey, merchantPublicKey: merchantPublicKey.trim(), callbackUrl: merchantCallbackUrl.trim() }; updateMerchantData({ posList: [...posList, newPos] }); setPosName(''); setMerchantPublicKey(''); setMerchantCallbackUrl(''); setShowingAddForm(false); }; // Edit POS action const handleEditPos = () => { if (!posName.trim() || !targetPos) return; updateMerchantData({ posList: posList.map(p => p.id === targetPos.id ? { ...p, name: posName.trim() } : p) }); setPosName(''); setTargetPos(null); setShowEditPosModal(false); }; // Revoke POS action const handleRevokePos = () => { if (!targetPos) return; updateMerchantData({ posList: posList.map(p => p.id === targetPos.id ? { ...p, status: 'revoked' } : p) }); setTargetPos(null); setShowRevokePosModal(false); }; // Activate POS Web Lite simulation const handleActivatePos = (posId) => { const code = Math.floor(100000 + Math.random() * 900000).toString(); setActivationCode(code); setShowActivationMessage(true); // Update targetPos locally so details modal updates dynamically setTargetPos(prev => prev ? { ...prev, status: 'active' } : null); // Update in parent state updateMerchantData({ posList: posList.map(p => p.id === posId ? { ...p, status: 'active' } : p) }); }; // Assign POS to Store action const handleAssignPos = () => { if (!targetStore) return; updateMerchantData({ storeList: storeList.map(s => s.id === targetStore.id ? { ...s, assignedPos: selectedPosIds } : s) }); setTargetStore(null); setShowAssignModal(false); }; // Add Store action const handleAddStore = () => { if (!storeName.trim()) { alert('Nama Store / Outlet harus diisi!'); return; } if (!storeAddress.trim()) { alert('Alamat Store / Outlet harus diisi!'); return; } const nextSeq = storeList.length + 1; const storeId = `STR-${activeMerchant.id.replace('MRC-', '')}-${String(nextSeq).padStart(2, '0')}`; const newStore = { id: storeId, name: storeName.trim(), address: storeAddress.trim(), assignedPos: selectedPosIds, status: storeStatus }; updateMerchantData({ storeList: [...storeList, newStore] }); setStoreName(''); setStoreAddress(''); setStoreStatus('active'); setSelectedPosIds([]); setShowingAddStoreForm(false); }; // Edit Store action const handleEditStore = () => { if (!storeName.trim() || !targetStore) return; updateMerchantData({ storeList: storeList.map(s => s.id === targetStore.id ? { ...s, name: storeName.trim(), address: storeAddress.trim(), assignedPos: selectedPosIds, status: storeStatus } : s) }); setStoreName(''); setStoreAddress(''); setStoreStatus('active'); setSelectedPosIds([]); setTargetStore(null); setShowingEditStoreForm(false); }; // ── Product Management actions ── const resetProdForm = () => { setProdName(''); setProdSku(''); setProdCategory('Minuman'); setProdPrice(''); setProdStock(''); }; const handleAddProduct = () => { if (!prodName.trim() || !prodPrice) return; const nextSeq = productList.length + 1; const newProduct = { id: `PRD-${String(nextSeq).padStart(3, '0')}`, name: prodName.trim(), sku: prodSku.trim() || `SKU-${String(nextSeq).padStart(3, '0')}`, category: prodCategory, price: Number(prodPrice), stock: Number(prodStock) || 0, status: 'active' }; updateMerchantData({ productList: [...productList, newProduct] }); resetProdForm(); setShowAddProductModal(false); }; const handleEditProduct = () => { if (!prodName.trim() || !targetProduct) return; updateMerchantData({ productList: productList.map(p => p.id === targetProduct.id ? { ...p, name: prodName.trim(), sku: prodSku.trim(), category: prodCategory, price: Number(prodPrice), stock: Number(prodStock) || 0 } : p) }); resetProdForm(); setTargetProduct(null); setShowEditProductModal(false); }; const handleDeleteProduct = () => { if (!targetProduct) return; updateMerchantData({ productList: productList.filter(p => p.id !== targetProduct.id) }); setTargetProduct(null); setShowDeleteProductModal(false); }; const toggleProductStatus = (id) => { updateMerchantData({ productList: productList.map(p => p.id === id ? { ...p, status: p.status === 'active' ? 'inactive' : 'active' } : p) }); }; // ── User Management Actions ── const resetUserForm = () => { setNewUsername(''); setNewRole('operator'); setNewEmail(''); setNewPhone(''); setTargetUser(null); }; const handleAddUser = () => { if (!newUsername.trim()) { alert('Username harus diisi!'); return; } const nextSeq = userList.length + 1; const newUser = { id: `USR-${String(nextSeq).padStart(3, '0')}`, username: newUsername.trim(), role: newRole, email: newEmail.trim(), phone: newPhone.trim() }; setUserList([...userList, newUser]); resetUserForm(); setShowAddUserForm(false); }; const handleEditUser = () => { if (!newUsername.trim() || !targetUser) return; setUserList(userList.map(u => u.id === targetUser.id ? { ...u, username: newUsername.trim(), role: newRole, email: newEmail.trim(), phone: newPhone.trim() } : u )); resetUserForm(); setShowEditUserForm(false); }; const handleDeleteUser = (id) => { if (confirm('Apakah Anda yakin ingin menghapus pengguna ini?')) { setUserList(userList.filter(u => u.id !== id)); } }; return (
{/* Header & Selector */}
Merchant Business

Bisnis & POS Merchant

Kelola profil outlet, Integrasi, dan produk.

{/* Dropdown Selector */}
Pilih Merchant:
{/* ── SECTION 1: MERCHANT PROFILE ── */}

{activeMerchant.merchantName}

MID: {activeMerchant.id} | Tipe: {activeMerchant.businessType}
Nama Pemilik / PIC
{activeMerchant.picName || '—'}
Email Kontak
{activeMerchant.email || '—'}
Telepon PIC
{activeMerchant.picPhone || '—'}
NPWP Usaha
{activeMerchant.npwp || '—'}
Alamat Operasional
{activeMerchant.mAddress ? `${activeMerchant.mAddress}, ${activeMerchant.mKelurahan}, ${activeMerchant.mKecamatan}, ${activeMerchant.mKabupaten}, ${activeMerchant.mProvinsi} - ${activeMerchant.mPostal}` : '—'}
{/* ── SECTION 2: TABS NAVIGATION ── */}
{[ { id: 'rules', label: 'Business Rules', icon: 'bx-notepad' }, { id: 'integration', label: 'Third Party Integration', icon: 'bx-code-curly' }, { id: 'stores', label: 'Outlet & Store Management', icon: 'bx-store' }, { id: 'products', label: 'Product Management', icon: 'bx-package' }, { id: 'users', label: 'User Management', icon: 'bx-user' } ].map(tab => { const isActive = activeTab === tab.id; return ( ); })}
{/* ── TAB CONTENT ── */} {/* ── TAB 1: BUSINESS RULES (VIEW ONLY) ── */} {activeTab === 'rules' && (
{businessRules.map(rule => ( e.currentTarget.style.background = '#fafbfc'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> ))}
Rule ID Nama Aturan Batasan / Nilai Kategori Deskripsi Status
{rule.id} {rule.parameter} {rule.value} {rule.category} {rule.desc} Active
)} {/* ── TAB 2: INTEGRATION (POS LIST WITH ADD, EDIT, REVOKE) ── */} {activeTab === 'integration' && ( showingAddForm ? (

Tambah Integrasi Baru

Daftarkan integrasi baru dan konfigurasikan parameter integrasi serta kunci enkripsi.

{/* Left Column: Form Inputs */}
setPosName(e.target.value)} style={{ width: '100%', boxSizing: 'border-box', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#171919', outline: 'none', fontFamily: 'inherit' }} onFocus={e => e.target.style.borderColor = TEAL} onBlur={e => e.target.style.borderColor = '#efefef'} />