Checkpoint: - Ajout table opex_bases_repartition (11e table) avec 58 bases de répartition 2026 seedées depuis le JSON source
- Ajout procédure tRPC opex.getBasesRepartition et opex.setBaseRepartition - DsiOpex utilise maintenant les bases de répartition depuis la BDD (priorité BDD > JSON fallback pour 2025) - Total en bas à droite de la vue établissements affiche totalGlobal (somme postes BDD = 1 257 678 €) au lieu de la somme des arrondis par établissement (1 259 641 €) - 0 erreur TypeScript
This commit is contained in:
@@ -386,6 +386,8 @@ export default function DsiOpex() {
|
||||
const { data: postesRaw, isLoading: loadingPostes } = trpc.opex.getPostes.useQuery({ annee });
|
||||
const { data: montantsEtabRaw, isLoading: loadingMontants } = trpc.opex.getMontantsEtab.useQuery({ annee });
|
||||
const { data: validatedRow } = trpc.opex.getValidated.useQuery({ annee });
|
||||
// Bases de répartition depuis la BDD (charges classe 6 par établissement)
|
||||
const { data: basesRepartitionRaw } = trpc.opex.getBasesRepartition.useQuery({ annee });
|
||||
|
||||
// ── Mutations tRPC ─────────────────────────────────────────────────────────
|
||||
const upsertPoste = trpc.opex.upsertPoste.useMutation({
|
||||
@@ -685,12 +687,26 @@ export default function DsiOpex() {
|
||||
);
|
||||
|
||||
// Vue établissements : recalcul dynamique depuis les postes éditables
|
||||
// Les bases de répartition viennent de la BDD (table opex_bases_repartition)
|
||||
// Fallback sur le JSON source si la BDD n'a pas de données pour cette année
|
||||
const etablissementsRecalcules = useMemo(() => {
|
||||
const srcData = getOpexDataForAnnee(annee);
|
||||
const totalBase = srcData.etablissements.reduce((s, e) => s + e.base_repartition, 0);
|
||||
if (totalBase === 0) return srcData.etablissements.map(e => ({ ...e, montants: {} as Record<string, number>, total: 0 }));
|
||||
// Priorité : BDD > JSON source
|
||||
const basesFromBdd = basesRepartitionRaw && basesRepartitionRaw.length > 0
|
||||
? basesRepartitionRaw.map(b => ({
|
||||
code: b.etablissementCode,
|
||||
nom: b.etablissementNom ?? b.etablissementCode,
|
||||
base_repartition: parseFloat(b.baseRepartition ?? '0') || 0,
|
||||
}))
|
||||
: getOpexDataForAnnee(annee).etablissements.map(e => ({
|
||||
code: e.code,
|
||||
nom: e.nom,
|
||||
base_repartition: e.base_repartition,
|
||||
}));
|
||||
|
||||
return srcData.etablissements.map(etab => {
|
||||
const totalBase = basesFromBdd.reduce((s, e) => s + e.base_repartition, 0);
|
||||
if (totalBase === 0) return basesFromBdd.map(e => ({ ...e, montants: {} as Record<string, number>, total: 0 }));
|
||||
|
||||
return basesFromBdd.map(etab => {
|
||||
const ratio = etab.base_repartition / totalBase;
|
||||
const etabOverrides: Record<string, number> = montantsManuelEtab[etab.code] ?? {};
|
||||
|
||||
@@ -712,7 +728,7 @@ export default function DsiOpex() {
|
||||
|
||||
return { ...etab, montants: montantsRecalcules, total: totalEtab };
|
||||
});
|
||||
}, [lignes, montantsManuelEtab, annee]);
|
||||
}, [lignes, montantsManuelEtab, annee, basesRepartitionRaw]);
|
||||
|
||||
const filteredEtabs = useMemo(() => {
|
||||
return etablissementsRecalcules.filter(e => {
|
||||
@@ -1398,7 +1414,8 @@ export default function DsiOpex() {
|
||||
</td>
|
||||
{lignes.map(ligne => {
|
||||
const isManuel = ligne.mode_ventilation === 'Manuel' || ligne.isCustom;
|
||||
const totalColonne = sortedEtabs.reduce((s, e) => s + (e.montants[ligne.libelle] ?? 0), 0);
|
||||
// Utiliser le montant du poste (source BDD) pour éviter les écarts d'arrondis
|
||||
const montantPoste = ligne.montant;
|
||||
return (
|
||||
<td
|
||||
key={ligne.id}
|
||||
@@ -1406,14 +1423,15 @@ export default function DsiOpex() {
|
||||
isManuel ? 'text-blue-700 bg-blue-50/40' : 'text-emerald-700 bg-emerald-50/30'
|
||||
}`}
|
||||
>
|
||||
{totalColonne > 0
|
||||
? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(totalColonne) + ' €'
|
||||
{montantPoste > 0
|
||||
? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(montantPoste) + ' €'
|
||||
: '—'}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="px-3 py-3 text-right font-bold text-orange-600 text-sm tabular-nums" style={{ position: 'sticky', right: 0, zIndex: 20, background: 'oklch(0.94 0.005 80)', boxShadow: '-3px 0 8px -2px rgba(0,0,0,0.18)', width: '120px', minWidth: '120px' }}>
|
||||
{formatEuros(sortedEtabs.reduce((s, e) => s + e.total, 0))}
|
||||
{/* Total depuis les montants BDD — pas la somme des arrondis par établissement */}
|
||||
{formatEuros(totalGlobal)}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
||||
9
drizzle/0001_flaky_kitty_pryde.sql
Normal file
9
drizzle/0001_flaky_kitty_pryde.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE `opex_bases_repartition` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`annee` int NOT NULL,
|
||||
`etablissementCode` varchar(50) NOT NULL,
|
||||
`etablissementNom` varchar(255),
|
||||
`baseRepartition` decimal(20,6) NOT NULL,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `opex_bases_repartition_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
864
drizzle/meta/0001_snapshot.json
Normal file
864
drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,864 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "7cfbd553-42c7-4195-8927-d7db39cd57d6",
|
||||
"prevId": "973efedb-ae3f-4e9d-b1e4-fb7ab4115377",
|
||||
"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
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(now())"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,13 @@
|
||||
"when": 1781092850222,
|
||||
"tag": "0000_fat_falcon",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "5",
|
||||
"when": 1781158775157,
|
||||
"tag": "0001_flaky_kitty_pryde",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -160,3 +160,19 @@ export const capexLignes = mysqlTable("capex_lignes", {
|
||||
|
||||
export type CapexLigne = typeof capexLignes.$inferSelect;
|
||||
export type InsertCapexLigne = typeof capexLignes.$inferInsert;
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────────
|
||||
// OPEX — Bases de répartition par établissement et par année
|
||||
// (charges de classe 6 utilisées pour le prorata)
|
||||
// ───────────────────────────────────────────────────────────────────────────────
|
||||
export const opexBasesRepartition = mysqlTable("opex_bases_repartition", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
annee: int("annee").notNull(),
|
||||
etablissementCode: varchar("etablissementCode", { length: 50 }).notNull(),
|
||||
etablissementNom: varchar("etablissementNom", { length: 255 }),
|
||||
baseRepartition: decimal("baseRepartition", { precision: 20, scale: 6 }).notNull(),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export type OpexBaseRepartition = typeof opexBasesRepartition.$inferSelect;
|
||||
export type InsertOpexBaseRepartition = typeof opexBasesRepartition.$inferInsert;
|
||||
|
||||
45
scripts/compare-opex-bdd-json.mjs
Normal file
45
scripts/compare-opex-bdd-json.mjs
Normal file
@@ -0,0 +1,45 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
dotenv.config({ path: join(__dirname, '..', '.env') });
|
||||
|
||||
const conn = await mysql.createConnection(process.env.DATABASE_URL);
|
||||
const [rows] = await conn.execute('SELECT libelle, montant FROM opex_postes WHERE annee = 2026 ORDER BY colIdx');
|
||||
|
||||
const data = JSON.parse(readFileSync(join(__dirname, '../client/src/data_opex.json'), 'utf8'));
|
||||
const libellesJson = new Set(data.postes.map(p => p.libelle));
|
||||
const libellesBdd = new Set(rows.map(r => r.libelle));
|
||||
|
||||
console.log('Nb postes BDD:', rows.length);
|
||||
console.log('Nb postes JSON:', libellesJson.size);
|
||||
|
||||
let totalBdd = 0;
|
||||
rows.forEach(r => { totalBdd += parseFloat(r.montant || '0'); });
|
||||
console.log('Total BDD:', totalBdd.toFixed(2));
|
||||
|
||||
console.log('\n=== En BDD mais PAS dans JSON ===');
|
||||
for (const lib of libellesBdd) {
|
||||
if (!libellesJson.has(lib)) {
|
||||
const row = rows.find(r => r.libelle === lib);
|
||||
console.log(' +', lib, ':', row.montant);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Dans JSON mais PAS en BDD ===');
|
||||
for (const lib of libellesJson) {
|
||||
if (!libellesBdd.has(lib)) {
|
||||
console.log(' -', lib);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Tous les postes BDD avec montant ===');
|
||||
rows.forEach(r => {
|
||||
const m = parseFloat(r.montant || '0');
|
||||
if (m > 0) console.log(` ${r.libelle}: ${m.toFixed(2)}`);
|
||||
});
|
||||
|
||||
await conn.end();
|
||||
53
scripts/seed-opex-bases.mjs
Normal file
53
scripts/seed-opex-bases.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Seed des bases de répartition OPEX (charges classe 6) pour 2025 et 2026
|
||||
* depuis les fichiers JSON sources.
|
||||
*/
|
||||
import mysql from 'mysql2/promise';
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
dotenv.config({ path: join(__dirname, '..', '.env') });
|
||||
|
||||
const conn = await mysql.createConnection(process.env.DATABASE_URL);
|
||||
|
||||
const dataFiles = {
|
||||
2025: join(__dirname, '../client/src/data_opex_2025.json'),
|
||||
2026: join(__dirname, '../client/src/data_opex.json'),
|
||||
};
|
||||
|
||||
for (const [annee, filePath] of Object.entries(dataFiles)) {
|
||||
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
||||
const etabs = data.etablissements || [];
|
||||
|
||||
if (etabs.length === 0) {
|
||||
console.log(`Année ${annee}: aucun établissement trouvé dans le JSON`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Supprimer les bases existantes pour cette année
|
||||
await conn.execute('DELETE FROM opex_bases_repartition WHERE annee = ?', [parseInt(annee)]);
|
||||
|
||||
// Insérer en batch
|
||||
const values = etabs.map(e => [
|
||||
parseInt(annee),
|
||||
e.code,
|
||||
e.nom || null,
|
||||
e.base_repartition || 0,
|
||||
]);
|
||||
|
||||
if (values.length > 0) {
|
||||
await conn.query(
|
||||
'INSERT INTO opex_bases_repartition (annee, etablissementCode, etablissementNom, baseRepartition) VALUES ?',
|
||||
[values]
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Année ${annee}: ${values.length} bases de répartition insérées`);
|
||||
console.log(` Total base: ${etabs.reduce((s, e) => s + (e.base_repartition || 0), 0).toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €`);
|
||||
}
|
||||
|
||||
await conn.end();
|
||||
console.log('\nSeed bases de répartition terminé.');
|
||||
35
server/db.ts
35
server/db.ts
@@ -10,6 +10,7 @@ import {
|
||||
InsertUser,
|
||||
inventaireMeta,
|
||||
inventairePostes,
|
||||
opexBasesRepartition,
|
||||
opexMontantsEtab,
|
||||
opexPostes,
|
||||
opexValidated,
|
||||
@@ -326,3 +327,37 @@ export async function insertCapexLignes(rows: InsertCapexLigne[]) {
|
||||
await db.insert(capexLignes).values(rows.slice(i, i + 100));
|
||||
}
|
||||
}
|
||||
|
||||
// ── OPEX Bases de répartition ────────────────────────────────────────────────
|
||||
export async function getOpexBasesRepartition(annee: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
return db
|
||||
.select()
|
||||
.from(opexBasesRepartition)
|
||||
.where(eq(opexBasesRepartition.annee, annee));
|
||||
}
|
||||
|
||||
export async function upsertOpexBaseRepartition(input: {
|
||||
annee: number;
|
||||
etablissementCode: string;
|
||||
etablissementNom?: string | null;
|
||||
baseRepartition: number;
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
await db
|
||||
.insert(opexBasesRepartition)
|
||||
.values({
|
||||
annee: input.annee,
|
||||
etablissementCode: input.etablissementCode,
|
||||
etablissementNom: input.etablissementNom ?? null,
|
||||
baseRepartition: String(input.baseRepartition),
|
||||
})
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
baseRepartition: String(input.baseRepartition),
|
||||
etablissementNom: input.etablissementNom ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -174,6 +174,16 @@ export const appRouter = router({
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
// Bases de répartition par établissement (charges classe 6)
|
||||
getBasesRepartition: protectedProcedure
|
||||
.input(z.object({ annee: z.number() }))
|
||||
.query(async ({ input }) => db.getOpexBasesRepartition(input.annee)),
|
||||
|
||||
// Upsert une base de répartition pour un établissement
|
||||
setBaseRepartition: writeProcedure
|
||||
.input(z.object({ annee: z.number(), etablissementCode: z.string(), etablissementNom: z.string().optional().nullable(), baseRepartition: z.number() }))
|
||||
.mutation(async ({ input }) => { await db.upsertOpexBaseRepartition(input); return { success: true }; }),
|
||||
|
||||
// Supprimer toutes les lignes OPEX d'une année
|
||||
deleteAll: writeProcedure
|
||||
.input(z.object({ annee: z.number() }))
|
||||
|
||||
Reference in New Issue
Block a user