Checkpoint: Application complète de dématérialisation de facturation avec extraction IA (Mistral), authentification locale + Azure AD, stockage local, et export SFTP.
Fonctionnalités implémentées : ✅ Authentification locale (email/password) + Azure AD + Manus OAuth ✅ Upload drag-and-drop de fichiers PDF avec suivi en temps réel ✅ Extraction automatique avec Mistral AI (OCR + LLM) ✅ Détection de doublons (fournisseur, numéro, date) ✅ Score de qualité d'extraction (0-100) ✅ Tableau de bord avec statistiques ✅ Liste des factures avec recherche et filtres ✅ Paramètres utilisateur (LLM, keywords, SFTP) ✅ Historique des imports avec logs détaillés ✅ Gestion des utilisateurs (admin) ✅ Export SFTP manuel/automatique ✅ Stockage local avec organisation YYYY-MM ✅ Tests unitaires d'authentification Architecture : - Frontend : React 19 + Vite + TailwindCSS + Radix UI - Backend : Express + tRPC + Drizzle ORM - Base de données : MySQL (6 tables) - IA : Mistral AI pour extraction - Stockage : Local filesystem - Export : SFTP Pages : - Login (choix local/Azure/Manus) - Dashboard (statistiques) - Upload (drag-and-drop) - Invoices (liste avec recherche) - Settings (LLM, keywords, SFTP) - History (logs d'import) - Users (gestion admin)
This commit is contained in:
@@ -1,22 +1,24 @@
|
||||
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core";
|
||||
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, uniqueIndex, decimal } from "drizzle-orm/mysql-core";
|
||||
|
||||
/**
|
||||
* Core user table backing auth flow.
|
||||
* Extend this file with additional tables as your product grows.
|
||||
* Columns use camelCase to match both database fields and generated types.
|
||||
* Supports multiple authentication methods: Manus OAuth, local, and Azure AD
|
||||
*/
|
||||
export const users = mysqlTable("users", {
|
||||
/**
|
||||
* Surrogate primary key. Auto-incremented numeric value managed by the database.
|
||||
* Use this for relations between tables.
|
||||
*/
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
/** Manus OAuth identifier (openId) returned from the OAuth callback. Unique per user. */
|
||||
openId: varchar("openId", { length: 64 }).notNull().unique(),
|
||||
/** Manus OAuth identifier (openId) - Optional for backward compatibility */
|
||||
openId: varchar("openId", { length: 64 }).unique(),
|
||||
/** Azure AD Object ID - Unique identifier from Azure AD */
|
||||
azureAdId: varchar("azureAdId", { length: 64 }).unique(),
|
||||
name: text("name"),
|
||||
email: varchar("email", { length: 320 }),
|
||||
loginMethod: varchar("loginMethod", { length: 64 }),
|
||||
email: varchar("email", { length: 320 }).notNull().unique(),
|
||||
/** Hashed password for local authentication (bcrypt) */
|
||||
passwordHash: varchar("passwordHash", { length: 255 }),
|
||||
/** Authentication method: 'manus', 'local', 'azure-ad' */
|
||||
loginMethod: mysqlEnum("loginMethod", ["manus", "local", "azure-ad"]).notNull(),
|
||||
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
||||
/** Account status for manual user management */
|
||||
isActive: int("isActive").default(1).notNull(), // 0 = inactive, 1 = active
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
|
||||
@@ -25,4 +27,145 @@ export const users = mysqlTable("users", {
|
||||
export type User = typeof users.$inferSelect;
|
||||
export type InsertUser = typeof users.$inferInsert;
|
||||
|
||||
// TODO: Add your tables here
|
||||
/**
|
||||
* Source files table storing uploaded PDF files that may contain multiple invoices
|
||||
*/
|
||||
export const sourceFiles = mysqlTable("sourceFiles", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
userId: int("userId").notNull(),
|
||||
fileName: varchar("fileName", { length: 255 }).notNull(),
|
||||
fileKey: text("fileKey").notNull(), // Local storage key with YYYY-MM prefix
|
||||
fileUrl: text("fileUrl").notNull(), // Public URL
|
||||
totalInvoicesDetected: int("totalInvoicesDetected").default(0).notNull(),
|
||||
processingStatus: mysqlEnum("processingStatus", ["processing", "completed", "error"]).default("processing").notNull(),
|
||||
processingProgress: varchar("processingProgress", { length: 255 }), // Progress message (e.g., "Extraction 3/9 factures...")
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||
});
|
||||
|
||||
export type SourceFile = typeof sourceFiles.$inferSelect;
|
||||
export type InsertSourceFile = typeof sourceFiles.$inferInsert;
|
||||
|
||||
/**
|
||||
* Invoices table storing individual invoices extracted from source files
|
||||
*/
|
||||
export const invoices = mysqlTable("invoices", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
userId: int("userId").notNull(),
|
||||
sourceFileId: int("sourceFileId").notNull(), // Reference to the source PDF file
|
||||
invoiceIndexInFile: int("invoiceIndexInFile").default(1).notNull(), // Position in the source file (1, 2, 3...)
|
||||
|
||||
// File storage information (for individual invoice if split, or reference to source)
|
||||
fileName: varchar("fileName", { length: 255 }).notNull(),
|
||||
fileKey: text("fileKey").notNull(), // Local storage key
|
||||
fileUrl: text("fileUrl").notNull(), // Public URL
|
||||
|
||||
// Extracted metadata
|
||||
supplierName: varchar("supplierName", { length: 255 }),
|
||||
invoiceNumber: varchar("invoiceNumber", { length: 100 }),
|
||||
invoiceDate: timestamp("invoiceDate"),
|
||||
deliveryNoteNumber: varchar("deliveryNoteNumber", { length: 100 }),
|
||||
orderNumber: varchar("orderNumber", { length: 100 }),
|
||||
totalAmount: decimal("totalAmount", { precision: 10, scale: 2 }),
|
||||
pageRange: varchar("pageRange", { length: 20 }), // ex: "1-2" ou "5"
|
||||
qualityScore: int("qualityScore"), // Score de qualité de l'extraction (0-100)
|
||||
|
||||
// Metadata JSON file
|
||||
metadataFileKey: text("metadataFileKey"), // Storage key for JSON metadata
|
||||
metadataFileUrl: text("metadataFileUrl"), // Public URL for JSON
|
||||
|
||||
// Processing status
|
||||
status: mysqlEnum("status", ["processing", "completed", "error"]).default("processing").notNull(),
|
||||
errorMessage: text("errorMessage"),
|
||||
|
||||
// Manual correction tracking
|
||||
manuallyEdited: int("manuallyEdited").default(0).notNull(), // 0 = false, 1 = true
|
||||
|
||||
// SFTP Export tracking
|
||||
exportedAt: timestamp("exportedAt"),
|
||||
exportMode: mysqlEnum("exportMode", ["manual", "automatic"]),
|
||||
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => {
|
||||
return {
|
||||
// Unique constraint: no duplicate invoices with same supplier, invoice number, and date
|
||||
supplierInvoiceDateIdx: uniqueIndex("supplier_invoice_date_unique").on(table.supplierName, table.invoiceNumber, table.invoiceDate),
|
||||
};
|
||||
});
|
||||
|
||||
export type Invoice = typeof invoices.$inferSelect;
|
||||
export type InsertInvoice = typeof invoices.$inferInsert;
|
||||
|
||||
/**
|
||||
* User settings table for application preferences
|
||||
*/
|
||||
export const userSettings = mysqlTable("userSettings", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
userId: int("userId").notNull().unique(), // One settings record per user
|
||||
llmModel: varchar("llmModel", { length: 50 }).default("mistral-large-latest").notNull(), // Mistral model for invoice extraction
|
||||
orderNumberFormat: text("orderNumberFormat"), // Format/pattern du numéro de commande pour aider l'extraction
|
||||
// Mots-clés personnalisés pour améliorer la détection (séparés par des virgules)
|
||||
invoiceNumberKeywords: text("invoiceNumberKeywords"), // Ex: "Référence, Ref facture, Invoice ref"
|
||||
deliveryNoteKeywords: text("deliveryNoteKeywords"), // Ex: "Livraison, Delivery, Expédition"
|
||||
orderNumberKeywords: text("orderNumberKeywords"), // Ex: "Cde client, Référence commande, PO Number"
|
||||
supplierKeywords: text("supplierKeywords"), // Ex: "Vendeur, Société, Émetteur"
|
||||
totalAmountKeywords: text("totalAmountKeywords"), // Ex: "Net à payer, Total à régler, Amount due"
|
||||
// SFTP Configuration
|
||||
sftpHost: varchar("sftpHost", { length: 255 }),
|
||||
sftpPort: int("sftpPort").default(22),
|
||||
sftpUsername: varchar("sftpUsername", { length: 255 }),
|
||||
sftpPassword: text("sftpPassword"), // Encrypted password
|
||||
sftpRemotePath: varchar("sftpRemotePath", { length: 500 }).default("/"), // Remote directory path
|
||||
sftpAutoExport: int("sftpAutoExport").default(0).notNull(), // 0 = manual, 1 = automatic
|
||||
// LLM Logs retention
|
||||
llmLogsRetentionMonths: int("llmLogsRetentionMonths").default(3).notNull(), // Durée de conservation des logs LLM en mois (défaut: 3 mois)
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||
});
|
||||
|
||||
export type UserSettings = typeof userSettings.$inferSelect;
|
||||
export type InsertUserSettings = typeof userSettings.$inferInsert;
|
||||
|
||||
/**
|
||||
* Import logs table for tracking all import operations
|
||||
*/
|
||||
export const importLogs = mysqlTable("importLogs", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
userId: int("userId").notNull(),
|
||||
sourceFileId: int("sourceFileId").notNull(), // Reference to source file
|
||||
fileName: varchar("fileName", { length: 255 }).notNull(),
|
||||
totalInvoicesDetected: int("totalInvoicesDetected").default(0).notNull(),
|
||||
invoicesImported: int("invoicesImported").default(0).notNull(),
|
||||
duplicatesIgnored: int("duplicatesIgnored").default(0).notNull(),
|
||||
errors: int("errors").default(0).notNull(),
|
||||
duplicateDetails: text("duplicateDetails"), // JSON array of duplicate invoice info
|
||||
errorDetails: text("errorDetails"), // JSON array of error messages
|
||||
importedAt: timestamp("importedAt").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type ImportLog = typeof importLogs.$inferSelect;
|
||||
export type InsertImportLog = typeof importLogs.$inferInsert;
|
||||
|
||||
/**
|
||||
* LLM Logs table for storing raw LLM responses for debugging and improvement
|
||||
*/
|
||||
export const llmLogs = mysqlTable("llmLogs", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
userId: int("userId").notNull(),
|
||||
sourceFileId: int("sourceFileId"), // Optional: link to source file if applicable
|
||||
invoiceId: int("invoiceId"), // Optional: link to invoice if applicable
|
||||
operation: varchar("operation", { length: 50 }).notNull(), // "detection" or "extraction"
|
||||
model: varchar("model", { length: 50 }).notNull(), // LLM model used
|
||||
promptSent: text("promptSent").notNull(), // Full prompt sent to LLM
|
||||
rawResponse: text("rawResponse").notNull(), // Raw response from LLM (before cleaning)
|
||||
cleanedResponse: text("cleanedResponse"), // Response after markdown cleaning
|
||||
success: int("success").default(1).notNull(), // 1 = success, 0 = error
|
||||
errorMessage: text("errorMessage"), // Error message if failed
|
||||
processingTimeMs: int("processingTimeMs"), // Processing time in milliseconds
|
||||
pageRange: varchar("pageRange", { length: 50 }), // Page range for this log (e.g., "1-2")
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type LlmLog = typeof llmLogs.$inferSelect;
|
||||
export type InsertLlmLog = typeof llmLogs.$inferInsert;
|
||||
|
||||
Reference in New Issue
Block a user