diff --git a/client/src/pages/DsiOpex.tsx b/client/src/pages/DsiOpex.tsx index b2affaf..3f8bd19 100644 --- a/client/src/pages/DsiOpex.tsx +++ b/client/src/pages/DsiOpex.tsx @@ -384,6 +384,7 @@ interface BaseRow { etablissementNom: string | null; baseRepartition: string; baseRepartitionHep: string | null; + modeManuel: boolean; } interface ClesRepartitionViewProps { @@ -432,10 +433,22 @@ function ClesRepartitionView({ annee, basesRepartitionRaw, isLoadingBases, isErr etablissementNom: row.etablissementNom, baseRepartition, baseRepartitionHep, + modeManuel: row.modeManuel, }); setEditingCell(null); } + function toggleModeManuel(row: BaseRow) { + setBaseRepartition.mutate({ + annee, + etablissementCode: row.etablissementCode, + etablissementNom: row.etablissementNom, + baseRepartition: parseFloat(row.baseRepartition ?? '0'), + baseRepartitionHep: parseFloat(row.baseRepartitionHep ?? '0'), + modeManuel: !row.modeManuel, + }); + } + async function handleFileImport(file: File) { try { const XLSX = await import('xlsx'); @@ -629,12 +642,15 @@ function ClesRepartitionView({ annee, basesRepartitionRaw, isLoadingBases, isErr Base répartition {anneeBase} / HEP + + Tout manuel + {rows.length === 0 && ( - + {basesRepartitionRaw.length === 0 ? `Aucune base de répartition pour ${annee}. Importez un fichier ou saisissez les valeurs manuellement.` : 'Aucun résultat pour cette recherche.'} @@ -642,9 +658,16 @@ function ClesRepartitionView({ annee, basesRepartitionRaw, isLoadingBases, isErr )} {rows.map((row, idx) => ( - + {row.etablissementCode} - {row.etablissementNom ?? row.etablissementCode} + + {row.etablissementNom ?? row.etablissementCode} + {row.modeManuel && ( + + Manuel + + )} + {/* Colonne Base de répartition */} {editingCell?.code === row.etablissementCode && editingCell.col === 'base' ? ( @@ -691,18 +714,48 @@ function ClesRepartitionView({ annee, basesRepartitionRaw, isLoadingBases, isErr )} + {/* Colonne Mode Manuel */} + + + ))} - TOTAL — {basesRepartitionRaw.length} établissements + TOTAL — {basesRepartitionRaw.length} établissements + {basesRepartitionRaw.filter(r => r.modeManuel).length > 0 && ( + + ({basesRepartitionRaw.filter(r => r.modeManuel).length} en mode manuel) + + )} + {new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(totalBase)} € {totalHep > 0 ? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(totalHep) + ' €' : '—'} + + {basesRepartitionRaw.filter(r => r.modeManuel).length > 0 + ? `${basesRepartitionRaw.filter(r => r.modeManuel).length} actif${basesRepartitionRaw.filter(r => r.modeManuel).length > 1 ? 's' : ''}` + : '—'} + @@ -1043,12 +1096,14 @@ export default function DsiOpex() { nom: b.etablissementNom ?? b.etablissementCode, base_repartition: parseFloat(b.baseRepartition ?? '0') || 0, base_repartition_hep: parseFloat(b.baseRepartitionHep ?? '0') || 0, + modeManuel: b.modeManuel ?? false, })) : getOpexDataForAnnee(annee).etablissements.map(e => ({ code: e.code, nom: e.nom, base_repartition: e.base_repartition, base_repartition_hep: 0, // JSON source n'a pas de base HEP + modeManuel: false, })); const totalBase = basesFromBdd.reduce((s, e) => s + e.base_repartition, 0); @@ -1071,7 +1126,11 @@ export default function DsiOpex() { (ligne.libelleDetail ?? '').toLowerCase().includes('hep')); const ratioApplique = isHepPoste ? ratioHep : ratio; let montant: number; - if (isManuel && etabOverrides[ligne.libelle] !== undefined) { + if (etab.modeManuel) { + // Mode tout manuel : utiliser uniquement les montants saisis manuellement, + // sans aucun calcul prorata — si pas de saisie, le montant est 0 + montant = etabOverrides[ligne.libelle] ?? 0; + } else if (isManuel && etabOverrides[ligne.libelle] !== undefined) { montant = etabOverrides[ligne.libelle]; } else if (!isManuel && ligne.montant > 0) { montant = Math.round(ligne.montant * ratioApplique); @@ -1707,12 +1766,21 @@ export default function DsiOpex() { - {sortedEtabs.map((etab, idx) => ( - - + {sortedEtabs.map((etab, idx) => { + const rowBg = etab.modeManuel ? 'oklch(0.98 0.02 80)' : (idx % 2 === 0 ? 'oklch(1 0 0)' : 'oklch(0.97 0.003 80)'); + return ( + + {etab.code} - {etab.nom} + + {etab.nom} + {etab.modeManuel && ( + + Manuel + + )} + {lignes.map(ligne => { const isManuel = ligne.mode_ventilation === 'Manuel' || ligne.isCustom; const montant = etab.montants[ligne.libelle] ?? 0; @@ -1770,7 +1838,8 @@ export default function DsiOpex() { - ))} + ); + })} diff --git a/drizzle/0003_tense_havok.sql b/drizzle/0003_tense_havok.sql new file mode 100644 index 0000000..c609c23 --- /dev/null +++ b/drizzle/0003_tense_havok.sql @@ -0,0 +1,2 @@ +ALTER TABLE `opex_bases_repartition` ADD `modeManuel` boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `opex_bases_repartition` ADD CONSTRAINT `opex_bases_repartition_annee_etab_idx` UNIQUE(`annee`,`etablissementCode`); \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..f11224a --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,889 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "44abeed3-e7bf-47cd-a29c-59a27bf57f1a", + "prevId": "baff50d1-531e-4ba7-a82a-6f9c64aa1186", + "tables": { + "capex_lignes": { + "name": "capex_lignes", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "annee": { + "name": "annee", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etablissementCode": { + "name": "etablissementCode", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cle": { + "name": "cle", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "montant": { + "name": "montant", + "type": "decimal(12,2)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "capex_lignes_id": { + "name": "capex_lignes_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "etablissements": { + "name": "etablissements", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nom": { + "name": "nom", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "groupe": { + "name": "groupe", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ville": { + "name": "ville", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actif": { + "name": "actif", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "etablissements_id": { + "name": "etablissements_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "etablissements_code_unique": { + "name": "etablissements_code_unique", + "columns": [ + "code" + ] + } + }, + "checkConstraint": {} + }, + "inventaire_meta": { + "name": "inventaire_meta", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "annee": { + "name": "annee", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "filename": { + "name": "filename", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateImport": { + "name": "dateImport", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "nbEtablissements": { + "name": "nbEtablissements", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "nbFixes": { + "name": "nbFixes", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "nbPortables": { + "name": "nbPortables", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inventaire_meta_id": { + "name": "inventaire_meta_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "inventaire_meta_annee_unique": { + "name": "inventaire_meta_annee_unique", + "columns": [ + "annee" + ] + } + }, + "checkConstraint": {} + }, + "inventaire_postes": { + "name": "inventaire_postes", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "annee": { + "name": "annee", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etablissementCode": { + "name": "etablissementCode", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "libelle": { + "name": "libelle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typePoste": { + "name": "typePoste", + "type": "enum('fixe','portable')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateRef": { + "name": "dateRef", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ageAns": { + "name": "ageAns", + "type": "decimal(5,2)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "modele": { + "name": "modele", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fabricant": { + "name": "fabricant", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inventaire_postes_id": { + "name": "inventaire_postes_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opex_bases_repartition": { + "name": "opex_bases_repartition", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "annee": { + "name": "annee", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etablissementCode": { + "name": "etablissementCode", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etablissementNom": { + "name": "etablissementNom", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "baseRepartition": { + "name": "baseRepartition", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "baseRepartitionHep": { + "name": "baseRepartitionHep", + "type": "decimal(20,6)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0'" + }, + "modeManuel": { + "name": "modeManuel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "opex_bases_repartition_annee_etab_idx": { + "name": "opex_bases_repartition_annee_etab_idx", + "columns": [ + "annee", + "etablissementCode" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "opex_bases_repartition_id": { + "name": "opex_bases_repartition_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opex_montants_etab": { + "name": "opex_montants_etab", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "annee": { + "name": "annee", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etablissementCode": { + "name": "etablissementCode", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "libellePoste": { + "name": "libellePoste", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "montant": { + "name": "montant", + "type": "decimal(12,2)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "opex_montants_etab_id": { + "name": "opex_montants_etab_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opex_postes": { + "name": "opex_postes", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "annee": { + "name": "annee", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "colIdx": { + "name": "colIdx", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "libelle": { + "name": "libelle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "libelleCourt": { + "name": "libelleCourt", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "libelleDetail": { + "name": "libelleDetail", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fournisseur": { + "name": "fournisseur", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "categorie": { + "name": "categorie", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "facturation": { + "name": "facturation", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "modeVentilation": { + "name": "modeVentilation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'Prorata C 6'" + }, + "compte": { + "name": "compte", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "budgetN1": { + "name": "budgetN1", + "type": "decimal(12,2)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "montant": { + "name": "montant", + "type": "decimal(12,2)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "isCustom": { + "name": "isCustom", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "opex_postes_id": { + "name": "opex_postes_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "opex_validated": { + "name": "opex_validated", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "annee": { + "name": "annee", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validatedAt": { + "name": "validatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "validatedBy": { + "name": "validatedBy", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "opex_validated_id": { + "name": "opex_validated_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "opex_validated_annee_unique": { + "name": "opex_validated_annee_unique", + "columns": [ + "annee" + ] + } + }, + "checkConstraint": {} + }, + "parametres_app": { + "name": "parametres_app", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "cle": { + "name": "cle", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valeur": { + "name": "valeur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "parametres_app_id": { + "name": "parametres_app_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "parametres_app_cle_unique": { + "name": "parametres_app_cle_unique", + "columns": [ + "cle" + ] + } + }, + "checkConstraint": {} + }, + "user_etablissements": { + "name": "user_etablissements", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "etablissementCode": { + "name": "etablissementCode", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_etablissements_id": { + "name": "user_etablissements_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "login": { + "name": "login", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "passwordHash": { + "name": "passwordHash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "firstName": { + "name": "firstName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastName": { + "name": "lastName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('admin','standard','readonly')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'standard'" + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "lastSignedIn": { + "name": "lastSignedIn", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_login_unique": { + "name": "users_login_unique", + "columns": [ + "login" + ] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 82dff59..3f1442d 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1781161698458, "tag": "0002_faithful_famine", "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1781163262493, + "tag": "0003_tense_havok", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index f57211c..d37d82f 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -6,6 +6,7 @@ import { mysqlTable, text, timestamp, + uniqueIndex, varchar, } from "drizzle-orm/mysql-core"; @@ -174,8 +175,12 @@ export const opexBasesRepartition = mysqlTable("opex_bases_repartition", { baseRepartition: decimal("baseRepartition", { precision: 20, scale: 6 }).notNull(), /** Charges classe 6 — base de répartition pôle HEP uniquement (0 pour les étblissements hors HEP) */ baseRepartitionHep: decimal("baseRepartitionHep", { precision: 20, scale: 6 }).default("0"), + /** Si true, tous les montants OPEX de cet établissement sont saisis manuellement (pas de calcul prorata) */ + modeManuel: boolean("modeManuel").default(false).notNull(), createdAt: timestamp("createdAt").defaultNow().notNull(), -}); +}, (t) => ({ + uniqAnneeEtab: uniqueIndex("opex_bases_repartition_annee_etab_idx").on(t.annee, t.etablissementCode), +})); export type OpexBaseRepartition = typeof opexBasesRepartition.$inferSelect; export type InsertOpexBaseRepartition = typeof opexBasesRepartition.$inferInsert; diff --git a/server/db.ts b/server/db.ts index 39d072a..9611241 100644 --- a/server/db.ts +++ b/server/db.ts @@ -344,6 +344,7 @@ export async function upsertOpexBaseRepartition(input: { etablissementNom?: string | null; baseRepartition: number; baseRepartitionHep?: number; + modeManuel?: boolean; }) { const db = await getDb(); if (!db) throw new Error("Database not available"); @@ -355,12 +356,14 @@ export async function upsertOpexBaseRepartition(input: { etablissementNom: input.etablissementNom ?? null, baseRepartition: String(input.baseRepartition), baseRepartitionHep: String(input.baseRepartitionHep ?? 0), + modeManuel: input.modeManuel ?? false, }) .onDuplicateKeyUpdate({ set: { baseRepartition: String(input.baseRepartition), baseRepartitionHep: String(input.baseRepartitionHep ?? 0), etablissementNom: input.etablissementNom ?? null, + modeManuel: input.modeManuel ?? false, }, }); } diff --git a/server/routers.ts b/server/routers.ts index 3b2c9c2..396207a 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -187,6 +187,7 @@ export const appRouter = router({ etablissementNom: z.string().optional().nullable(), baseRepartition: z.number(), baseRepartitionHep: z.number().optional().default(0), + modeManuel: z.boolean().optional().default(false), })) .mutation(async ({ input }) => { await db.upsertOpexBaseRepartition(input); return { success: true }; }), diff --git a/todo.md b/todo.md index 4ad568f..df9a26d 100644 --- a/todo.md +++ b/todo.md @@ -56,3 +56,8 @@ - [x] Ajouter un vrai loading/error state pour l'onglet « Clés de répartition » basé sur la requête `opex.getBasesRepartition` - [x] Rendre l'import des clés de répartition compatible avec le classeur source réel : détection intelligente de la bonne feuille (mots-clés OPEX/DSI/répartition/base), normalisation des accents, validation des colonnes attendues avec message d'erreur détaillé + +## Bugs et nouvelles fonctionnalités (post-checkpoint cfeb5196) + +- [x] Bug : doublon de ligne dans le tableau Clés de répartition au clic (mutation setBaseRepartition provoque un double rendu) +- [x] Fonctionnalité : mode "tout manuel" par établissement dans l'onglet Clés de répartition (toutes les colonnes OPEX de cet établissement saisies manuellement, ignorant le calcul prorata)