// fds.jsx — Fraud Detection System (FDS) Dashboard & Sub-pages for Hibank QRIS BackOffice const getInitialFdsAlerts = () => { const defaultAlerts = [ { id: 'ALT-9901', txId: 'TX-260624-7701', time: '2026-06-24T14:10:00', merchant: 'Kopi Senja Nusantara', location: 'Bandung, ID', amount: 45000, score: 92, rule: 'Velocity Check: >5 transaksi dlm 1 menit', status: 'pending' }, { id: 'ALT-9902', txId: 'TX-260624-7708', time: '2026-06-24T14:05:00', merchant: 'Batik Lestari Pekalongan', location: 'Jakarta, ID', amount: 5200000, score: 87, rule: 'Amount Outlier: >10x nominal rata-rata bulanan', status: 'investigating' }, { id: 'ALT-9903', txId: 'TX-260624-7681', time: '2026-06-24T13:48:00', merchant: 'Toko Bangunan Maju Jaya', location: 'Surabaya, ID', amount: 1500000, score: 68, rule: 'Location Anomaly: Login & Trx bersamaan di 2 kota', status: 'approved' }, { id: 'ALT-9904', txId: 'TX-260624-7540', time: '2026-06-24T12:15:00', merchant: 'Kopi Senja Nusantara', location: 'Medan, ID', amount: 25000, score: 55, rule: 'Velocity Check: Berulang dlm waktu sangat singkat', status: 'blocked' } ]; if (!window.__fdsAlerts) { window.__fdsAlerts = defaultAlerts; } return window.__fdsAlerts; }; const saveFdsAlerts = (data) => { window.__fdsAlerts = data; }; // ── 1. FDS DASHBOARD PAGE ── function FdsDashboardPage({ merchants, primary, secondary }) { const TEAL = primary || '#0056D2'; const ORANGE = secondary || '#cf5a27'; const GREEN = '#74b50c'; const RED = '#ed3151'; const AMBER = '#d9920b'; const [alerts, setAlerts] = React.useState(getInitialFdsAlerts()); const [toast, setToast] = React.useState(null); const [activeAlertDetail, setActiveAlertDetail] = React.useState(null); 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' }) + ' ' + d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); }; const triggerToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), 3000); }; const handleSimulateAlert = () => { const names = ['Kopi Senja Nusantara', 'Batik Lestari Pekalongan', 'Toko Bangunan Maju Jaya']; const rules = [ 'Velocity Check: >5 transaksi dlm 1 menit', 'Amount Outlier: >10x nominal rata-rata bulanan', 'Card Bin Suspicious: Transaksi luar negeri dlm volume berulang', 'Location Anomaly: Pergerakan lokasi tidak wajar dlm 10 menit' ]; const cities = ['Makassar, ID', 'Palembang, ID', 'Semarang, ID', 'Denpasar, ID']; const randomName = names[Math.floor(Math.random() * names.length)]; const randomRule = rules[Math.floor(Math.random() * rules.length)]; const randomCity = cities[Math.floor(Math.random() * cities.length)]; const randomAmount = Math.floor(Math.random() * 500) * 10000 + 50000; const randomScore = Math.floor(Math.random() * 35) + 65; // score between 65 and 99 const newAlert = { id: 'ALT-' + (Math.floor(Math.random() * 9000) + 1000), txId: 'TX-260624-' + (Math.floor(Math.random() * 9000) + 1000), time: new Date().toISOString(), merchant: randomName, location: randomCity, amount: randomAmount, score: randomScore, rule: randomRule, status: 'pending' }; const updated = [newAlert, ...alerts]; setAlerts(updated); saveFdsAlerts(updated); triggerToast(`Alert baru raised! Score: ${randomScore} (Risiko Tinggi)`); }; const handleUpdateStatus = (id, newStatus) => { const updated = alerts.map(a => { if (a.id !== id) return a; return { ...a, status: newStatus }; }); setAlerts(updated); saveFdsAlerts(updated); if (activeAlertDetail && activeAlertDetail.id === id) { setActiveAlertDetail({ ...activeAlertDetail, status: newStatus }); } triggerToast(`Status alert ${id} diubah ke ${newStatus.toUpperCase()}`); }; const counts = { total: alerts.length, pending: alerts.filter(a => a.status === 'pending').length, investigating: alerts.filter(a => a.status === 'investigating').length, resolved: alerts.filter(a => a.status === 'approved' || a.status === 'blocked').length, blocked: alerts.filter(a => a.status === 'blocked').length }; // Threat Vector indicators const threatVectors = [ { label: 'Velocity Trx Exceeded', pct: 68, color: RED }, { label: 'Outlier Transaction Amount', pct: 45, color: AMBER }, { label: 'Location & IP Anomaly', pct: 28, color: TEAL }, { label: 'Suspicious BIN / Card Present', pct: 15, color: '#64748b' } ]; return (
{/* Header */}
FDS Dashboard
PROTECTION ACTIVE

Fraud Detection System

Pantau ancaman penipuan, deteksi anomali real-time, dan respon cepat alert mencurigakan.

{/* Stats Summary Cards */}
{[ { label: 'Total Checked Trx', value: '142.842', sub: 'Semua transaksi terfilter', icon: 'bx-shield-quarter', color: TEAL }, { label: 'Alert Aktif (Warning)', value: counts.pending + counts.investigating, sub: 'Butuh respon segera', icon: 'bx-error-circle', color: counts.pending > 0 ? RED : '#a7a8a8' }, { label: 'Transaksi Diblokir', value: counts.blocked, sub: 'Pencegahan fraud berhasil', icon: 'bx-block', color: counts.blocked > 0 ? RED : '#a7a8a8' } ].map((card, i) => (
{card.label}
{card.value}
{card.sub}
))}
{/* Threat Vectors Breakdown */}
Threat Vectors Breakdown
Distribusi anomali fraud yang paling sering mendominasi sistem
{threatVectors.map((v, i) => (
{v.label} {v.pct}%
))}
{/* Real-time Suspect Alerts table */}
Antrean Alert Suspicious Real-Time
Menampilkan log transaksi mencurigakan dengan nilai score anomali tinggi
{alerts.map(a => { const isHigh = a.score >= 80; const isMedium = a.score >= 60 && a.score < 80; const scoreColor = isHigh ? RED : isMedium ? AMBER : TEAL; return ( e.currentTarget.style.background = '#fafbfc'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> ); })}
ID Alert Merchant Lokasi & Trx Nominal Trigger Rule Risk Score Status Aksi
{a.id}
{a.merchant}
{a.txId}
{a.location}
{fmtDate(a.time)}
{fmtRp(a.amount)} {a.rule} {a.score} {a.status.toUpperCase()}
{/* Drawer Detail Alert */} {activeAlertDetail && (
setActiveAlertDetail(null)} style={{ position: 'absolute', inset: 0, background: 'rgba(23,25,25,0.4)', backdropFilter: 'blur(1.5px)' }} />
{/* Header */}
{activeAlertDetail.id}

Detail Temuan Suspicious

{/* Content Body */}
{/* Risk Level Alert */}
= 80 ? RED : AMBER}10`, border: `1.5px solid ${activeAlertDetail.score >= 80 ? RED : AMBER}20`, borderRadius: 10, padding: '16px', display: 'flex', alignItems: 'center', gap: 14, marginBottom: 20 }}>
= 80 ? RED : AMBER, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
= 80 ? RED : AMBER, fontWeight: 700, textTransform: 'uppercase' }}>Anomali HIGH RISK
{/* Transaction Data */}
ID Transaksi {activeAlertDetail.txId}
Nama Merchant {activeAlertDetail.merchant}
Nominal Transaksi {fmtRp(activeAlertDetail.amount)}
Lokasi / IP asal {activeAlertDetail.location}
Waktu Deteksi {fmtDate(activeAlertDetail.time)}
{/* Trigger Rule Card */}
Aturan FDS yang Terlanggar

{activeAlertDetail.rule}

{/* Action Buttons for investigation */} {activeAlertDetail.status === 'pending' && (
)} {activeAlertDetail.status === 'investigating' && (
)} {(activeAlertDetail.status === 'approved' || activeAlertDetail.status === 'blocked') && (
Alert telah diselesaikan sebagai {activeAlertDetail.status.toUpperCase()}. Tidak ada aksi lanjutan.
)}
)} {/* Toast Alert */} {toast && (
{toast}
)}
); } // ── 2. MONITORING FDS PAGE ── function FdsMonitoringPage({ merchants, primary, secondary }) { const TEAL = primary || '#0056D2'; const ORANGE = secondary || '#cf5a27'; const GREEN = '#74b50c'; const RED = '#ed3151'; const AMBER = '#d9920b'; const defaultScans = [ { id: 'SCN-1082', txId: 'TX-260624-9102', time: '2026-06-24T14:48:00', merchant: 'Kopi Senja Nusantara', location: 'Bandung, ID', ip: '182.16.24.8', amount: 35000, score: 12, result: 'safe', rule: '—', cardBin: '4512 88xx', issuer: 'Bank Mandiri' }, { id: 'SCN-1081', txId: 'TX-260624-9098', time: '2026-06-24T14:45:00', merchant: 'Toko Bangunan Maju Jaya', location: 'Surakarta, ID', ip: '103.24.120.12', amount: 890000, score: 28, result: 'safe', rule: '—', cardBin: '5264 12xx', issuer: 'Bank BNI' }, { id: 'SCN-1080', txId: 'TX-260624-9041', time: '2026-06-24T14:32:00', merchant: 'Batik Lestari Pekalongan', location: 'Jakarta, ID', ip: '36.85.12.98', amount: 4800000, score: 78, result: 'flagged', rule: 'Amount Outlier: >5x nominal rata-rata bulanan', cardBin: '4218 09xx', issuer: 'Bank BCA' }, { id: 'SCN-1079', txId: 'TX-260624-8991', time: '2026-06-24T14:28:00', merchant: 'Kopi Senja Nusantara', location: 'Singapore, SG', ip: '202.164.88.9', amount: 120000, score: 92, result: 'blocked', rule: 'Geo Mismatch: Login & Trx bersamaan di 2 negara dlm 5 mnt', cardBin: '4624 33xx', issuer: 'DBS Bank' }, { id: 'SCN-1078', txId: 'TX-260624-8840', time: '2026-06-24T14:15:00', merchant: 'Batik Lestari Pekalongan', location: 'Pekalongan, ID', ip: '110.138.9.15', amount: 150000, score: 8, result: 'safe', rule: '—', cardBin: '4512 88xx', issuer: 'Bank Mandiri' }, { id: 'SCN-1077', txId: 'TX-260624-8730', time: '2026-06-24T13:58:00', merchant: 'Kopi Senja Nusantara', location: 'Medan, ID', ip: '180.244.11.45', amount: 25000, score: 58, result: 'flagged', rule: 'Velocity Check: >3 transaksi dlm 1 menit', cardBin: '4512 88xx', issuer: 'Bank Mandiri' } ]; const [scans, setScans] = React.useState(() => { if (!window.__fdsScans) { window.__fdsScans = defaultScans; } return window.__fdsScans; }); const [searchQuery, setSearchQuery] = React.useState(''); const [filterStatus, setFilterStatus] = React.useState('all'); const [filterRisk, setFilterRisk] = React.useState('all'); const [selectedTx, setSelectedTx] = React.useState(null); const [toast, setToast] = React.useState(null); 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' }) + ' ' + d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); }; const triggerToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), 3000); }; const handleSimulateTrx = () => { const names = ['Kopi Senja Nusantara', 'Batik Lestari Pekalongan', 'Toko Bangunan Maju Jaya']; const locations = ['Jakarta, ID', 'Bandung, ID', 'Surabaya, ID', 'Medan, ID', 'Kuala Lumpur, MY', 'Tokyo, JP']; const ips = ['36.85.12.98', '182.16.24.8', '110.138.9.15', '202.164.88.9', '180.244.11.45']; const issuers = ['Bank Mandiri', 'Bank BNI', 'Bank BCA', 'Bank BRI', 'CIMB Niaga']; const cardBins = ['4512 88xx', '5264 12xx', '4218 09xx', '4624 33xx', '4912 44xx']; const rules = [ 'Velocity Check: >5 transaksi dlm 1 menit', 'Amount Outlier: >10x nominal rata-rata bulanan', 'Card Bin Suspicious: Transaksi luar negeri dlm volume berulang', 'Location Anomaly: Pergerakan lokasi tidak wajar dlm 10 mnt' ]; const randomName = names[Math.floor(Math.random() * names.length)]; const randomLoc = locations[Math.floor(Math.random() * locations.length)]; const randomIp = ips[Math.floor(Math.random() * ips.length)]; const randomIssuer = issuers[Math.floor(Math.random() * issuers.length)]; const randomBin = cardBins[Math.floor(Math.random() * cardBins.length)]; const randomAmount = Math.floor(Math.random() * 300) * 15000 + 15000; // Determine random FDS result const randVal = Math.random(); let result = 'safe'; let score = Math.floor(Math.random() * 35) + 5; // 5 to 39 let rule = '—'; if (randVal > 0.85) { // 15% chance blocked (High Risk) result = 'blocked'; score = Math.floor(Math.random() * 20) + 80; // 80 to 99 rule = rules[Math.floor(Math.random() * rules.length)]; } else if (randVal > 0.65) { // 20% chance flagged (Medium Risk) result = 'flagged'; score = Math.floor(Math.random() * 25) + 50; // 50 to 74 rule = rules[Math.floor(Math.random() * rules.length)]; } const newScan = { id: 'SCN-' + (Math.floor(Math.random() * 9000) + 1000), txId: 'TX-260624-' + (Math.floor(Math.random() * 9000) + 1000), time: new Date().toISOString(), merchant: randomName, location: randomLoc, ip: randomIp, amount: randomAmount, score: score, result: result, rule: rule, cardBin: randomBin, issuer: randomIssuer }; const updated = [newScan, ...scans]; setScans(updated); window.__fdsScans = updated; triggerToast(`Transaksi diproses FDS: ${result.toUpperCase()} (Score: ${score})`); // Sync to alerts if flagged or blocked if (result === 'flagged' || result === 'blocked') { const currentAlerts = window.__fdsAlerts || []; const newAlert = { id: 'ALT-' + (Math.floor(Math.random() * 9000) + 1000), txId: newScan.txId, time: newScan.time, merchant: newScan.merchant, location: newScan.location, amount: newScan.amount, score: newScan.score, rule: newScan.rule, status: result === 'blocked' ? 'blocked' : 'pending' }; window.__fdsAlerts = [newAlert, ...currentAlerts]; } }; const filteredScans = scans.filter(s => { const matchesSearch = s.txId.toLowerCase().includes(searchQuery.toLowerCase()) || s.merchant.toLowerCase().includes(searchQuery.toLowerCase()) || s.id.toLowerCase().includes(searchQuery.toLowerCase()); const matchesStatus = filterStatus === 'all' || s.result === filterStatus; let matchesRisk = true; if (filterRisk === 'low') matchesRisk = s.score < 50; else if (filterRisk === 'medium') matchesRisk = s.score >= 50 && s.score < 80; else if (filterRisk === 'high') matchesRisk = s.score >= 80; return matchesSearch && matchesStatus && matchesRisk; }); const summary = { total: scans.length, safe: scans.filter(s => s.result === 'safe').length, flagged: scans.filter(s => s.result === 'flagged').length, blocked: scans.filter(s => s.result === 'blocked').length }; return (
{/* Header */}
FDS Monitoring

Monitoring Transaksi FDS

Inspeksi real-time untuk seluruh transaksi QRIS yang melewati mesin analisis fraud.

{/* Mini Stats Summary */}
{[ { label: 'Total Scanned Trx', value: summary.total, icon: 'bx-shield-quarter', color: TEAL }, { label: 'SAFE (Passed)', value: summary.safe, icon: 'bx-check-double', color: GREEN }, { label: 'FLAGGED (Review)', value: summary.flagged, icon: 'bx-error', color: AMBER }, { label: 'BLOCKED (Tolak)', value: summary.blocked, icon: 'bx-block', color: RED } ].map((c, i) => (
{c.label}
{c.value}
))}
{/* Filters Bar */}
{/* Search */}
setSearchQuery(e.target.value)} style={{ border: 'none', background: 'none', outline: 'none', fontSize: 13, color: '#171919', width: '100%', fontFamily: 'inherit' }} />
{/* Filter Status */}
{/* Filter Risk */}
{/* Scans List Table */}
{filteredScans.length === 0 ? ( ) : ( filteredScans.map(s => { const isHigh = s.score >= 80; const isMedium = s.score >= 50 && s.score < 80; const scoreColor = isHigh ? RED : isMedium ? AMBER : GREEN; return ( e.currentTarget.style.background = '#fafbfc'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> ); }) )}
ID Scan ID Transaksi Merchant Lokasi & IP Nominal Score Hasil Inspeksi
Tidak ada data transaksi yang cocok dengan filter pencarian.
{s.id} {s.txId}
{s.merchant}
{fmtDate(s.time)}
{s.location}
IP: {s.ip}
{fmtRp(s.amount)} {s.score} {s.result.toUpperCase()}
{/* Drawer: Detailed Inspection Panel */} {selectedTx && (
setSelectedTx(null)} style={{ position: 'absolute', inset: 0, background: 'rgba(23,25,25,0.4)', backdropFilter: 'blur(1.5px)' }} />
{/* Header */}
{selectedTx.id}

Inspeksi Audit FDS

{/* Content Body */}
{/* Scan Assessment Indicator */}
Hasil Engine: {selectedTx.result.toUpperCase()}
Fraud Score: {selectedTx.score} / 100
{/* Transaction payload data */}

Data Transaksi Terkait

{[ { label: 'ID Transaksi', val: selectedTx.txId, mono: true }, { label: 'Merchant Penerima', val: selectedTx.merchant }, { label: 'Nominal Rupiah', val: fmtRp(selectedTx.amount), bold: true }, { label: 'Lokasi Perangkat', val: selectedTx.location }, { label: 'IP Address', val: selectedTx.ip, mono: true }, { label: 'Waktu Scan', val: fmtDate(s => s.time) }, { label: 'Card BIN / Issuer', val: `${selectedTx.cardBin} (${selectedTx.issuer})` } ].map((item, idx) => (
{item.label} {item.val}
))}
{/* Rules Evaluation */}

Hasil Analisis Aturan (Rules)

Pemicu Rule Warning
{selectedTx.rule}
Rekomendasi Tindakan
{selectedTx.result === 'blocked' ? 'Transaksi telah ditolak secara otomatis untuk mengamankan limit dana merchant.' : (selectedTx.result === 'flagged' ? 'Transaksi memerlukan review manual oleh tim investigasi melalui antrean FDS Dashboard.' : 'Transaksi dinyatakan aman dan sudah diteruskan untuk settlement.')}
{/* Close button */}
)} {/* Toast Alert */} {toast && (
{toast}
)}
); } // ── 3. PARAMETER FDS PAGE (Structural Placeholder) ── // ── 3. PARAMETER FDS PAGE ── function FdsParameterPage({ merchants, primary, secondary }) { const TEAL = primary || '#0056D2'; const ORANGE = secondary || '#cf5a27'; const GREEN = '#74b50c'; const RED = '#ed3151'; const AMBER = '#d9920b'; const [activeTab, setActiveTab] = React.useState('suspect'); // 'limits' | 'suspect' | 'whitelist' | 'blacklist' const [toast, setToast] = React.useState(null); const [showingForm, setShowingForm] = React.useState(false); // ── New Rule Form States ── const [ruleName, setRuleName] = React.useState(''); const [ruleDesc, setRuleDesc] = React.useState(''); const [ruleCategory, setRuleCategory] = React.useState('Indikasi Gestun / Pencucian Uang'); const [ruleSeverity, setRuleSeverity] = React.useState('High Risk'); const [activeChannels, setActiveChannels] = React.useState(['Mobile Banking']); // Logical builder groups/conditions: const [logicalGroup, setLogicalGroup] = React.useState('AND'); const [conditions, setConditions] = React.useState([ { dataField: 'A.1', thresholdLowMin: '500000', thresholdLowMax: '2000000', thresholdMediumMin: '2000000', thresholdMediumMax: '5000000', thresholdHigh: '5000000' } ]); // Velocity & Aggregation const [aggType, setAggType] = React.useState('Count'); const [timeWindow, setTimeWindow] = React.useState('30'); const [timeUnit, setTimeUnit] = React.useState('Minutes'); const [groupBy, setGroupBy] = React.useState('Account_Number'); // Action/Decision const [ruleAction, setRuleAction] = React.useState('Fraud'); const [escalationQueue, setEscalationQueue] = React.useState('L2_Investigator'); // Severity Risk Weight Scores when condition is met const [normalScore, setNormalScore] = React.useState('0'); const [lowRiskScore, setLowRiskScore] = React.useState('15'); const [mediumRiskScore, setMediumRiskScore] = React.useState('45'); const [highRiskScore, setHighRiskScore] = React.useState('75'); const [veryHighRiskScore, setVeryHighRiskScore] = React.useState('99'); // ── Parameters State ── const [suspectParams, setSuspectParams] = React.useState([ { id: 'SUS-001', name: 'Threshold Alert Score', value: '75', unit: 'score', desc: 'Ambang batas nilai skor kecurigaan untuk memicu antrean alert review manual', conditionsCount: 1 }, { id: 'SUS-002', name: 'Max Failed Pin Attempts', value: '3', unit: 'kali / 1 jam', desc: 'Batas toleransi kesalahan input PIN kartu/akun sebelum ditandai sebagai indikasi fraud hijack', conditionsCount: 1 }, { id: 'SUS-003', name: 'Velocity Limit (Trx/Min)', value: '5', unit: 'trx / 1 menit', desc: 'Kecepatan transaksi wajar dari pengguna/kartu yang sama dlm kurun waktu satu menit', conditionsCount: 1 }, { id: 'SUS-004', name: 'High-Risk Geo Mismatch Window', value: '15', unit: 'menit', desc: 'Rentang waktu minimal perpindahan lokasi jarak jauh yang wajar dlm mendeteksi kloning lokasi', conditionsCount: 1 } ]); const [limitParams, setLimitParams] = React.useState([ { id: 'LIM-001', name: 'Max Nominal Single Trx', value: '10000000', unit: 'Rupiah', desc: 'Nominal maksimal yang diperbolehkan untuk satu kali transaksi QRIS merchant' }, { id: 'LIM-002', name: 'Max Accumulated Daily Limit', value: '50000000', unit: 'Rupiah', desc: 'Akumulasi total nominal transaksi maksimal per hari dlm satu ID pengguna' }, { id: 'LIM-003', name: 'Max Trx Frequency Hourly', value: '20', unit: 'kali / 1 jam', desc: 'Batas frekuensi transaksi maksimal per jam untuk mencegah penarikan berlebih otomatis' } ]); const [editingParam, setEditingParam] = React.useState(null); const [editingValue, setEditingValue] = React.useState(''); const [editingDesc, setEditingDesc] = React.useState(''); // ── Whitelist & Blacklist State ── const [whitelist, setWhitelist] = React.useState([ { id: 'WHL-01', type: 'Merchant MID', value: 'MRC-00244', desc: 'Merchant VIP dengan transaksi volume besar terpercaya', date: '2026-06-20T09:00:00', by: 'Arief Budiman' }, { id: 'WHL-02', type: 'IP Address', value: '127.0.0.1', desc: 'IP local server monitoring IT hibank', date: '2026-06-21T10:15:00', by: 'System Admin' }, { id: 'WHL-03', type: 'Card BIN', value: '4512 88xx', desc: 'BIN kartu debit internal Bank hibank', date: '2026-06-22T11:30:00', by: 'Bambang P' } ]); const [blacklist, setBlacklist] = React.useState([ { id: 'BLK-01', type: 'IP Address', value: '203.0.113.50', desc: 'Terindikasi server bot spamming trx', date: '2026-06-18T16:45:00', by: 'System FDS' }, { id: 'BLK-02', type: 'Card BIN', value: '4999 11xx', desc: 'BIN teridentifikasi sebagai sumber test card fraud internasional', date: '2026-06-19T08:12:00', by: 'Arief Budiman' } ]); // Modals for adding trust/block entries const [showAddModal, setShowAddModal] = React.useState(false); const [addType, setAddType] = React.useState('IP Address'); const [addValue, setAddValue] = React.useState(''); const [addDesc, setAddDesc] = React.useState(''); const triggerToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), 3000); }; const fmtRp = (v) => { 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' }); }; // Action: Save edited parameter const handleSaveParam = (e) => { e.preventDefault(); if (!editingParam) return; if (editingParam.isNew) { const newParam = { id: editingParam.id, name: editingParam.name, value: editingValue, unit: editingParam.unit || 'score', desc: editingDesc, conditionsCount: 1 }; setSuspectParams(prev => [...prev, newParam]); triggerToast(`Parameter ${editingParam.name} berhasil ditambahkan.`); } else { if (editingParam.id.startsWith('SUS')) { setSuspectParams(prev => prev.map(p => p.id === editingParam.id ? { ...p, value: editingValue, desc: editingDesc } : p)); } else { setLimitParams(prev => prev.map(p => p.id === editingParam.id ? { ...p, value: editingValue, desc: editingDesc } : p)); } triggerToast(`Parameter ${editingParam.name} berhasil diperbarui.`); } setEditingParam(null); setShowingForm(false); }; const handleEditClick = (p) => { setEditingParam(p); setEditingValue(p.value); setEditingDesc(p.desc); if (p.id.startsWith('SUS')) { // It's a suspect rule parameter. We want to edit it using the complex rule builder form! setRuleName(p.name); setRuleDesc(p.ruleDescOnly || p.desc || ''); setRuleCategory(p.ruleCategory || ( p.id === 'SUS-002' ? 'Account Takeover (ATO)' : p.id === 'SUS-004' ? 'Anomali Perilaku Merchant' : 'Indikasi Gestun / Pencucian Uang' )); setRuleSeverity(p.ruleSeverity || 'High'); setActiveChannels(p.activeChannels || ( p.id === 'SUS-001' || p.id === 'SUS-004' ? ['Mobile Banking', 'Internet Banking', 'API B2B'] : ['Mobile Banking', 'Internet Banking'] )); setLogicalGroup(p.logicalGroup || 'AND'); setAggType(p.aggType || 'Count'); setTimeWindow(p.timeWindow || ( p.id === 'SUS-002' ? '1' : p.id === 'SUS-003' ? '1' : p.id === 'SUS-004' ? '15' : '5' )); setTimeUnit(p.timeUnit || ( p.id === 'SUS-002' ? 'Hours' : 'Minutes' )); setGroupBy(p.groupBy || ( p.id === 'SUS-004' ? 'IP_Address' : 'Account_Number' )); setRuleAction(p.ruleAction || 'Fraud'); setEscalationQueue(p.escalationQueue || 'FDS Operational'); if (p.conditions) { setConditions(p.conditions); } else { const defaultField = p.id === 'SUS-001' ? 'A.1' : p.id === 'SUS-002' ? 'B.2' : p.id === 'SUS-003' ? 'C.1' : p.id === 'SUS-004' ? 'D.2' : 'A.1'; const lowMin = p.id === 'SUS-001' ? '25' : (p.id === 'SUS-002' ? '1' : (p.id === 'SUS-003' ? '2' : '5')); const lowMax = p.id === 'SUS-001' ? '50' : (p.id === 'SUS-002' ? '2' : (p.id === 'SUS-003' ? '3' : '10')); const medMin = p.id === 'SUS-001' ? '50' : (p.id === 'SUS-002' ? '2' : (p.id === 'SUS-003' ? '3' : '10')); const medMax = p.id === 'SUS-001' ? '75' : (p.id === 'SUS-002' ? '3' : (p.id === 'SUS-003' ? '5' : '15')); const high = p.value; setConditions([{ dataField: defaultField, thresholdLowMin: String(lowMin), thresholdLowMax: String(lowMax), thresholdMediumMin: String(medMin), thresholdMediumMax: String(medMax), thresholdHigh: String(high) }]); } } else { // For non-SUS parameters, reset state setConditions([{ dataField: 'A.1', thresholdLowMin: '500000', thresholdLowMax: '2000000', thresholdMediumMin: '2000000', thresholdMediumMax: '5000000', thresholdHigh: '5000000' }]); } setShowingForm(true); }; // Action: Add Whitelist / Blacklist Entity const handleAddSubmit = (e) => { e.preventDefault(); if (!addValue || !addDesc) return; const newEntry = { id: (activeTab === 'whitelist' ? 'WHL-' : 'BLK-') + (Math.floor(Math.random() * 90) + 10), type: addType, value: addValue, desc: addDesc, date: new Date().toISOString(), by: 'Arief Budiman' }; if (activeTab === 'whitelist') { setWhitelist([newEntry, ...whitelist]); triggerToast(`Entitas ${addValue} berhasil ditambahkan ke White List.`); } else { setBlacklist([newEntry, ...blacklist]); triggerToast(`Entitas ${addValue} berhasil ditambahkan ke Black List.`); } // Reset setShowAddModal(false); setAddValue(''); setAddDesc(''); }; // Action: Delete from Whitelist / Blacklist const handleDeleteEntry = (id, val) => { if (activeTab === 'whitelist') { setWhitelist(whitelist.filter(w => w.id !== id)); triggerToast(`Entitas ${val} dihapus dari White List.`); } else { setBlacklist(blacklist.filter(b => b.id !== id)); triggerToast(`Entitas ${val} dihapus dari Black List.`); } }; // Action: Save suspect rule configuration const handleSaveSuspectRule = (e) => { e.preventDefault(); if (!ruleName || !ruleDesc) return; // Generate a user-friendly condition description const condDesc = conditions.map((c, idx) => { const lowVal = c.thresholdLowMin && c.thresholdLowMax ? `${c.thresholdLowMin} - ${c.thresholdLowMax}` : (c.thresholdLowMin || c.thresholdLowMax || '-'); const medVal = c.thresholdMediumMin && c.thresholdMediumMax ? `${c.thresholdMediumMin} - ${c.thresholdMediumMax}` : (c.thresholdMediumMin || c.thresholdMediumMax || '-'); const thresholdText = `[Low Threshold: ${lowVal}, Med Threshold: ${medVal}, High Threshold: ${c.thresholdHigh || '-'}]`; return `Kondisi #${idx + 1}: ${c.dataField} ${thresholdText}`; }).join(` ${logicalGroup} `); const needsVelocity = conditions.some(c => ['A.2', 'B.2', 'C.1', 'C.2', 'C.3', 'D.1', 'D.2', 'E.2'].includes(c.dataField)); const aggregationText = needsVelocity ? ` (${aggType} of ${groupBy} over ${timeWindow} ${timeUnit})` : ''; const updatedParam = { id: editingParam ? editingParam.id : `SUS-00${suspectParams.length + 1}`, name: ruleName, value: conditions[0]?.thresholdHigh || 'Custom', unit: ['A.1', 'A.2', 'E.3'].includes(conditions[0]?.dataField) ? 'Rupiah' : (['B.1', 'B.2', 'C.1', 'C.2', 'C.3'].includes(conditions[0]?.dataField) ? 'Transaksi' : 'unit'), desc: `${ruleDesc}. Kategori: ${ruleCategory}. Jalur: ${activeChannels.join(', ')}. ${condDesc}${aggregationText}. Tindakan: ${ruleAction} (${escalationQueue}). Urgensi: ${ruleSeverity}.`, conditionsCount: conditions.length, conditions: conditions, ruleDescOnly: ruleDesc, ruleCategory: ruleCategory, ruleSeverity: ruleSeverity, activeChannels: activeChannels, logicalGroup: logicalGroup, aggType: aggType, timeWindow: timeWindow, timeUnit: timeUnit, groupBy: groupBy, ruleAction: ruleAction, escalationQueue: escalationQueue }; if (editingParam) { setSuspectParams(prev => prev.map(p => p.id === editingParam.id ? updatedParam : p)); triggerToast(`Aturan FDS "${ruleName}" berhasil diperbarui.`); } else { setSuspectParams(prev => [...prev, updatedParam]); triggerToast(`Aturan FDS "${ruleName}" berhasil dibuat.`); } setShowingForm(false); setEditingParam(null); }; if (showingForm) { const isSimpleParam = editingParam && !editingParam.id.startsWith('SUS'); return (
{/* Breadcrumb / Back button */}
{isSimpleParam ? 'Edit Parameter Threshold' : 'Suspect Rule Engine'}

{editingParam ? `Edit Parameter — ${editingParam.id}` : 'Buat Parameter Aturan Baru'}

{isSimpleParam ? `Perbarui nilai ambang batas toleransi dan keterangan untuk parameter ${editingParam.name}.` : 'Definisikan metadata, kriteria logika kondisi, agregasi temporal, dan aksi penindakan otomatis.'}

{isSimpleParam ? (
setEditingValue(e.target.value)} style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#171919', outline: 'none', fontFamily: 'inherit' }} />