"use client"; import type React from "react"; import { useState, useCallback } from "react"; import { useDropzone } from "react-dropzone"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Progress } from "@/components/ui/progress"; import { Upload, FileText, CheckCircle2, AlertCircle, Loader2, } from "lucide-react"; import { parseOFX } from "@/lib/ofx-parser"; import { loadData, addAccount, updateAccount, addTransactions, generateId, autoCategorize, } from "@/lib/store-db"; import type { OFXAccount, Account, Transaction, Folder, BankingData, } from "@/lib/types"; import { cn } from "@/lib/utils"; interface OFXImportDialogProps { children: React.ReactNode; onImportComplete?: () => void; } type ImportStep = "upload" | "configure" | "importing" | "success" | "error"; interface ImportResult { fileName: string; accountName: string; transactionsImported: number; isNew: boolean; error?: string; } export function OFXImportDialog({ children, onImportComplete, }: OFXImportDialogProps) { const [open, setOpen] = useState(false); const [step, setStep] = useState("upload"); // Single file mode const [parsedData, setParsedData] = useState(null); const [accountName, setAccountName] = useState(""); const [selectedFolder, setSelectedFolder] = useState("folder-root"); const [folders, setFolders] = useState([]); const [existingAccountId, setExistingAccountId] = useState( null, ); // Multi-file mode const [importResults, setImportResults] = useState([]); const [importProgress, setImportProgress] = useState(0); const [totalFiles, setTotalFiles] = useState(0); const [error, setError] = useState(null); // Import a single OFX file directly (for multi-file mode) const importOFXDirect = async ( parsed: OFXAccount, fileName: string, data: BankingData, ): Promise => { try { // Check if account already exists const existing = data.accounts.find( (a) => a.accountNumber === parsed.accountId && a.bankId === parsed.bankId, ); let accountId: string; let accountName: string; let isNew = false; if (existing) { accountId = existing.id; accountName = existing.name; await updateAccount({ ...existing, balance: parsed.balance, lastImport: new Date().toISOString(), }); } else { isNew = true; accountName = `${parsed.accountId}`; const newAccount = await addAccount({ name: accountName, bankId: parsed.bankId, accountNumber: parsed.accountId, type: parsed.accountType as Account["type"], folderId: "folder-root", balance: parsed.balance, currency: parsed.currency, lastImport: new Date().toISOString(), externalUrl: null, }); accountId = newAccount.id; } // Add transactions with auto-categorization const existingFitIds = new Set( data.transactions .filter((t) => t.accountId === accountId) .map((t) => t.fitId), ); const newTransactions: Transaction[] = parsed.transactions .filter((t) => !existingFitIds.has(t.fitId)) .map((t) => ({ id: generateId(), accountId, date: t.date, amount: t.amount, description: t.name, type: t.amount >= 0 ? "CREDIT" : "DEBIT", categoryId: autoCategorize( t.name + " " + (t.memo || ""), data.categories, ), isReconciled: false, fitId: t.fitId, memo: t.memo, checkNum: t.checkNum, })); if (newTransactions.length > 0) { await addTransactions(newTransactions); } return { fileName, accountName, transactionsImported: newTransactions.length, isNew, }; } catch (err) { return { fileName, accountName: "Erreur", transactionsImported: 0, isNew: false, error: err instanceof Error ? err.message : "Erreur inconnue", }; } }; const onDrop = useCallback( async (acceptedFiles: File[]) => { if (acceptedFiles.length === 0) return; // Multi-file mode: import directly if (acceptedFiles.length > 1) { setStep("importing"); setTotalFiles(acceptedFiles.length); setImportProgress(0); setImportResults([]); const data = await loadData(); const results: ImportResult[] = []; for (let i = 0; i < acceptedFiles.length; i++) { const file = acceptedFiles[i]; const content = await file.text(); const parsed = parseOFX(content); if (parsed) { // Reload data after each import to get updated accounts/transactions const freshData = i === 0 ? data : await loadData(); const result = await importOFXDirect(parsed, file.name, freshData); results.push(result); } else { results.push({ fileName: file.name, accountName: "Erreur", transactionsImported: 0, isNew: false, error: "Format OFX invalide", }); } setImportProgress(((i + 1) / acceptedFiles.length) * 100); setImportResults([...results]); } setStep("success"); onImportComplete?.(); return; } // Single file mode: show configuration const file = acceptedFiles[0]; const content = await file.text(); const parsed = parseOFX(content); if (parsed) { setParsedData(parsed); setAccountName(`Compte ${parsed.accountId}`); try { const data = await loadData(); setFolders(data.folders); const existing = data.accounts.find( (a) => a.accountNumber === parsed.accountId && a.bankId === parsed.bankId, ); if (existing) { setExistingAccountId(existing.id); setAccountName(existing.name); setSelectedFolder(existing.folderId || "folder-root"); } setStep("configure"); } catch (err) { console.error("Error loading data:", err); setError("Erreur lors du chargement des données"); setStep("error"); } } else { setError("Impossible de lire le fichier OFX. Vérifiez le format."); setStep("error"); } }, [onImportComplete], ); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { "application/x-ofx": [".ofx"], "application/vnd.intu.qfx": [".qfx"], "text/plain": [".ofx", ".qfx"], }, // No maxFiles limit - accept multiple files }); const handleImport = async () => { if (!parsedData) return; try { setStep("importing"); const data = await loadData(); let accountId: string; if (existingAccountId) { accountId = existingAccountId; const existingAccount = data.accounts.find( (a) => a.id === existingAccountId, ); if (existingAccount) { await updateAccount({ ...existingAccount, name: accountName, folderId: selectedFolder, balance: parsedData.balance, lastImport: new Date().toISOString(), }); } } else { const newAccount = await addAccount({ name: accountName, bankId: parsedData.bankId, accountNumber: parsedData.accountId, type: parsedData.accountType as Account["type"], folderId: selectedFolder, balance: parsedData.balance, currency: parsedData.currency, lastImport: new Date().toISOString(), externalUrl: null, }); accountId = newAccount.id; } const existingFitIds = new Set( data.transactions .filter((t) => t.accountId === accountId) .map((t) => t.fitId), ); const newTransactions: Transaction[] = parsedData.transactions .filter((t) => !existingFitIds.has(t.fitId)) .map((t) => ({ id: generateId(), accountId, date: t.date, amount: t.amount, description: t.name, type: t.amount >= 0 ? "CREDIT" : "DEBIT", categoryId: autoCategorize( t.name + " " + (t.memo || ""), data.categories, ), isReconciled: false, fitId: t.fitId, memo: t.memo, checkNum: t.checkNum, })); if (newTransactions.length > 0) { await addTransactions(newTransactions); } setImportResults([ { fileName: "Import", accountName, transactionsImported: newTransactions.length, isNew: !existingAccountId, }, ]); setStep("success"); onImportComplete?.(); } catch (err) { console.error("Error importing:", err); setError("Erreur lors de l'import"); setStep("error"); } }; const handleClose = () => { setOpen(false); setTimeout(() => { setStep("upload"); setParsedData(null); setAccountName(""); setSelectedFolder("folder-root"); setExistingAccountId(null); setError(null); setImportResults([]); setImportProgress(0); setTotalFiles(0); }, 200); }; const totalTransactions = importResults.reduce( (sum, r) => sum + r.transactionsImported, 0, ); const successCount = importResults.filter((r) => !r.error).length; const errorCount = importResults.filter((r) => r.error).length; return ( { if (!o) handleClose(); else setOpen(true); }} > {children} {step === "upload" && "Importer des fichiers OFX"} {step === "configure" && "Configurer le compte"} {step === "importing" && "Import en cours..."} {step === "success" && "Import terminé"} {step === "error" && "Erreur d'import"} {step === "upload" && "Glissez-déposez vos fichiers OFX ou cliquez pour sélectionner"} {step === "configure" && "Vérifiez les informations du compte avant l'import"} {step === "importing" && `Import de ${totalFiles} fichier${totalFiles > 1 ? "s" : ""}...`} {step === "success" && (importResults.length > 1 ? `${successCount} fichier${successCount > 1 ? "s" : ""} importé${successCount > 1 ? "s" : ""}, ${totalTransactions} transactions` : `${totalTransactions} nouvelles transactions importées`)} {step === "error" && error} {step === "upload" && (

{isDragActive ? "Déposez les fichiers ici..." : "Fichiers .ofx ou .qfx acceptés"}

Un fichier = configuration manuelle • Plusieurs fichiers = import direct

)} {step === "configure" && parsedData && (

{parsedData.transactions.length} transactions

Solde:{" "} {new Intl.NumberFormat("fr-FR", { style: "currency", currency: parsedData.currency, }).format(parsedData.balance)}

setAccountName(e.target.value)} placeholder="Ex: Compte courant BNP" />
{existingAccountId && (

Ce compte existe déjà. Les nouvelles transactions seront ajoutées.

)}
)} {step === "importing" && (
Import en cours...
{totalFiles > 1 && ( )} {importResults.length > 0 && (
{importResults.map((result, i) => (
{result.error ? ( ) : ( )} {result.fileName} {!result.error && ( +{result.transactionsImported} )}
))}
)}
)} {step === "success" && (
{importResults.length > 1 && (
{importResults.map((result, i) => (
{result.error ? ( ) : ( )}

{result.accountName}

{result.fileName}

{result.error ? ( {result.error} ) : ( {result.isNew ? "Nouveau" : "Mis à jour"} • + {result.transactionsImported} )}
))}
)} {errorCount > 0 && (

{errorCount} fichier{errorCount > 1 ? "s" : ""} en erreur

)}
)} {step === "error" && (
)}
); }