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.
This commit is contained in:
Manus
2026-02-10 12:05:23 -05:00
parent 4690b16c9f
commit 35efbf632d
10 changed files with 1418 additions and 2 deletions

View File

@@ -17,7 +17,10 @@ import {
ImportLog,
llmLogs,
InsertLlmLog,
LlmLog
LlmLog,
importSettings,
InsertImportSettings,
ImportSettings
} from "../drizzle/schema";
import { ENV } from './_core/env';
@@ -434,3 +437,40 @@ export async function getLlmLogsByInvoice(invoiceId: number): Promise<LlmLog[]>
if (!db) return [];
return db.select().from(llmLogs).where(eq(llmLogs.invoiceId, invoiceId)).orderBy(desc(llmLogs.createdAt));
}
// ============= IMPORT SETTINGS OPERATIONS =============
export async function getImportSettingsByUser(userId: number): Promise<ImportSettings | null> {
const db = await getDb();
if (!db) return null;
const result = await db.select().from(importSettings).where(eq(importSettings.userId, userId)).limit(1);
return result[0] || null;
}
export async function upsertImportSettings(data: InsertImportSettings): Promise<ImportSettings> {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Check if settings exist for this user
const existing = await getImportSettingsByUser(data.userId!);
if (existing) {
// Update existing settings
await db.update(importSettings)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(importSettings.userId, data.userId!));
const updated = await getImportSettingsByUser(data.userId!);
return updated!;
} else {
// Insert new settings
const result = await db.insert(importSettings).values(data);
const insertedId = Number(result[0].insertId);
const inserted = await db.select().from(importSettings).where(eq(importSettings.id, insertedId)).limit(1);
return inserted[0]!;
}
}

View File

@@ -26,6 +26,8 @@ import {
getImportLogsByUser,
getLlmLogsBySourceFile,
getLlmLogsByInvoice,
getImportSettingsByUser,
upsertImportSettings,
} from "./db";
import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth";
import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor";
@@ -572,6 +574,54 @@ export const appRouter = router({
return getLlmLogsByInvoice(input.invoiceId);
}),
}),
// ============= IMPORT SETTINGS ROUTES =============
importSettings: router({
get: protectedProcedure.query(async ({ ctx }) => {
const settings = await getImportSettingsByUser(ctx.user.id);
// Return default settings if none exist
if (!settings) {
return {
userId: ctx.user.id,
manualImportEnabled: 1,
autoImportEnabled: 0,
autoImportSourcePath: null,
autoImportFrequency: 60,
emailImportEnabled: 0,
emailImportAddress: null,
emailImportPassword: null,
emailImportHost: null,
emailImportPort: 993,
emailImportFrequency: 30,
};
}
return settings;
}),
update: protectedProcedure
.input(z.object({
manualImportEnabled: z.number().min(0).max(1),
autoImportEnabled: z.number().min(0).max(1),
autoImportSourcePath: z.string().nullable().optional(),
autoImportFrequency: z.number().min(1).optional(),
emailImportEnabled: z.number().min(0).max(1),
emailImportAddress: z.string().email().nullable().optional(),
emailImportPassword: z.string().nullable().optional(),
emailImportHost: z.string().nullable().optional(),
emailImportPort: z.number().min(1).max(65535).optional(),
emailImportFrequency: z.number().min(1).optional(),
}))
.mutation(async ({ input, ctx }) => {
const settings = await upsertImportSettings({
userId: ctx.user.id,
...input,
});
return settings;
}),
}),
});
export type AppRouter = typeof appRouter;