Checkpoint: Ajout de 3 champs métier aux factures (Service concerné, Type d'achat, Ventilation comptable) avec interface d'administration pour gérer les listes enrichissables. Les champs apparaissent dans la liste des factures et dans le formulaire d'édition.

This commit is contained in:
Manus
2026-02-11 08:03:30 -05:00
parent 280abba902
commit 4a8be36d00
12 changed files with 1665 additions and 4 deletions

View File

@@ -20,7 +20,13 @@ import {
LlmLog,
importSettings,
InsertImportSettings,
ImportSettings
ImportSettings,
departmentList,
InsertDepartment,
Department,
accountingAllocationList,
InsertAccountingAllocation,
AccountingAllocation
} from "../drizzle/schema";
import { ENV } from './_core/env';
@@ -480,3 +486,72 @@ export async function upsertImportSettings(data: InsertImportSettings): Promise<
return inserted[0]!;
}
}
// ============= DEPARTMENT LIST OPERATIONS =============
export async function getDepartmentsByUser(userId: number): Promise<Department[]> {
const db = await getDb();
if (!db) return [];
return await db.select().from(departmentList).where(eq(departmentList.userId, userId)).orderBy(departmentList.name);
}
export async function createDepartment(data: InsertDepartment): Promise<Department> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const [department] = await db.insert(departmentList).values(data).$returningId();
return await db.select().from(departmentList).where(eq(departmentList.id, department.id)).then(rows => rows[0]);
}
export async function deleteDepartment(id: number): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.delete(departmentList).where(eq(departmentList.id, id));
}
// ============= ACCOUNTING ALLOCATION LIST OPERATIONS =============
export async function getAccountingAllocationsByUser(userId: number): Promise<AccountingAllocation[]> {
const db = await getDb();
if (!db) return [];
return await db.select().from(accountingAllocationList).where(eq(accountingAllocationList.userId, userId)).orderBy(accountingAllocationList.name);
}
export async function createAccountingAllocation(data: InsertAccountingAllocation): Promise<AccountingAllocation> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const [allocation] = await db.insert(accountingAllocationList).values(data).$returningId();
return await db.select().from(accountingAllocationList).where(eq(accountingAllocationList.id, allocation.id)).then(rows => rows[0]);
}
export async function deleteAccountingAllocation(id: number): Promise<void> {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.delete(accountingAllocationList).where(eq(accountingAllocationList.id, id));
}
// ============= INITIALIZE DEFAULT VALUES =============
export async function initializeDefaultLists(userId: number): Promise<void> {
const db = await getDb();
if (!db) return;
// Initialize default departments
const defaultDepartments = ["DSI", "TRAVAUX", "AUTRE"];
for (const name of defaultDepartments) {
try {
await db.insert(departmentList).values({ userId, name }).onDuplicateKeyUpdate({ set: { name } });
} catch (error) {
// Ignore duplicate errors
}
}
// Initialize default accounting allocations
const defaultAllocations = ["TOUS", "PA", "HEP", "SANITAIRE", "AUTRE"];
for (const name of defaultAllocations) {
try {
await db.insert(accountingAllocationList).values({ userId, name }).onDuplicateKeyUpdate({ set: { name } });
} catch (error) {
// Ignore duplicate errors
}
}
}

View File

@@ -29,6 +29,13 @@ import {
getLlmLogsByInvoice,
getImportSettingsByUser,
upsertImportSettings,
getDepartmentsByUser,
createDepartment,
deleteDepartment,
getAccountingAllocationsByUser,
createAccountingAllocation,
deleteAccountingAllocation,
initializeDefaultLists,
} from "./db";
import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth";
import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor";
@@ -297,6 +304,9 @@ export const appRouter = router({
deliveryNoteNumber: z.string().optional(),
orderNumber: z.string().optional(),
totalAmount: z.string().optional(),
serviceConcerne: z.string().optional(),
typeAchat: z.enum(["CAPEX", "OPEX"]).optional(),
ventilationComptable: z.string().optional(),
}),
}))
.mutation(async ({ input, ctx }) => {
@@ -670,6 +680,72 @@ export const appRouter = router({
return settings;
}),
}),
// ============= DEPARTMENT LIST ROUTES =============
departments: router({
getByUser: protectedProcedure.query(async ({ ctx }) => {
return await getDepartmentsByUser(ctx.user.id);
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1).max(100),
}))
.mutation(async ({ input, ctx }) => {
// Initialize default lists if this is the first department
const existing = await getDepartmentsByUser(ctx.user.id);
if (existing.length === 0) {
await initializeDefaultLists(ctx.user.id);
}
return await createDepartment({
userId: ctx.user.id,
name: input.name,
});
}),
delete: protectedProcedure
.input(z.object({
id: z.number(),
}))
.mutation(async ({ input }) => {
await deleteDepartment(input.id);
return { success: true };
}),
}),
// ============= ACCOUNTING ALLOCATION LIST ROUTES =============
accountingAllocations: router({
getByUser: protectedProcedure.query(async ({ ctx }) => {
return await getAccountingAllocationsByUser(ctx.user.id);
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1).max(100),
}))
.mutation(async ({ input, ctx }) => {
// Initialize default lists if this is the first allocation
const existing = await getAccountingAllocationsByUser(ctx.user.id);
if (existing.length === 0) {
await initializeDefaultLists(ctx.user.id);
}
return await createAccountingAllocation({
userId: ctx.user.id,
name: input.name,
});
}),
delete: protectedProcedure
.input(z.object({
id: z.number(),
}))
.mutation(async ({ input }) => {
await deleteAccountingAllocation(input.id);
return { success: true };
}),
}),
});
export type AppRouter = typeof appRouter;