Checkpoint: Automatismes séparé en onglets Import et Export. Les règles d’export ciblent un destinataire ou une ventilation, avec destination locale, Teams ou SharePoint et ouverture navigateur optionnelle. La validation BAP unique ou en masse applique la première règle active par priorité puis conserve le paramétrage historique comme repli. Paramètres d’export déplacés vers Automatismes et page renommée Paramètres. Migration additive, tests, TypeScript, build et contrôles visuels validés.

This commit is contained in:
Manus
2026-09-02 12:13:23 +00:00
parent da19629ee0
commit 582ca77f28
15 changed files with 2978 additions and 23 deletions

View File

@@ -66,6 +66,11 @@ import {
createAutomationRule,
updateAutomationRule,
deleteAutomationRule,
getExportAutomationRulesByUser,
getExportAutomationRuleById,
createExportAutomationRule,
updateExportAutomationRule,
deleteExportAutomationRule,
getSignaturesByUser,
getSignatureById,
createSignature,
@@ -97,6 +102,7 @@ import { calculateFileSha256 } from "./fileFingerprint";
import { localStoragePut, generateStorageKey } from "./localStorage";
import { testSftpConnection, exportInvoiceToSftp, getUserSftpConfig } from "./sftpExport";
import { drawBapCartouche } from "./bapCartouche";
import { resolveBapExportDestination } from "./exportDestinationResolver";
import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning, triggerEmailCheck, triggerManualEmailCheck, testImapConnection } from "./emailImportService";
import { startFolderImportService, stopFolderImportService, isFolderImportServiceRunning } from "./folderImportService";
import { TRPCError } from "@trpc/server";
@@ -521,9 +527,10 @@ export const appRouter = router({
const { localStoragePut, generateStorageKey } = await import('./localStorage');
const importSettings = await getImportSettingsByUser(ctx.user.id);
const bapExportMode = importSettings?.bapExportMode || 'browser';
const exportFolder = importSettings?.exportFolder || null;
const exportFolderType = (importSettings as any)?.exportFolderType || 'local';
const resolvedDestination = await resolveBapExportDestination(ctx.user.id, invoice, importSettings);
const bapExportMode = resolvedDestination.exportMode;
const exportFolder = resolvedDestination.destinationPath;
const exportFolderType = resolvedDestination.destinationType;
const STORAGE_BASE_PATH = process.env.STORAGE_BASE_PATH || path.join(process.cwd(), 'storage');
let pdfUrl: string | null = null;
@@ -609,11 +616,11 @@ export const appRouter = router({
const _bapNumber = (invoice.invoiceNumber || '').replace(/[^a-zA-Z0-9\-]/g, '').trim();
const bapFilename = [_bapDateStr, _bapSupplier, _bapNumber].filter(Boolean).join(' - ') + '.pdf';
console.log(`[BAP] Mode export: ${bapExportMode}, type: ${exportFolderType}, dossier: ${exportFolder ? 'configuré' : 'non configuré'}`);
console.log(`[BAP] Mode export: ${bapExportMode}, type: ${exportFolderType}, source: ${resolvedDestination.source}, dossier: ${exportFolder ? 'configuré' : 'non configuré'}`);
if ((bapExportMode === 'folder' || bapExportMode === 'both') && exportFolder) {
if (exportFolderType === 'sharepoint') {
// Mode SharePoint : upload via Microsoft Graph
console.log('[BAP] Démarrage upload SharePoint pour:', bapFilename);
if (exportFolderType === 'sharepoint' || exportFolderType === 'teams') {
// Les fichiers Teams sont déposés via le site SharePoint de léquipe.
console.log('[BAP] Démarrage upload Microsoft 365 pour:', bapFilename);
const { uploadToSharePoint } = await import('./sharepoint');
const spResult = await uploadToSharePoint(
{
@@ -740,9 +747,6 @@ export const appRouter = router({
const { PDFDocument, rgb, StandardFonts } = await import('pdf-lib');
const { localStoragePut, generateStorageKey } = await import('./localStorage');
const importSettings = await getImportSettingsByUser(ctx.user.id);
const bapExportMode = importSettings?.bapExportMode || 'browser';
const exportFolder = importSettings?.exportFolder || null;
const exportFolderType = (importSettings as any)?.exportFolderType || 'local';
const STORAGE_BASE_PATH = process.env.STORAGE_BASE_PATH || path.join(process.cwd(), 'storage');
const serviceSignaturesList = await getServiceSignaturesByUser(ctx.user.id);
const results: Array<{ id: number; success: boolean; pdfUrl?: string | null; exportPath?: string | null; error?: string }> = [];
@@ -756,6 +760,10 @@ export const appRouter = router({
let sharepointUploadStatus: 'success' | 'error' | 'skipped' | null = null;
let sharepointUploadPath: string | null = null;
let sharepointUploadError: string | null = null;
const resolvedDestination = await resolveBapExportDestination(ctx.user.id, invoice, importSettings);
const bapExportMode = resolvedDestination.exportMode;
const exportFolder = resolvedDestination.destinationPath;
const exportFolderType = resolvedDestination.destinationType;
try {
// ─ Lecture du PDF source ─
let sourcePdfBytes: Buffer;
@@ -826,7 +834,7 @@ export const appRouter = router({
const _bapNumber2 = (invoice.invoiceNumber || '').replace(/[^a-zA-Z0-9\-]/g, '').trim();
const bapFilename = [_bapDateStr2, _bapSupplier2, _bapNumber2].filter(Boolean).join(' - ') + '.pdf';
if ((bapExportMode === 'folder' || bapExportMode === 'both') && exportFolder) {
if (exportFolderType === 'sharepoint') {
if (exportFolderType === 'sharepoint' || exportFolderType === 'teams') {
const { uploadToSharePoint } = await import('./sharepoint');
const spResult = await uploadToSharePoint(
{
@@ -2276,6 +2284,55 @@ export const appRouter = router({
}),
}),
// ============= EXPORT AUTOMATION RULES =============
exportAutomationRules: router({
list: protectedProcedure.query(async ({ ctx }) => {
return getExportAutomationRulesByUser(ctx.user.id);
}),
create: protectedProcedure
.input(z.object({
name: z.string().trim().min(1).max(255),
conditionField: z.enum(["recipientName", "ventilationComptable"]),
conditionValue: z.string().trim().min(1).max(255),
destinationType: z.enum(["local", "teams", "sharepoint"]),
destinationPath: z.string().trim().min(1),
openInBrowser: z.number().int().min(0).max(1).default(0),
isActive: z.number().int().min(0).max(1).default(1),
priority: z.number().int().min(0).default(0),
}))
.mutation(async ({ input, ctx }) => createExportAutomationRule({ userId: ctx.user.id, ...input })),
update: protectedProcedure
.input(z.object({
id: z.number().int().positive(),
name: z.string().trim().min(1).max(255).optional(),
conditionField: z.enum(["recipientName", "ventilationComptable"]).optional(),
conditionValue: z.string().trim().min(1).max(255).optional(),
destinationType: z.enum(["local", "teams", "sharepoint"]).optional(),
destinationPath: z.string().trim().min(1).optional(),
openInBrowser: z.number().int().min(0).max(1).optional(),
isActive: z.number().int().min(0).max(1).optional(),
priority: z.number().int().min(0).optional(),
}))
.mutation(async ({ input, ctx }) => {
const existing = await getExportAutomationRuleById(input.id);
if (!existing || (ctx.user.role !== "admin" && existing.userId !== ctx.user.id)) {
throw new TRPCError({ code: "NOT_FOUND" });
}
const { id, ...data } = input;
return updateExportAutomationRule(id, data);
}),
delete: protectedProcedure
.input(z.object({ id: z.number().int().positive() }))
.mutation(async ({ input, ctx }) => {
const existing = await getExportAutomationRuleById(input.id);
if (!existing || (ctx.user.role !== "admin" && existing.userId !== ctx.user.id)) {
throw new TRPCError({ code: "NOT_FOUND" });
}
await deleteExportAutomationRule(input.id);
return { success: true };
}),
}),
// ============= SIGNATURES ROUTES =============
signatures: router({
list: protectedProcedure.query(async ({ ctx }) => {