);
}
// ── 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.
e.currentTarget.style.background = '#0041A8'}
onMouseLeave={e => e.currentTarget.style.background = TEAL}
>
Simulasikan Transaksi Masuk
{/* 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) => (
))}
{/* Filters Bar */}
{/* Search */}
setSearchQuery(e.target.value)}
style={{ border: 'none', background: 'none', outline: 'none', fontSize: 13, color: '#171919', width: '100%', fontFamily: 'inherit' }}
/>
{/* Filter Status */}
setFilterStatus(e.target.value)}
style={{ padding: '9px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#515252', outline: 'none', background: '#fff', fontFamily: 'inherit', minWidth: 150 }}
>
Semua Status FDS
SAFE (Passed)
FLAGGED (Review)
BLOCKED (Tolak)
{/* Filter Risk */}
setFilterRisk(e.target.value)}
style={{ padding: '9px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#515252', outline: 'none', background: '#fff', fontFamily: 'inherit', minWidth: 150 }}
>
Semua Tingkat Risiko
Risiko Rendah (<50)
Risiko Sedang (50-79)
Risiko Tinggi (≥80)
{/* Scans List Table */}
ID Scan
ID Transaksi
Merchant
Lokasi & IP
Nominal
Score
Hasil
Inspeksi
{filteredScans.length === 0 ? (
Tidak ada data transaksi yang cocok dengan filter pencarian.
) : (
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'}>
{s.id}
{s.txId}
{s.merchant}
{fmtDate(s.time)}
{s.location}
IP: {s.ip}
{fmtRp(s.amount)}
{s.score}
{s.result.toUpperCase()}
setSelectedTx(s)}
style={{
background: '#fff', border: '1.5px solid #efefef', borderRadius: 6, padding: '6px 12px',
fontSize: 11.5, fontWeight: 750, cursor: 'pointer', transition: 'all 150ms', color: '#515252'
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = TEAL; e.currentTarget.style.color = TEAL; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = '#efefef'; e.currentTarget.style.color = '#515252'; }}
>
Buka
);
})
)}
{/* 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
setSelectedTx(null)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#515252', padding: 4 }}>
{/* 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 */}
setSelectedTx(null)}
style={{ width: '100%', padding: '12px', background: TEAL, color: '#fff', border: 'none', borderRadius: 8, fontSize: 13.5, fontWeight: 750, cursor: 'pointer', boxShadow: `0 4px 12px ${TEAL}25` }}
>
Selesai Inspeksi
)}
{/* 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 */}
{ setShowingForm(false); setEditingParam(null); }}
style={{
background: 'none', border: 'none', color: '#515252', fontSize: 13, fontWeight: 600,
cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 6, padding: '4px 0'
}}
>
Kembali ke Parameter FDS
{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 ? (
) : (
{/* COLUMN 1: Basic Info & Conditions */}
{/* 1. INFORMASI DASAR */}
1. Informasi Dasar (Skenario Metadata)
Skenario Name
setRuleName(e.target.value)}
style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, outline: 'none', fontFamily: 'inherit' }}
/>
Description
setRuleDesc(e.target.value)}
style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, outline: 'none', fontFamily: 'inherit', resize: 'vertical', lineHeight: 1.4 }}
/>
{/* KODE BARU - Hapus layout grid agar elemen menggunakan lebar penuh */}
Rule Category
setRuleCategory(e.target.value)}
style={{
width: '100%',
boxSizing: 'border-box', /* Penting agar padding tidak membuat elemen melebihi 100% */
padding: '10px 12px',
borderRadius: 8,
border: '1.5px solid #efefef',
fontSize: 13,
outline: 'none',
background: '#fff',
fontFamily: 'inherit'
}}
>
1. Indikasi Gesek Tunai (Gestun) atau Pencucian Uang
2. Promo Abuse (Penyalahgunaan Promosi)
3. Account Takeover (ATO) / Pengambilalihan Akun
4. Anomali Perilaku Merchant (Sisi Acquirer)
5. Anomali Waktu Operasional (Time-Based Rules)
{/* 2. PEMBANGUN KONDISI */}
2. Pembangun Kondisi (Condition Builder)
{['AND', 'OR'].map(grp => (
setLogicalGroup(grp)}
style={{
background: logicalGroup === grp ? TEAL : 'transparent',
color: logicalGroup === grp ? '#fff' : '#515252',
border: 'none', borderRadius: 4, padding: '3px 10px', fontSize: 11, fontWeight: 750,
cursor: 'pointer', fontFamily: 'inherit'
}}
>
{grp}
))}
{conditions.map((cond, idx) => {
return (
{
const newConds = [...conditions];
newConds[idx].dataField = e.target.value;
setConditions(newConds);
}}
style={{ width: '100%', padding: '8px 10px', borderRadius: 6, border: '1px solid #efefef', fontSize: 12.5, outline: 'none', background: '#fff', fontFamily: 'inherit' }}
>
A.1 — Nominal Transaksi
A.2 — Nominal Top-up Besar
B.1 — Transaksi Jam Tidak Wajar
B.2 — Frekuensi Transaksi Harian
C.1 — Transaksi Nominal Kecil Berulang
C.2 — Multiple Attempts ke 1 Tujuan
C.3 — Frekuensi Transaksi Mingguan
D.1 — Pergantian Device ID
D.2 — Pergantian IP Address
E.1 — Merchant Category Berisiko Tinggi (MCC)
E.2 — Scan Frekuensi Tinggi
E.3 — Nominal QRIS Melebihi Ketentuan BI
{conditions.length > 1 && (
setConditions(conditions.filter((_, i) => i !== idx))}
style={{ background: 'none', border: 'none', color: RED, cursor: 'pointer', padding: 6, display: 'inline-flex' }}
>
)}
{(() => {
const isCurrency = ['A.1', 'A.2', 'E.3'].includes(cond.dataField);
const isMCC = cond.dataField === 'E.1';
const valPlaceholderMin = isCurrency ? 'Min (e.g. 500000)' : 'Min';
const valPlaceholderMax = isCurrency ? 'Max (e.g. 2000000)' : 'Max';
return (
{/* THRESHOLD LOW */}
Threshold LOW
{isMCC ? (
{
const newConds = [...conditions];
newConds[idx].thresholdLowMin = e.target.value;
setConditions(newConds);
}}
style={{ width: '100%', padding: '6px 10px', borderRadius: 6, border: '1px solid #efefef', fontSize: 12, outline: 'none', background: '#fff', fontFamily: 'inherit' }}
/>
) : (
{
const newConds = [...conditions];
newConds[idx].thresholdLowMin = e.target.value;
setConditions(newConds);
}}
style={{ flex: 1, padding: '6px 10px', borderRadius: 6, border: '1px solid #efefef', fontSize: 12, outline: 'none', fontFamily: 'inherit' }}
/>
s/d
{
const newConds = [...conditions];
newConds[idx].thresholdLowMax = e.target.value;
setConditions(newConds);
}}
style={{ flex: 1, padding: '6px 10px', borderRadius: 6, border: '1px solid #efefef', fontSize: 12, outline: 'none', fontFamily: 'inherit' }}
/>
)}
{/* THRESHOLD MEDIUM */}
Threshold MEDIUM
{isMCC ? (
{
const newConds = [...conditions];
newConds[idx].thresholdMediumMin = e.target.value;
setConditions(newConds);
}}
style={{ width: '100%', padding: '6px 10px', borderRadius: 6, border: '1px solid #efefef', fontSize: 12, outline: 'none', background: '#fff', fontFamily: 'inherit' }}
/>
) : (
{
const newConds = [...conditions];
newConds[idx].thresholdMediumMin = e.target.value;
setConditions(newConds);
}}
style={{ flex: 1, padding: '6px 10px', borderRadius: 6, border: '1px solid #efefef', fontSize: 12, outline: 'none', fontFamily: 'inherit' }}
/>
s/d
{
const newConds = [...conditions];
newConds[idx].thresholdMediumMax = e.target.value;
setConditions(newConds);
}}
style={{ flex: 1, padding: '6px 10px', borderRadius: 6, border: '1px solid #efefef', fontSize: 12, outline: 'none', fontFamily: 'inherit' }}
/>
)}
{/* THRESHOLD HIGH */}
);
})()}
);
})}
setConditions([...conditions, {
dataField: 'A.1',
thresholdLowMin: '',
thresholdLowMax: '',
thresholdMediumMin: '',
thresholdMediumMax: '',
thresholdHigh: ''
}])}
style={{
background: 'none', border: '1.5px dashed #efefef', borderRadius: 8, padding: '10px',
color: TEAL, fontSize: 12, fontWeight: 700, cursor: 'pointer', display: 'flex',
alignItems: 'center', gap: 6, justifyContent: 'center', marginTop: 4, fontFamily: 'inherit'
}}
>
Tambah Kondisi Kriteria
{/* COLUMN 2: Velocity & Rule Action */}
{/* 3. PARAMETER KECEPATAN & WAKTU */}
{conditions.some(c => ['A.2', 'B.2', 'C.1', 'C.2', 'C.3', 'D.1', 'D.2', 'E.2'].includes(c.dataField)) && (
3. Parameter Kecepatan & Waktu (Velocity & Aggregation)
Aggregation Type
setAggType(e.target.value)}
style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, outline: 'none', background: '#fff', fontFamily: 'inherit' }}
>
Count (Frekuensi Transaksi)
Sum (Total Akumulasi Nominal)
Time Window
setTimeWindow(e.target.value)}
style={{ flex: 1, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, outline: 'none', fontFamily: 'inherit' }}
/>
setTimeUnit(e.target.value)}
style={{ width: 120, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, outline: 'none', background: '#fff', fontFamily: 'inherit' }}
>
Minutes
Hours
Days
Group By
setGroupBy(e.target.value)}
style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, outline: 'none', background: '#fff', fontFamily: 'inherit' }}
>
Account_Number (Nomor Rekening)
Device ID
Merchant ID (MID)
IP Address
)}
{/* 4. TINDAKAN & KEPUTUSAN */}
4. Tindakan & Keputusan (Rule Action)
Action / Decision
setRuleAction(e.target.value)}
style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, outline: 'none', background: '#fff', fontFamily: 'inherit' }}
>
Izinkan
Blokir
Izinkan dan Notifikasi
{ setShowingForm(false); setEditingParam(null); }}
style={{ background: '#fff', border: '1.5px solid #efefef', borderRadius: 8, padding: '10px 18px', fontSize: 13, fontWeight: 600, color: '#515252', cursor: 'pointer', fontFamily: 'inherit' }}
>
Batal
{editingParam ? 'Perbarui Aturan FDS' : 'Simpan Aturan FDS'}
)}
);
}
return (
{/* Header */}
FDS Rules Parameter
Parameter FDS & Reputasi
Konfigurasi ambang batas indikasi suspect, batasan limit, serta kelola reputasi bypass entitas.
{(activeTab === 'whitelist' || activeTab === 'blacklist') && (
setShowAddModal(true)}
style={{
background: TEAL, color: '#fff', border: 'none', borderRadius: 8, padding: '10px 16px',
fontSize: 12.5, fontWeight: 750, cursor: 'pointer', display: 'inline-flex',
alignItems: 'center', gap: 8, boxShadow: `0 4px 12px ${TEAL}25`, transition: 'all 150ms',
fontFamily: 'inherit'
}}
onMouseEnter={e => e.currentTarget.style.background = '#0041A8'}
onMouseLeave={e => e.currentTarget.style.background = TEAL}
>
Tambah ke {activeTab === 'whitelist' ? 'White List' : 'Black List'}
)}
{activeTab === 'suspect' && (
{
setShowingForm(true);
setRuleName('');
setRuleDesc('');
setConditions([{
dataField: 'A.1',
thresholdLowMin: '500000',
thresholdLowMax: '2000000',
thresholdMediumMin: '2000000',
thresholdMediumMax: '5000000',
thresholdHigh: '5000000'
}]);
}}
style={{
background: TEAL, color: '#fff', border: 'none', borderRadius: 8, padding: '10px 16px',
fontSize: 12.5, fontWeight: 750, cursor: 'pointer', display: 'inline-flex',
alignItems: 'center', gap: 8, boxShadow: `0 4px 12px ${TEAL}25`, transition: 'all 150ms',
fontFamily: 'inherit'
}}
onMouseEnter={e => e.currentTarget.style.background = '#0041A8'}
onMouseLeave={e => e.currentTarget.style.background = TEAL}
>
Tambah Parameter Suspect
)}
{/* Tabs Switcher */}
{[
{ id: 'suspect', label: 'Suspect Rules', icon: 'bx-error-circle' },
{ id: 'limits', label: 'Transaction Limits', icon: 'bx-block' },
{ id: 'whitelist', label: 'White List (Bypass)', icon: 'bx-check-circle' },
{ id: 'blacklist', label: 'Black List (Blokir)', icon: 'bx-x-circle' }
].map(tab => {
const isActive = activeTab === tab.id;
return (
setActiveTab(tab.id)}
style={{
background: 'none', border: 'none', padding: '12px 4px', fontSize: 13.5, fontWeight: isActive ? 750 : 600,
color: isActive ? TEAL : '#a7a8a8', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 8,
borderBottom: isActive ? `3.5px solid ${TEAL}` : '3.5px solid transparent', marginBottom: -3,
transition: 'all 150ms', fontFamily: 'inherit'
}}
>
{tab.label}
);
})}
{/* TAB CONTENT: 1. SUSPECT PARAMETERS */}
{activeTab === 'suspect' && (
Suspect Rules Thresholds
Batas toleransi indikasi anomali sebelum transaksi dinaikkan ke FDS Dashboard Alert.
ID
Nama Skema
Nilai Jumlah Rules
Keterangan / Deskripsi Aturan
Aksi
{suspectParams.map(p => (
e.currentTarget.style.background = '#fafbfc'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
{p.id}
{p.name}
{p.conditionsCount || 1} Parameter
{p.desc}
handleEditClick(p)}
style={{ background: '#fff', border: '1.5px solid #efefef', borderRadius: 6, padding: '5px 12px', fontSize: 11.5, fontWeight: 750, cursor: 'pointer', transition: 'all 150ms', color: '#515252' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = TEAL; e.currentTarget.style.color = TEAL; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = '#efefef'; e.currentTarget.style.color = '#515252'; }}
>
Edit
))}
)}
{/* TAB CONTENT: 2. TRANSACTION LIMIT PARAMETERS */}
{activeTab === 'limits' && (
Transaction Hard Limits
Aturan batas absolut yang akan langsung menolak (BLOCKED) transaksi jika terlewati.
ID
Nama Parameter
Batas Maksimal
Keterangan / Deskripsi Aturan
Aksi
{limitParams.map(p => (
e.currentTarget.style.background = '#fafbfc'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
{p.id}
{p.name}
{p.unit === 'Rupiah' ? fmtRp(p.value) : `${p.value} ${p.unit}`}
{p.desc}
handleEditClick(p)}
style={{ background: '#fff', border: '1.5px solid #efefef', borderRadius: 6, padding: '5px 12px', fontSize: 11.5, fontWeight: 750, cursor: 'pointer', transition: 'all 150ms', color: '#515252' }}
onMouseEnter={e => { e.currentTarget.style.borderColor = TEAL; e.currentTarget.style.color = TEAL; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = '#efefef'; e.currentTarget.style.color = '#515252'; }}
>
Edit
))}
)}
{/* TAB CONTENT: 3. WHITE LIST & 4. BLACK LIST */}
{(activeTab === 'whitelist' || activeTab === 'blacklist') && (
ID
Jenis Entitas
Nilai / Value
Keterangan Pembenaran
Tanggal Ditambahkan
Petugas
Aksi
{((activeTab === 'whitelist' ? whitelist : blacklist).length === 0) ? (
Daftar reputasi {activeTab === 'whitelist' ? 'White List' : 'Black List'} saat ini kosong.
) : (
(activeTab === 'whitelist' ? whitelist : blacklist).map(entry => (
e.currentTarget.style.background = '#fafbfc'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
{entry.id}
{entry.type}
{entry.value}
{entry.desc}
{fmtDate(entry.date)}
{entry.by}
handleDeleteEntry(entry.id, entry.value)}
style={{ background: 'none', border: 'none', color: RED, cursor: 'pointer', padding: 6, display: 'inline-flex', borderRadius: '50%' }}
onMouseEnter={e => e.currentTarget.style.background = '#fef2f2'}
onMouseLeave={e => e.currentTarget.style.background = 'none'}
title="Hapus Entitas"
>
))
)}
)}
{/* Modal: Add to Whitelist / Blacklist */}
{showAddModal && (
setShowAddModal(false)} style={{ position: 'absolute', inset: 0, background: 'rgba(23,25,25,0.4)', backdropFilter: 'blur(2px)' }} />
Tambah Entitas Reputasi
setShowAddModal(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#515252', padding: 4 }}>
Jenis Entitas
setAddType(e.target.value)}
style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#171919', outline: 'none', background: '#fff', fontFamily: 'inherit' }}
>
IP Address
Card BIN (6 Digit)
Merchant MID
Customer Phone
Nilai / Value
setAddValue(e.target.value)}
style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#171919', outline: 'none', fontFamily: 'inherit' }}
/>
Alasan Pembenaran / Justifikasi
setAddDesc(e.target.value)}
style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, color: '#171919', outline: 'none', resize: 'vertical', fontFamily: 'inherit', lineHeight: 1.4 }}
/>
setShowAddModal(false)} style={{ background: '#fff', border: '1.5px solid #efefef', borderRadius: 8, padding: '9px 16px', fontSize: 13, fontWeight: 600, color: '#515252', cursor: 'pointer' }}>Batal
Tambah Entitas
)}
{/* Toast Alert */}
{toast && (
{toast}
)}
);
}
// ── 4. SUSPECT MANAGEMENT PAGE ──
function FdsSuspectPage({ 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 [searchQuery, setSearchQuery] = React.useState('');
const [filterStatus, setFilterStatus] = React.useState('all');
const [filterRisk, setFilterRisk] = React.useState('all');
const [activeAlertDetail, setActiveAlertDetail] = React.useState(null);
const [showingDetail, setShowingDetail] = React.useState(false);
const [toast, setToast] = React.useState(null);
// Investigation form states
const [selectedAction, setSelectedAction] = React.useState('Fraud');
const [investigationNote, setInvestigationNote] = React.useState('');
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', 'Mart Sejahtera', 'RM Padang Sederhana'];
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',
'Indikasi Gesek Tunai (Gestun): Transaksi mendekati limit berulang'
];
const cities = ['Makassar, ID', 'Palembang, ID', 'Semarang, ID', 'Denpasar, ID', 'Balikpapan, 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 disimulasikan! 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 handleSubmitDecision = (e) => {
e.preventDefault();
if (!activeAlertDetail) return;
if (!investigationNote.trim()) {
alert('Mohon masukkan catatan investigasi kronologi kasus.');
return;
}
const isResolvedBlocked = ['Fraud', 'Blacklist'].includes(selectedAction);
const resolvedStatus = isResolvedBlocked ? 'blocked' : 'approved';
const updated = alerts.map(a => {
if (a.id !== activeAlertDetail.id) return a;
return {
...a,
status: resolvedStatus,
action: selectedAction,
note: investigationNote,
resolvedTime: new Date().toISOString()
};
});
setAlerts(updated);
saveFdsAlerts(updated);
triggerToast(`Kasus ${activeAlertDetail.id} berhasil diselesaikan (${selectedAction})`);
setActiveAlertDetail(null);
setInvestigationNote('');
setShowingDetail(false);
};
const handleReopenCase = (id) => {
const updated = alerts.map(a => {
if (a.id !== id) return a;
return {
...a,
status: 'investigating',
action: undefined,
note: undefined,
resolvedTime: undefined
};
});
setAlerts(updated);
saveFdsAlerts(updated);
if (activeAlertDetail && activeAlertDetail.id === id) {
setActiveAlertDetail({ ...activeAlertDetail, status: 'investigating', action: undefined, note: undefined, resolvedTime: undefined });
}
triggerToast(`Kasus ${id} dibuka kembali untuk investigasi ulang.`);
};
// Filter alerts
const filteredAlerts = alerts.filter(a => {
const matchesSearch =
a.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
a.txId.toLowerCase().includes(searchQuery.toLowerCase()) ||
a.merchant.toLowerCase().includes(searchQuery.toLowerCase()) ||
a.rule.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus =
filterStatus === 'all' ? true :
filterStatus === 'pending' ? a.status === 'pending' :
filterStatus === 'investigating' ? a.status === 'investigating' :
filterStatus === 'approved' ? a.status === 'approved' :
filterStatus === 'blocked' ? a.status === 'blocked' : true;
const matchesRisk =
filterRisk === 'all' ? true :
filterRisk === 'high' ? a.score >= 80 :
filterRisk === 'medium' ? a.score < 80 : true;
return matchesSearch && matchesStatus && matchesRisk;
});
// Calculate statistics
const stats = {
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
};
if (showingDetail && activeAlertDetail) {
const isPending = activeAlertDetail.status === 'pending';
const isInvestigating = activeAlertDetail.status === 'investigating';
const isResolved = ['approved', 'blocked'].includes(activeAlertDetail.status);
return (
{/* Breadcrumb / Back button */}
{ setShowingDetail(false); setActiveAlertDetail(null); }}
style={{
background: 'none', border: 'none', color: '#515252', fontSize: 13, fontWeight: 600,
cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 6, padding: '4px 0'
}}
>
Kembali ke Suspect Management
{/* Header */}
Investigasi Kasus — {activeAlertDetail.id}
Detail Pemeriksaan Suspect
Tinjau data transaksi, aturan yang terlanggar, dan submit keputusan penanganan fraud.
{/* Layout Grid */}
{/* Column 1: Info Transaksi & Kriteria Trigger */}
{/* Score & Urgensi */}
= 80 ? RED : AMBER, fontSize: 18 }} />
Hasil FDS
= 80 ? RED : AMBER}08`, border: `1.5px solid ${activeAlertDetail.score >= 80 ? RED : AMBER}15`, borderRadius: 10, padding: '16px', display: 'flex', alignItems: 'center', gap: 14 }}>
= 80 ? RED : AMBER, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
= 80 ? RED : AMBER, fontWeight: 800, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
{activeAlertDetail.score >= 80 ? 'Anomali Risiko Tinggi' : 'Risiko Sedang'}
{/* Detail Transaksi */}
Informasi Detail Transaksi
ID Transaksi
{activeAlertDetail.txId}
Nama Merchant
{activeAlertDetail.merchant}
Nominal Transaksi
{fmtRp(activeAlertDetail.amount)}
Lokasi Pengguna
{activeAlertDetail.location}
Waktu Deteksi FDS
{fmtDate(activeAlertDetail.time)}
{/* Aturan Terlanggar */}
Kriteria Trigger Suspect
Aturan FDS Terlanggar
{activeAlertDetail.rule}
{/* Column 2: Alur Penanganan & Form Keputusan */}
Keputusan & Resolusi
{['pending', 'investigating'].includes(activeAlertDetail.status) ? (
{activeAlertDetail.status === 'pending' && (
Kasus suspect ini belum diproses. Silakan klik tombol di bawah untuk memulai investigasi.
handleUpdateStatus(activeAlertDetail.id, 'investigating')}
style={{
width: '100%', padding: '12px', background: TEAL, color: '#fff', border: 'none',
borderRadius: 8, fontSize: 13.5, fontWeight: 750, cursor: 'pointer',
boxShadow: `0 4px 12px ${TEAL}25`, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8
}}
>
Mulai Investigasi Kasus
)}
{activeAlertDetail.status === 'investigating' && (
<>
Identifikasi
setSelectedAction(e.target.value)}
style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 13, outline: 'none', background: '#fff', fontFamily: 'inherit' }}
>
Fraud (Tidak Aman)
Tidak Fraud (Aman)
Tindakan
Masukan Ke Blacklist
Masukan Ke White List
Abaikan & Monitor
Catatan Analisis Kasus (Kronologi)
setInvestigationNote(e.target.value)}
style={{ width: '100%', padding: '10px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 12.5, outline: 'none', fontFamily: 'inherit', resize: 'vertical', minHeight: 120 }}
/>
Kirim Keputusan Akhir
>
)}
) : (
/* Resolved View */
KASUS TERESOLUSI ({activeAlertDetail.action?.toUpperCase()})
Catatan Analisis:
{activeAlertDetail.note || 'Tidak ada catatan.'}
Diselesaikan pada: {activeAlertDetail.resolvedTime ? fmtDate(activeAlertDetail.resolvedTime) : '—'}
handleReopenCase(activeAlertDetail.id)}
style={{
width: '100%', padding: '11px', background: '#fff', border: '1.5px solid #efefef',
color: '#515252', borderRadius: 8, fontSize: 13, fontWeight: 700, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = TEAL; e.currentTarget.style.color = TEAL; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = '#efefef'; e.currentTarget.style.color = '#515252'; }}
>
Buka Kembali Kasus
)}
{/* Floating Toast Notification */}
{toast && (
{toast}
)}
);
}
return (
{/* Header */}
Fraud Detection System
Suspect Management & Investigasi
Kelola temuan suspect anomali merchant, lakukan investigasi detail, dan ambil tindakan preventif fraud secara real-time.
e.currentTarget.style.background = '#0041A8'}
onMouseLeave={e => e.currentTarget.style.background = TEAL}
>
Simulasikan Temuan Suspect
{/* Summary Row */}
{[
{ label: 'Total Alerts Suspect', value: stats.total, sub: 'Semua kasus terdeteksi', icon: 'bx-shield', color: TEAL },
{ label: 'Pending Review', value: stats.pending, sub: 'Menunggu penanganan', icon: 'bx-time-five', color: AMBER },
{ label: 'Sedang Diinvestigasi', value: stats.investigating, sub: 'Dalam proses analisis', icon: 'bx-analyse', color: '#0056D2' },
{ label: 'Kasus Diselesaikan', value: stats.resolved, sub: 'Telah diputuskan aksinya', icon: 'bx-check-circle', color: GREEN }
].map((card, i) => (
{card.label}
{card.value}
{card.sub}
))}
{/* Filter & Search Controls */}
setSearchQuery(e.target.value)}
style={{ width: '100%', padding: '8px 12px 8px 36px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 12.5, outline: 'none', fontFamily: 'inherit' }}
/>
setFilterStatus(e.target.value)}
style={{ padding: '8px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 12.5, outline: 'none', background: '#fff', fontFamily: 'inherit', color: '#515252' }}
>
Semua Status Review
Pending Review
Dalam Investigasi
Aman (Tidak Fraud / White List)
Ditolak (Fraud / Blacklist)
setFilterRisk(e.target.value)}
style={{ padding: '8px 12px', borderRadius: 8, border: '1.5px solid #efefef', fontSize: 12.5, outline: 'none', background: '#fff', fontFamily: 'inherit', color: '#515252' }}
>
Semua Skala Risiko
Risiko Tinggi (Score >= 80)
Risiko Sedang (Score < 80)
{/* Main Alerts Table */}
ID Alert
Entitas Merchant / ID
Waktu
Nominal
Pelanggaran Aturan FDS
Status
Aksi
{filteredAlerts.length === 0 ? (
Tidak ada temuan suspect yang cocok dengan kriteria filter.
) : (
filteredAlerts.map(a => {
const isHigh = a.score >= 80;
// Status mapping to color/text
let statusBg = 'rgba(217,146,11,0.08)';
let statusColor = AMBER;
let statusText = 'MEDIUM RISK';
if (a.status === 'investigating') {
statusBg = 'rgba(0,86,210,0.08)';
statusColor = '#0056D2';
statusText = 'LOW RISK';
} else if (a.status === 'approved') {
statusBg = 'rgba(116,181,12,0.08)';
statusColor = GREEN;
statusText = a.action ? a.action.toUpperCase() : 'LOW RISK';
} else if (a.status === 'blocked') {
statusBg = 'rgba(237,49,81,0.08)';
statusColor = RED;
statusText = a.action ? a.action.toUpperCase() : 'HIGH RISK';
}
return (
e.currentTarget.style.background = '#fafbfc'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
{a.id}
{a.merchant}
{a.txId}
{fmtDate(a.time)}
{fmtRp(a.amount)}
{a.rule}
{statusText}
{
setActiveAlertDetail(a);
setSelectedAction(a.action || 'Fraud');
setInvestigationNote(a.note || '');
setShowingDetail(true);
}}
style={{
background: '#fff', border: '1.5px solid #efefef', borderRadius: 6,
padding: '5px 12px', fontSize: 11.5, fontWeight: 750, cursor: 'pointer',
transition: 'all 150ms', color: '#515252', width: '100%'
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = TEAL; e.currentTarget.style.color = TEAL; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = '#efefef'; e.currentTarget.style.color = '#515252'; }}
>
{['approved', 'blocked'].includes(a.status) ? 'Detail' : 'Periksa'}
);
})
)}
{/* Floating Toast Notification */}
{toast && (
{toast}
)}
);
}
// Register components globally
Object.assign(window, { FdsDashboardPage, FdsMonitoringPage, FdsParameterPage, FdsSuspectPage });