Nouvelle fonctionnalité : ✅ Champ "Dossier d'export" ajouté dans la page Paramètres de réception ✅ Permet de configurer le chemin du dossier de destination pour l'export des factures ✅ Stockage en base de données dans la table importSettings ✅ Interface utilisateur avec carte dédiée et champ de saisie Modifications techniques : - drizzle/schema.ts : Ajout du champ exportFolder (type text, nullable) dans la table importSettings - server/routers.ts : Ajout de exportFolder dans les routes importSettings.get et importSettings.update - client/src/pages/ImportSettings.tsx : Ajout du state, initialisation, et carte UI pour le dossier d'export - Migration de base de données : 0008_orange_prism.sql appliquée avec succès Utilisation : 1. Accéder à "Paramètres de réception" dans le menu 2. Descendre jusqu'à la section "Dossier d'export" 3. Saisir le chemin absolu du dossier (ex: /chemin/vers/dossier/export) 4. Cliquer sur "Enregistrer les paramètres" 5. Le chemin est sauvegardé et pourra être utilisé pour les exports futurs Ce paramètre permettra de centraliser la configuration du dossier d'export pour toutes les fonctionnalités d'export de l'application.
256 lines
12 KiB
TypeScript
256 lines
12 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.)
|
|
|
|
// 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;
|