diff --git a/client/src/pages/History.tsx b/client/src/pages/History.tsx index 0265866..a90b0a6 100644 --- a/client/src/pages/History.tsx +++ b/client/src/pages/History.tsx @@ -1,6 +1,7 @@ -import { useState, useMemo } from "react"; +import { useMemo, useState } from "react"; import DashboardLayout from "@/components/DashboardLayout"; -import { Card, CardContent } from "@/components/ui/card"; +import { useAuth } from "@/_core/hooks/useAuth"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, TableBody, @@ -12,20 +13,24 @@ import { import { Badge } from "@/components/ui/badge"; import { trpc } from "@/lib/trpc"; import { - History as HistoryIcon, - FileText, - Trash2, - Eye, - CheckCircle2, - XCircle, AlertTriangle, CalendarDays, - RotateCcw, - Package, - X, - Upload, + CheckCircle2, + Clock3, + Eye, + FileText, FolderOpen, + History as HistoryIcon, + Loader2, Mail, + Package, + Play, + RotateCcw, + Trash2, + Upload, + UserRound, + X, + XCircle, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; @@ -55,6 +60,39 @@ import { } from "@/components/ui/select"; type StatusFilter = "all" | "imported" | "not_imported" | "duplicates" | "errors"; +type SourceFilter = "all" | "email" | "folder" | "file"; +type TriggerFilter = "all" | "manual" | "automatic" | "unknown"; + +type DetailedImportLog = { + id: number; + userId: number; + sourceFileId: number; + fileName: string; + totalInvoicesDetected: number; + invoicesImported: number; + duplicatesIgnored: number; + errors: number; + duplicateDetails: string | null; + errorDetails: string | null; + warningMessage: string | null; + importSource: "file" | "folder" | "email"; + importTrigger: "manual" | "automatic" | "unknown"; + importedAt: Date; + userName: string | null; + userEmail: string | null; + sourceCreatedAt: Date | null; + sourceStatus: "processing" | "completed" | "error" | null; +}; + +type EmailAccount = { + userId: number; + userName: string | null; + userEmail: string; + emailAddress: string | null; + authMode: "basic" | "oauth2"; + automaticEnabled: number; + isConfigured: number; +}; const MONTHS = [ { value: "1", label: "Janvier" }, @@ -71,111 +109,149 @@ const MONTHS = [ { value: "12", label: "Décembre" }, ]; -const YEARS = Array.from({ length: 5 }, (_, i) => String(new Date().getFullYear() - i)); +const YEARS = Array.from({ length: 5 }, (_, index) => String(new Date().getFullYear() - index)); + +function formatAccount(account: Pick) { + const owner = account.userName?.trim() || account.userEmail; + return account.emailAddress ? `${owner} — ${account.emailAddress}` : owner; +} + +function SourceBadge({ source }: { source: DetailedImportLog["importSource"] }) { + if (source === "email") { + return E-mail; + } + if (source === "folder") { + return Dossier; + } + return Fichier; +} + +function TriggerBadge({ trigger }: { trigger: DetailedImportLog["importTrigger"] }) { + if (trigger === "manual") { + return Manuel; + } + if (trigger === "automatic") { + return Planifié; + } + return Non tracé; +} export default function History() { + const { user } = useAuth(); + const isAdmin = user?.role === "admin"; const { data: logs, isLoading } = trpc.importLogs.getByUser.useQuery(); + const { data: emailAccounts, isLoading: accountsLoading } = trpc.emailImportService.getConfiguredAccounts.useQuery(undefined, { + enabled: isAdmin, + staleTime: 30_000, + }); const deleteAllMutation = trpc.importLogs.deleteAll.useMutation(); + const checkAccountNowMutation = trpc.emailImportService.checkAccountNow.useMutation(); const utils = trpc.useUtils(); - // Filtres + const detailedLogs = (logs ?? []) as DetailedImportLog[]; + const accounts = (emailAccounts ?? []) as EmailAccount[]; + const configuredAccounts = useMemo(() => accounts.filter((account) => account.isConfigured === 1), [accounts]); + const [statusFilter, setStatusFilter] = useState("all"); + const [sourceFilter, setSourceFilter] = useState("all"); + const [triggerFilter, setTriggerFilter] = useState("all"); const [yearFilter, setYearFilter] = useState(String(new Date().getFullYear())); const [monthFilter, setMonthFilter] = useState("all"); - - // Dialog visualisation détails const [detailDialogOpen, setDetailDialogOpen] = useState(false); - const [selectedLog, setSelectedLog] = useState<(typeof logs extends (infer T)[] | undefined ? T : never) | null>(null); + const [selectedLog, setSelectedLog] = useState(null); + const [selectedAccountId, setSelectedAccountId] = useState("none"); + const [confirmManualCheckOpen, setConfirmManualCheckOpen] = useState(false); + + const selectedAccount = useMemo( + () => configuredAccounts.find((account) => String(account.userId) === selectedAccountId), + [configuredAccounts, selectedAccountId], + ); const handleDeleteAll = async () => { try { await deleteAllMutation.mutateAsync(); await utils.importLogs.getByUser.invalidate(); - toast.success("Logs supprimés", { - description: "Tous les logs d'import ont été supprimés avec succès.", - }); + toast.success("Historique supprimé", { description: "Les enregistrements visibles pour votre rôle ont été supprimés." }); } catch { - toast.error("Erreur", { - description: "Impossible de supprimer les logs d'import.", - }); + toast.error("Impossible de supprimer l’historique des imports."); } }; - // Filtrage par période - const periodFiltered = useMemo(() => { - if (!logs) return []; - return logs.filter((log) => { - const d = new Date(log.importedAt); - if (yearFilter !== "all" && d.getFullYear() !== Number(yearFilter)) return false; - if (monthFilter !== "all" && (d.getMonth() + 1) !== Number(monthFilter)) return false; - return true; - }); - }, [logs, yearFilter, monthFilter]); + const handleManualEmailCheck = async () => { + if (!selectedAccount) return; + try { + toast.info("Lecture ponctuelle de la boîte e-mail en cours…"); + const result = await checkAccountNowMutation.mutateAsync({ userId: selectedAccount.userId }); + if (!result.success) { + toast.error(result.message); + return; + } + await utils.importLogs.getByUser.invalidate(); + toast.success("Lecture e-mail terminée", { description: result.message }); + } catch (error: any) { + toast.error(error?.message || "Impossible de vérifier cette boîte e-mail."); + } finally { + setConfirmManualCheckOpen(false); + } + }; + + const periodFiltered = useMemo(() => detailedLogs.filter((log) => { + const date = new Date(log.importedAt); + if (yearFilter !== "all" && date.getFullYear() !== Number(yearFilter)) return false; + if (monthFilter !== "all" && date.getMonth() + 1 !== Number(monthFilter)) return false; + return true; + }), [detailedLogs, yearFilter, monthFilter]); - // Compteurs sur la période filtrée const counts = useMemo(() => { - const totalImported = periodFiltered.reduce((s, l) => s + l.invoicesImported, 0); - const totalDuplicates = periodFiltered.reduce((s, l) => s + l.duplicatesIgnored, 0); - const totalErrors = periodFiltered.reduce((s, l) => s + l.errors, 0); - const totalDetected = periodFiltered.reduce((s, l) => s + l.totalInvoicesDetected, 0); - const totalNotImported = totalDuplicates + totalErrors; - return { totalImported, totalDuplicates, totalErrors, totalDetected, totalNotImported }; + const totalImported = periodFiltered.reduce((sum, log) => sum + log.invoicesImported, 0); + const totalDuplicates = periodFiltered.reduce((sum, log) => sum + log.duplicatesIgnored, 0); + const totalErrors = periodFiltered.reduce((sum, log) => sum + log.errors, 0); + const totalDetected = periodFiltered.reduce((sum, log) => sum + log.totalInvoicesDetected, 0); + return { totalImported, totalDuplicates, totalErrors, totalDetected, totalNotImported: totalDuplicates + totalErrors }; }, [periodFiltered]); - // Filtrage par statut - const filteredLogs = useMemo(() => { - return periodFiltered.filter((log) => { - if (statusFilter === "all") return true; - if (statusFilter === "imported") return log.invoicesImported > 0; - if (statusFilter === "not_imported") return log.duplicatesIgnored > 0 || log.errors > 0; - if (statusFilter === "duplicates") return log.duplicatesIgnored > 0; - if (statusFilter === "errors") return log.errors > 0; - return true; - }); - }, [periodFiltered, statusFilter]); + const filteredLogs = useMemo(() => periodFiltered.filter((log) => { + if (sourceFilter !== "all" && log.importSource !== sourceFilter) return false; + if (triggerFilter !== "all" && log.importTrigger !== triggerFilter) return false; + if (statusFilter === "imported") return log.invoicesImported > 0; + if (statusFilter === "not_imported") return log.duplicatesIgnored > 0 || log.errors > 0; + if (statusFilter === "duplicates") return log.duplicatesIgnored > 0; + if (statusFilter === "errors") return log.errors > 0; + return true; + }), [periodFiltered, sourceFilter, statusFilter, triggerFilter]); - const hasPeriodFilter = yearFilter !== "all" || monthFilter !== "all"; - - const filterButtons: { key: StatusFilter; label: string; count: number; color: string; activeColor: string }[] = [ - { key: "all", label: "Tous", count: periodFiltered.length, color: "bg-gray-100 text-gray-700 hover:bg-gray-200", activeColor: "bg-gray-700 text-white" }, - { key: "imported", label: "Importées", count: counts.totalImported, color: "bg-green-50 text-green-700 hover:bg-green-100 border border-green-200", activeColor: "bg-green-600 text-white" }, - { key: "not_imported", label: "Non importées", count: counts.totalNotImported, color: "bg-orange-50 text-orange-700 hover:bg-orange-100 border border-orange-200", activeColor: "bg-orange-500 text-white" }, - { key: "duplicates", label: "Doublons", count: counts.totalDuplicates, color: "bg-yellow-50 text-yellow-700 hover:bg-yellow-100 border border-yellow-200", activeColor: "bg-yellow-500 text-white" }, - { key: "errors", label: "Erreurs", count: counts.totalErrors, color: "bg-red-50 text-red-700 hover:bg-red-100 border border-red-200", activeColor: "bg-red-600 text-white" }, + const statusButtons: { key: StatusFilter; label: string; count: number; base: string; active: string }[] = [ + { key: "all", label: "Tous", count: periodFiltered.length, base: "bg-slate-100 text-slate-700 hover:bg-slate-200", active: "bg-slate-700 text-white" }, + { key: "imported", label: "Importées", count: counts.totalImported, base: "border border-green-200 bg-green-50 text-green-700 hover:bg-green-100", active: "bg-green-600 text-white" }, + { key: "not_imported", label: "Non importées", count: counts.totalNotImported, base: "border border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100", active: "bg-orange-500 text-white" }, + { key: "duplicates", label: "Doublons", count: counts.totalDuplicates, base: "border border-yellow-200 bg-yellow-50 text-yellow-700 hover:bg-yellow-100", active: "bg-yellow-500 text-white" }, + { key: "errors", label: "Erreurs", count: counts.totalErrors, base: "border border-red-200 bg-red-50 text-red-700 hover:bg-red-100", active: "bg-red-600 text-white" }, ]; + const hasFilters = yearFilter !== "all" || monthFilter !== "all" || sourceFilter !== "all" || triggerFilter !== "all"; + return (
- {/* En-tête */} -
+

Historique des imports

-

Consultez l'historique de tous vos imports de factures

+

Suivez la date, la source, le compte et le mode de déclenchement de chaque ajout.

- {logs && logs.length > 0 && ( + {detailedLogs.length > 0 && ( - + - Êtes-vous sûr ? - - Cette action supprimera définitivement tous les logs d'import. Cette opération est irréversible. - + Supprimer l’historique ? + Cette action est irréversible et supprime les enregistrements accessibles avec votre rôle. Annuler - - {deleteAllMutation.isPending ? "Suppression..." : "Supprimer"} + + {deleteAllMutation.isPending ? "Suppression…" : "Supprimer"} @@ -183,340 +259,108 @@ export default function History() { )}
- {/* Compteurs */} -
- - - -
-

{counts.totalImported}

-

Factures importées

-
-
-
- - - -
-

{counts.totalNotImported}

-

Non importées

-
-
-
- - - -
-

{counts.totalDuplicates}

-

Doublons ignorés

-
-
-
- - - -
-

{counts.totalErrors}

-

Erreurs

+ {isAdmin && ( + + +
+
+
+
+ Lecture e-mail ponctuelle + Déclenchez une seule vérification pour une boîte configurée, sans réactiver le traitement automatique. +
+
+ Aucune planification créée
+
+ +
+ + +
+ + + + + Lancer une lecture e-mail ponctuelle ? + + La boîte « {selectedAccount ? formatAccount(selectedAccount) : ""} » sera vérifiée une fois. Les nouveaux PDF pourront être analysés, mais l’import automatique restera désactivé. + + + + Annuler + + {checkAccountNowMutation.isPending ? "Vérification…" : "Lancer la vérification"} + + + +
+ )} + +
+

{counts.totalImported}

Factures importées

+

{counts.totalNotImported}

Non importées

+

{counts.totalDuplicates}

Doublons ignorés

+

{counts.totalErrors}

Erreurs

- {/* Filtres */} -
- {/* Filtres statut */} +
- {filterButtons.map((btn) => ( - - ))} + {statusButtons.map((button) => )}
- - {/* Séparateur */} -
- - {/* Filtre Période */} -
+
+
- - - {hasPeriodFilter && ( - - )} + + + + + {hasFilters && }
+ {filteredLogs.length} import{filteredLogs.length > 1 ? "s" : ""} · {counts.totalDetected} facture{counts.totalDetected > 1 ? "s" : ""} détectée{counts.totalDetected > 1 ? "s" : ""} + - {/* Résumé */} - - {filteredLogs.length} import{filteredLogs.length > 1 ? "s" : ""} — {counts.totalDetected} facture{counts.totalDetected > 1 ? "s" : ""} détectée{counts.totalDetected > 1 ? "s" : ""} - -
- - {/* Tableau */} - - - {isLoading ? ( -
- -

Chargement...

-
- ) : filteredLogs.length > 0 ? ( - - - - Fichier importé - Date d'import - Source - Détectées - Importées - Doublons - Erreurs - Détails - - - - {filteredLogs.map((log) => ( - - -
- - {log.fileName} -
-
- - {new Date(log.importedAt).toLocaleString("fr-FR")} - - - {log.importSource === "email" ? ( - - - Email - - ) : log.importSource === "folder" ? ( - - - Dossier - - ) : ( - - - Fichier - - )} - - - - {log.totalInvoicesDetected} - - - - {log.invoicesImported > 0 ? ( - - {log.invoicesImported} - - ) : ( - 0 - )} - - - {log.duplicatesIgnored > 0 ? ( - - {log.duplicatesIgnored} - - ) : ( - - - )} - - - {log.errors > 0 ? ( - - {log.errors} - - ) : ( - - - )} - - - - -
- ))} -
-
- ) : ( -
- -

Aucun import trouvé

-

Modifiez les filtres pour afficher plus de résultats

-
- )} -
-
+ + {isLoading ?

Chargement de l’historique…

: filteredLogs.length > 0 ? ( + Fichier importéDate et heureCompteSourceModeRésultatDétails + {filteredLogs.map((log) =>
{log.fileName}
{new Date(log.importedAt).toLocaleString("fr-FR")}
{log.userName?.trim() || log.userEmail || "Compte supprimé"}
+{log.invoicesImported}{log.duplicatesIgnored > 0 && / {log.duplicatesIgnored} d.}{log.errors > 0 && / {log.errors} e.}
)}
+
+ ) :

Aucun import trouvé

Modifiez les filtres pour afficher plus de résultats.

} +
- {/* Dialog visualisation détails */} - - - - - - Détails de l'import - - - {selectedLog && ( -
- {/* Infos générales */} -
-
-

Fichier

-

{selectedLog.fileName}

-
-
-

Date d'import

-

{new Date(selectedLog.importedAt).toLocaleString("fr-FR")}

-
-
-

Source import

-

- {selectedLog.importSource === "email" ? ( - Email - ) : selectedLog.importSource === "folder" ? ( - Dossier - ) : ( - Fichier - )} -

-
-
- - {/* Statistiques */} -
-
-

{selectedLog.totalInvoicesDetected}

-

Détectées

-
-
-

{selectedLog.invoicesImported}

-

Importées

-
-
-

{selectedLog.duplicatesIgnored}

-

Doublons

-
-
-

{selectedLog.errors}

-

Erreurs

-
-
- - {/* Détails doublons */} - {selectedLog.duplicateDetails && (() => { - try { - const details = JSON.parse(selectedLog.duplicateDetails); - if (Array.isArray(details) && details.length > 0) { - return ( -
-

- - Doublons ignorés ({details.length}) -

-
- {details.map((d: string | { supplier?: string; invoiceNumber?: string; reason?: string }, i: number) => ( -
- {typeof d === "string" ? d : `${d.supplier || ""} — ${d.invoiceNumber || ""} ${d.reason ? `(${d.reason})` : ""}`} -
- ))} -
-
- ); - } - } catch { /* JSON invalide */ } - return null; - })()} - - {/* Détails erreurs */} - {selectedLog.errorDetails && (() => { - try { - const details = JSON.parse(selectedLog.errorDetails); - if (Array.isArray(details) && details.length > 0) { - return ( -
-

- - Erreurs ({details.length}) -

-
- {details.map((d: string | { message?: string; file?: string }, i: number) => ( -
- {typeof d === "string" ? d : `${d.file ? `[${d.file}] ` : ""}${d.message || ""}`} -
- ))} -
-
- ); - } - } catch { /* JSON invalide */ } - return null; - })()} -
- )} -
-
+ Détails de l’import + {selectedLog &&

Fichier

{selectedLog.fileName}

Date et heure d’import

{new Date(selectedLog.importedAt).toLocaleString("fr-FR")}

Compte responsable

{selectedLog.userName?.trim() || selectedLog.userEmail || "Compte supprimé"}

{selectedLog.userName && selectedLog.userEmail &&

{selectedLog.userEmail}

}

Origine

Fichier source

ID {selectedLog.sourceFileId} · {selectedLog.sourceStatus || "indisponible"}

{selectedLog.sourceCreatedAt &&

Créé le {new Date(selectedLog.sourceCreatedAt).toLocaleString("fr-FR")}

}
+

{selectedLog.totalInvoicesDetected}

Détectées

{selectedLog.invoicesImported}

Importées

{selectedLog.duplicatesIgnored}

Doublons

{selectedLog.errors}

Erreurs

+ {selectedLog.warningMessage &&
Avertissement : {selectedLog.warningMessage}
} + {selectedLog.duplicateDetails && } />} + {selectedLog.errorDetails && } />} +
} +
); } + +function DetailsList({ title, tone, value, icon }: { title: string; tone: "yellow" | "red"; value: string; icon: React.ReactNode }) { + try { + const details = JSON.parse(value); + if (!Array.isArray(details) || details.length === 0) return null; + const styles = tone === "yellow" ? "border-yellow-100 bg-yellow-50 text-yellow-800" : "border-red-100 bg-red-50 text-red-700"; + const heading = tone === "yellow" ? "text-yellow-700" : "text-red-600"; + return

{icon}{title} ({details.length})

{details.map((detail: string | Record, index: number) =>
{typeof detail === "string" ? detail : Object.values(detail).filter(Boolean).join(" — ")}
)}
; + } catch { + return null; + } +} diff --git a/drizzle/0038_late_thunderbolt.sql b/drizzle/0038_late_thunderbolt.sql new file mode 100644 index 0000000..c53e4bc --- /dev/null +++ b/drizzle/0038_late_thunderbolt.sql @@ -0,0 +1 @@ +ALTER TABLE `importLogs` ADD `importTrigger` enum('manual','automatic','unknown') DEFAULT 'unknown' NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0038_snapshot.json b/drizzle/meta/0038_snapshot.json new file mode 100644 index 0000000..3450d2d --- /dev/null +++ b/drizzle/meta/0038_snapshot.json @@ -0,0 +1,2380 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "be9b9ca2-8898-431d-9945-887bff1f16bb", + "prevId": "d0ab0659-6a58-45a5-9da2-a3bc5538bb3c", + "tables": { + "accountingAllocationList": { + "name": "accountingAllocationList", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "user_allocation_unique": { + "name": "user_allocation_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "accountingAllocationList_id": { + "name": "accountingAllocationList_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automationRules": { + "name": "automationRules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "isActive": { + "name": "isActive", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "priority": { + "name": "priority", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "conditions": { + "name": "conditions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conditionsLogic": { + "name": "conditionsLogic", + "type": "enum('AND','OR')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AND'" + }, + "actions": { + "name": "actions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "automationRules_id": { + "name": "automationRules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "bapHistory": { + "name": "bapHistory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceDate": { + "name": "invoiceDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeAchat": { + "name": "typeAchat", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "serviceConcerne": { + "name": "serviceConcerne", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ventilationComptable": { + "name": "ventilationComptable", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientName": { + "name": "recipientName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportMode": { + "name": "exportMode", + "type": "enum('browser','folder')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'browser'" + }, + "exportPath": { + "name": "exportPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pdfUrl": { + "name": "pdfUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureName": { + "name": "signatureName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadStatus": { + "name": "sharepointUploadStatus", + "type": "enum('success','error','skipped')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadPath": { + "name": "sharepointUploadPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadError": { + "name": "sharepointUploadError", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "validatedAt": { + "name": "validatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "bapHistory_id": { + "name": "bapHistory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "deletedInvoices": { + "name": "deletedInvoices", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "deletedInvoices_id": { + "name": "deletedInvoices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "departmentList": { + "name": "departmentList", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "user_department_unique": { + "name": "user_department_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "departmentList_id": { + "name": "departmentList_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "freeproImports": { + "name": "freeproImports", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "moisLabel": { + "name": "moisLabel", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "annee": { + "name": "annee", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mois": { + "name": "mois", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refPiece": { + "name": "refPiece", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nbLignes": { + "name": "nbLignes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "totalTtc": { + "name": "totalTtc", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadStatus": { + "name": "sharepointUploadStatus", + "type": "enum('success','error')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadPath": { + "name": "sharepointUploadPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadError": { + "name": "sharepointUploadError", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointExportedAt": { + "name": "sharepointExportedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "freeproImports_id": { + "name": "freeproImports_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "freeproSettings": { + "name": "freeproSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "portalUrl": { + "name": "portalUrl", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'https://pro.free.fr'" + }, + "loginEmail": { + "name": "loginEmail", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginPassword": { + "name": "loginPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frequency": { + "name": "frequency", + "type": "enum('manual','daily','weekly','monthly')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "maxAnteriority": { + "name": "maxAnteriority", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoEnabled": { + "name": "autoEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lastSuccessAt": { + "name": "lastSuccessAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastStatus": { + "name": "lastStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastImportCount": { + "name": "lastImportCount", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "freeproSettings_id": { + "name": "freeproSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "freeproSettings_userId_unique": { + "name": "freeproSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "freeproVentilationLines": { + "name": "freeproVentilationLines", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "importId": { + "name": "importId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "structure": { + "name": "structure", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "montantCentimes": { + "name": "montantCentimes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "freeproVentilationLines_id": { + "name": "freeproVentilationLines_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "importLogs": { + "name": "importLogs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalInvoicesDetected": { + "name": "totalInvoicesDetected", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "invoicesImported": { + "name": "invoicesImported", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "duplicatesIgnored": { + "name": "duplicatesIgnored", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "errors": { + "name": "errors", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "duplicateDetails": { + "name": "duplicateDetails", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "errorDetails": { + "name": "errorDetails", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "warningMessage": { + "name": "warningMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "importSource": { + "name": "importSource", + "type": "enum('file','folder','email')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'file'" + }, + "importTrigger": { + "name": "importTrigger", + "type": "enum('manual','automatic','unknown')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "importedAt": { + "name": "importedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "importLogs_id": { + "name": "importLogs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "importSettings": { + "name": "importSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manualImportEnabled": { + "name": "manualImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "autoImportEnabled": { + "name": "autoImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "autoImportSourcePath": { + "name": "autoImportSourcePath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoImportFrequency": { + "name": "autoImportFrequency", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 60 + }, + "emailImportEnabled": { + "name": "emailImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "emailImportAddress": { + "name": "emailImportAddress", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportPassword": { + "name": "emailImportPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportHost": { + "name": "emailImportHost", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportPort": { + "name": "emailImportPort", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 993 + }, + "emailImportFrequency": { + "name": "emailImportFrequency", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "emailImportSinceDate": { + "name": "emailImportSinceDate", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportAuthMode": { + "name": "emailImportAuthMode", + "type": "enum('basic','oauth2')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'basic'" + }, + "exportFolder": { + "name": "exportFolder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportFolderType": { + "name": "exportFolderType", + "type": "enum('local','teams','sharepoint')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'local'" + }, + "bapExportMode": { + "name": "bapExportMode", + "type": "enum('browser','folder','both')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'browser'" + }, + "azureTenantId": { + "name": "azureTenantId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureClientId": { + "name": "azureClientId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureClientSecret": { + "name": "azureClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureSecretExpiresAt": { + "name": "azureSecretExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aiProvider": { + "name": "aiProvider", + "type": "enum('mistral','manus','gemini')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral'" + }, + "mistralApiKey": { + "name": "mistralApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiKey": { + "name": "manusForgeApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiUrl": { + "name": "manusForgeApiUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "geminiApiKey": { + "name": "geminiApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "importSettings_id": { + "name": "importSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "importSettings_userId_unique": { + "name": "importSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "invoiceLearnings": { + "name": "invoiceLearnings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierKey": { + "name": "supplierKey", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fieldName": { + "name": "fieldName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "originalValue": { + "name": "originalValue", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "correctedValue": { + "name": "correctedValue", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applyCount": { + "name": "applyCount", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invoiceLearnings_id": { + "name": "invoiceLearnings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invoices": { + "name": "invoices", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceIndexInFile": { + "name": "invoiceIndexInFile", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileKey": { + "name": "fileKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileUrl": { + "name": "fileUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceDate": { + "name": "invoiceDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deliveryNoteNumber": { + "name": "deliveryNoteNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "orderNumber": { + "name": "orderNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "decimal(10,2)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientName": { + "name": "recipientName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pageRange": { + "name": "pageRange", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qualityScore": { + "name": "qualityScore", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadataFileKey": { + "name": "metadataFileKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadataFileUrl": { + "name": "metadataFileUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('processing','completed','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'processing'" + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportStatus": { + "name": "exportStatus", + "type": "enum('not_exported','exported','export_error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'not_exported'" + }, + "manuallyEdited": { + "name": "manuallyEdited", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "serviceConcerne": { + "name": "serviceConcerne", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeAchat": { + "name": "typeAchat", + "type": "enum('CAPEX','OPEX')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ventilationComptable": { + "name": "ventilationComptable", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoFilledFields": { + "name": "autoFilledFields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extractedText": { + "name": "extractedText", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "isSubscription": { + "name": "isSubscription", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "bapValidated": { + "name": "bapValidated", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "bapValidatedAt": { + "name": "bapValidatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportedAt": { + "name": "exportedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportMode": { + "name": "exportMode", + "type": "enum('manual','automatic')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "supplier_invoice_date_unique": { + "name": "supplier_invoice_date_unique", + "columns": [ + "supplierName", + "invoiceNumber", + "invoiceDate" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invoices_id": { + "name": "invoices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llmFieldsConfig": { + "name": "llmFieldsConfig", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fieldName": { + "name": "fieldName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "displayName": { + "name": "displayName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "isRequired": { + "name": "isRequired", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "displayOrder": { + "name": "displayOrder", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llmFieldsConfig_id": { + "name": "llmFieldsConfig_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llmLogs": { + "name": "llmLogs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "operation": { + "name": "operation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "promptSent": { + "name": "promptSent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rawResponse": { + "name": "rawResponse", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanedResponse": { + "name": "cleanedResponse", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "processingTimeMs": { + "name": "processingTimeMs", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pageRange": { + "name": "pageRange", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llmLogs_id": { + "name": "llmLogs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "serviceSignatures": { + "name": "serviceSignatures", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "serviceName": { + "name": "serviceName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signatureId": { + "name": "signatureId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "user_service_unique": { + "name": "user_service_unique", + "columns": [ + "userId", + "serviceName" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "serviceSignatures_id": { + "name": "serviceSignatures_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "signatures": { + "name": "signatures", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "firstName": { + "name": "firstName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastName": { + "name": "lastName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imageKey": { + "name": "imageKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "signatures_id": { + "name": "signatures_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sourceFiles": { + "name": "sourceFiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileKey": { + "name": "fileKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileUrl": { + "name": "fileUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contentHash": { + "name": "contentHash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalInvoicesDetected": { + "name": "totalInvoicesDetected", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "processingStatus": { + "name": "processingStatus", + "type": "enum('processing','completed','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'processing'" + }, + "processingProgress": { + "name": "processingProgress", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "source_file_content_hash_unique": { + "name": "source_file_content_hash_unique", + "columns": [ + "contentHash" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sourceFiles_id": { + "name": "sourceFiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "userSettings": { + "name": "userSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "llmModel": { + "name": "llmModel", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral-large-latest'" + }, + "orderNumberFormat": { + "name": "orderNumberFormat", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumberKeywords": { + "name": "invoiceNumberKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deliveryNoteKeywords": { + "name": "deliveryNoteKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "orderNumberKeywords": { + "name": "orderNumberKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supplierKeywords": { + "name": "supplierKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmountKeywords": { + "name": "totalAmountKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscriptionKeywords": { + "name": "subscriptionKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientKeywords": { + "name": "recipientKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpRecipientFilter": { + "name": "sftpRecipientFilter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpHost": { + "name": "sftpHost", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpPort": { + "name": "sftpPort", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "sftpUsername": { + "name": "sftpUsername", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpPassword": { + "name": "sftpPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpRemotePath": { + "name": "sftpRemotePath", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/'" + }, + "sftpAutoExport": { + "name": "sftpAutoExport", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "llmLogsRetentionMonths": { + "name": "llmLogsRetentionMonths", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "learningConfidenceThreshold": { + "name": "learningConfidenceThreshold", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2 + }, + "aiProvider": { + "name": "aiProvider", + "type": "enum('mistral','manus','gemini')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral'" + }, + "mistralApiKey": { + "name": "mistralApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiKey": { + "name": "manusForgeApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiUrl": { + "name": "manusForgeApiUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "geminiApiKey": { + "name": "geminiApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "userSettings_id": { + "name": "userSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "userSettings_userId_unique": { + "name": "userSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "openId": { + "name": "openId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureAdId": { + "name": "azureAdId", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "passwordHash": { + "name": "passwordHash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginMethod": { + "name": "loginMethod", + "type": "enum('manus','local','azure-ad')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('user','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "isActive": { + "name": "isActive", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "lastSignedIn": { + "name": "lastSignedIn", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_openId_unique": { + "name": "users_openId_unique", + "columns": [ + "openId" + ] + }, + "users_azureAdId_unique": { + "name": "users_azureAdId_unique", + "columns": [ + "azureAdId" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ] + } + }, + "checkConstraint": {} + }, + "webImportSources": { + "name": "webImportSources", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connectorType": { + "name": "connectorType", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "portalUrl": { + "name": "portalUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "loginEmail": { + "name": "loginEmail", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "loginPassword": { + "name": "loginPassword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "frequency": { + "name": "frequency", + "type": "enum('manual','daily','weekly','monthly')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'monthly'" + }, + "autoEnabled": { + "name": "autoEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lastSuccessAt": { + "name": "lastSuccessAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastStatus": { + "name": "lastStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastImportCount": { + "name": "lastImportCount", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "apiToken": { + "name": "apiToken", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "webImportSources_id": { + "name": "webImportSources_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 102f761..9e54473 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -267,6 +267,13 @@ "when": 1787394418464, "tag": "0037_goofy_quentin_quire", "breakpoints": true + }, + { + "idx": 38, + "version": "5", + "when": 1788349114141, + "tag": "0038_late_thunderbolt", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 8a0bfad..81c723c 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -231,6 +231,8 @@ export const importLogs = mysqlTable("importLogs", { warningMessage: text("warningMessage"), // Warning message (e.g. quota exhausted) /** Source du mode d'import : 'file' = upload manuel, 'folder' = dossier automatique, 'email' = import par email */ importSource: mysqlEnum("importSource", ["file", "folder", "email"]).default("file").notNull(), + /** Déclencheur : manuel, planifié automatiquement, ou non tracé pour les historiques antérieurs. */ + importTrigger: mysqlEnum("importTrigger", ["manual", "automatic", "unknown"]).default("unknown").notNull(), importedAt: timestamp("importedAt").defaultNow().notNull(), }); diff --git a/server/db.ts b/server/db.ts index 096af1a..6f135c8 100644 --- a/server/db.ts +++ b/server/db.ts @@ -1,4 +1,4 @@ -import { eq, and, desc, sql, inArray } from "drizzle-orm"; +import { eq, and, desc, sql, inArray, getTableColumns } from "drizzle-orm"; import { drizzle } from "drizzle-orm/mysql2"; import { InsertUser, @@ -538,6 +538,44 @@ export async function getAllImportLogs(): Promise { return db.select().from(importLogs).orderBy(desc(importLogs.importedAt)); } +/** + * Retourne les informations nécessaires à l'audit d'un import sans exposer + * les paramètres sensibles de messagerie. L'administrateur peut ainsi relier + * chaque import à son compte, fichier source et déclencheur. + */ +export async function getImportLogsWithDetailsByUser(userId: number) { + const db = await getDb(); + if (!db) return []; + return db.select({ + ...getTableColumns(importLogs), + userName: users.name, + userEmail: users.email, + sourceCreatedAt: sourceFiles.createdAt, + sourceStatus: sourceFiles.processingStatus, + }) + .from(importLogs) + .leftJoin(users, eq(importLogs.userId, users.id)) + .leftJoin(sourceFiles, eq(importLogs.sourceFileId, sourceFiles.id)) + .where(eq(importLogs.userId, userId)) + .orderBy(desc(importLogs.importedAt)); +} + +export async function getAllImportLogsWithDetails() { + const db = await getDb(); + if (!db) return []; + return db.select({ + ...getTableColumns(importLogs), + userName: users.name, + userEmail: users.email, + sourceCreatedAt: sourceFiles.createdAt, + sourceStatus: sourceFiles.processingStatus, + }) + .from(importLogs) + .leftJoin(users, eq(importLogs.userId, users.id)) + .leftJoin(sourceFiles, eq(importLogs.sourceFileId, sourceFiles.id)) + .orderBy(desc(importLogs.importedAt)); +} + export async function deleteAllImportLogs(userId: number | null): Promise { const db = await getDb(); if (!db) throw new Error("Database not available"); @@ -611,6 +649,24 @@ export async function upsertImportSettings(data: InsertImportSettings): Promise< } } +/** Liste administrative des comptes e-mail configurés, sans secret IMAP ni Azure AD. */ +export async function getEmailImportAccounts() { + const db = await getDb(); + if (!db) return []; + return db.select({ + userId: importSettings.userId, + userName: users.name, + userEmail: users.email, + emailAddress: importSettings.emailImportAddress, + authMode: importSettings.emailImportAuthMode, + automaticEnabled: importSettings.emailImportEnabled, + isConfigured: sql`CASE WHEN ${importSettings.emailImportAddress} IS NOT NULL AND ${importSettings.emailImportAddress} <> '' AND ${importSettings.emailImportHost} IS NOT NULL AND ${importSettings.emailImportHost} <> '' THEN 1 ELSE 0 END`.as("isConfigured"), + }) + .from(importSettings) + .innerJoin(users, eq(importSettings.userId, users.id)) + .orderBy(users.name, users.email); +} + // ============= DEPARTMENT LIST OPERATIONS ============= export async function getDepartmentsByUser(_userId?: number): Promise { diff --git a/server/emailImportService.test.ts b/server/emailImportService.test.ts index bd75a0f..2d151b7 100644 --- a/server/emailImportService.test.ts +++ b/server/emailImportService.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { createImapFlowOptions, type EmailImportConfig } from "./emailImportService"; +import { + createImapFlowOptions, + type EmailImportConfig, + validateEmailImportConfiguration, +} from "./emailImportService"; const baseConfig: EmailImportConfig = { userId: 2, @@ -36,3 +40,39 @@ describe("createImapFlowOptions", () => { expect(options.auth).not.toHaveProperty("accessToken"); }); }); + +describe("validateEmailImportConfiguration", () => { + it("accepte une boîte OAuth2 configurée même lorsque la planification automatique est désactivée", () => { + const error = validateEmailImportConfiguration({ + emailImportEnabled: 0, + emailImportAddress: "compta@example.org", + emailImportPassword: null, + emailImportHost: "outlook.office365.com", + emailImportPort: 993, + emailImportSinceDate: null, + emailImportAuthMode: "oauth2", + azureTenantId: "tenant", + azureClientId: "client", + azureClientSecret: "secret", + }); + + expect(error).toBeNull(); + }); + + it("refuse une boîte dont la configuration IMAP est incomplète", () => { + const error = validateEmailImportConfiguration({ + emailImportEnabled: 0, + emailImportAddress: "compta@example.org", + emailImportPassword: null, + emailImportHost: null, + emailImportPort: 993, + emailImportSinceDate: null, + emailImportAuthMode: "oauth2", + azureTenantId: "tenant", + azureClientId: "client", + azureClientSecret: "secret", + }); + + expect(error).toBe("Configuration IMAP incomplète"); + }); +}); diff --git a/server/emailImportService.ts b/server/emailImportService.ts index 8f03a6a..c6f51da 100644 --- a/server/emailImportService.ts +++ b/server/emailImportService.ts @@ -32,6 +32,40 @@ export interface EmailImportConfig { azureClientSecret?: string; } +export type EmailImportTrigger = "manual" | "automatic"; + +type EmailImportSettingsSnapshot = { + emailImportEnabled: number; + emailImportAddress: string | null; + emailImportPassword: string | null; + emailImportHost: string | null; + emailImportPort: number | null; + emailImportSinceDate: number | null; + emailImportAuthMode: "basic" | "oauth2"; + azureTenantId: string | null; + azureClientId: string | null; + azureClientSecret: string | null; +}; + +/** Vérifie une configuration IMAP sans tenir compte de l'activation de la planification. */ +export function validateEmailImportConfiguration( + settings: EmailImportSettingsSnapshot | null | undefined, +): string | null { + if (!settings || !settings.emailImportAddress || !settings.emailImportHost) { + return "Configuration IMAP incomplète"; + } + if (settings.emailImportAuthMode === "basic" && !settings.emailImportPassword) { + return "Mot de passe IMAP manquant"; + } + if ( + settings.emailImportAuthMode === "oauth2" && + (!settings.azureTenantId || !settings.azureClientId || !settings.azureClientSecret) + ) { + return "Configuration OAuth2 IMAP incomplète"; + } + return null; +} + /** * Construit les options ImapFlow sans effectuer d'appel réseau. * ImapFlow reçoit le jeton brut et construit lui-même SASL XOAUTH2. @@ -67,14 +101,17 @@ const activeIntervals = new Map(); // qu'un second cycle IMAP traite les mêmes messages avant la fin du premier. const activeChecks = new Map>(); -function runEmailCheckExclusive(config: EmailImportConfig): Promise { +function runEmailCheckExclusive( + config: EmailImportConfig, + trigger: EmailImportTrigger = "automatic", +): Promise { const runningCheck = activeChecks.get(config.userId); if (runningCheck) { console.log(`[EmailImport] Vérification déjà en cours pour user ${config.userId}, cycle ignoré`); return runningCheck; } - const check = checkEmailsForPDFs(config).finally(() => { + const check = checkEmailsForPDFs(config, trigger).finally(() => { if (activeChecks.get(config.userId) === check) activeChecks.delete(config.userId); }); activeChecks.set(config.userId, check); @@ -88,7 +125,8 @@ function runEmailCheckExclusive(config: EmailImportConfig): Promise { async function processEmailAttachment( userId: number, attachment: Attachment, - emailSubject: string + emailSubject: string, + trigger: EmailImportTrigger, ): Promise<{ success: boolean; totalInvoices: number; imported: number; duplicates: number; errors: number; quotaError?: boolean }> { const fileName = attachment.filename || `email-attachment-${Date.now()}.pdf`; console.log(`[EmailImport] Processing attachment: ${fileName} from email: ${emailSubject}`); @@ -323,6 +361,7 @@ async function processEmailAttachment( errorDetails: errorDetails.length > 0 ? JSON.stringify(errorDetails) : null, warningMessage, importSource: "email", + importTrigger: trigger, }); console.log(`[EmailImport] Successfully processed attachment: ${fileName}`); @@ -386,7 +425,10 @@ async function buildImapConfig(config: EmailImportConfig): Promise { +async function checkEmailsForPDFs( + config: EmailImportConfig, + trigger: EmailImportTrigger, +): Promise { const imapConfig = await buildImapConfig(config); const client = new ImapFlow(imapConfig); client.on("error", (error) => { @@ -450,6 +492,7 @@ async function checkEmailsForPDFs(config: EmailImportConfig): Promise { config.userId, attachment, parsed.subject || "No subject", + trigger, ); allAttachmentsSucceeded = allAttachmentsSucceeded && result.success; @@ -615,29 +658,28 @@ export function isEmailImportServiceRunning(userId: number): boolean { return activeIntervals.has(userId); } -/** - * Manually trigger an immediate email check for a user - */ -export async function triggerEmailCheck(userId: number): Promise<{ success: boolean; message: string }> { +async function triggerConfiguredEmailCheck( + userId: number, + requireAutomaticEnabled: boolean, +): Promise<{ success: boolean; message: string }> { try { - // Get user's import settings const settings = await getImportSettingsByUser(userId); - - if (!settings || settings.emailImportEnabled !== 1) { + + if (!settings || (requireAutomaticEnabled && settings.emailImportEnabled !== 1)) { return { success: false, message: "Import par email non activé" }; } - if (!settings.emailImportAddress || !settings.emailImportHost) { - return { success: false, message: "Configuration IMAP incomplète" }; - } + const validationError = validateEmailImportConfiguration(settings); + if (validationError) return { success: false, message: validationError }; const authMode = (settings as any).emailImportAuthMode as "basic" | "oauth2" || "basic"; const config: EmailImportConfig = { userId, - emailAddress: settings.emailImportAddress, + // validateEmailImportConfiguration garantit ces deux valeurs avant ce point. + emailAddress: settings.emailImportAddress!, password: settings.emailImportPassword || "", - host: settings.emailImportHost, + host: settings.emailImportHost!, port: settings.emailImportPort || 993, sinceDate: settings.emailImportSinceDate ?? undefined, authMode, @@ -647,7 +689,8 @@ export async function triggerEmailCheck(userId: number): Promise<{ success: bool }; console.log(`[EmailImport] Manual check triggered for user ${userId}`); - await runEmailCheckExclusive(config); + // Cette voie exécute une vérification unique : elle ne crée pas de setInterval. + await runEmailCheckExclusive(config, "manual"); return { success: true, message: "Vérification terminée avec succès" }; } catch (error: any) { @@ -656,6 +699,16 @@ export async function triggerEmailCheck(userId: number): Promise<{ success: bool } } +/** Déclenchement manuel réservé aux appels administrateurs, même si le planificateur est désactivé. */ +export async function triggerManualEmailCheck(userId: number): Promise<{ success: boolean; message: string }> { + return triggerConfiguredEmailCheck(userId, false); +} + +/** Compatibilité avec le bouton existant : la vérification personnelle exige toujours l’activation automatique. */ +export async function triggerEmailCheck(userId: number): Promise<{ success: boolean; message: string }> { + return triggerConfiguredEmailCheck(userId, true); +} + /** * Stop all email import services */ diff --git a/server/folderImportService.ts b/server/folderImportService.ts index 47b6ae7..dbad019 100644 --- a/server/folderImportService.ts +++ b/server/folderImportService.ts @@ -211,6 +211,7 @@ async function processFolderFile( duplicateDetails: duplicateDetails.length > 0 ? JSON.stringify(duplicateDetails) : null, errorDetails: errorDetails.length > 0 ? JSON.stringify(errorDetails) : null, importSource: "folder", + importTrigger: "automatic", }); // Move file to processed folder diff --git a/server/routers.ts b/server/routers.ts index 5b904d5..961d201 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -40,13 +40,16 @@ import { isInvoiceBlacklisted, getAllInvoices, getAllImportLogs, + getAllImportLogsWithDetails, getAllBapHistory, createImportLog, getImportLogsByUser, + getImportLogsWithDetailsByUser, deleteAllImportLogs, getLlmLogsBySourceFile, getLlmLogsByInvoice, getImportSettingsByUser, + getEmailImportAccounts, upsertImportSettings, getDepartmentsByUser, createDepartment, @@ -94,7 +97,7 @@ import { calculateFileSha256 } from "./fileFingerprint"; import { localStoragePut, generateStorageKey } from "./localStorage"; import { testSftpConnection, exportInvoiceToSftp, getUserSftpConfig } from "./sftpExport"; import { drawBapCartouche } from "./bapCartouche"; -import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning, triggerEmailCheck, testImapConnection } from "./emailImportService"; +import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning, triggerEmailCheck, triggerManualEmailCheck, testImapConnection } from "./emailImportService"; import { startFolderImportService, stopFolderImportService, isFolderImportServiceRunning } from "./folderImportService"; import { TRPCError } from "@trpc/server"; import { processFreeproExcel } from "./freeproService"; @@ -409,6 +412,7 @@ export const appRouter = router({ duplicateDetails: JSON.stringify(duplicateDetails), errorDetails: JSON.stringify(errorDetails), importSource: "file", + importTrigger: "manual", }); } catch (error: any) { @@ -1646,9 +1650,9 @@ export const appRouter = router({ getByUser: protectedProcedure.query(async ({ ctx }) => { // Les admins voient tous les logs d'import if (ctx.user.role === 'admin') { - return getAllImportLogs(); + return getAllImportLogsWithDetails(); } - return getImportLogsByUser(ctx.user.id); + return getImportLogsWithDetailsByUser(ctx.user.id); }), deleteAll: protectedProcedure.mutation(async ({ ctx }) => { @@ -1715,6 +1719,18 @@ export const appRouter = router({ const result = await triggerEmailCheck(ctx.user.id); return result; }), + + /** Liste sans secrets les boîtes IMAP disponibles pour une action manuelle d’administrateur. */ + getConfiguredAccounts: adminProcedure.query(async () => { + return getEmailImportAccounts(); + }), + + /** Déclenche une seule lecture IMAP sans activer ni planifier le service automatique. */ + checkAccountNow: adminProcedure + .input(z.object({ userId: z.number().int().positive() })) + .mutation(async ({ input }) => { + return triggerManualEmailCheck(input.userId); + }), }), // ============= IMPORT SETTINGS ROUTES ============= diff --git a/todo.md b/todo.md index 4b2ea54..fa58708 100644 --- a/todo.md +++ b/todo.md @@ -846,3 +846,10 @@ - [x] Identifier les deux factures créées et leur source d’import sans modifier les données - [x] Corréler les factures avec l’historique, les fichiers source et les journaux applicatifs - [x] Vérifier l’état de l’import e-mail au moment de leur création et déterminer la cause : le compte utilisateur 2 était activé, avec une cadence de 30 minutes + +## Historique détaillé et déclenchement manuel des imports +- [x] Analyser les données d’import, les routes et la page Historique existantes +- [x] Ajouter une vue détaillée de l’historique avec source, compte, date, résultats et erreurs +- [x] Ajouter une action administrateur d’import e-mail manuel par compte configuré +- [x] Garantir que le déclenchement manuel ne modifie jamais la planification automatique +- [x] Ajouter les tests de non-régression et valider le parcours en sandbox diff --git a/verification_notes.md b/verification_notes.md new file mode 100644 index 0000000..405ded2 --- /dev/null +++ b/verification_notes.md @@ -0,0 +1,6 @@ +# Vérification visuelle — Historique des imports + +- Le 2 septembre 2026, la page `/history` a été vérifiée en format bureau et mobile. +- Le cartouche administrateur de lecture e-mail ponctuelle est visible, explicite sur l’absence de planification créée et ne propose aucune activation automatique. +- Les filtres et indicateurs restent lisibles sur mobile. +- Le tableau est enveloppé dans un conteneur à défilement horizontal afin de préserver toutes les colonnes de traçabilité sur les écrans étroits.