Checkpoint: Ajout de l'onglet "Signatures" dans la page Paramètres : table DB signatures (firstName, lastName, imageKey, imageUrl), migration appliquée, routes tRPC (list, upload, create, delete), composant SignaturesSection avec formulaire d'ajout (prénom+nom+upload image), grille d'affichage des signatures avec aperçu, bouton de suppression au survol

This commit is contained in:
Manus
2026-03-14 18:52:47 -04:00
parent 9868a1afda
commit 33a82e2ad2
8 changed files with 1764 additions and 3 deletions

View File

@@ -32,7 +32,10 @@ import {
AutomationRule,
llmFieldsConfig,
InsertLlmFieldConfig,
LlmFieldConfig
LlmFieldConfig,
signatures,
InsertSignature,
Signature
} from "../drizzle/schema";
import { ENV } from './_core/env';
@@ -667,3 +670,34 @@ export async function initializeDefaultLlmFields(userId: number): Promise<void>
}
}
}
// ============= SIGNATURES HELPERS =============
export async function getSignaturesByUser(userId: number): Promise<Signature[]> {
const db = await getDb();
if (!db) return [];
return db.select().from(signatures).where(eq(signatures.userId, userId));
}
export async function getSignatureById(id: number): Promise<Signature | undefined> {
const db = await getDb();
if (!db) return undefined;
const results = await db.select().from(signatures).where(eq(signatures.id, id));
return results[0];
}
export async function createSignature(data: InsertSignature): Promise<Signature> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.insert(signatures).values(data);
const insertId = (result[0] as any).insertId;
const created = await getSignatureById(insertId);
if (!created) throw new Error("Failed to retrieve created signature");
return created;
}
export async function deleteSignature(id: number): Promise<void> {
const db = await getDb();
if (!db) return;
await db.delete(signatures).where(eq(signatures.id, id));
}

View File

@@ -56,6 +56,10 @@ import {
createAutomationRule,
updateAutomationRule,
deleteAutomationRule,
getSignaturesByUser,
getSignatureById,
createSignature,
deleteSignature,
} from "./db";
import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth";
import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor";
@@ -1170,6 +1174,65 @@ export const appRouter = router({
});
}),
}),
// ============= SIGNATURES ROUTES =============
signatures: router({
list: protectedProcedure.query(async ({ ctx }) => {
return await getSignaturesByUser(ctx.user.id);
}),
upload: protectedProcedure
.input(z.object({
firstName: z.string().min(1).max(100),
lastName: z.string().min(1).max(100),
fileName: z.string().min(1),
fileData: z.string(), // Base64 encoded image
mimeType: z.string().default("image/png"),
}))
.mutation(async ({ input, ctx }) => {
const userId = ctx.user.id;
const fileBuffer = Buffer.from(input.fileData, "base64");
const safeFileName = `${input.firstName}-${input.lastName}-${Date.now()}-${input.fileName}`
.replace(/[^a-zA-Z0-9._-]/g, "_");
const imageKey = generateStorageKey(userId, safeFileName);
const result = await localStoragePut(imageKey, fileBuffer, input.mimeType);
return await createSignature({
userId,
firstName: input.firstName,
lastName: input.lastName,
imageKey,
imageUrl: result.url,
});
}),
create: protectedProcedure
.input(z.object({
firstName: z.string().min(1).max(100),
lastName: z.string().min(1).max(100),
imageKey: z.string().min(1),
imageUrl: z.string().min(1),
}))
.mutation(async ({ input, ctx }) => {
return await createSignature({
userId: ctx.user.id,
firstName: input.firstName,
lastName: input.lastName,
imageKey: input.imageKey,
imageUrl: input.imageUrl,
});
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input, ctx }) => {
const sig = await getSignatureById(input.id);
if (!sig || sig.userId !== ctx.user.id) {
throw new TRPCError({ code: "NOT_FOUND" });
}
await deleteSignature(input.id);
return { success: true };
}),
}),
});
export type AppRouter = typeof appRouter;