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 */}
+
+ toggleModeManuel(row)}
+ disabled={setBaseRepartition.isPending}
+ title={row.modeManuel
+ ? 'Mode manuel activé — cliquer pour revenir au calcul prorata automatique'
+ : 'Cliquer pour activer le mode tout manuel (montants OPEX saisis manuellement pour cet établissement)'}
+ className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary/30 disabled:opacity-50 ${
+ row.modeManuel ? 'bg-amber-500' : 'bg-muted-foreground/30'
+ }`}
+ >
+
+
+
))}
- 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)