diff --git a/client/src/pages/ImportSettings.tsx b/client/src/pages/ImportSettings.tsx index 9014fb6..eec0971 100644 --- a/client/src/pages/ImportSettings.tsx +++ b/client/src/pages/ImportSettings.tsx @@ -11,10 +11,13 @@ import DashboardLayout from "@/components/DashboardLayout"; export default function ImportSettings() { const { data: settings, isLoading } = trpc.importSettings.get.useQuery(); - const { data: serviceStatus } = trpc.emailImportService.status.useQuery(); + const { data: emailServiceStatus } = trpc.emailImportService.status.useQuery(); + const { data: folderServiceStatus } = trpc.folderImportService.status.useQuery(); const updateMutation = trpc.importSettings.update.useMutation(); - const startServiceMutation = trpc.emailImportService.start.useMutation(); - const stopServiceMutation = trpc.emailImportService.stop.useMutation(); + const startEmailServiceMutation = trpc.emailImportService.start.useMutation(); + const stopEmailServiceMutation = trpc.emailImportService.stop.useMutation(); + const startFolderServiceMutation = trpc.folderImportService.start.useMutation(); + const stopFolderServiceMutation = trpc.folderImportService.stop.useMutation(); // Manual import const [manualImportEnabled, setManualImportEnabled] = useState(true); @@ -174,9 +177,73 @@ export default function ImportSettings() { onChange={(e) => setAutoImportFrequency(parseInt(e.target.value) || 60)} />

- Intervalle de temps entre chaque vérification du dossier (minimum: 1 minute) -

- + Intervalle de temps entre chaque vérification du dossier (minimum: 1 minute) +

+ + + {/* Service Control Buttons */} +
+ {folderServiceStatus?.isRunning ? ( + + ) : ( + + )} + {folderServiceStatus?.isRunning && ( + + + Service actif + + )} +
)} @@ -286,12 +353,12 @@ export default function ImportSettings() { {/* Service Control Buttons */}
- {serviceStatus?.isRunning ? ( + {emailServiceStatus?.isRunning ? ( )} - {serviceStatus?.isRunning && ( + {emailServiceStatus?.isRunning && ( Service actif diff --git a/package.json b/package.json index 2b76337..f3ec63c 100644 --- a/package.json +++ b/package.json @@ -48,12 +48,14 @@ "@trpc/react-query": "^11.6.0", "@trpc/server": "^11.6.0", "@types/bcrypt": "^6.0.0", + "@types/chokidar": "^2.1.7", "@types/imap": "^0.8.43", "@types/jsonwebtoken": "^9.0.10", "@types/mailparser": "^3.4.6", "@types/ssh2-sftp-client": "^9.0.6", "axios": "^1.12.0", "bcrypt": "^6.0.0", + "chokidar": "^5.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f56c35..f80ab1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: '@types/bcrypt': specifier: ^6.0.0 version: 6.0.0 + '@types/chokidar': + specifier: ^2.1.7 + version: 2.1.7 '@types/imap': specifier: ^0.8.43 version: 0.8.43 @@ -139,6 +142,9 @@ importers: bcrypt: specifier: ^6.0.0 version: 6.0.0 + chokidar: + specifier: ^5.0.0 + version: 5.0.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2291,6 +2297,10 @@ packages: '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chokidar@2.1.7': + resolution: {integrity: sha512-A7/MFHf6KF7peCzjEC1BBTF8jpmZTokb3vr/A0NxRGfwRLK3Ws+Hq6ugVn6cJIMfM6wkCak/aplWrxbTcu8oig==} + deprecated: This is a stub types definition. chokidar provides its own type definitions, so you do not need this installed. + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -2657,6 +2667,10 @@ packages: chevrotain@11.0.3: resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -4188,6 +4202,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + recharts-scale@0.4.5: resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} @@ -6985,6 +7003,10 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 24.7.0 + '@types/chokidar@2.1.7': + dependencies: + chokidar: 5.0.0 + '@types/connect@3.4.38': dependencies: '@types/node': 24.7.0 @@ -7421,6 +7443,10 @@ snapshots: '@chevrotain/utils': 11.0.3 lodash-es: 4.17.21 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + chownr@3.0.0: {} class-variance-authority@0.7.1: @@ -9213,6 +9239,8 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdirp@5.0.0: {} + recharts-scale@0.4.5: dependencies: decimal.js-light: 2.5.1 diff --git a/server/emailImportService.ts b/server/emailImportService.ts index 3f78b93..fa43fe0 100644 --- a/server/emailImportService.ts +++ b/server/emailImportService.ts @@ -11,6 +11,7 @@ import { } from "./db"; import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; import { localStoragePut, generateStorageKey } from "./localStorage"; +import { sendImportNotification } from "./notificationService"; interface EmailImportConfig { userId: number; @@ -31,7 +32,7 @@ async function processEmailAttachment( userId: number, attachment: Attachment, emailSubject: string -): Promise { +): Promise<{ success: boolean; totalInvoices: number; imported: number; duplicates: number; errors: number }> { const fileName = attachment.filename || `email-attachment-${Date.now()}.pdf`; console.log(`[EmailImport] Processing attachment: ${fileName} from email: ${emailSubject}`); @@ -189,9 +190,23 @@ async function processEmailAttachment( console.log(`[EmailImport] Successfully processed attachment: ${fileName}`); console.log(`[EmailImport] Results: ${importedCount} imported, ${duplicatesCount} duplicates, ${errorsCount} errors`); + + return { + success: true, + totalInvoices: result.invoiceCount, + imported: importedCount, + duplicates: duplicatesCount, + errors: errorsCount, + }; } catch (error) { console.error(`[EmailImport] Error processing attachment ${attachment.filename}:`, error); - throw error; + return { + success: false, + totalInvoices: 0, + imported: 0, + duplicates: 0, + errors: 1, + }; } } @@ -276,7 +291,7 @@ async function checkEmailsForPDFs(config: EmailImportConfig): Promise { // Process each PDF attachment for (const attachment of pdfAttachments) { try { - await processEmailAttachment( + const result = await processEmailAttachment( config.userId, attachment, parsed.subject || "No subject" @@ -286,6 +301,18 @@ async function checkEmailsForPDFs(config: EmailImportConfig): Promise { if (!processedEmails.includes(seqno)) { processedEmails.push(seqno); } + + // Send notification after successful processing + if (result.success) { + await sendImportNotification(config.userId, { + source: "email", + fileName: attachment.filename || "email-attachment.pdf", + totalInvoices: result.totalInvoices, + imported: result.imported, + duplicates: result.duplicates, + errors: result.errors, + }); + } } catch (error) { console.error( `[EmailImport] Failed to process attachment from email ${seqno}:`, diff --git a/server/folderImportService.ts b/server/folderImportService.ts new file mode 100644 index 0000000..8c34c1c --- /dev/null +++ b/server/folderImportService.ts @@ -0,0 +1,398 @@ +import chokidar from "chokidar"; +import fs from "fs/promises"; +import path from "path"; +import { + getImportSettingsByUser, + createSourceFile, + updateSourceFile, + getUserSettings, + findDuplicateInvoice, + createInvoice, + createImportLog, +} from "./db"; +import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; +import { localStoragePut, generateStorageKey } from "./localStorage"; +import { sendImportNotification } from "./notificationService"; + +interface FolderImportConfig { + userId: number; + folderPath: string; + frequency: number; // in minutes +} + +// Store active watchers for each user +const activeWatchers = new Map(); + +// Track processed files to avoid reprocessing +const processedFiles = new Map>(); + +/** + * Process a single PDF file from the watched folder + */ +async function processFolderFile( + userId: number, + filePath: string, + folderPath: string +): Promise<{ success: boolean; imported: number; duplicates: number; errors: number }> { + const fileName = path.basename(filePath); + console.log(`[FolderImport] Processing file: ${fileName} for user ${userId}`); + + try { + // Read the file + const fileBuffer = await fs.readFile(filePath); + console.log(`[FolderImport] File size: ${fileBuffer.length} bytes`); + + // Store source file + const sourceFileKey = generateStorageKey(userId, fileName); + console.log(`[FolderImport] Generated storage key: ${sourceFileKey}`); + + let sourceFileUrl: string; + try { + const result = await localStoragePut(sourceFileKey, fileBuffer, "application/pdf"); + sourceFileUrl = result.url; + console.log(`[FolderImport] File stored successfully at: ${sourceFileUrl}`); + } catch (error) { + console.error(`[FolderImport] FAILED to store file:`, error); + throw new Error("Failed to store PDF file"); + } + + // Create source file record + const sourceFile = await createSourceFile({ + userId, + fileName, + fileKey: sourceFileKey, + fileUrl: sourceFileUrl, + processingStatus: "processing", + }); + + console.log(`[FolderImport] Source file record created with ID: ${sourceFile.id}`); + + // Get user settings for custom keywords + const settings = await getUserSettings(userId); + const customKeywords = settings ? { + invoiceNumber: settings.invoiceNumberKeywords, + deliveryNote: settings.deliveryNoteKeywords, + orderNumber: settings.orderNumberKeywords, + supplier: settings.supplierKeywords, + totalAmount: settings.totalAmountKeywords, + } : undefined; + + const model = settings?.llmModel || "mistral-large-latest"; + + // Extract invoices + console.log(`[FolderImport] Starting invoice extraction...`); + const result = await extractInvoicesWithMistral( + fileBuffer, + userId, + sourceFile.id, + model, + customKeywords + ); + + console.log(`[FolderImport] Extraction complete: ${result.invoiceCount} invoice(s) detected`); + + // Update source file with total count + await updateSourceFile(sourceFile.id, { + totalInvoicesDetected: result.invoiceCount, + processingProgress: `Extraction ${result.invoiceCount} facture(s) détectée(s)`, + }); + + // Process each invoice + let importedCount = 0; + let duplicatesCount = 0; + let errorsCount = 0; + const duplicateDetails: any[] = []; + const errorDetails: any[] = []; + + for (let i = 0; i < result.invoices.length; i++) { + const invoiceData = result.invoices[i]!; + + try { + // Update progress + await updateSourceFile(sourceFile.id, { + processingProgress: `Extraction ${i + 1}/${result.invoiceCount} factures...`, + }); + + // Check for duplicates + const duplicate = await findDuplicateInvoice( + invoiceData.supplierName, + invoiceData.invoiceNumber, + invoiceData.invoiceDate + ); + + if (duplicate) { + duplicatesCount++; + duplicateDetails.push({ + supplierName: invoiceData.supplierName, + invoiceNumber: invoiceData.invoiceNumber, + invoiceDate: invoiceData.invoiceDate, + }); + continue; + } + + // Generate metadata JSON + const metadataJson = generateMetadataJSON(invoiceData); + const metadataKey = generateStorageKey(userId, `${fileName}-${i + 1}-metadata.json`); + const { url: metadataUrl } = await localStoragePut( + metadataKey, + Buffer.from(metadataJson), + "application/json" + ); + + // Create invoice record + await createInvoice({ + userId, + sourceFileId: sourceFile.id, + invoiceIndexInFile: i + 1, + fileName: `${fileName} - Facture ${i + 1}`, + fileKey: sourceFileKey, + fileUrl: sourceFileUrl, + supplierName: invoiceData.supplierName, + invoiceNumber: invoiceData.invoiceNumber, + invoiceDate: invoiceData.invoiceDate, + deliveryNoteNumber: invoiceData.deliveryNoteNumber, + orderNumber: invoiceData.orderNumber, + totalAmount: invoiceData.totalAmount?.toString(), + pageRange: invoiceData.pageRange, + qualityScore: invoiceData.qualityScore, + metadataFileKey: metadataKey, + metadataFileUrl: metadataUrl, + status: "completed", + }); + + importedCount++; + } catch (error: any) { + errorsCount++; + errorDetails.push({ + invoiceIndex: i + 1, + error: error.message, + }); + } + } + + // Update source file status + await updateSourceFile(sourceFile.id, { + processingStatus: "completed", + processingProgress: `Terminé: ${importedCount} importée(s), ${duplicatesCount} doublon(s)`, + }); + + // Create import log + await createImportLog({ + userId, + sourceFileId: sourceFile.id, + fileName, + totalInvoicesDetected: result.invoiceCount, + invoicesImported: importedCount, + duplicatesIgnored: duplicatesCount, + errors: errorsCount, + duplicateDetails: duplicateDetails.length > 0 ? JSON.stringify(duplicateDetails) : null, + errorDetails: errorDetails.length > 0 ? JSON.stringify(errorDetails) : null, + }); + + // Move file to processed folder + try { + const processedDir = path.join(folderPath, "processed"); + await fs.mkdir(processedDir, { recursive: true }); + const newPath = path.join(processedDir, fileName); + await fs.rename(filePath, newPath); + console.log(`[FolderImport] File moved to processed folder: ${newPath}`); + } catch (error) { + console.error(`[FolderImport] Failed to move file to processed folder:`, error); + } + + console.log(`[FolderImport] Successfully processed file: ${fileName}`); + console.log(`[FolderImport] Results: ${importedCount} imported, ${duplicatesCount} duplicates, ${errorsCount} errors`); + + return { + success: true, + imported: importedCount, + duplicates: duplicatesCount, + errors: errorsCount, + }; + } catch (error) { + console.error(`[FolderImport] Error processing file ${fileName}:`, error); + return { + success: false, + imported: 0, + duplicates: 0, + errors: 1, + }; + } +} + +/** + * Scan folder for PDF files and process them + */ +async function scanAndProcessFolder(config: FolderImportConfig): Promise { + console.log(`[FolderImport] Scanning folder: ${config.folderPath} for user ${config.userId}`); + + try { + // Check if folder exists + try { + await fs.access(config.folderPath); + } catch (error) { + console.error(`[FolderImport] Folder does not exist: ${config.folderPath}`); + return; + } + + // Read all files in the folder + const files = await fs.readdir(config.folderPath); + const pdfFiles = files.filter(file => file.toLowerCase().endsWith('.pdf')); + + if (pdfFiles.length === 0) { + console.log(`[FolderImport] No PDF files found in folder for user ${config.userId}`); + return; + } + + console.log(`[FolderImport] Found ${pdfFiles.length} PDF file(s) for user ${config.userId}`); + + // Get or create processed files set for this user + if (!processedFiles.has(config.userId)) { + processedFiles.set(config.userId, new Set()); + } + const userProcessedFiles = processedFiles.get(config.userId)!; + + // Track totals for notification + let totalImported = 0; + let totalDuplicates = 0; + let totalErrors = 0; + let totalInvoices = 0; + let processedCount = 0; + + // Process each PDF file + for (const file of pdfFiles) { + const filePath = path.join(config.folderPath, file); + + // Skip if already processed + if (userProcessedFiles.has(filePath)) { + console.log(`[FolderImport] Skipping already processed file: ${file}`); + continue; + } + + // Process the file + const result = await processFolderFile(config.userId, filePath, config.folderPath); + + // Accumulate results + if (result.success) { + totalImported += result.imported; + totalDuplicates += result.duplicates; + totalErrors += result.errors; + totalInvoices += result.imported + result.duplicates; + processedCount++; + } + + // Mark as processed + userProcessedFiles.add(filePath); + } + + // Send notification if any files were processed + if (processedCount > 0) { + await sendImportNotification(config.userId, { + source: "folder", + totalFiles: processedCount, + totalInvoices, + imported: totalImported, + duplicates: totalDuplicates, + errors: totalErrors, + }); + } + } catch (error) { + console.error(`[FolderImport] Error scanning folder for user ${config.userId}:`, error); + } +} + +/** + * Start folder import service for a user + */ +export async function startFolderImportService(userId: number): Promise { + try { + // Get user's import settings + const settings = await getImportSettingsByUser(userId); + + if (!settings || settings.autoImportEnabled !== 1) { + console.log(`[FolderImport] Auto import not enabled for user ${userId}`); + return false; + } + + if (!settings.autoImportSourcePath) { + console.log(`[FolderImport] Folder path not configured for user ${userId}`); + return false; + } + + // Stop existing service if running + stopFolderImportService(userId); + + const config: FolderImportConfig = { + userId, + folderPath: settings.autoImportSourcePath, + frequency: settings.autoImportFrequency || 30, + }; + + const frequencyMs = config.frequency * 60 * 1000; // Convert minutes to milliseconds + + console.log( + `[FolderImport] Starting folder import service for user ${userId} with frequency ${config.frequency} minutes` + ); + + // Run immediately on start + scanAndProcessFolder(config).catch((error) => { + console.error(`[FolderImport] Error scanning folder for user ${userId}:`, error); + }); + + // Set up interval for periodic scans + const interval = setInterval(() => { + scanAndProcessFolder(config).catch((error) => { + console.error(`[FolderImport] Error scanning folder for user ${userId}:`, error); + }); + }, frequencyMs); + + // Note: We're not using chokidar watcher for now, just periodic scans + // This is simpler and more reliable for the initial implementation + const dummyWatcher = chokidar.watch(config.folderPath, { ignored: /processed/ }); + + activeWatchers.set(userId, { watcher: dummyWatcher, interval }); + console.log(`[FolderImport] Folder import service started for user ${userId}`); + + return true; + } catch (error) { + console.error(`[FolderImport] Error starting folder import service for user ${userId}:`, error); + return false; + } +} + +/** + * Stop folder import service for a user + */ +export function stopFolderImportService(userId: number): void { + const service = activeWatchers.get(userId); + if (service) { + clearInterval(service.interval); + service.watcher.close(); + activeWatchers.delete(userId); + + // Clear processed files tracking + processedFiles.delete(userId); + + console.log(`[FolderImport] Folder import service stopped for user ${userId}`); + } +} + +/** + * Check if folder import service is running for a user + */ +export function isFolderImportServiceRunning(userId: number): boolean { + return activeWatchers.has(userId); +} + +/** + * Stop all folder import services + */ +export function stopAllFolderImportServices(): void { + activeWatchers.forEach((service, userId) => { + clearInterval(service.interval); + service.watcher.close(); + console.log(`[FolderImport] Stopped folder import service for user ${userId}`); + }); + activeWatchers.clear(); + processedFiles.clear(); +} diff --git a/server/notificationService.ts b/server/notificationService.ts new file mode 100644 index 0000000..e4ff0b4 --- /dev/null +++ b/server/notificationService.ts @@ -0,0 +1,89 @@ +import { notifyOwner } from "./_core/notification"; + +export interface ImportNotificationData { + source: "email" | "folder"; + fileName?: string; + totalFiles?: number; + totalInvoices: number; + imported: number; + duplicates: number; + errors: number; +} + +/** + * Send notification after automatic import + */ +export async function sendImportNotification( + userId: number, + data: ImportNotificationData +): Promise { + try { + const sourceLabel = data.source === "email" ? "email" : "dossier"; + + let title: string; + let content: string; + + if (data.source === "email" && data.fileName) { + // Single file from email + title = `📧 Import automatique email - ${data.fileName}`; + content = ` +**Fichier traité :** ${data.fileName} + +**Résultats :** +- ✅ **${data.imported}** facture(s) importée(s) +- 🔄 **${data.duplicates}** doublon(s) ignoré(s) +- ❌ **${data.errors}** erreur(s) + +**Total détecté :** ${data.totalInvoices} facture(s) + +--- +*Import automatique depuis email* + `.trim(); + } else if (data.source === "folder") { + // Folder scan (potentially multiple files) + title = `📁 Import automatique dossier - ${data.totalFiles || 0} fichier(s)`; + content = ` +**Fichiers traités :** ${data.totalFiles || 0} fichier(s) PDF + +**Résultats :** +- ✅ **${data.imported}** facture(s) importée(s) +- 🔄 **${data.duplicates}** doublon(s) ignoré(s) +- ❌ **${data.errors}** erreur(s) + +**Total détecté :** ${data.totalInvoices} facture(s) + +--- +*Import automatique depuis dossier* + `.trim(); + } else { + // Fallback + title = `📥 Import automatique - ${sourceLabel}`; + content = ` +**Résultats :** +- ✅ **${data.imported}** facture(s) importée(s) +- 🔄 **${data.duplicates}** doublon(s) ignoré(s) +- ❌ **${data.errors}** erreur(s) + +**Total détecté :** ${data.totalInvoices} facture(s) + `.trim(); + } + + console.log(`[Notification] Sending import notification for user ${userId}:`, title); + + const success = await notifyOwner({ + title, + content, + }); + + if (success) { + console.log(`[Notification] Import notification sent successfully for user ${userId}`); + } else { + console.error(`[Notification] Failed to send import notification for user ${userId}`); + } + + return success; + } catch (error) { + console.error(`[Notification] Error sending import notification for user ${userId}:`, error); + return false; + } +} diff --git a/server/routers.ts b/server/routers.ts index 9951cd5..af8d4f3 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -34,6 +34,7 @@ import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtra import { localStoragePut, generateStorageKey } from "./localStorage"; import { testSftpConnection, exportInvoiceToSftp, getUserSftpConfig } from "./sftpExport"; import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning } from "./emailImportService"; +import { startFolderImportService, stopFolderImportService, isFolderImportServiceRunning } from "./folderImportService"; import { TRPCError } from "@trpc/server"; // Admin-only procedure @@ -576,6 +577,24 @@ export const appRouter = router({ }), }), + // ============= FOLDER IMPORT SERVICE ROUTES ============= + folderImportService: router({ + start: protectedProcedure.mutation(async ({ ctx }) => { + const started = await startFolderImportService(ctx.user.id); + return { success: started }; + }), + + stop: protectedProcedure.mutation(async ({ ctx }) => { + stopFolderImportService(ctx.user.id); + return { success: true }; + }), + + status: protectedProcedure.query(async ({ ctx }) => { + const isRunning = isFolderImportServiceRunning(ctx.user.id); + return { isRunning }; + }), + }), + // ============= EMAIL IMPORT SERVICE ROUTES ============= emailImportService: router({ start: protectedProcedure.mutation(async ({ ctx }) => { diff --git a/todo.md b/todo.md index 656fb98..07adad2 100644 --- a/todo.md +++ b/todo.md @@ -159,3 +159,20 @@ - [x] Ajouter des routes tRPC pour démarrer/arrêter le service - [x] Ajouter des boutons de contrôle du service dans la page Paramètres de réception - [ ] Tester l'import automatique par email avec un vrai compte + +## Import automatique depuis dossier +- [x] Installer la dépendance chokidar pour surveiller les dossiers +- [x] Créer le service folderImportService.ts pour surveiller un dossier +- [x] Implémenter la détection des nouveaux fichiers PDF dans le dossier +- [x] Implémenter le traitement des fichiers détectés +- [x] Implémenter le déplacement des fichiers traités vers un sous-dossier "processed" +- [x] Créer un scheduler pour vérifier le dossier selon la fréquence configurée +- [x] Ajouter des routes tRPC pour démarrer/arrêter le service +- [x] Ajouter des boutons de contrôle dans la page Paramètres de réception + +## Système de notifications +- [x] Créer le service notificationService.ts pour gérer les notifications +- [x] Implémenter l'envoi de notifications après import automatique email +- [x] Implémenter l'envoi de notifications après import automatique dossier +- [x] Inclure le résumé : nombre de factures importées, doublons ignorés, erreurs +- [ ] Tester les fonctionnalités avec des données réelles