Checkpoint: Ajout de l'onglet "Paramétrage" dans la page Ventilation FreePro :

- Table DB freeproSettings (URL portail, credentials, fréquence, date antériorité, statut dernière récupération)
- Service freeproAutoImport.ts : connexion HTTP au portail FreePro, téléchargement CSV, pipeline d'import
- Procédures tRPC : getSettings, saveSettings, testConnection, forceImport
- Job périodique en mémoire (daily/weekly/monthly) via setInterval
- Frontend : wrapper Tabs (onglet 1 = Import & Historique, onglet 2 = Paramétrage)
- Onglet Paramétrage : credentials, fréquence, date antériorité, bouton "Forcer récupération", statut
- Tests unitaires : 8 tests passés
This commit is contained in:
Manus
2026-06-08 09:24:05 -04:00
parent 51830b88be
commit e32d540bc9
10 changed files with 3615 additions and 295 deletions

View File

@@ -86,11 +86,14 @@ import { startEmailImportService, stopEmailImportService, isEmailImportServiceRu
import { startFolderImportService, stopFolderImportService, isFolderImportServiceRunning } from "./folderImportService";
import { TRPCError } from "@trpc/server";
import { processFreeproExcel } from "./freeproService";
import { runFreeproAutoImport, testFreeproConnection, startFreeproAutoJob, stopFreeproAutoJob, frequencyToMs } from "./freeproAutoImport";
import {
createFreeproImport,
getFreeproImportsByUser,
getFreeproImportWithLines,
deleteFreeproImport,
getFreeproSettings,
upsertFreeproSettings,
} from "./db";
// Admin-only procedure
@@ -2293,6 +2296,78 @@ export const appRouter = router({
return { base64, fileName };
}),
/** Récupère les paramètres de connexion automatique FreePro */
getSettings: protectedProcedure.query(async ({ ctx }) => {
const s = await getFreeproSettings(ctx.user.id);
// Ne pas exposer le mot de passe en clair
if (s) {
return {
...s,
loginPassword: s.loginPassword ? '••••••••' : null,
hasPassword: !!s.loginPassword,
};
}
return null;
}),
/** Sauvegarde les paramètres de connexion automatique FreePro */
saveSettings: protectedProcedure
.input(
z.object({
portalUrl: z.string().url().optional(),
loginEmail: z.string().email().optional().or(z.literal('')),
loginPassword: z.string().optional(), // vide = ne pas changer
frequency: z.enum(['manual', 'daily', 'weekly', 'monthly']).optional(),
maxAnteriority: z.number().nullable().optional(), // timestamp Unix en secondes
autoEnabled: z.number().min(0).max(1).optional(),
})
)
.mutation(async ({ input, ctx }) => {
const existing = await getFreeproSettings(ctx.user.id);
const updateData: any = {};
if (input.portalUrl !== undefined) updateData.portalUrl = input.portalUrl;
if (input.loginEmail !== undefined) updateData.loginEmail = input.loginEmail || null;
// Ne mettre à jour le mot de passe que si une vraie valeur est fournie
if (input.loginPassword && input.loginPassword !== '••••••••') {
updateData.loginPassword = input.loginPassword;
}
if (input.frequency !== undefined) updateData.frequency = input.frequency;
if (input.maxAnteriority !== undefined) updateData.maxAnteriority = input.maxAnteriority;
if (input.autoEnabled !== undefined) updateData.autoEnabled = input.autoEnabled;
await upsertFreeproSettings(ctx.user.id, updateData);
// Gérer le job périodique
const newSettings = await getFreeproSettings(ctx.user.id);
if (newSettings?.autoEnabled && newSettings.frequency !== 'manual') {
const ms = frequencyToMs(newSettings.frequency);
if (ms > 0) startFreeproAutoJob(ctx.user.id, ms);
} else {
stopFreeproAutoJob(ctx.user.id);
}
return { success: true };
}),
/** Teste la connexion au portail FreePro */
testConnection: protectedProcedure
.input(
z.object({
email: z.string().email(),
password: z.string().min(1),
})
)
.mutation(async ({ input }) => {
return testFreeproConnection(input.email, input.password);
}),
/** Force la récupération immédiate des factures FreePro */
forceImport: protectedProcedure.mutation(async ({ ctx }) => {
const result = await runFreeproAutoImport(ctx.user.id);
return result;
}),
/** Exporte la ventilation FreePro vers SharePoint */
exportToSharePoint: protectedProcedure
.input(z.object({ id: z.number(), pdfBase64: z.string().optional() }))