diff --git a/client/src/App.tsx b/client/src/App.tsx index b10a1a4..3c0e26a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -20,6 +20,7 @@ import AutomationRules from "./pages/AutomationRules"; import BapHistory from "./pages/BapHistory"; import ImportReport from "./pages/ImportReport"; import LearningSettings from "./pages/LearningSettings"; +import VentilationFreePro from "./pages/VentilationFreePro"; function Router() { return ( @@ -40,6 +41,7 @@ function Router() { + diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index ad1966b..43d376c 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -25,7 +25,7 @@ import { import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { getLoginUrl } from "@/const"; import { useIsMobile } from "@/hooks/useMobile"; -import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings, Download, List, Zap, ChevronDown, Receipt, Cog, ClipboardList, CheckSquare, Brain, BarChart2 } from "lucide-react"; +import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings, Download, List, Zap, ChevronDown, Receipt, Cog, ClipboardList, CheckSquare, Brain, BarChart2, BarChart3 } from "lucide-react"; import { CSSProperties, useEffect, useRef, useState } from "react"; import { useLocation } from "wouter"; import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton'; @@ -57,6 +57,14 @@ const menuStructure: MenuItem[] = [ { icon: FileText, label: "Factures BAP", path: "/invoices-bap" }, ], }, + { + icon: BarChart3, + label: "Ventilations", + color: "from-teal-500 to-cyan-500", + children: [ + { icon: BarChart3, label: "FreePro", path: "/ventilation-freepro" }, + ], + }, { icon: Cog, label: "Configuration", diff --git a/client/src/pages/VentilationFreePro.tsx b/client/src/pages/VentilationFreePro.tsx new file mode 100644 index 0000000..3df2e1b --- /dev/null +++ b/client/src/pages/VentilationFreePro.tsx @@ -0,0 +1,543 @@ +import { useState, useRef, useCallback } from "react"; +import { trpc } from "@/lib/trpc"; +import DashboardLayout from "@/components/DashboardLayout"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { toast } from "sonner"; +import { + Upload, + FileSpreadsheet, + Trash2, + Download, + ChevronRight, + ArrowLeft, + Calendar, + Hash, + BarChart3, + Euro, +} from "lucide-react"; +import jsPDF from "jspdf"; +import autoTable from "jspdf-autotable"; + +// ── Types ────────────────────────────────────────────────────────────────── + +interface VentilationLine { + id: number; + structure: string | null; + type: string; + montantCentimes: number; +} + +interface ImportRecord { + id: number; + moisLabel: string; + annee: number; + mois: number; + refPiece: string | null; + fileName: string; + nbLignes: number; + totalTtc: string | null; + createdAt: Date; +} + +// ── Helpers ──────────────────────────────────────────────────────────────── + +function formatMontant(centimes: number): string { + return (centimes / 100).toLocaleString("fr-FR", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + " €"; +} + +function formatTotalTtc(ttc: string | null | number): string { + const val = typeof ttc === "string" ? parseFloat(ttc) : (ttc ?? 0); + return val.toLocaleString("fr-FR", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }) + " €"; +} + +function typeBadgeColor(type: string): string { + if (type === "Lien fibre") return "bg-blue-100 text-blue-800 border-blue-200"; + if (type === "Lien 5G") return "bg-purple-100 text-purple-800 border-purple-200"; + return "bg-orange-100 text-orange-800 border-orange-200"; +} + +// ── Export PDF ───────────────────────────────────────────────────────────── + +function exportToPdf( + importRecord: ImportRecord, + lines: VentilationLine[] +) { + const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" }); + + // En-tête + doc.setFontSize(14); + doc.setFont("helvetica", "bold"); + doc.text("Ventiltion facture FREE PRO", 105, 15, { align: "center" }); + + // Date du mois + const [moisStr, anneeStr] = importRecord.moisLabel.split("/"); + const dateLabel = `01/${moisStr}/${anneeStr}`; + doc.setFontSize(12); + doc.text(dateLabel, 105, 22, { align: "center" }); + + // Date d'édition (haut droite) + const now = new Date(); + const editDate = `Edité le ${now.toLocaleDateString("fr-FR")} - ${now.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}`; + doc.setFontSize(9); + doc.setFont("helvetica", "normal"); + doc.text(editDate, 200, 10, { align: "right" }); + + // Référence pièce + doc.setFontSize(10); + doc.setFont("helvetica", "bold"); + doc.text("ref_piece", 14, 32); + doc.setFont("helvetica", "normal"); + doc.text(importRecord.refPiece ?? "", 55, 32); + + // Tableau principal + const tableData = lines.map((l) => [ + l.structure ?? "(vide)", + l.type, + formatMontant(l.montantCentimes), + ]); + + // Total général + const totalCentimes = lines.reduce((s, l) => s + l.montantCentimes, 0); + + autoTable(doc, { + startY: 38, + head: [["structure", "Type", "Montant"]], + body: tableData, + foot: [["Total général", "", formatMontant(totalCentimes)]], + styles: { fontSize: 9, cellPadding: 2 }, + headStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: "bold", lineWidth: 0.3, lineColor: [0, 0, 0] }, + footStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: "bold", lineWidth: 0.3, lineColor: [0, 0, 0] }, + bodyStyles: { lineWidth: 0.1, lineColor: [180, 180, 180] }, + columnStyles: { + 0: { cellWidth: 80 }, + 1: { cellWidth: 50 }, + 2: { cellWidth: 50, halign: "right" }, + }, + alternateRowStyles: { fillColor: [255, 255, 255] }, + }); + + const moisPad = moisStr.padStart(2, "0"); + const anneeCourt = anneeStr.slice(2); + doc.save(`Ventilation FreePro ${moisPad}.${anneeCourt}.pdf`); +} + +// ── Composant principal ──────────────────────────────────────────────────── + +function VentilationFreeProContent() { + const utils = trpc.useUtils(); + + // Vue : "list" | "detail" + const [view, setView] = useState<"list" | "detail">("list"); + const [selectedImportId, setSelectedImportId] = useState(null); + const [deleteId, setDeleteId] = useState(null); + + // Import state + const [moisLabel, setMoisLabel] = useState(() => { + const now = new Date(); + const m = String(now.getMonth() + 1).padStart(2, "0"); + const y = String(now.getFullYear()); + return `${m}/${y}`; + }); + const [isDragging, setIsDragging] = useState(false); + const fileInputRef = useRef(null); + + // Queries + const { data: imports = [], isLoading: loadingList } = trpc.freepro.list.useQuery(); + const { data: detail, isLoading: loadingDetail } = trpc.freepro.getById.useQuery( + { id: selectedImportId! }, + { enabled: !!selectedImportId && view === "detail" } + ); + + // Mutations + const importMutation = trpc.freepro.import.useMutation({ + onSuccess: (data) => { + utils.freepro.list.invalidate(); + toast.success(`Import réussi — ${data.nbLignes} lignes traitées — Total TTC : ${formatTotalTtc(data.totalTtc)}`); + setSelectedImportId(data.importId); + setView("detail"); + }, + onError: (err) => { + toast.error(`Erreur d'import : ${err.message}`); + }, + }); + + const deleteMutation = trpc.freepro.delete.useMutation({ + onSuccess: () => { + utils.freepro.list.invalidate(); + if (view === "detail") setView("list"); + toast.success("Import supprimé"); + }, + }); + + // Gestion du fichier + const handleFile = useCallback( + (file: File) => { + if (!file.name.match(/\.(xlsx|xls)$/i)) { + toast.error("Format invalide : veuillez sélectionner un fichier Excel (.xlsx ou .xls)"); + return; + } + const reader = new FileReader(); + reader.onload = (e) => { + const base64 = (e.target?.result as string).split(",")[1]; + importMutation.mutate({ moisLabel, fileName: file.name, fileBase64: base64 }); + }; + reader.readAsDataURL(file); + }, + [moisLabel, importMutation, toast] + ); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + const file = e.dataTransfer.files[0]; + if (file) handleFile(file); + }, + [handleFile] + ); + + // ── Rendu liste ────────────────────────────────────────────────────────── + + if (view === "list") { + return ( +
+ {/* En-tête */} +
+
+ +
+
+

Ventilation FreePro

+

Import et ventilation des factures Free Pro par structure

+
+
+ + {/* Zone d'import */} + + + + + Importer une facture FreePro + + + + {/* Sélecteur de mois */} +
+ +
+ + setMoisLabel(e.target.value)} + placeholder="MM/YYYY" + className="border rounded px-3 py-1.5 text-sm w-28 focus:outline-none focus:ring-2 focus:ring-blue-400" + pattern="\d{2}/\d{4}" + /> + Format : MM/YYYY +
+
+ + {/* Zone de dépôt */} +
{ e.preventDefault(); setIsDragging(true); }} + onDragLeave={() => setIsDragging(false)} + onDrop={handleDrop} + onClick={() => fileInputRef.current?.click()} + > + { const f = e.target.files?.[0]; if (f) handleFile(f); }} + /> + {importMutation.isPending ? ( +
+
+

Traitement en cours…

+
+ ) : ( +
+ +

+ Glissez-déposez le fichier Excel FreePro ici +

+

ou cliquez pour sélectionner (.xlsx, .xls)

+
+ )} +
+ + + + {/* Historique */} + + + + + Historique des imports ({imports.length}) + + + + {loadingList ? ( +
+
+
+ ) : imports.length === 0 ? ( +
+ +

Aucun import enregistré

+
+ ) : ( + + + + Mois + Réf. pièce + Fichier + Lignes + Total TTC + Importé le + Actions + + + + {(imports as ImportRecord[]).map((imp) => ( + { setSelectedImportId(imp.id); setView("detail"); }} + > + + + {imp.moisLabel} + + + {imp.refPiece ?? "—"} + {imp.fileName} + + + + {imp.nbLignes} + + + + {formatTotalTtc(imp.totalTtc)} + + + {new Date(imp.createdAt).toLocaleDateString("fr-FR")} + + +
e.stopPropagation()}> + + +
+
+
+ ))} +
+
+ )} + + + + {/* Dialog de confirmation de suppression */} + setDeleteId(null)}> + + + Supprimer cet import ? + + Cette action est irréversible. Toutes les lignes de ventilation associées seront supprimées. + + + + Annuler + { if (deleteId) deleteMutation.mutate({ id: deleteId }); setDeleteId(null); }} + > + Supprimer + + + + +
+ ); + } + + // ── Rendu détail ───────────────────────────────────────────────────────── + // (suite ci-dessous) + + const imp = detail?.import as ImportRecord | undefined; + const lines = (detail?.lines ?? []) as VentilationLine[]; + const totalCentimes = lines.reduce((s, l) => s + l.montantCentimes, 0); + + return ( +
+ {/* En-tête détail */} +
+
+ +
+
+ +

+ Ventilation FreePro — {imp?.moisLabel ?? "…"} +

+
+
+ {imp && ( + + )} +
+ + {loadingDetail ? ( +
+
+
+ ) : imp ? ( + <> + {/* Métadonnées */} +
+ + +

Référence pièce

+

{imp.refPiece ?? "—"}

+
+
+ + +

Mois

+

01/{imp.moisLabel}

+
+
+ + +

Lignes traitées

+

{imp.nbLignes}

+
+
+ + +

+ Total TTC +

+

{formatTotalTtc(imp.totalTtc)}

+
+
+
+ + {/* Tableau de ventilation */} + + + Ventilation par structure et type + + + + + + structure + Type + Montant + + + + {lines.map((line) => ( + + + {line.structure ?? (vide)} + + + + {line.type} + + + + {formatMontant(line.montantCentimes)} + + + ))} + + + + + + + +
Total général + {formatMontant(totalCentimes)} +
+
+
+ + ) : ( +
Import introuvable.
+ )} +
+ ); +} + +export default function VentilationFreePro() { + return ( + + + + ); +} diff --git a/drizzle/0028_dusty_kat_farrell.sql b/drizzle/0028_dusty_kat_farrell.sql new file mode 100644 index 0000000..044059a --- /dev/null +++ b/drizzle/0028_dusty_kat_farrell.sql @@ -0,0 +1,24 @@ +CREATE TABLE `freeproImports` ( + `id` int AUTO_INCREMENT NOT NULL, + `userId` int NOT NULL, + `moisLabel` varchar(10) NOT NULL, + `annee` int NOT NULL, + `mois` int NOT NULL, + `refPiece` varchar(50), + `fileName` varchar(255) NOT NULL, + `nbLignes` int NOT NULL DEFAULT 0, + `totalTtc` varchar(30), + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `freeproImports_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `freeproVentilationLines` ( + `id` int AUTO_INCREMENT NOT NULL, + `importId` int NOT NULL, + `structure` varchar(100), + `type` varchar(50) NOT NULL, + `montantCentimes` int NOT NULL, + `createdAt` timestamp NOT NULL DEFAULT (now()), + CONSTRAINT `freeproVentilationLines_id` PRIMARY KEY(`id`) +); diff --git a/drizzle/meta/0028_snapshot.json b/drizzle/meta/0028_snapshot.json new file mode 100644 index 0000000..51845ba --- /dev/null +++ b/drizzle/meta/0028_snapshot.json @@ -0,0 +1,1990 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "ede525fc-9b6f-4129-ab08-aa451ec9f790", + "prevId": "9058cdd1-d372-462c-a13c-23b208e8f3b7", + "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": {} + }, + "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 + }, + "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": {} + }, + "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 + }, + "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')", + "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 + }, + "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 + }, + "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": {}, + "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')", + "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 + }, + "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(64)", + "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": {} + } + }, + "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 ed0f036..708ae16 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1780653000245, "tag": "0027_complex_ultimo", "breakpoints": true + }, + { + "idx": 28, + "version": "5", + "when": 1780660548610, + "tag": "0028_dusty_kat_farrell", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 38468fc..5b3e9e8 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -421,3 +421,46 @@ export const invoiceLearnings = mysqlTable("invoiceLearnings", { export type InvoiceLearning = typeof invoiceLearnings.$inferSelect; export type InsertInvoiceLearning = typeof invoiceLearnings.$inferInsert; + +/** + * FreePro ventilation imports — one record per imported monthly file + */ +export const freeproImports = mysqlTable("freeproImports", { + id: int("id").autoincrement().primaryKey(), + userId: int("userId").notNull(), + /** Label du mois, ex: "06/2025" */ + moisLabel: varchar("moisLabel", { length: 10 }).notNull(), + /** Année (ex: 2025) */ + annee: int("annee").notNull(), + /** Mois numérique (1-12) */ + mois: int("mois").notNull(), + /** Référence de la pièce comptable, ex: F202506006010 */ + refPiece: varchar("refPiece", { length: 50 }), + /** Nom du fichier Excel importé */ + fileName: varchar("fileName", { length: 255 }).notNull(), + /** Nombre de lignes traitées */ + nbLignes: int("nbLignes").default(0).notNull(), + /** Total général TTC calculé */ + totalTtc: varchar("totalTtc", { length: 30 }), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); +export type FreeproImport = typeof freeproImports.$inferSelect; +export type InsertFreeproImport = typeof freeproImports.$inferInsert; + +/** + * FreePro ventilation lines — aggregated result (structure + type → montant TTC) + */ +export const freeproVentilationLines = mysqlTable("freeproVentilationLines", { + id: int("id").autoincrement().primaryKey(), + importId: int("importId").notNull(), // FK to freeproImports.id + /** Code structure, ex: "1083ADV" ou null pour (vide) */ + structure: varchar("structure", { length: 100 }), + /** Type: "Lien fibre" | "Lien 5G" | "Tél. mobile" */ + type: varchar("type", { length: 50 }).notNull(), + /** Montant TTC agrégé en centimes (pour éviter les flottants) */ + montantCentimes: int("montantCentimes").notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), +}); +export type FreeproVentilationLine = typeof freeproVentilationLines.$inferSelect; +export type InsertFreeproVentilationLine = typeof freeproVentilationLines.$inferInsert; diff --git a/package.json b/package.json index 411b7ae..bda85c8 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,8 @@ "input-otp": "^1.4.2", "jose": "6.1.0", "jsonwebtoken": "^9.0.3", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.8", "lucide-react": "^0.453.0", "mailparser": "^3.9.3", "mysql2": "^3.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31082ab..79389ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,6 +199,12 @@ importers: jsonwebtoken: specifier: ^9.0.3 version: 9.0.3 + jspdf: + specifier: ^4.2.1 + version: 4.2.1 + jspdf-autotable: + specifier: ^5.0.8 + version: 5.0.8(jspdf@4.2.1) lucide-react: specifier: ^0.453.0 version: 0.453.0(react@19.2.1) @@ -649,6 +655,10 @@ packages: resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -2752,9 +2762,15 @@ packages: '@types/node@24.7.0': resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==} + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} @@ -2975,6 +2991,10 @@ packages: bare-url@2.4.0: resolution: {integrity: sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -3044,6 +3064,10 @@ packages: caniuse-lite@1.0.30001748: resolution: {integrity: sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==} + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -3166,6 +3190,9 @@ packages: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3192,6 +3219,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3457,6 +3487,9 @@ packages: dompurify@3.3.0: resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} + dompurify@3.4.8: + resolution: {integrity: sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -3717,6 +3750,9 @@ packages: fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + fast-xml-parser@5.2.5: resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} hasBin: true @@ -3730,6 +3766,9 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + finalhandler@1.3.1: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} @@ -3894,6 +3933,10 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} @@ -3951,6 +3994,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -4036,6 +4082,14 @@ packages: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} + jspdf-autotable@5.0.8: + resolution: {integrity: sha512-Hy05N86yBO7CXBrnSLOge7i1ZYpKH2DjQ94iybaP7vBhSInjvRBgDc99ngKzSbSO8Jc98ZCally8I6n0tj2RJQ==} + peerDependencies: + jspdf: ^2 || ^3 || ^4 + + jspdf@4.2.1: + resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==} + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -4532,6 +4586,9 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -4594,6 +4651,9 @@ packages: peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4668,6 +4728,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -4802,6 +4865,9 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -4842,6 +4908,10 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} @@ -4979,6 +5049,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -5038,6 +5112,10 @@ packages: resolution: {integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==} engines: {node: '>=10'} + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} @@ -5066,6 +5144,9 @@ packages: text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -5222,6 +5303,9 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -6115,6 +6199,8 @@ snapshots: '@babel/runtime@7.28.4': {} + '@babel/runtime@7.29.7': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -8104,8 +8190,13 @@ snapshots: dependencies: undici-types: 7.14.0 + '@types/pako@2.0.4': {} + '@types/qs@6.14.0': {} + '@types/raf@3.4.3': + optional: true + '@types/range-parser@1.2.7': {} '@types/react-dom@19.2.1(@types/react@19.2.1)': @@ -8343,6 +8434,9 @@ snapshots: dependencies: bare-path: 3.0.0 + base64-arraybuffer@1.0.2: + optional: true + base64-js@1.5.1: {} baseline-browser-mapping@2.8.12: {} @@ -8421,6 +8515,18 @@ snapshots: caniuse-lite@1.0.30001748: {} + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.29.7 + '@types/raf': 3.4.3 + core-js: 3.49.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + ccount@2.0.1: {} cfb@1.2.2: @@ -8539,6 +8645,9 @@ snapshots: dependencies: is-what: 4.1.16 + core-js@3.49.0: + optional: true + core-util-is@1.0.3: {} cose-base@1.0.3: @@ -8568,6 +8677,11 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + optional: true + cssesc@3.0.0: {} csstype@3.1.3: {} @@ -8832,6 +8946,11 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 + dompurify@3.4.8: + optionalDependencies: + '@types/trusted-types': 2.0.7 + optional: true + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -9078,6 +9197,12 @@ snapshots: fast-fifo@1.3.2: {} + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.1.0 + fast-xml-parser@5.2.5: dependencies: strnum: 2.1.1 @@ -9086,6 +9211,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.8.3: {} + finalhandler@1.3.1: dependencies: debug: 2.6.9 @@ -9326,6 +9453,12 @@ snapshots: html-void-elements@3.0.0: {} + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + optional: true + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 @@ -9391,6 +9524,8 @@ snapshots: internmap@2.0.3: {} + iobuffer@5.4.0: {} + ipaddr.js@1.9.1: {} is-alphabetical@2.0.1: {} @@ -9459,6 +9594,21 @@ snapshots: ms: 2.1.3 semver: 7.7.3 + jspdf-autotable@5.0.8(jspdf@4.2.1): + dependencies: + jspdf: 4.2.1 + + jspdf@4.2.1: + dependencies: + '@babel/runtime': 7.29.7 + fast-png: 6.4.0 + fflate: 0.8.3 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.49.0 + dompurify: 3.4.8 + html2canvas: 1.4.1 + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -10153,6 +10303,8 @@ snapshots: pako@1.0.11: {} + pako@2.1.0: {} + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -10216,6 +10368,9 @@ snapshots: peberminta@0.9.0: {} + performance-now@2.1.0: + optional: true + picocolors@1.1.1: {} picomatch@4.0.3: {} @@ -10285,6 +10440,11 @@ snapshots: quansync@0.2.11: {} + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + range-parser@1.2.1: {} raw-body@2.5.2: @@ -10454,6 +10614,9 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + regenerator-runtime@0.13.11: + optional: true + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -10529,6 +10692,9 @@ snapshots: resolve-pkg-maps@1.0.0: {} + rgbcolor@1.0.1: + optional: true + robust-predicates@3.0.2: {} rollup@4.52.4: @@ -10736,6 +10902,9 @@ snapshots: stackback@0.0.2: {} + stackblur-canvas@2.7.0: + optional: true + statuses@2.0.1: {} std-env@3.9.0: {} @@ -10820,6 +10989,9 @@ snapshots: dependencies: copy-anything: 3.0.5 + svg-pathdata@6.0.3: + optional: true + tailwind-merge@3.3.1: {} tailwindcss-animate@1.0.7(tailwindcss@4.1.14): @@ -10862,6 +11034,11 @@ snapshots: transitivePeerDependencies: - react-native-b4a + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + optional: true + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -11001,6 +11178,11 @@ snapshots: utils-merge@1.0.1: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + optional: true + uuid@11.1.0: {} uuid@8.3.2: {} diff --git a/server/db.ts b/server/db.ts index ca262fe..19737ce 100644 --- a/server/db.ts +++ b/server/db.ts @@ -944,3 +944,70 @@ export async function updateBapHistoryPdfUrl(id: number, pdfUrl: string): Promis if (!db) return; await db.update(bapHistory).set({ pdfUrl }).where(eq(bapHistory.id, id)); } + +// ============= FREEPRO VENTILATION OPERATIONS ============= + +import { + freeproImports, + InsertFreeproImport, + FreeproImport, + freeproVentilationLines, + InsertFreeproVentilationLine, + FreeproVentilationLine, +} from "../drizzle/schema"; + +/** Crée un import FreePro et ses lignes de ventilation */ +export async function createFreeproImport( + data: InsertFreeproImport, + lines: Omit[] +): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const result = await db.insert(freeproImports).values(data); + const importId = (result as unknown as { insertId: number }).insertId; + if (lines.length > 0) { + await db.insert(freeproVentilationLines).values( + lines.map((l) => ({ ...l, importId })) + ); + } + return importId; +} + +/** Récupère tous les imports FreePro d'un utilisateur (sans les lignes) */ +export async function getFreeproImportsByUser(userId: number): Promise { + const db = await getDb(); + if (!db) return []; + return db + .select() + .from(freeproImports) + .where(eq(freeproImports.userId, userId)) + .orderBy(desc(freeproImports.annee), desc(freeproImports.mois)); +} + +/** Récupère un import FreePro avec ses lignes */ +export async function getFreeproImportWithLines( + importId: number +): Promise<{ import: FreeproImport; lines: FreeproVentilationLine[] } | null> { + const db = await getDb(); + if (!db) return null; + const imports = await db + .select() + .from(freeproImports) + .where(eq(freeproImports.id, importId)) + .limit(1); + if (!imports[0]) return null; + const lines = await db + .select() + .from(freeproVentilationLines) + .where(eq(freeproVentilationLines.importId, importId)) + .orderBy(freeproVentilationLines.structure, freeproVentilationLines.type); + return { import: imports[0], lines }; +} + +/** Supprime un import FreePro et ses lignes */ +export async function deleteFreeproImport(importId: number): Promise { + const db = await getDb(); + if (!db) return; + await db.delete(freeproVentilationLines).where(eq(freeproVentilationLines.importId, importId)); + await db.delete(freeproImports).where(eq(freeproImports.id, importId)); +} diff --git a/server/freeproService.ts b/server/freeproService.ts new file mode 100644 index 0000000..ca9a52c --- /dev/null +++ b/server/freeproService.ts @@ -0,0 +1,167 @@ +/** + * FreePro Ventilation Service + * Transforms a FreePro Excel invoice file into a ventilation summary (structure × type → TTC) + * + * Transformation rules (from Process Scripting facture freepro.txt): + * 1. Colonne "script": =SI(OU(F2="Offre Freebox Pro";F2="Support Premium";ESTNUM(TROUVE("Remise première année";F2)));"Lien fibre";"Tél. mobile") + * 2. Colonne "ttc": =ARRONDI(total_ht * 1.2, 2) + * 3. Colonne "type": =SI(ESTNUM(TROUVE("Routeur 5G"; label_client)); "Lien 5G"; script) + * 4. TCD: agrégation par (structure, type) → somme des TTC + */ + +import * as xlsx from "xlsx"; + +export interface FreeproSourceRow { + ref_piece: string; + date: Date | null; + reference_service: string; + label_client: string; + structure: string; + description: string; + debut_factu: Date | null; + fin_factu: Date | null; + quantite: number; + prix: number; + total: number; +} + +export interface FreeproVentilationResult { + refPiece: string; + moisLabel: string; + annee: number; + mois: number; + nbLignes: number; + totalTtc: number; + lines: Array<{ + structure: string | null; + type: string; + montant: number; // TTC en euros + }>; +} + +/** + * Détermine la colonne "script" selon la description + */ +function getScript(description: string): string { + if (!description) return "Tél. mobile"; + if ( + description === "Offre Freebox Pro" || + description === "Support Premium" || + description.includes("Remise première année") + ) { + return "Lien fibre"; + } + return "Tél. mobile"; +} + +/** + * Calcule le TTC : arrondi(total_ht * 1.2, 2) + */ +function getTtc(totalHt: number): number { + return Math.round(totalHt * 1.2 * 100) / 100; +} + +/** + * Détermine le "type" final selon label_client et script + */ +function getType(labelClient: string, script: string): string { + if (labelClient && labelClient.includes("Routeur 5G")) { + return "Lien 5G"; + } + return script; +} + +/** + * Parse et transforme un fichier Excel FreePro (Buffer) + * Retourne les données de ventilation agrégées + */ +export function processFreeproExcel( + buffer: Buffer, + moisLabel: string +): FreeproVentilationResult { + const wb = xlsx.read(buffer, { type: "buffer", cellDates: true }); + const ws = wb.Sheets[wb.SheetNames[0]]; + const rawRows = xlsx.utils.sheet_to_json>(ws, { + header: [ + "ref_piece", + "date", + "reference_service", + "label_client", + "structure", + "description", + "debut_factu", + "fin_factu", + "quantite", + "prix", + "total", + ], + range: 1, // skip header row + defval: null, + }); + + if (rawRows.length === 0) { + throw new Error("Le fichier Excel est vide ou ne contient pas de données."); + } + + // Extraire ref_piece et date du mois depuis la première ligne + const firstRow = rawRows[0]; + const refPiece = String(firstRow.ref_piece || ""); + + // Parser moisLabel "MM/YYYY" + const [moisStr, anneeStr] = moisLabel.split("/"); + const mois = parseInt(moisStr, 10); + const annee = parseInt(anneeStr, 10); + + // Agrégation par (structure, type) → somme TTC en centimes (pour éviter les flottants) + const aggregation = new Map(); + + let nbLignes = 0; + for (const row of rawRows) { + if (!row.ref_piece) continue; + nbLignes++; + + const description = String(row.description || ""); + const labelClient = String(row.label_client || ""); + const structure = row.structure ? String(row.structure) : null; + const totalHt = parseFloat(String(row.total || "0").replace(",", ".")) || 0; + + const script = getScript(description); + const ttc = getTtc(totalHt); + const type = getType(labelClient, script); + + const key = `${structure ?? ""}|||${type}`; + aggregation.set(key, (aggregation.get(key) || 0) + Math.round(ttc * 100)); + } + + // Construire les lignes de résultat + const lines: FreeproVentilationResult["lines"] = []; + for (const [key, centimes] of Array.from(aggregation.entries())) { + const [structureRaw, type] = key.split("|||"); + lines.push({ + structure: structureRaw === "" ? null : structureRaw, + type, + montant: centimes / 100, + }); + } + + // Trier par structure (null en dernier) puis type + lines.sort((a, b) => { + const sa = a.structure ?? "\uFFFF"; // null → fin de liste + const sb = b.structure ?? "\uFFFF"; + if (sa !== sb) return sa.localeCompare(sb); + return a.type.localeCompare(b.type); + }); + + const totalTtc = + Math.round(lines.reduce((sum, l) => sum + l.montant, 0) * 100) / 100; + + return { + refPiece, + moisLabel, + annee, + mois, + nbLignes, + totalTtc, + lines, + }; +} diff --git a/server/routers.ts b/server/routers.ts index 6477918..94904a5 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -85,6 +85,13 @@ import { drawBapCartouche } from "./bapCartouche"; import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning, triggerEmailCheck, testImapConnection } from "./emailImportService"; import { startFolderImportService, stopFolderImportService, isFolderImportServiceRunning } from "./folderImportService"; import { TRPCError } from "@trpc/server"; +import { processFreeproExcel } from "./freeproService"; +import { + createFreeproImport, + getFreeproImportsByUser, + getFreeproImportWithLines, + deleteFreeproImport, +} from "./db"; // Admin-only procedure const adminProcedure = protectedProcedure.use(({ ctx, next }) => { @@ -2197,5 +2204,70 @@ export const appRouter = router({ return { success: true }; }), }), + + // ============= FREEPRO VENTILATION ============= + freepro: router({ + /** Importe un fichier Excel FreePro et calcule la ventilation */ + import: protectedProcedure + .input( + z.object({ + moisLabel: z.string().regex(/^\d{2}\/\d{4}$/, "Format MM/YYYY requis"), + fileName: z.string(), + fileBase64: z.string(), // fichier Excel encodé en base64 + }) + ) + .mutation(async ({ input, ctx }) => { + const buffer = Buffer.from(input.fileBase64, "base64"); + const result = processFreeproExcel(buffer, input.moisLabel); + + const importId = await createFreeproImport( + { + userId: ctx.user.id, + moisLabel: result.moisLabel, + annee: result.annee, + mois: result.mois, + refPiece: result.refPiece || null, + fileName: input.fileName, + nbLignes: result.nbLignes, + totalTtc: result.totalTtc.toFixed(2), + }, + result.lines.map((l) => ({ + structure: l.structure ?? null, + type: l.type, + montantCentimes: Math.round(l.montant * 100), + })) + ); + + return { importId, ...result }; + }), + + /** Liste tous les imports FreePro de l'utilisateur */ + list: protectedProcedure.query(async ({ ctx }) => { + return getFreeproImportsByUser(ctx.user.id); + }), + + /** Récupère un import FreePro avec ses lignes de ventilation */ + getById: protectedProcedure + .input(z.object({ id: z.number() })) + .query(async ({ input, ctx }) => { + const data = await getFreeproImportWithLines(input.id); + if (!data) throw new TRPCError({ code: "NOT_FOUND" }); + if (data.import.userId !== ctx.user.id) + throw new TRPCError({ code: "FORBIDDEN" }); + return data; + }), + + /** Supprime un import FreePro */ + delete: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input, ctx }) => { + const data = await getFreeproImportWithLines(input.id); + if (!data) throw new TRPCError({ code: "NOT_FOUND" }); + if (data.import.userId !== ctx.user.id) + throw new TRPCError({ code: "FORBIDDEN" }); + await deleteFreeproImport(input.id); + return { success: true }; + }), + }), }); export type AppRouter = typeof appRouter; diff --git a/todo.md b/todo.md index d1e7f30..331e149 100644 --- a/todo.md +++ b/todo.md @@ -642,5 +642,18 @@ - [x] Ajouter emailImportAuthMode dans le schéma Zod de importSettings.update - [x] Ajouter sélecteur mode auth (basic/oauth2) dans ImportSettings.tsx - [x] Ajouter bouton "Tester la connexion IMAP" dans ImportSettings.tsx -- [ ] Déployer sur recette et production -- [ ] Appliquer migration emailImportAuthMode sur recette et production +- [x] Déployer sur recette (git push + docker compose up --build) +- [x] Appliquer migration emailImportAuthMode sur recette (colonne ajoutée en DB) +- [ ] Déployer sur production (en attente de validation recette) + +## Module Ventilation FreePro + +- [x] Schéma DB : tables freeproImports et freeproVentilationLines +- [x] Migration DB appliquée (pnpm db:push) +- [x] Service freeproService.ts : transformation Excel → ventilation (script/TTC/type) +- [x] Helpers DB dans db.ts : createFreeproImport, getFreeproImportsByUser, getFreeproImportWithLines, deleteFreeproImport +- [x] Procédures tRPC : freepro.import, freepro.list, freepro.getById, freepro.delete +- [x] Page VentilationFreePro.tsx : import drag&drop, tableau de ventilation, historique, export PDF +- [x] Menu "Ventilations > FreePro" ajouté dans DashboardLayout +- [x] Route /ventilation-freepro ajoutée dans App.tsx +- [ ] Déploiement sur recette