Checkpoint: OPEX DSI : flèches de tendance colorées (rouge=hausse, orange=stable, vert=baisse) sur chaque vignette de catégorie, avec montant N-1 affiché. Données 2025 intégrées depuis le fichier Excel. Support multi-années 2025/2026 avec données sources distinctes.
This commit is contained in:
@@ -3355,5 +3355,43 @@
|
||||
"meta": {
|
||||
"nb_postes": 35,
|
||||
"nb_etablissements": 67
|
||||
},
|
||||
"tendances_categories": {
|
||||
"App HEP": {
|
||||
"montant_n1": 51000,
|
||||
"montant_n": 98000,
|
||||
"variation_pct": 92.2,
|
||||
"tendance": "hausse"
|
||||
},
|
||||
"App SMR": {
|
||||
"montant_n1": 11000,
|
||||
"montant_n": 14500,
|
||||
"variation_pct": 31.8,
|
||||
"tendance": "hausse"
|
||||
},
|
||||
"App global": {
|
||||
"montant_n1": 400750,
|
||||
"montant_n": 504980,
|
||||
"variation_pct": 26.0,
|
||||
"tendance": "hausse"
|
||||
},
|
||||
"Infogérance": {
|
||||
"montant_n1": 275020,
|
||||
"montant_n": 377198,
|
||||
"variation_pct": 37.2,
|
||||
"tendance": "hausse"
|
||||
},
|
||||
"Sécurité": {
|
||||
"montant_n1": 144400,
|
||||
"montant_n": 229400,
|
||||
"variation_pct": 58.9,
|
||||
"tendance": "hausse"
|
||||
},
|
||||
"Téléphonie": {
|
||||
"montant_n1": 62000,
|
||||
"montant_n": 33600,
|
||||
"variation_pct": -45.8,
|
||||
"tendance": "baisse"
|
||||
}
|
||||
}
|
||||
}
|
||||
493
client/src/data_opex_2025.json
Normal file
493
client/src/data_opex_2025.json
Normal file
@@ -0,0 +1,493 @@
|
||||
{
|
||||
"annee": 2025,
|
||||
"total_global": 1017082.0,
|
||||
"postes": [
|
||||
{
|
||||
"col_idx": 5,
|
||||
"fournisseur": "AGEVAL",
|
||||
"libelle": "SAAS AGEVAL Gestion de la qualité",
|
||||
"detail": "Gestion qualité (procédures,FEI...)",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "App global",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 30000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 6,
|
||||
"fournisseur": "OCTIME",
|
||||
"libelle": "SAAS OCTIME Gestion de temps",
|
||||
"detail": "GTA",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "App global",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 46000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 7,
|
||||
"fournisseur": "LUCCA",
|
||||
"libelle": "SAAS FIGGO Gestion congés",
|
||||
"detail": "Gestion des congés Directions",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Manuel",
|
||||
"categorie": "App global",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 2400.0
|
||||
},
|
||||
{
|
||||
"col_idx": 8,
|
||||
"fournisseur": "LUCCA",
|
||||
"libelle": "SAAS CLEEMY Gestion frais professionnels",
|
||||
"detail": "Gestion des notes de frais directions",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Manuel",
|
||||
"categorie": "App global",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 1700.0
|
||||
},
|
||||
{
|
||||
"col_idx": 9,
|
||||
"fournisseur": "ISI-DSI",
|
||||
"libelle": "SAAS ISI-APP Gestion de parc",
|
||||
"detail": "Outil de pilotage SI du parc + Connexion AD + Module Santé",
|
||||
"facturation": "Mensuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Infogérance",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 14500.0
|
||||
},
|
||||
{
|
||||
"col_idx": 10,
|
||||
"fournisseur": "DATA LEGAL DRIVE",
|
||||
"libelle": "SAAS DLD Gestion de la Conformité RGPD",
|
||||
"detail": "RGPD",
|
||||
"facturation": "Mensuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "App global",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 5000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 11,
|
||||
"fournisseur": "DOCUSIGN",
|
||||
"libelle": "SAAS Gestion signature électronique",
|
||||
"detail": "DE électroniques",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "App global",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 11200.0
|
||||
},
|
||||
{
|
||||
"col_idx": 12,
|
||||
"fournisseur": "MICROSOFT",
|
||||
"libelle": "SAAS Licences M365",
|
||||
"detail": "Licences pack office (outlook, excel, word...) + sauvegardes onde drive + intranet + sharepoint",
|
||||
"facturation": "Mensuelle",
|
||||
"mode_ventilation": "Manuel",
|
||||
"categorie": "Infogérance",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 141000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 13,
|
||||
"fournisseur": "DIMO",
|
||||
"libelle": "Redevance SXS Gestion trésorerie SAGE",
|
||||
"detail": "Communications bancaires",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "App global",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 16000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 14,
|
||||
"fournisseur": "LIFEN",
|
||||
"libelle": "SAAS LIFEN Plateforme E-Santé Envois",
|
||||
"detail": "SMR / outil de d'envoi vers le DMP et la MSS connecté à la GAP et au DPI + envoi des courriers papiers",
|
||||
"facturation": "Mensuelle",
|
||||
"mode_ventilation": "Manuel",
|
||||
"categorie": "App SMR",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 11000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 15,
|
||||
"fournisseur": "LIFEN",
|
||||
"libelle": "SAAS LIFEN Plateforme E-Santé Réceptions",
|
||||
"detail": "SMR / outil d'intégration dans le DPI depuis le DMP et/ou la MSS",
|
||||
"facturation": "Mensuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "New",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 3500.0
|
||||
},
|
||||
{
|
||||
"col_idx": 16,
|
||||
"fournisseur": "NOVRH",
|
||||
"libelle": "NOVRH",
|
||||
"detail": "SIRH (année pleine)",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "App global",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 232750.0
|
||||
},
|
||||
{
|
||||
"col_idx": 17,
|
||||
"fournisseur": "DARKTRACE",
|
||||
"libelle": "SAAS Protection mails DARKTRACE",
|
||||
"detail": "Sécurité",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "New",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 25000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 18,
|
||||
"fournisseur": "IMAGO",
|
||||
"libelle": "SAAS DUI Pole HEP",
|
||||
"detail": "Outil métier",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "App HEP",
|
||||
"type": "SAAS",
|
||||
"compte": "613511",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 51000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 19,
|
||||
"fournisseur": "BOUYGUES + MY TELECOM",
|
||||
"libelle": "Solution VoIP Trunks + Liens + Canaux",
|
||||
"detail": "Télphonie centralisée VOIP (20 ets)",
|
||||
"facturation": "Mensuelle",
|
||||
"mode_ventilation": "Manuel",
|
||||
"categorie": "Téléphonie",
|
||||
"type": "Téléphonie VoIP",
|
||||
"compte": "626200",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 62000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 20,
|
||||
"fournisseur": "FREE PRO DATA CENTER",
|
||||
"libelle": "Infrastructures Data Center + Liens Internet",
|
||||
"detail": "DataCenter",
|
||||
"facturation": "Mensuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Infogérance",
|
||||
"type": "Loc. Infra Data Center",
|
||||
"compte": "613512",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 53000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 21,
|
||||
"fournisseur": "FREE PRO LIENS FIBRES",
|
||||
"libelle": "Liens internet Siège Data Center",
|
||||
"detail": "DataCenter",
|
||||
"facturation": "Mensuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Infogérance",
|
||||
"type": "Liens Internet",
|
||||
"compte": "626203",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 14000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 22,
|
||||
"fournisseur": "LASOTEL",
|
||||
"libelle": "Abonnements Liaisons internet siège et Data Center",
|
||||
"detail": "DataCenter",
|
||||
"facturation": "Mensuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Infogérance",
|
||||
"type": "Liens Internet",
|
||||
"compte": "626203",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 0.0
|
||||
},
|
||||
{
|
||||
"col_idx": 23,
|
||||
"fournisseur": "OCTIME",
|
||||
"libelle": "Prestation TMA réglementaire",
|
||||
"detail": "Gestion du temps module TMA",
|
||||
"facturation": "Au fil de l'eau",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "App global",
|
||||
"type": "Prestation",
|
||||
"compte": "628400",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 3500.0
|
||||
},
|
||||
{
|
||||
"col_idx": 24,
|
||||
"fournisseur": "ISO-RA",
|
||||
"libelle": "Crédit jours technicien",
|
||||
"detail": "Intervention technicien externe sur 5 ets",
|
||||
"facturation": "Au fil de l'eau",
|
||||
"mode_ventilation": "Manuel",
|
||||
"categorie": "Infogérance",
|
||||
"type": "Prestation",
|
||||
"compte": "628400",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 16000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 25,
|
||||
"fournisseur": "CEGI",
|
||||
"libelle": "Maintenance applicatifs",
|
||||
"detail": "Compta-first, paye-fisrt, GAP-first...",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "App global",
|
||||
"type": "Maintenance",
|
||||
"compte": "615610",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 52200.0
|
||||
},
|
||||
{
|
||||
"col_idx": 26,
|
||||
"fournisseur": "ARCHE-MC2 (ex SOLWARE)",
|
||||
"libelle": "Maintenance D3i +PSI (Résilié)",
|
||||
"detail": "Outil métier pôle HEP",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Manuel",
|
||||
"categorie": "App HEP",
|
||||
"type": "Maintenance",
|
||||
"compte": "615610",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 0.0
|
||||
},
|
||||
{
|
||||
"col_idx": 27,
|
||||
"fournisseur": "OSTENDI",
|
||||
"libelle": "Maintenance OSIDOC (A RESILIER)",
|
||||
"detail": "Gestion des contrats",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "App global",
|
||||
"type": "Maintenance",
|
||||
"compte": "615610",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 0.0
|
||||
},
|
||||
{
|
||||
"col_idx": 28,
|
||||
"fournisseur": "SOFT4EUROPE",
|
||||
"libelle": "Maintenance FLEXICLOUD",
|
||||
"detail": "Connexions aux serveurs du DataCenter",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Infogérance",
|
||||
"type": "Maintenance",
|
||||
"compte": "615610",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 3000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 29,
|
||||
"fournisseur": "TEAM VIEWER",
|
||||
"libelle": "Abonnement Solution de télémaintenance",
|
||||
"detail": "Prise de main distante pour équipe IT",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Infogérance",
|
||||
"type": "Maintenance",
|
||||
"compte": "615610",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 520.0
|
||||
},
|
||||
{
|
||||
"col_idx": 30,
|
||||
"fournisseur": "ISO-RA",
|
||||
"libelle": "Maintenance WATCHGUARD",
|
||||
"detail": "Sécurité /Pare-feux renouvellement des pare-feux tous les 5 ans",
|
||||
"facturation": "Au fil de l'eau",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Sécurité",
|
||||
"type": "Maintenance",
|
||||
"compte": "615610",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 55000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 31,
|
||||
"fournisseur": "ISO-RA",
|
||||
"libelle": "Maintenance EPDR WG",
|
||||
"detail": "Sécurité / Anti-virus",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Sécurité",
|
||||
"type": "Maintenance",
|
||||
"compte": "615610",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 29400.0
|
||||
},
|
||||
{
|
||||
"col_idx": 32,
|
||||
"fournisseur": "ISO-RA",
|
||||
"libelle": "Maintenance VEEAM",
|
||||
"detail": "DataCenter / Sauvegardes machines + Sauvegardes 0365",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Infogérance",
|
||||
"type": "Maintenance",
|
||||
"compte": "615610",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 21000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 33,
|
||||
"fournisseur": "DARKTRACE",
|
||||
"libelle": "Maintenance DARKTRACE",
|
||||
"detail": "Sécurité / Inteligence artificielle surveillance réseau",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Sécurité",
|
||||
"type": "Maintenance",
|
||||
"compte": "615610",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 36000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 34,
|
||||
"fournisseur": "DARKTRACE",
|
||||
"libelle": "Maintenance DARKTRACE ANTIGENA",
|
||||
"detail": "Sécurité / Inteligence artificielle surveillance réseau",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Sécurité",
|
||||
"type": "Maintenance",
|
||||
"compte": "615610",
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 24000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 35,
|
||||
"fournisseur": "3069UDA",
|
||||
"libelle": "Budget Petit matériel et Autres dépenses",
|
||||
"detail": "Divers matériels",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "Infogérance",
|
||||
"type": "Petits matériels",
|
||||
"compte": null,
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 12000.0
|
||||
},
|
||||
{
|
||||
"col_idx": 36,
|
||||
"fournisseur": "One System",
|
||||
"libelle": "Abo 4G",
|
||||
"detail": "Abo 4G siège et Concha",
|
||||
"facturation": "MEnsuelle",
|
||||
"mode_ventilation": "Siège + Concha",
|
||||
"categorie": "New",
|
||||
"type": "Liens internet",
|
||||
"compte": null,
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 7512.0
|
||||
},
|
||||
{
|
||||
"col_idx": 37,
|
||||
"fournisseur": "GAMMEO",
|
||||
"libelle": "Maintenance GAMMEO",
|
||||
"detail": "GMAO",
|
||||
"facturation": "Annuelle",
|
||||
"mode_ventilation": "Prorata classe 6",
|
||||
"categorie": "New",
|
||||
"type": "Maintenance",
|
||||
"compte": null,
|
||||
"budget_n1": null,
|
||||
"montant_previsionnel_2025": 36900.0
|
||||
}
|
||||
],
|
||||
"etablissements": [],
|
||||
"categories_totaux": {
|
||||
"App HEP": 51000,
|
||||
"App SMR": 11000,
|
||||
"App global": 400750,
|
||||
"Infogérance": 275020,
|
||||
"New": 72912,
|
||||
"Sécurité": 144400,
|
||||
"Téléphonie": 62000
|
||||
},
|
||||
"tendances_categories": {
|
||||
"App HEP": {
|
||||
"montant_n1": null,
|
||||
"montant_n": 51000,
|
||||
"variation_pct": null,
|
||||
"tendance": "stable"
|
||||
},
|
||||
"App SMR": {
|
||||
"montant_n1": null,
|
||||
"montant_n": 11000,
|
||||
"variation_pct": null,
|
||||
"tendance": "stable"
|
||||
},
|
||||
"App global": {
|
||||
"montant_n1": null,
|
||||
"montant_n": 400750,
|
||||
"variation_pct": null,
|
||||
"tendance": "stable"
|
||||
},
|
||||
"Infogérance": {
|
||||
"montant_n1": null,
|
||||
"montant_n": 275020,
|
||||
"variation_pct": null,
|
||||
"tendance": "stable"
|
||||
},
|
||||
"New": {
|
||||
"montant_n1": null,
|
||||
"montant_n": 72912,
|
||||
"variation_pct": null,
|
||||
"tendance": "stable"
|
||||
},
|
||||
"Sécurité": {
|
||||
"montant_n1": null,
|
||||
"montant_n": 144400,
|
||||
"variation_pct": null,
|
||||
"tendance": "stable"
|
||||
},
|
||||
"Téléphonie": {
|
||||
"montant_n1": null,
|
||||
"montant_n": 62000,
|
||||
"variation_pct": null,
|
||||
"tendance": "stable"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"source": "Prévi OPEX 2025 v1",
|
||||
"nb_postes": 33
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
import { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
TrendingDown,
|
||||
TrendingUp,
|
||||
Search,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
@@ -29,6 +30,7 @@ import { AppSidebar } from '../components/AppSidebar';
|
||||
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||
import { useAnnee, getOpexStorageKey } from '../contexts/AnneeContext';
|
||||
import opexRaw from '../data_opex.json';
|
||||
import opex2025Raw from '../data_opex_2025.json';
|
||||
import { formatEuros } from '../lib/format';
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -63,7 +65,8 @@ interface Poste {
|
||||
type: string | null;
|
||||
compte: string | null;
|
||||
budget_n1: number | null;
|
||||
montant_previsionnel_2026: number | null;
|
||||
montant_previsionnel_2026?: number | null;
|
||||
montant_previsionnel_2025?: number | null;
|
||||
}
|
||||
|
||||
interface Etablissement {
|
||||
@@ -74,13 +77,21 @@ interface Etablissement {
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface TendanceCategorie {
|
||||
montant_n1: number | null;
|
||||
montant_n: number;
|
||||
variation_pct: number | null;
|
||||
tendance: 'hausse' | 'baisse' | 'stable' | 'new';
|
||||
}
|
||||
|
||||
interface OpexData {
|
||||
annee: number;
|
||||
total_global: number;
|
||||
postes: Poste[];
|
||||
etablissements: Etablissement[];
|
||||
categories_totaux: Record<string, number>;
|
||||
meta: { nb_postes: number; nb_etablissements: number };
|
||||
tendances_categories?: Record<string, TendanceCategorie>;
|
||||
meta: { nb_postes: number; nb_etablissements?: number };
|
||||
}
|
||||
|
||||
// Ligne persistée (peut être source ou ajoutée par l'utilisateur)
|
||||
@@ -107,7 +118,13 @@ interface OpexAnneeState {
|
||||
|
||||
// ─── Constantes ───────────────────────────────────────────────────────────────
|
||||
|
||||
const opexData = opexRaw as OpexData;
|
||||
const opexData2026 = opexRaw as OpexData;
|
||||
const opexData2025 = opex2025Raw as OpexData;
|
||||
|
||||
function getOpexDataForAnnee(annee: number): OpexData {
|
||||
if (annee === 2025) return opexData2025;
|
||||
return opexData2026; // 2026 et autres années
|
||||
}
|
||||
|
||||
const CATEGORIES_FIXES = ['App global', 'Infogérance', 'Sécurité', 'Téléphonie', 'App HEP', 'App SMR'];
|
||||
|
||||
@@ -132,8 +149,9 @@ function formatNum(v: number | null | undefined): string {
|
||||
return new Intl.NumberFormat('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(v) + ' €';
|
||||
}
|
||||
|
||||
function buildLignesFromSource(): LigneOpex[] {
|
||||
return opexData.postes.map(p => ({
|
||||
function buildLignesFromSource(annee?: number): LigneOpex[] {
|
||||
const src = getOpexDataForAnnee(annee ?? 2026);
|
||||
return src.postes.map(p => ({
|
||||
id: `src-${p.col_idx}`,
|
||||
libelle: p.libelle,
|
||||
fournisseur: p.fournisseur ?? '',
|
||||
@@ -143,7 +161,7 @@ function buildLignesFromSource(): LigneOpex[] {
|
||||
compte: p.compte ?? '',
|
||||
detail: p.detail ?? '',
|
||||
budget_n1: p.budget_n1 ?? 0,
|
||||
montant: p.montant_previsionnel_2026 ?? 0,
|
||||
montant: (annee === 2025 ? p.montant_previsionnel_2025 : p.montant_previsionnel_2026) ?? 0,
|
||||
isCustom: false,
|
||||
}));
|
||||
}
|
||||
@@ -153,11 +171,14 @@ function loadOpexState(annee: number): OpexAnneeState {
|
||||
const raw = localStorage.getItem(getOpexStorageKey(annee));
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as OpexAnneeState;
|
||||
// Compatibilité ascendante : si l'ancien format (montants dict) est détecté
|
||||
if (parsed.lignes && Array.isArray(parsed.lignes)) return parsed;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return { lignes: buildLignesFromSource(), validated: false };
|
||||
// Données sources disponibles pour 2025 et 2026 uniquement
|
||||
if (annee === 2025 || annee === 2026) {
|
||||
return { lignes: buildLignesFromSource(annee), validated: false };
|
||||
}
|
||||
return { lignes: [], validated: false };
|
||||
}
|
||||
|
||||
function saveOpexState(annee: number, state: OpexAnneeState): void {
|
||||
@@ -426,7 +447,7 @@ export default function DsiOpex() {
|
||||
// Réinitialiser aux valeurs du JSON source
|
||||
const handleReset = useCallback(() => {
|
||||
if (isValidated) return;
|
||||
const newState: OpexAnneeState = { lignes: buildLignesFromSource(), validated: false };
|
||||
const newState: OpexAnneeState = { lignes: buildLignesFromSource(annee), validated: false };
|
||||
setOpexState(newState);
|
||||
saveOpexState(annee, newState);
|
||||
setIsDirty(false);
|
||||
@@ -551,7 +572,8 @@ export default function DsiOpex() {
|
||||
|
||||
// Établissements (vue par établissement)
|
||||
const filteredEtabs = useMemo(() => {
|
||||
return opexData.etablissements.filter(e => {
|
||||
const srcData = getOpexDataForAnnee(annee);
|
||||
return srcData.etablissements.filter(e => {
|
||||
if (search && !e.nom.toLowerCase().includes(search.toLowerCase()) &&
|
||||
!e.code.toLowerCase().includes(search.toLowerCase())) return false;
|
||||
return true;
|
||||
@@ -592,7 +614,7 @@ export default function DsiOpex() {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
{opexState.lignes.length} postes de charges · {opexData.meta.nb_etablissements} établissements
|
||||
{opexState.lignes.length} postes de charges · {getOpexDataForAnnee(annee).meta.nb_etablissements ?? opexData2026.meta.nb_etablissements} établissements
|
||||
{opexState.savedAt && !isValidated && (
|
||||
<span className="ml-2 text-xs">· Enregistré le {new Date(opexState.savedAt).toLocaleDateString('fr-FR')} à {new Date(opexState.savedAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
)}
|
||||
@@ -675,16 +697,50 @@ export default function DsiOpex() {
|
||||
const montant = totalParCategorie[cat] || 0;
|
||||
const pct = totalGlobal > 0 ? (montant / totalGlobal * 100).toFixed(1) : '0';
|
||||
const colors = getCatColors(cat);
|
||||
// Tendance depuis les données sources de l'année courante
|
||||
const srcData = getOpexDataForAnnee(annee);
|
||||
const tendanceInfo: TendanceCategorie | undefined = srcData.tendances_categories?.[cat];
|
||||
const tendance = tendanceInfo?.tendance;
|
||||
const variationPct = tendanceInfo?.variation_pct;
|
||||
const montantN1 = tendanceInfo?.montant_n1;
|
||||
return (
|
||||
<div key={cat} className="bg-card border border-border rounded-xl p-4 min-w-[140px] flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<BarChart3 className={`w-4 h-4 ${colors.icon}`} />
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wide font-medium truncate">{cat}</span>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<BarChart3 className={`w-4 h-4 ${colors.icon}`} />
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-wide font-medium truncate">{cat}</span>
|
||||
</div>
|
||||
{/* Flèche de tendance */}
|
||||
{tendance && tendance !== 'stable' && tendance !== 'new' && variationPct !== null && variationPct !== undefined && (
|
||||
<div className={`flex items-center gap-0.5 text-xs font-semibold px-1.5 py-0.5 rounded-full ${
|
||||
tendance === 'hausse' ? 'bg-red-100 text-red-600' : 'bg-green-100 text-green-600'
|
||||
}`}>
|
||||
{tendance === 'hausse'
|
||||
? <TrendingUp className="w-3 h-3" />
|
||||
: <TrendingDown className="w-3 h-3" />}
|
||||
<span>{variationPct > 0 ? '+' : ''}{variationPct}%</span>
|
||||
</div>
|
||||
)}
|
||||
{tendance === 'stable' && variationPct !== null && (
|
||||
<div className="flex items-center gap-0.5 text-xs font-semibold px-1.5 py-0.5 rounded-full bg-orange-100 text-orange-600">
|
||||
<span>≈ stable</span>
|
||||
</div>
|
||||
)}
|
||||
{tendance === 'new' && (
|
||||
<div className="flex items-center gap-0.5 text-xs font-semibold px-1.5 py-0.5 rounded-full bg-blue-100 text-blue-600">
|
||||
<span>Nouveau</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className={`text-xl font-bold tabular-nums ${colors.kpi}`} style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
{formatEuros(montant)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{pct}% du total</p>
|
||||
<div className="flex items-center justify-between mt-0.5">
|
||||
<p className="text-xs text-muted-foreground">{pct}% du total</p>
|
||||
{montantN1 !== null && montantN1 !== undefined && montantN1 > 0 && (
|
||||
<p className="text-xs text-muted-foreground">N-1 : {formatEuros(montantN1)}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -977,7 +1033,7 @@ export default function DsiOpex() {
|
||||
<SortBtn col="nom" label="Établissement" />
|
||||
</th>
|
||||
<th className="text-right px-4 py-3 font-medium text-muted-foreground hidden lg:table-cell">Base répartition</th>
|
||||
{opexData.postes.slice(0, showDetails ? 8 : 4).map(p => (
|
||||
{opexData2026.postes.slice(0, showDetails ? 8 : 4).map(p => (
|
||||
<th key={p.libelle} className="text-right px-3 py-3 font-medium text-muted-foreground text-xs max-w-24 hidden xl:table-cell">
|
||||
<span className="block truncate max-w-20" title={p.libelle}>{p.libelle.split(' ').slice(0, 3).join(' ')}</span>
|
||||
</th>
|
||||
@@ -999,7 +1055,7 @@ export default function DsiOpex() {
|
||||
? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(etab.base_repartition) + ' €'
|
||||
: '—'}
|
||||
</td>
|
||||
{opexData.postes.slice(0, showDetails ? 8 : 4).map(p => (
|
||||
{opexData2026.postes.slice(0, showDetails ? 8 : 4).map(p => (
|
||||
<td key={p.libelle} className="px-3 py-2.5 text-right text-xs tabular-nums text-muted-foreground hidden xl:table-cell">
|
||||
{etab.montants[p.libelle] > 0
|
||||
? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(Math.round(etab.montants[p.libelle])) + ' €'
|
||||
|
||||
Reference in New Issue
Block a user