Spaces:
Running
Running
| document.addEventListener('DOMContentLoaded', function() { | |
| // Initialize Feather icons | |
| feather.replace(); | |
| // DOM Elements | |
| const analyzeBtn = document.getElementById('analyzeBtn'); | |
| const analyzeBilanNormalBtn = document.getElementById('analyzeBilanNormalBtn'); | |
| const analyzeBilanDiversBtn = document.getElementById('analyzeBilanDiversBtn'); | |
| const fecFile = document.getElementById('fecFile'); | |
| const bilanNormalFile = document.getElementById('bilanNormalFile'); | |
| const bilanDiversFile = document.getElementById('bilanDiversFile'); | |
| const riskBar = document.getElementById('riskBar'); | |
| const riskScore = document.getElementById('riskScore'); | |
| const fecAnomalyCount = document.getElementById('fecAnomalyCount'); | |
| const bilanNormalAnomalyCount = document.getElementById('bilanNormalAnomalyCount'); | |
| const diversAccountCount = document.getElementById('diversAccountCount'); | |
| const locationResults = document.getElementById('locationResults'); | |
| const bilanNormalResults = document.getElementById('bilanNormalResults'); | |
| const bilanDiversResults = document.getElementById('bilanDiversResults'); | |
| let currentFecFile = null; | |
| let currentBilanNormalFile = null; | |
| let currentBilanDiversFile = null; | |
| let scanHistoryData = JSON.parse(localStorage.getItem('scanHistory')) || []; | |
| // Initialize scan history | |
| renderScanHistory(); | |
| // Expose global functions for full detection page | |
| // Global API | |
| window.JanusFraudDetector = { | |
| analyzeCryptoTransactions: analyzeCryptoTransactions, | |
| detectOffshoreEntities: detectOffshoreEntities, | |
| performBilanNormalAnalysis: performBilanNormalAnalysis, | |
| performBilanDiversAnalysis: performBilanDiversAnalysis, | |
| processExcelFile: processExcelFile, | |
| processPDFFile: processPDFFile, | |
| extractAmount: extractAmount, | |
| extractAccountNumber: extractAccountNumber, | |
| extractAccountName: extractAccountName, | |
| calculateFinancialRatios: calculateFinancialRatios, | |
| performFraudAnalysis: performFraudAnalysis, | |
| performDiversAccountAnalysis: performDiversAccountAnalysis | |
| }; | |
| // File upload handlers | |
| fecFile.addEventListener('change', (e) => handleFileSelect(e, 'fec')); | |
| bilanNormalFile.addEventListener('change', (e) => handleFileSelect(e, 'bilanNormal')); | |
| bilanDiversFile.addEventListener('change', (e) => handleFileSelect(e, 'bilanDivers')); | |
| // Drag and drop functionality | |
| const dropZones = document.querySelectorAll('.border-dashed'); | |
| if (dropZones[0]) setupDropZone(dropZones[0], 'fec'); | |
| if (dropZones[1]) setupDropZone(dropZones[1], 'bilanNormal'); | |
| if (dropZones[2]) setupDropZone(dropZones[2], 'bilanDivers'); | |
| // Analyze button handlers | |
| analyzeBtn.addEventListener('click', () => analyzeFile('fec')); | |
| analyzeBilanNormalBtn.addEventListener('click', () => analyzeFile('bilanNormal')); | |
| analyzeBilanDiversBtn.addEventListener('click', () => analyzeFile('bilanDivers')); | |
| function setupDropZone(zone, type) { | |
| zone.addEventListener('dragover', handleDragOver); | |
| zone.addEventListener('dragleave', handleDragLeave); | |
| zone.addEventListener('drop', (e) => handleDrop(e, type)); | |
| } | |
| function handleFileSelect(event, type) { | |
| const file = event.target.files[0]; | |
| if (file) { | |
| if (type === 'fec') { | |
| currentFecFile = file; | |
| } else if (type === 'bilanNormal') { | |
| currentBilanNormalFile = file; | |
| } else if (type === 'bilanDivers') { | |
| currentBilanDiversFile = file; | |
| } | |
| updateFileDisplay(file.name, type); | |
| } | |
| } | |
| function handleDragOver(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| e.currentTarget.classList.add('border-red-500'); | |
| } | |
| function handleDragLeave(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| e.currentTarget.classList.remove('border-red-500'); | |
| } | |
| function handleDrop(e, type) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| e.currentTarget.classList.remove('border-red-500'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| if (type === 'fec') { | |
| currentFecFile = files[0]; | |
| fecFile.files = files; | |
| } else if (type === 'bilanNormal') { | |
| currentBilanNormalFile = files[0]; | |
| bilanNormalFile.files = files; | |
| } else if (type === 'bilanDivers') { | |
| currentBilanDiversFile = files[0]; | |
| bilanDiversFile.files = files; | |
| } | |
| updateFileDisplay(files[0].name, type); | |
| } | |
| } | |
| function updateFileDisplay(fileName, type) { | |
| const dropZones = document.querySelectorAll('.border-dashed'); | |
| let dropZone; | |
| if (type === 'fec') dropZone = dropZones[0]; | |
| else if (type === 'bilanNormal') dropZone = dropZones[1]; | |
| else if (type === 'bilanDivers') dropZone = dropZones[2]; | |
| if (dropZone) { | |
| const icon = type === 'bilanDivers' ? 'alert-triangle' : 'file-text'; | |
| dropZone.innerHTML = ` | |
| <i data-feather="${icon}" class="mx-auto mb-4 text-red-500"></i> | |
| <p class="text-green-400 mb-2">File Selected:</p> | |
| <p class="text-white font-medium">${fileName}</p> | |
| <p class="text-gray-400 text-sm mt-2">Ready for ${type.toUpperCase()} analysis</p> | |
| `; | |
| feather.replace(); | |
| } | |
| } | |
| async function analyzeFile(type) { | |
| let file, btn; | |
| if (type === 'fec') { | |
| file = currentFecFile; | |
| btn = analyzeBtn; | |
| } else if (type === 'bilanNormal') { | |
| file = currentBilanNormalFile; | |
| btn = analyzeBilanNormalBtn; | |
| } else if (type === 'bilanDivers') { | |
| file = currentBilanDiversFile; | |
| btn = analyzeBilanDiversBtn; | |
| } | |
| if (!file) { | |
| alert(`Please select a ${type.toUpperCase()} file first`); | |
| return; | |
| } | |
| btn.disabled = true; | |
| btn.innerHTML = `<i data-feather="loader" class="animate-spin inline mr-2"></i>Analyzing ${type.toUpperCase()}...`; | |
| feather.replace(); | |
| try { | |
| let results; | |
| if (type === 'fec') { | |
| results = await performAnalysis(file); | |
| displayFecResults(results); | |
| } else if (type === 'bilanNormal') { | |
| results = await performBilanNormalAnalysis(file); | |
| displayBilanNormalResults(results); | |
| } else if (type === 'bilanDivers') { | |
| results = await performBilanDiversAnalysis(file); | |
| displayBilanDiversResults(results); | |
| } | |
| // Combine and update overall stats | |
| updateCombinedStats(); | |
| addToScanHistory({...results, type: type.toUpperCase()}); | |
| } catch (error) { | |
| console.error(`${type} analysis failed:`, error); | |
| alert(`${type.toUpperCase()} analysis failed: ` + error.message); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = type === 'fec' ? '[LANCER ANALYSE FEC]' : | |
| type === 'bilanNormal' ? '[LANCER ANALYSE NORMALE]' : | |
| '[LANCER ANALYSE DIVERS]'; | |
| } | |
| } | |
| async function performBilanNormalAnalysis(file) { | |
| // Analyse financière normale du bilan | |
| await new Promise(resolve => setTimeout(resolve, 2000)); | |
| const fileExtension = file.name.split('.').pop().toLowerCase(); | |
| let data = []; | |
| if (fileExtension === 'xlsx' || fileExtension === 'xls') { | |
| data = await processExcelFile(file); | |
| } else if (fileExtension === 'pdf') { | |
| data = await processPDFFile(file); | |
| } else { | |
| throw new Error('Unsupported file format'); | |
| } | |
| // Perform normal bilan analysis | |
| const analysisResults = performNormalBilanAnalysis(data); | |
| return { | |
| fileName: file.name, | |
| date: new Date().toISOString(), | |
| entries: data.length, | |
| normalAnomalies: analysisResults.anomalies, | |
| financialRatios: analysisResults.ratios, | |
| riskScore: analysisResults.riskScore, | |
| summary: `Normal bilan analysis completed: Financial health assessment done` | |
| }; | |
| } | |
| async function performBilanDiversAnalysis(file) { | |
| // Analyse spΓ©cialisΓ©e des comptes divers | |
| await new Promise(resolve => setTimeout(resolve, 2500)); | |
| const fileExtension = file.name.split('.').pop().toLowerCase(); | |
| let data = []; | |
| if (fileExtension === 'xlsx' || fileExtension === 'xls') { | |
| data = await processExcelFile(file); | |
| } else if (fileExtension === 'pdf') { | |
| data = await processPDFFile(file); | |
| } else { | |
| throw new Error('Unsupported file format'); | |
| } | |
| // Perform divers account analysis | |
| const analysisResults = performDiversAccountAnalysis(data); | |
| return { | |
| fileName: file.name, | |
| date: new Date().toISOString(), | |
| entries: data.length, | |
| diversAnomalies: analysisResults.anomalies, | |
| diversAccounts: analysisResults.diversAccounts, | |
| offshoreEntities: analysisResults.offshoreEntities, | |
| riskScore: analysisResults.riskScore, | |
| summary: `Divers analysis completed: ${analysisResults.diversAccounts.length} divers accounts detected` | |
| }; | |
| } | |
| function performNormalBilanAnalysis(data) { | |
| const anomalies = []; | |
| const ratios = calculateFinancialRatios(data); | |
| // Analyse des ratios financiers | |
| if (ratios.liquidite < 1.0) { | |
| anomalies.push({ | |
| type: 'Poor Liquidity', | |
| severity: 'HIGH', | |
| description: `Liquidity ratio below 1.0: ${ratios.liquidite.toFixed(2)}` | |
| }); | |
| } | |
| if (ratios.solvabilite < 0.2) { | |
| anomalies.push({ | |
| type: 'Low Solvency', | |
| severity: 'MEDIUM', | |
| description: `Solvency ratio below 20%: ${(ratios.solvabilite * 100).toFixed(1)}%` | |
| }); | |
| } | |
| if (ratios.endettement > 80) { | |
| anomalies.push({ | |
| type: 'High Debt Ratio', | |
| severity: 'HIGH', | |
| description: `Debt ratio above 80%: ${ratios.endettement.toFixed(1)}%` | |
| }); | |
| } | |
| // Calcul du risque normal | |
| const highRiskCount = anomalies.filter(a => a.severity === 'HIGH').length; | |
| const mediumRiskCount = anomalies.filter(a => a.severity === 'MEDIUM').length; | |
| const riskScore = Math.min(100, (highRiskCount * 40) + (mediumRiskCount * 20)); | |
| return { | |
| anomalies, | |
| ratios, | |
| riskScore | |
| }; | |
| } | |
| function performDiversAccountAnalysis(data) { | |
| const anomalies = []; | |
| const diversAccounts = []; | |
| const offshoreEntities = []; | |
| const offshorePatterns = [ | |
| 'panama', 'seychelles', 'cayman', 'bvi', 'bahamas', 'bermuda', 'marshall islands', | |
| 'luxembourg', 'singapore', 'hong kong', 'delaware', 'wyoming', 'nevis' | |
| ]; | |
| const diversAccounts = ['481', '486', '487', 'divers', 'suspens', 'provision']; | |
| // Analyse des comptes divers suspects | |
| data.forEach((entry, index) => { | |
| const text = entry.text || JSON.stringify(entry); | |
| const lowerText = text.toLowerCase(); | |
| // DΓ©tection des comptes divers | |
| diversAccounts.forEach(account => { | |
| if (lowerText.includes(account)) { | |
| const amount = extractAmount(lowerText); | |
| if (amount > 5000) { | |
| diversAccounts.push({ | |
| accountNumber: account, | |
| accountName: extractAccountName(text), | |
| amount: amount, | |
| severity: amount > 50000 ? 'HIGH' : amount > 10000 ? 'MEDIUM' : 'LOW', | |
| description: `Suspicious ${account} account activity`, | |
| line: entry.line || index | |
| }); | |
| anomalies.push({ | |
| type: 'Suspicious Divers Account', | |
| line: entry.line || index, | |
| severity: amount > 50000 ? 'HIGH' : 'MEDIUM', | |
| description: `Account ${account} with amount ${amount.toLocaleString()}` | |
| }); | |
| } | |
| } | |
| }); | |
| // DΓ©tection d'entitΓ©s offshore | |
| offshorePatterns.forEach(location => { | |
| if (lowerText.includes(location)) { | |
| offshoreEntities.push({ | |
| location: location, | |
| severity: 'HIGH', | |
| description: `Potential offshore entity in ${location.toUpperCase()}`, | |
| line: entry.line || index | |
| }); | |
| anomalies.push({ | |
| type: 'Offshore Entity', | |
| line: entry.line || index, | |
| severity: 'HIGH', | |
| description: `Potential offshore entity in ${location.toUpperCase()}` | |
| }); | |
| } | |
| }); | |
| // Transactions en cash suspectes | |
| if (lowerText.includes('cash') && extractAmount(lowerText) > 10000) { | |
| anomalies.push({ | |
| type: 'Large Cash Transaction', | |
| line: entry.line || index, | |
| severity: 'HIGH', | |
| description: 'Large cash payment detected' | |
| }); | |
| } | |
| }); | |
| // Score de risque divers | |
| const highRiskCount = anomalies.filter(a => a.severity === 'HIGH').length; | |
| const mediumRiskCount = anomalies.filter(a => a.severity === 'MEDIUM').length; | |
| const diversRiskScore = Math.min(100, (diversAccounts.length * 15) + (offshoreEntities.length * 25) + (highRiskCount * 30) + (mediumRiskCount * 10)); | |
| return { | |
| anomalies, | |
| diversAccounts, | |
| offshoreEntities, | |
| riskScore: diversRiskScore | |
| }; | |
| } | |
| function extractAmount(text) { | |
| const amountMatch = text.match(/(\d{1,3}(?:,\d{3})*(?:\.\d{2})?)/g); | |
| if (amountMatch) { | |
| return parseFloat(amountMatch[0].replace(/,/g, '')) || 0; | |
| } | |
| return 0; | |
| } | |
| function extractAccountNumber(text) { | |
| const match = text.match(/(481|486|487|\d{3,6})/); | |
| return match ? match[0] : 'N/A'; | |
| } | |
| function extractAccountName(text) { | |
| const match = text.match(/[A-Za-z\s]{5,}/); | |
| return match ? match[0].trim() : 'Divers Account'; | |
| } | |
| function calculateFinancialRatios(data) { | |
| // Calcul simplifiΓ© des ratios financiers | |
| const totalAssets = data.reduce((sum, entry) => { | |
| const amount = extractAmount(JSON.stringify(entry)); | |
| return entry.text?.toLowerCase().includes('actif') ? sum + amount : sum; | |
| }, 0); | |
| const totalLiabilities = data.reduce((sum, entry) => { | |
| const amount = extractAmount(JSON.stringify(entry)); | |
| return entry.text?.toLowerCase().includes('passif') ? sum + amount : sum; | |
| }, 0); | |
| const equity = Math.max(0, totalAssets - totalLiabilities); | |
| return { | |
| liquidite: totalAssets > 0 ? (totalAssets / Math.max(totalLiabilities, 1)) : 0, | |
| solvabilite: totalAssets > 0 ? (equity / totalAssets) : 0, | |
| endettement: totalAssets > 0 ? (totalLiabilities / totalAssets * 100) : 0 | |
| }; | |
| } | |
| // Fonction pour analyser les transactions crypto | |
| async function analyzeCryptoTransactions(walletAddress) { | |
| try { | |
| const response = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd`); | |
| const prices = await response.json(); | |
| let transactions = { result: [] }; | |
| if (walletAddress.startsWith('0x')) { | |
| const etherscanResponse = await fetch(`https://api.etherscan.io/api?module=account&action=txlist&address=${walletAddress}&startblock=0&endblock=99999999&page=1&offset=10&sort=desc&apikey=YourApiKeyToken`); | |
| transactions = await etherscanResponse.json(); | |
| } | |
| const anomalies = []; | |
| let totalValue = 0; | |
| if (transactions.result && transactions.result.length > 0) { | |
| transactions.result.forEach(tx => { | |
| const value = parseInt(tx.value) / 1e18; | |
| totalValue += value; | |
| if (value > 50000) { | |
| anomalies.push({ | |
| type: 'High Volume Transfer', | |
| description: `Transaction > $50,000 detected: ${value} ETH`, | |
| severity: 'HIGH' | |
| }); | |
| } | |
| }); | |
| } | |
| return { | |
| wallet: walletAddress, | |
| riskScore: Math.min(100, anomalies.length * 20 + (totalValue > 100000 ? 30 : 0)), | |
| anomalies: anomalies, | |
| totalValue: totalValue, | |
| prices: prices | |
| }; | |
| } catch (error) { | |
| console.error('Crypto analysis failed:', error); | |
| return { | |
| wallet: walletAddress, | |
| riskScore: 0, | |
| anomalies: [], | |
| totalValue: 0, | |
| prices: {} | |
| }; | |
| } | |
| } | |
| // Fonction pour dΓ©tecter les structures offshore | |
| function detectOffshoreEntities(textData) { | |
| const offshoreJurisdictions = [ | |
| 'Panama', 'Seychelles', 'Cayman Islands', 'British Virgin Islands', 'BVI', | |
| 'Bahamas', 'Bermuda', 'Marshall Islands', 'Luxembourg', 'Singapore', | |
| 'Hong Kong', 'Delaware', 'Wyoming', 'Nevis', 'Anguilla', 'Barbados' | |
| ]; | |
| const suspiciousPatterns = [ | |
| 'nominee director', 'bearer shares', 'anonymous', 'shell company', | |
| 'trust services', 'virtual office', 'PO Box', 'registered agent' | |
| ]; | |
| const detections = []; | |
| offshoreJurisdictions.forEach(jurisdiction => { | |
| if (textData.toLowerCase().includes(jurisdiction.toLowerCase())) { | |
| detections.push({ | |
| type: 'Offshore Jurisdiction', | |
| entity: jurisdiction, | |
| severity: 'HIGH' | |
| }); | |
| } | |
| }); | |
| suspiciousPatterns.forEach(pattern => { | |
| if (textData.toLowerCase().includes(pattern)) { | |
| detections.push({ | |
| type: 'Suspicious Pattern', | |
| pattern: pattern, | |
| severity: 'MEDIUM' | |
| }); | |
| } | |
| }); | |
| return detections; | |
| } | |
| async function performAnalysis(file) { | |
| const fileExtension = file.name.split('.').pop().toLowerCase(); | |
| let data = []; | |
| if (fileExtension === 'xlsx' || fileExtension === 'xls') { | |
| data = await processExcelFile(file); | |
| } else if (fileExtension === 'pdf') { | |
| data = await processPDFFile(file); | |
| } else { | |
| throw new Error('Unsupported file format'); | |
| } | |
| // Perform local analysis instead of server | |
| const fraudAnalysis = performFraudAnalysis(data); | |
| return { | |
| fileName: file.name, | |
| date: new Date().toISOString(), | |
| entries: data.length, | |
| anomalies: fraudAnalysis.anomalies, | |
| riskScore: fraudAnalysis.riskScore, | |
| locations: fraudAnalysis.locations, | |
| suspiciousTransactions: fraudAnalysis.suspiciousTransactions, | |
| summary: fraudAnalysis.anomalies.length > 0 | |
| ? `Fraud analysis detected ${fraudAnalysis.anomalies.length} anomalies with risk score ${fraudAnalysis.riskScore}%` | |
| : 'No significant fraud patterns detected' | |
| }; | |
| } | |
| async function processExcelFile(file) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| const data = new Uint8Array(e.target.result); | |
| const workbook = XLSX.read(data, { type: 'array' }); | |
| const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; | |
| const jsonData = XLSX.utils.sheet_to_json(firstSheet); | |
| resolve(jsonData); | |
| } catch (error) { | |
| reject(new Error('Failed to process Excel file')); | |
| } | |
| }; | |
| reader.onerror = () => reject(new Error('Failed to read file')); | |
| reader.readAsArrayBuffer(file); | |
| }); | |
| } | |
| async function processPDFFile(file) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = async function(e) { | |
| try { | |
| const pdfData = new Uint8Array(e.target.result); | |
| const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise; | |
| let fullText = ''; | |
| for (let i = 1; i <= pdf.numPages; i++) { | |
| const page = await pdf.getPage(i); | |
| const textContent = await page.getTextContent(); | |
| const pageText = textContent.items.map(item => item.str).join(' '); | |
| fullText += pageText + '\n'; | |
| } | |
| // Parse PDF text for financial data | |
| const data = parseFinancialData(fullText); | |
| resolve(data); | |
| } catch (error) { | |
| reject(new Error('Failed to process PDF file')); | |
| } | |
| }; | |
| reader.onerror = () => reject(new Error('Failed to read file')); | |
| reader.readAsArrayBuffer(file); | |
| }); | |
| } | |
| function parseFinancialData(text) { | |
| // Simple regex patterns to extract financial data | |
| const patterns = [ | |
| { regex: /(\d{1,3}(?:,\d{3})*(?:\.\d{2})?)/g, type: 'amount' }, | |
| /([A-Z]{2})\s+(\d{5})/g, // State and zip | |
| /(\d{2}\/\d{2}\/\d{4})/g, // Dates | |
| ]; | |
| const data = []; | |
| const lines = text.split('\n'); | |
| lines.forEach((line, index) => { | |
| if (line.trim()) { | |
| const amounts = [...line.matchAll(patterns[0])].map(m => m[1]); | |
| const states = [...line.matchAll(patterns[1])].map(m => ({ state: m[1], zip: m[2] })); | |
| if (amounts.length > 0 || states.length > 0) { | |
| data.push({ | |
| line: index + 1, | |
| text: line.trim(), | |
| amounts: amounts, | |
| locations: states, | |
| raw: line | |
| }); | |
| } | |
| } | |
| }); | |
| return data; | |
| } | |
| function performFraudAnalysis(data) { | |
| const anomalies = []; | |
| const locations = new Map(); | |
| let totalAmount = 0; | |
| let suspiciousTransactions = []; | |
| data.forEach((entry, index) => { | |
| // Analyze amounts for anomalies | |
| entry.amounts?.forEach(amount => { | |
| const numAmount = parseFloat(amount.replace(/,/g, '')); | |
| totalAmount += numAmount; | |
| // Flag large transactions | |
| if (numAmount > 10000) { | |
| anomalies.push({ | |
| type: 'Large Transaction', | |
| line: entry.line, | |
| amount: amount, | |
| severity: numAmount > 50000 ? 'HIGH' : 'MEDIUM' | |
| }); | |
| } | |
| // Flag round numbers (potential fraud indicator) | |
| if (numAmount % 1000 === 0 && numAmount > 0) { | |
| anomalies.push({ | |
| type: 'Round Number', | |
| line: entry.line, | |
| amount: amount, | |
| severity: 'LOW' | |
| }); | |
| } | |
| }); | |
| // Track locations | |
| entry.locations?.forEach(loc => { | |
| const locationKey = `${loc.state}-${loc.zip}`; | |
| if (!locations.has(locationKey)) { | |
| locations.set(locationKey, { state: loc.state, zip: loc.zip, count: 0, amount: 0 }); | |
| } | |
| const locData = locations.get(locationKey); | |
| locData.count++; | |
| locData.amount += entry.amounts?.reduce((sum, amt) => sum + parseFloat(amt.replace(/,/g, '') || 0), 0) || 0; | |
| }); | |
| // Pattern analysis for suspicious behavior | |
| if (entry.raw.toLowerCase().includes('cash') || entry.raw.toLowerCase().includes('cash')) { | |
| anomalies.push({ | |
| type: 'Cash Transaction', | |
| line: entry.line, | |
| severity: 'MEDIUM', | |
| description: 'Large cash transaction detected' | |
| }); | |
| } | |
| }); | |
| // Calculate risk score | |
| const highRiskAnomalies = anomalies.filter(a => a.severity === 'HIGH').length; | |
| const mediumRiskAnomalies = anomalies.filter(a => a.severity === 'MEDIUM').length; | |
| const lowRiskAnomalies = anomalies.filter(a => a.severity === 'LOW').length; | |
| const riskScore = Math.min(100, Math.round( | |
| (highRiskAnomalies * 30 + mediumRiskAnomalies * 15 + lowRiskAnomalies * 5) / | |
| Math.max(data.length / 10, 1) | |
| )); | |
| // Flag suspicious location patterns | |
| locations.forEach((locData, key) => { | |
| if (locData.count > 5 && locData.amount > 100000) { | |
| anomalies.push({ | |
| type: 'Suspicious Location Pattern', | |
| severity: 'HIGH', | |
| description: `High volume (${locData.count} transactions) in ${key}` | |
| }); | |
| } | |
| }); | |
| suspiciousTransactions = anomalies | |
| .filter(a => a.severity === 'HIGH') | |
| .map(a => ({ ...a, fileIndex: data.findIndex(d => d.line === a.line) })); | |
| return { | |
| anomalies, | |
| riskScore, | |
| locations: Array.from(locations.values()), | |
| suspiciousTransactions | |
| }; | |
| } | |
| function displayFecResults(results) { | |
| // Update risk bar | |
| riskBar.style.width = results.riskScore + '%'; | |
| riskBar.classList.add('animate-risk'); | |
| riskScore.textContent = results.riskScore + '%'; | |
| // Update FEC-specific anomaly count | |
| fecAnomalyCount.textContent = results.anomalies.length; | |
| // Update location analysis | |
| displayLocationResults(results.locations, results.suspiciousTransactions); | |
| // Show success message | |
| showNotification('FEC analysis completed successfully!', 'success'); | |
| } | |
| function displayBilanNormalResults(results) { | |
| // Update bilan normal anomaly count | |
| bilanNormalAnomalyCount.textContent = results.normalAnomalies.length; | |
| // Update bilan normal results | |
| bilanNormalResults.innerHTML = ` | |
| <div class="space-y-3"> | |
| ${results.normalAnomalies.length === 0 ? ` | |
| <div class="text-center py-4 text-green-500"> | |
| <i data-feather="check-circle" class="mx-auto mb-2"></i> | |
| <p>Analyse normale: Aucun problème détecté</p> | |
| </div> | |
| ` : ` | |
| ${results.normalAnomalies.map(anomaly => ` | |
| <div class="bg-gray-700 rounded p-3 border ${anomaly.severity === 'HIGH' ? 'border-red-500' : 'border-yellow-500'}"> | |
| <div class="text-sm"> | |
| <span class="font-medium">${anomaly.type}</span> | |
| <div class="text-gray-400 mt-1">${anomaly.description}</div> | |
| </div> | |
| </div> | |
| `).join('')} | |
| `} | |
| </div> | |
| <div class="mt-4 p-3 bg-blue-900 bg-opacity-30 border border-blue-500 rounded"> | |
| <p class="text-blue-300 font-semibold mb-2">Ratios Financiers:</p> | |
| <div class="text-sm text-gray-300"> | |
| <p>LiquiditΓ©: ${results.financialRatios.liquidite?.toFixed(2) || 'N/A'}</p> | |
| <p>SolvabilitΓ©: ${results.financialRatios.solvabilite?.toFixed(2) || 'N/A'}</p> | |
| <p>Endettement: ${results.financialRatios.endettement?.toFixed(2) || 'N/A'}%</p> | |
| </div> | |
| </div> | |
| `; | |
| feather.replace(); | |
| showNotification('Analyse bilan normale terminΓ©e!', 'success'); | |
| } | |
| function displayBilanDiversResults(results) { | |
| // Update divers account count | |
| diversAccountCount.textContent = results.diversAccounts.length; | |
| // Update bilan divers results | |
| const diversAccounts = results.diversAccounts || []; | |
| const offshoreEntities = results.offshoreEntities || []; | |
| bilanDiversResults.innerHTML = ` | |
| <div class="space-y-3"> | |
| ${diversAccounts.length === 0 ? ` | |
| <div class="text-center py-4 text-green-500"> | |
| <i data-feather="check-circle" class="mx-auto mb-2"></i> | |
| <p>Aucun compte divers suspect dΓ©tectΓ©</p> | |
| </div> | |
| ` : ` | |
| ${diversAccounts.slice(0, 5).map(account => ` | |
| <div class="bg-gray-700 rounded p-3 border ${account.severity === 'HIGH' ? 'border-red-500' : 'border-orange-500'}"> | |
| <div class="flex justify-between items-center"> | |
| <span class="font-medium">${account.accountNumber}</span> | |
| <span class="text-orange-500 font-bold">${account.amount.toLocaleString()} β¬</span> | |
| </div> | |
| <div class="text-sm text-gray-400 mt-1"> | |
| ${account.accountName} | Risque: ${account.severity} | |
| </div> | |
| </div> | |
| `).join('')} | |
| `} | |
| ${offshoreEntities.length > 0 ? ` | |
| <div class="mt-3 p-3 bg-red-900 bg-opacity-50 border border-red-500 rounded"> | |
| <p class="text-red-300 font-semibold mb-2"> | |
| <i data-feather="globe" class="inline mr-2"></i> | |
| ${offshoreEntities.length} EntitΓ©s Offshore DΓ©tectΓ©es | |
| </p> | |
| <div class="text-sm text-gray-400"> | |
| ${offshoreEntities.map(entity => `${entity.location.toUpperCase()}`).join(', ')} | |
| </div> | |
| </div> | |
| ` : ''} | |
| </div> | |
| <div class="mt-4 p-3 bg-orange-900 bg-opacity-50 border border-orange-500 rounded"> | |
| <p class="text-orange-300 font-semibold mb-2"> | |
| <i data-feather="alert-triangle" class="inline mr-2"></i> | |
| Score Risque Divers: ${results.riskScore}% | |
| </p> | |
| <p class="text-sm text-gray-400">Investigation approfondie recommandΓ©e</p> | |
| </div> | |
| `; | |
| feather.replace(); | |
| showNotification('Analyse bilan divers terminΓ©e!', 'success'); | |
| } | |
| function updateCombinedStats() { | |
| // This function can be enhanced to combine both FEC and BILAN results | |
| // For now, it ensures the UI stays synchronized | |
| const fecAnomalies = parseInt(fecAnomalyCount.textContent) || 0; | |
| const bilanNormalAnomalies = parseInt(bilanNormalAnomalyCount.textContent) || 0; | |
| const diversAccounts = parseInt(diversAccountCount.textContent) || 0; | |
| // Calculate combined risk score | |
| const combinedRisk = Math.min(100, Math.round( | |
| (fecAnomalies * 20 + bilanNormalAnomalies * 15 + diversAccounts * 25) / | |
| Math.max(1, fecAnomalies + bilanNormalAnomalies + diversAccounts) | |
| )); | |
| // Update main risk score | |
| riskBar.style.width = combinedRisk + '%'; | |
| riskBar.classList.add('animate-risk'); | |
| riskScore.textContent = combinedRisk + '%'; | |
| } | |
| function displayLocationResults(locations, suspiciousTransactions) { | |
| if (locations.length === 0) { | |
| locationResults.innerHTML = ` | |
| <div class="text-center py-8 text-gray-500"> | |
| <i data-feather="map-pin" class="mx-auto mb-2"></i> | |
| <p>No location data found</p> | |
| </div> | |
| `; | |
| } else { | |
| locationResults.innerHTML = locations.slice(0, 5).map(loc => ` | |
| <div class="bg-gray-700 rounded p-3"> | |
| <div class="flex justify-between items-center"> | |
| <span class="font-medium">${loc.state} ${loc.zip}</span> | |
| <span class="text-red-500 font-bold">${loc.count} txns</span> | |
| </div> | |
| <div class="text-sm text-gray-400 mt-1"> | |
| Total: ${loc.amount.toLocaleString()} | |
| </div> | |
| </div> | |
| `).join(''); | |
| if (suspiciousTransactions.length > 0) { | |
| locationResults.innerHTML += ` | |
| <div class="mt-4 p-3 bg-red-900 bg-opacity-50 border border-red-500 rounded"> | |
| <p class="text-red-300 font-semibold mb-2"> | |
| <i data-feather="alert-triangle" class="inline mr-2"></i> | |
| ${suspiciousTransactions.length} High-Risk Transactions | |
| </p> | |
| <p class="text-sm text-gray-400">Review flagged transactions immediately</p> | |
| </div> | |
| `; | |
| } | |
| } | |
| feather.replace(); | |
| } | |
| function addToScanHistory(results) { | |
| scanHistoryData.unshift({ | |
| fileName: results.fileName, | |
| date: results.date, | |
| riskScore: results.riskScore, | |
| status: results.riskScore > 70 ? 'HIGH RISK' : results.riskScore > 40 ? 'MEDIUM RISK' : 'LOW RISK' | |
| }); | |
| // Keep only last 10 entries | |
| scanHistoryData = scanHistoryData.slice(0, 10); | |
| localStorage.setItem('scanHistory', JSON.stringify(scanHistoryData)); | |
| renderScanHistory(); | |
| } | |
| function renderScanHistory() { | |
| if (scanHistoryData.length === 0) { | |
| scanHistory.innerHTML = ` | |
| <tr> | |
| <td colspan="4" class="py-4 px-4 text-center text-gray-500"> | |
| No scans yet. Upload a file to get started. | |
| </td> | |
| </tr> | |
| `; | |
| } else { | |
| scanHistory.innerHTML = scanHistoryData.map(scan => ` | |
| <tr class="border-t border-gray-700"> | |
| <td class="py-3 px-4">${scan.fileName}</td> | |
| <td class="py-3 px-4">${new Date(scan.date).toLocaleDateString()}</td> | |
| <td class="py-3 px-4"> | |
| <span class="font-bold ${scan.riskScore > 70 ? 'text-red-500' : scan.riskScore > 40 ? 'text-yellow-500' : 'text-green-500'}"> | |
| ${scan.riskScore}% | |
| </span> | |
| </td> | |
| <td class="py-3 px-4"> | |
| <span class="px-2 py-1 rounded text-xs font-medium ${ | |
| scan.riskScore > 70 ? 'bg-red-900 text-red-300' : | |
| scan.riskScore > 40 ? 'bg-yellow-900 text-yellow-300' : | |
| 'bg-green-900 text-green-300' | |
| }"> | |
| ${scan.status} | |
| </span> | |
| </td> | |
| </tr> | |
| `).join(''); | |
| } | |
| } | |
| function showNotification(message, type = 'info') { | |
| const notification = document.createElement('div'); | |
| notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 ${ | |
| type === 'success' ? 'bg-green-600' : 'bg-blue-600' | |
| } text-white`; | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| setTimeout(() => { | |
| notification.remove(); | |
| }, 3000); | |
| } | |
| // Initialize tooltips and interactive elements | |
| initializeTooltips(); | |
| function initializeTooltips() { | |
| // Add hover effects to cards | |
| const cards = document.querySelectorAll('.bg-gray-800'); | |
| cards.forEach(card => { | |
| card.addEventListener('mouseenter', function() { | |
| this.classList.add('transform', 'scale-105', 'transition', 'duration-200'); | |
| }); | |
| card.addEventListener('mouseleave', function() { | |
| this.classList.remove('transform', 'scale-105'); | |
| }); | |
| }); | |
| } | |
| // Export des fonctions pour utilisaton globale | |
| window.JanusFraudDetector = { | |
| analyzeCryptoTransactions, | |
| detectOffshoreEntities, | |
| performBilanNormalAnalysis, | |
| performBilanDiversAnalysis, | |
| processExcelFile, | |
| processPDFFile, | |
| extractAmount, | |
| extractAccountNumber, | |
| extractAccountName, | |
| calculateFinancialRatios, | |
| performFraudAnalysis, | |
| performDiversAccountAnalysis | |
| }; | |
| }); | |