Nouvelle fonctionnalité visuelle : ✅ Valeurs remplies automatiquement par les règles : affichées en VERT ✅ Valeurs saisies manuellement : affichées en BLEU ✅ Traçage intelligent de l'origine des valeurs (automatique ou manuelle) ✅ Mise à jour automatique de la couleur lors d'une modification manuelle Architecture technique : - drizzle/schema.ts : Ajout du champ autoFilledFields (JSON) pour tracer l'origine - server/automationEngine.ts : Enregistrement des champs remplis automatiquement - server/routers.ts : Ajout de autoFilledFields dans le schéma d'update - client/src/pages/InvoicesBAP.tsx : Fonction getFieldColor() pour déterminer la couleur - Migration 0010_late_thor_girl.sql appliquée avec succès Fonctionnement : 1. **Import de facture** : Les règles d'automatisme s'appliquent et marquent les champs remplis dans autoFilledFields 2. **Affichage** : La fonction getFieldColor() analyse autoFilledFields pour déterminer la couleur : - Champ dans autoFilledFields → VERT (rempli automatiquement) - Champ avec valeur mais pas dans autoFilledFields → BLEU (rempli manuellement) - Champ vide → Pas de couleur 3. **Modification manuelle** : Lors d'une édition, le champ est retiré de autoFilledFields et passe en BLEU Bénéfices utilisateur : - Visibilité immédiate sur l'origine des données - Confiance accrue dans les automatismes (vert = validé par règle) - Identification rapide des saisies manuelles nécessitant vérification - Traçabilité complète du remplissage des champs métier Cette fonctionnalité améliore considérablement la transparence du système d'automatisation et facilite le contrôle qualité des données.
286 lines
14 KiB
TypeScript
286 lines
14 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
|
|
|
|
// Business fields (optional)
|
|
serviceConcerne: varchar("serviceConcerne", { length: 100 }), // Service concerné (DSI, TRAVAUX, etc.)
|
|
typeAchat: mysqlEnum("typeAchat", ["CAPEX", "OPEX"]), // Type d'achat
|
|
ventilationComptable: varchar("ventilationComptable", { length: 100 }), // Ventilation comptable (TOUS, PA, HEP, etc.)
|
|
|
|
// Automation tracking - JSON array of field names that were auto-filled by automation rules
|
|
// Example: ["typeAchat", "serviceConcerne", "ventilationComptable"]
|
|
autoFilledFields: text("autoFilledFields"), // JSON string array
|
|
|
|
// Extracted text from PDF
|
|
extractedText: text("extractedText"), // Full text extracted from the invoice PDF
|
|
|
|
// Subscription flag
|
|
isSubscription: int("isSubscription").default(0).notNull(), // 0 = NON, 1 = OUI
|
|
|
|
// 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"
|
|
subscriptionKeywords: text("subscriptionKeywords"), // Ex: "Abonnement, Subscription, Mensuel, Annuel"
|
|
// 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)
|
|
|
|
// Export folder settings
|
|
exportFolder: text("exportFolder"), // Path to folder for exporting invoices
|
|
|
|
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;
|
|
|
|
/**
|
|
* Department list table for managing "Service concerné" values
|
|
*/
|
|
export const departmentList = mysqlTable("departmentList", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(), // Each user has their own list
|
|
name: varchar("name", { length: 100 }).notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
}, (table) => {
|
|
return {
|
|
// Unique constraint: no duplicate department names for the same user
|
|
userDepartmentIdx: uniqueIndex("user_department_unique").on(table.userId, table.name),
|
|
};
|
|
});
|
|
|
|
export type Department = typeof departmentList.$inferSelect;
|
|
export type InsertDepartment = typeof departmentList.$inferInsert;
|
|
|
|
/**
|
|
* Accounting allocation list table for managing "Ventilation comptable" values
|
|
*/
|
|
export const accountingAllocationList = mysqlTable("accountingAllocationList", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(), // Each user has their own list
|
|
name: varchar("name", { length: 100 }).notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
}, (table) => {
|
|
return {
|
|
// Unique constraint: no duplicate allocation names for the same user
|
|
userAllocationIdx: uniqueIndex("user_allocation_unique").on(table.userId, table.name),
|
|
};
|
|
});
|
|
|
|
export type AccountingAllocation = typeof accountingAllocationList.$inferSelect;
|
|
export type InsertAccountingAllocation = typeof accountingAllocationList.$inferInsert;
|
|
|
|
/**
|
|
* Automation rules table for automatic field filling based on conditions
|
|
*/
|
|
export const automationRules = mysqlTable("automationRules", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(), // Each user has their own rules
|
|
name: varchar("name", { length: 255 }).notNull(), // Rule name for identification
|
|
isActive: int("isActive").default(1).notNull(), // 0 = disabled, 1 = enabled
|
|
priority: int("priority").default(0).notNull(), // Execution order (lower = higher priority)
|
|
|
|
// Conditions (IF) - JSON array of condition objects
|
|
// Example: [{"field": "supplierName", "operator": "contains", "value": "Microsoft"}, {"field": "totalAmount", "operator": ">", "value": "1000"}]
|
|
conditions: text("conditions").notNull(), // JSON string
|
|
conditionsLogic: mysqlEnum("conditionsLogic", ["AND", "OR"]).default("AND").notNull(), // How to combine conditions
|
|
|
|
// Actions (THEN) - JSON object with field assignments
|
|
// Example: {"typeAchat": "CAPEX", "serviceConcerne": "DSI", "ventilationComptable": "TOUS"}
|
|
actions: text("actions").notNull(), // JSON string
|
|
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type AutomationRule = typeof automationRules.$inferSelect;
|
|
export type InsertAutomationRule = typeof automationRules.$inferInsert;
|