Files
demat-facturation/drizzle/schema.ts
Manus 35efbf632d Checkpoint: Checkpoint saved: Ajout d'une page "Paramètres de réception" complète pour configurer les différentes méthodes d'import de factures.
Nouvelles fonctionnalités :
 Table importSettings dans la base de données (7 tables au total)
 Routes tRPC pour gérer les paramètres de réception (get, update)
 Page "Paramètres de réception" accessible via le menu de navigation

Configuration disponible :
1. **Import manuel** : Activation/désactivation de l'upload manuel de fichiers PDF via l'interface

2. **Import automatique depuis dossier** :
   - Activation/désactivation
   - Configuration du chemin du dossier source à surveiller
   - Fréquence de lecture du dossier (en minutes)

3. **Import par email** :
   - Activation/désactivation
   - Configuration du compte email (adresse + mot de passe)
   - Serveur IMAP (host + port)
   - Fréquence de lecture des emails (en minutes)

Interface utilisateur :
- 3 cartes distinctes avec icônes (Upload, FolderOpen, Mail)
- Switches pour activer/désactiver chaque méthode
- Champs de configuration qui apparaissent uniquement si la méthode est activée
- Bouton "Enregistrer les paramètres" avec feedback visuel
- Notifications toast pour confirmer la sauvegarde ou signaler les erreurs

Architecture :
- Schéma de base de données : table importSettings avec 14 colonnes
- Backend : Fonctions db.ts (getImportSettingsByUser, upsertImportSettings)
- Routes tRPC : importSettings.get et importSettings.update
- Frontend : Page ImportSettings.tsx avec gestion d'état React
- Navigation : Nouveau lien "Paramètres de réception" dans le menu latéral

La page est maintenant accessible via /import-settings et permet aux utilisateurs de configurer toutes les méthodes d'importation de factures de manière centralisée.
2026-02-10 12:05:23 -05:00

205 lines
10 KiB
TypeScript

import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, uniqueIndex, decimal } from "drizzle-orm/mysql-core";
/**
* Core user table backing auth flow.
* Supports multiple authentication methods: Manus OAuth, local, and Azure AD
*/
export const users = mysqlTable("users", {
id: int("id").autoincrement().primaryKey(),
/** 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 }).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(),
});
export type User = typeof users.$inferSelect;
export type InsertUser = typeof users.$inferInsert;
/**
* 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"),
// Export status
exportStatus: mysqlEnum("exportStatus", ["not_exported", "exported", "export_error"]).default("not_exported").notNull(),
// 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 settings table for configuring different import methods
*/
export const importSettings = mysqlTable("importSettings", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull().unique(), // One settings record per user
// Manual import settings
manualImportEnabled: int("manualImportEnabled").default(1).notNull(), // 0 = disabled, 1 = enabled
// Automatic import from folder settings
autoImportEnabled: int("autoImportEnabled").default(0).notNull(), // 0 = disabled, 1 = enabled
autoImportSourcePath: text("autoImportSourcePath"), // Path to folder to watch
autoImportFrequency: int("autoImportFrequency").default(60).notNull(), // Frequency in minutes (default: 60 = 1 hour)
// Email import settings
emailImportEnabled: int("emailImportEnabled").default(0).notNull(), // 0 = disabled, 1 = enabled
emailImportAddress: varchar("emailImportAddress", { length: 320 }), // Email address to monitor
emailImportPassword: text("emailImportPassword"), // Encrypted password
emailImportHost: varchar("emailImportHost", { length: 255 }), // IMAP server host
emailImportPort: int("emailImportPort").default(993), // IMAP port (default: 993 for SSL)
emailImportFrequency: int("emailImportFrequency").default(30).notNull(), // Frequency in minutes (default: 30)
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type ImportSettings = typeof importSettings.$inferSelect;
export type InsertImportSettings = typeof importSettings.$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;