diff --git a/CHANGELOG.md b/CHANGELOG.md index 8963bd860..d1b067e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,30 @@ # Changelog d'Helios +## [1.8.5](https://github.com/DNUM-SocialGouv/helios/compare/release/1.8.4...release/1.8.5) (TODO) + +### Bug Fixes + +### Features + +- hel-996: Ajout des données concernant les fonds de roulement, les besoins en fonds de roulement et la trésorerie +- hel-997: Ajout du graphique horizontal concernant les fonds de roulement +- hel-998: Ajout du graphique horizontal concernant le besoin en fonds de roulement +- hel-999: Ajout du graphique horizontal concernant la trésorerie +- hel-1004: Ajout d’une page de CGU +- hel-1005: Ajout d’un formulaire de validation des CGU obligatoire +- hel-1007: Ajout des nouvelles informations de budget dans la page de comparaison + ## [1.8.4](https://github.com/DNUM-SocialGouv/helios/compare/release/1.8.3...release/1.8.4) (2026-03-20) ### Bug Fixes + - vr-156: Correction du calcul du taux de variation dans la vignette "Effectifs" - vr-157: Position du libellé en cas de valeur nulle dans le graphique "Entrées et sorties" - vr-158: Taille des graphiques lorsque graphiques manquants dans un bloc - vr-159: Affichage de certains graphiques avec des données manquantes ### Features + - vr-146: Modification de la taille des points trimestriels sur les graphiques par filière et catégorie - vr-150: Wording détail effectifs par filière - vr-151: Nommage des sorties Excel effectifs par filière @@ -25,6 +41,7 @@ ### Bug Fixes ### Features + - vr-152: Renommage des colonnes de plusieurs tables ## [1.8.2.1](https://github.com/DNUM-SocialGouv/helios/compare/release/1.8.2...release/1.8.2.1) (2026-03-05) diff --git a/database/dataSource.ts b/database/dataSource.ts index 4b02cc3e0..22cda4fea 100644 --- a/database/dataSource.ts +++ b/database/dataSource.ts @@ -100,6 +100,9 @@ import { AjouterFailedAttemptsEtLockUntil1771495505819 } from "./migrations/1771 import { AjoutTableNoDataQualiteHasMS1772117266351 } from "./migrations/1772117266351-AjoutTableNoDataQualiteHasMS"; import { AjoutHasAuProfilMedicoSocial1772196772012 } from "./migrations/1772196772012-AjoutHasAuProfilMedicoSocial"; import { AjoutFondsRoulementEtTresorerieDansBudgetFinance1772702784578 } from "./migrations/1772702784578-AjoutFondsRoulementEtTresorerieDansBudgetFinance"; +import { AjoutProfilFondDeRoulement1773072615839 } from "./migrations/1773072615839-AjoutProfilFondDeRoulement"; +import { AjoutProfilBesoinFondDeRoulement1773152515839 } from "./migrations/1773152515839-AjoutProfilBesoinFondDeRoulement"; +import { AjoutProfilTresorerie1773223041788 } from "./migrations/1773223041788-AjoutProfilTresorerie"; import { updateProfileTable1796422585498 } from "./migrations/1796422585498-updateProfileTable"; import { AddCreatedByToProfileTable1796792910177 } from "./migrations/1796792910177-AddCreatedByToProfileTable"; import { ModificationValeurProfil1797341938070 } from "./migrations/1797341938070-modificationValeurProfil"; @@ -107,6 +110,7 @@ import { AjoutBudgetEtFinanceAProfilETSanitaire1797688226682 } from "./migration import { AjoutAllocationDeRessourcesToBudgetEtFinance1798688226682 } from "./migrations/1798688226682-AjoutAllocationDeRessourcesToBudgetEtFinance"; import { AjoutDesOccupationsDansLesProfils1799478704013 } from "./migrations/1799478704013-AjoutDesOccupationsDansLesProfils"; import { AjoutLesJourneesUsldDansLesProfils1799501916707 } from "./migrations/1799501916707-AjoutLesJourneesUsldDansLesProfils"; +import { AjoutCguDansUtilisateur1774024704472 } from "./migrations/1774024704472-AjoutCguDansUtilisateur"; import { AjoutVigieRhContrat1738320902145 } from "./migrations/vigie_rh/1738320902145-AjoutVigieRhContrat"; import { AjoutVigieRhProfessionFiliere1738328763147 } from "./migrations/vigie_rh/1738328763147-AjoutVigieRhProfessionFiliere"; import { AjoutVigieRhProfessionGroupe1738328796095 } from "./migrations/vigie_rh/1738328796095-AjoutVigieRhProfessionGroupe"; @@ -359,6 +363,7 @@ const datasource = new DataSource({ ClefEtrangereListEts1743596937227, AjoutNombreJourneeUSLDActivitesSanitaires1745321952709, AjoutLesJourneesUsldDansLesProfils1799501916707, + AjoutCguDansUtilisateur1774024704472, ReferentielCategoriesFiness1747831401347, AjoutCategorieRecherche1748009262073, AjoutLaColonneDomaineAuReferentielCategories1748332500947, @@ -398,6 +403,9 @@ const datasource = new DataSource({ AjoutTableNoDataQualiteHasMS1772117266351, AjoutHasAuProfilMedicoSocial1772196772012, AjoutFondsRoulementEtTresorerieDansBudgetFinance1772702784578, + AjoutProfilFondDeRoulement1773072615839, + AjoutProfilBesoinFondDeRoulement1773152515839, + AjoutProfilTresorerie1773223041788 ], type: "postgres", url: environmentVariables.DATABASE_URL, diff --git a/database/migrations/1773072615839-AjoutProfilFondDeRoulement.ts b/database/migrations/1773072615839-AjoutProfilFondDeRoulement.ts new file mode 100644 index 000000000..bd21af7db --- /dev/null +++ b/database/migrations/1773072615839-AjoutProfilFondDeRoulement.ts @@ -0,0 +1,66 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AjoutProfilFondDeRoulement1773072615839 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{autreRegion,profilEJ,budgetEtFinance}', + COALESCE(profil_value->'autreRegion'->'profilEJ'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'fondsDeRoulement', 'no' + ), + true + ); + + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{institution,profilEJ,budgetEtFinance}', + COALESCE(profil_value->'institution'->'profilEJ'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'fondsDeRoulement', 'no' + ), + true + ); + + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{autreRegion,profilETSanitaire,budgetEtFinance}', + COALESCE(profil_value->'autreRegion'->'profilETSanitaire'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'fondsDeRoulement', 'no' + ), + true + ); + + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{institution,profilETSanitaire,budgetEtFinance}', + COALESCE(profil_value->'institution'->'profilETSanitaire'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'fondsDeRoulement', 'no' + ), + true + ); + + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE profil + SET profil_value = profil_value #- '{autreRegion,profilEJ,budgetEtFinance,fondsDeRoulement}'; + + UPDATE profil + SET profil_value = profil_value #- '{institution,profilEJ,budgetEtFinance,fondsDeRoulement}'; + + UPDATE profil + SET profil_value = profil_value #- '{autreRegion,profilETSanitaire,budgetEtFinance,fondsDeRoulement}'; + + UPDATE profil + SET profil_value = profil_value #- '{institution,profilETSanitaire,budgetEtFinance,fondsDeRoulement}'; + + `); + } + +} diff --git a/database/migrations/1773152515839-AjoutProfilBesoinFondDeRoulement.ts b/database/migrations/1773152515839-AjoutProfilBesoinFondDeRoulement.ts new file mode 100644 index 000000000..f4f211c34 --- /dev/null +++ b/database/migrations/1773152515839-AjoutProfilBesoinFondDeRoulement.ts @@ -0,0 +1,64 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AjoutProfilBesoinFondDeRoulement1773152515839 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{autreRegion,profilEJ,budgetEtFinance}', + COALESCE(profil_value->'autreRegion'->'profilEJ'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'besoinFondsDeRoulement', 'no' + ), + true + ); + + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{institution,profilEJ,budgetEtFinance}', + COALESCE(profil_value->'institution'->'profilEJ'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'besoinFondsDeRoulement', 'no' + ), + true + ); + + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{autreRegion,profilETSanitaire,budgetEtFinance}', + COALESCE(profil_value->'autreRegion'->'profilETSanitaire'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'besoinFondsDeRoulement', 'no' + ), + true + ); + + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{institution,profilETSanitaire,budgetEtFinance}', + COALESCE(profil_value->'institution'->'profilETSanitaire'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'besoinFondsDeRoulement', 'no' + ), + true + ); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE profil + SET profil_value = profil_value #- '{autreRegion,profilEJ,budgetEtFinance,besoinFondsDeRoulement}'; + + UPDATE profil + SET profil_value = profil_value #- '{institution,profilEJ,budgetEtFinance,besoinFondsDeRoulement}'; + + UPDATE profil + SET profil_value = profil_value #- '{autreRegion,profilETSanitaire,budgetEtFinance,besoinFondsDeRoulement}'; + + UPDATE profil + SET profil_value = profil_value #- '{institution,profilETSanitaire,budgetEtFinance,besoinFondsDeRoulement}'; + `); + } + +} diff --git a/database/migrations/1773223041788-AjoutProfilTresorerie.ts b/database/migrations/1773223041788-AjoutProfilTresorerie.ts new file mode 100644 index 000000000..76385959c --- /dev/null +++ b/database/migrations/1773223041788-AjoutProfilTresorerie.ts @@ -0,0 +1,66 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AjoutProfilTresorerie1773223041788 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{autreRegion,profilEJ,budgetEtFinance}', + COALESCE(profil_value->'autreRegion'->'profilEJ'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'tresorerie', 'no' + ), + true + ); + + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{institution,profilEJ,budgetEtFinance}', + COALESCE(profil_value->'institution'->'profilEJ'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'tresorerie', 'no' + ), + true + ); + + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{autreRegion,profilETSanitaire,budgetEtFinance}', + COALESCE(profil_value->'autreRegion'->'profilETSanitaire'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'tresorerie', 'no' + ), + true + ); + + UPDATE profil + SET profil_value = jsonb_set( + COALESCE(profil_value, '{}'::jsonb), + '{institution,profilETSanitaire,budgetEtFinance}', + COALESCE(profil_value->'institution'->'profilETSanitaire'->'budgetEtFinance', '{}'::jsonb) || jsonb_build_object( + 'tresorerie', 'no' + ), + true + ); + + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE profil + SET profil_value = profil_value #- '{autreRegion,profilEJ,budgetEtFinance,tresorerie}'; + + UPDATE profil + SET profil_value = profil_value #- '{institution,profilEJ,budgetEtFinance,tresorerie}'; + + UPDATE profil + SET profil_value = profil_value #- '{autreRegion,profilETSanitaire,budgetEtFinance,tresorerie}'; + + UPDATE profil + SET profil_value = profil_value #- '{institution,profilETSanitaire,budgetEtFinance,tresorerie}'; + + `); + } + +} diff --git a/database/migrations/1774024704472-AjoutCguDansUtilisateur.ts b/database/migrations/1774024704472-AjoutCguDansUtilisateur.ts new file mode 100644 index 000000000..593343cab --- /dev/null +++ b/database/migrations/1774024704472-AjoutCguDansUtilisateur.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AjoutCguDansUtilisateur1774024704472 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE utilisateur + ADD COLUMN ut_cgu BOOLEAN NOT NULL DEFAULT false; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE utilisateur + DROP COLUMN ut_cgu; + `); + } +} diff --git a/database/models/UtilisateurModel.ts b/database/models/UtilisateurModel.ts index 50729487c..fb08d83df 100644 --- a/database/models/UtilisateurModel.ts +++ b/database/models/UtilisateurModel.ts @@ -71,4 +71,7 @@ export class UtilisateurModel { public lockUntil!: Date | null; @Column({ name: "ut_last_pwd_change_date", type: "date" }) public lastPwdChangeDate!: string; + + @Column({ name: "ut_cgu", type: "boolean", default: false }) + public cgu!: boolean; } diff --git a/src/backend/infrastructure/controllers/updateCguEndpoint.ts b/src/backend/infrastructure/controllers/updateCguEndpoint.ts new file mode 100644 index 000000000..1650e82f2 --- /dev/null +++ b/src/backend/infrastructure/controllers/updateCguEndpoint.ts @@ -0,0 +1,7 @@ +import { UtilisateursUseCase } from "../../métier/use-cases/UtilisateursUseCase"; +import { Dependencies } from "../dependencies"; + +export async function updateCguEndpoint(dependencies: Dependencies, userCode: string, cguAccepted: boolean): Promise { + const utilisateurUseCase = new UtilisateursUseCase(dependencies.utilisateurLoader); + return await utilisateurUseCase.updateCgu(userCode, cguAccepted); +} diff --git a/src/backend/infrastructure/gateways/comparaison-loader/TypeOrmComparaisonLoader.test.ts b/src/backend/infrastructure/gateways/comparaison-loader/TypeOrmComparaisonLoader.test.ts index 8c3782a27..dff246cf9 100644 --- a/src/backend/infrastructure/gateways/comparaison-loader/TypeOrmComparaisonLoader.test.ts +++ b/src/backend/infrastructure/gateways/comparaison-loader/TypeOrmComparaisonLoader.test.ts @@ -8,6 +8,7 @@ import { AllocationRessourceETModel } from "../../../../../database/models/Alloc import { AutorisationMédicoSocialModel } from "../../../../../database/models/AutorisationMédicoSocialModel"; import { BudgetEtFinancesEntiteJuridiqueModel } from "../../../../../database/models/BudgetEtFinancesEntiteJuridiqueModel"; import { BudgetEtFinancesMédicoSocialModel } from "../../../../../database/models/BudgetEtFinancesMédicoSocialModel"; +import { BudgetEtFinancesSanitaireModel } from "../../../../../database/models/BudgetEtFinancesSanitaireModel"; import { EntitéJuridiqueModel } from "../../../../../database/models/EntitéJuridiqueModel"; import { ProfilModel } from "../../../../../database/models/ProfilModel"; import { RessourcesHumainesEntiteJuridiqueModel } from "../../../../../database/models/RessourcesHumainesEntiteJuridiqueModel"; @@ -32,6 +33,7 @@ describe("La comparaison des établissements", () => { let ressourcesHumainesEJModelRepository: Repository; let ressourcesHumainesETSANModelRepository: Repository; let budgetEtFinancesMedicoSocialModelRepository: Repository; + let budgetEtFinancesSanitaireModelRepository: Repository; let activiteeMedicoSocialModelRepository: Repository; let autorisationMedicoSocialModelRepository: Repository; let activiteSanitaireRepository: Repository; @@ -86,6 +88,7 @@ describe("La comparaison des établissements", () => { ressourcesHumainesEJModelRepository = (await orm).getRepository(RessourcesHumainesEntiteJuridiqueModel); ressourcesHumainesETSANModelRepository = (await orm).getRepository(RessourcesHumainesEtablissementSanitaireModel); budgetEtFinancesMedicoSocialModelRepository = (await orm).getRepository(BudgetEtFinancesMédicoSocialModel); + budgetEtFinancesSanitaireModelRepository = (await orm).getRepository(BudgetEtFinancesSanitaireModel); activiteeMedicoSocialModelRepository = (await orm).getRepository(ActivitéMédicoSocialModel); autorisationMedicoSocialModelRepository = (await orm).getRepository(AutorisationMédicoSocialModel); activiteSanitaireRepository = (await orm).getRepository(ActivitéSanitaireModel); @@ -296,6 +299,23 @@ describe("La comparaison des établissements", () => { }) ]) + await budgetEtFinancesSanitaireModelRepository.insert([ + { + année: 2022, + numéroFinessEtablissementTerritorial: "200000000", + fondsDeRoulement: 11000, + besoinFondsDeRoulement: 12000, + tresorerie: 13000, + }, + { + année: 2022, + numéroFinessEtablissementTerritorial: "200000001", + fondsDeRoulement: 21000, + besoinFondsDeRoulement: 22000, + tresorerie: 23000, + }, + ]); + const budgetFinanceEntiteJuridique = new BudgetEtFinancesEntiteJuridiqueModel(); budgetFinanceEntiteJuridique.numéroFinessEntitéJuridique = "000000000"; budgetFinanceEntiteJuridique.année = 2022; @@ -317,6 +337,9 @@ describe("La comparaison des établissements", () => { budgetFinanceEntiteJuridique.resultatNetComptableSan = 0.1; budgetFinanceEntiteJuridique.tauxDeCafNetteSan = 0.2; budgetFinanceEntiteJuridique.ratioDependanceFinanciere = 0.3; + budgetFinanceEntiteJuridique.fondsDeRoulement = 1000; + budgetFinanceEntiteJuridique.besoinFondsDeRoulement = 2000; + budgetFinanceEntiteJuridique.tresorerie = 3000; await budgetEtFinancesEntiteJuridiqueRepository.insert(budgetFinanceEntiteJuridique); @@ -524,6 +547,9 @@ describe("La comparaison des établissements", () => { joursAbsenteismePnm: 40, nombreEtpPm: 10, nombreEtpPnm: 20, + fondsDeRoulement: 11000, + besoinFondsDeRoulement: 12000, + tresorerie: 13000, }, { numéroFiness: "200000001", @@ -547,6 +573,9 @@ describe("La comparaison des établissements", () => { joursAbsenteismePnm: 45, nombreEtpPm: 15, nombreEtpPnm: 25, + fondsDeRoulement: 21000, + besoinFondsDeRoulement: 22000, + tresorerie: 23000, }, { numéroFiness: "299999999", @@ -570,6 +599,9 @@ describe("La comparaison des établissements", () => { joursAbsenteismePnm: null, nombreEtpPm: null, nombreEtpPnm: null, + fondsDeRoulement: null, + besoinFondsDeRoulement: null, + tresorerie: null, }, { numéroFiness: "000000000", @@ -593,6 +625,9 @@ describe("La comparaison des établissements", () => { joursAbsenteismePnm: '', nombreEtpPm: '', nombreEtpPnm: '', + fondsDeRoulement: '', + besoinFondsDeRoulement: '', + tresorerie: '', }, ]); }); @@ -635,6 +670,9 @@ describe("La comparaison des établissements", () => { resultatNetComptableEj: 0.1, tauxCafEj: 20, ratioDependanceFinanciere: 30, + fondsDeRoulement: 1000, + besoinFondsDeRoulement: 2000, + tresorerie: 3000, sejoursHad: 12, enveloppe1: null, enveloppe2: null, @@ -670,6 +708,9 @@ describe("La comparaison des établissements", () => { joursAbsenteismePnm: '', nombreEtpPm: '', nombreEtpPnm: '', + fondsDeRoulement: '', + besoinFondsDeRoulement: '', + tresorerie: '', }, { numéroFiness: "200000001", @@ -696,6 +737,9 @@ describe("La comparaison des établissements", () => { joursAbsenteismePnm: '', nombreEtpPm: '', nombreEtpPnm: '', + fondsDeRoulement: '', + besoinFondsDeRoulement: '', + tresorerie: '', }, { numéroFiness: "299999999", @@ -722,9 +766,10 @@ describe("La comparaison des établissements", () => { joursAbsenteismePnm: '', nombreEtpPm: '', nombreEtpPnm: '', + fondsDeRoulement: '', + besoinFondsDeRoulement: '', + tresorerie: '', }, ]); }); }); - - diff --git a/src/backend/infrastructure/gateways/comparaison-loader/TypeOrmComparaisonLoader.ts b/src/backend/infrastructure/gateways/comparaison-loader/TypeOrmComparaisonLoader.ts index 1d97e424a..a0b0b6c37 100644 --- a/src/backend/infrastructure/gateways/comparaison-loader/TypeOrmComparaisonLoader.ts +++ b/src/backend/infrastructure/gateways/comparaison-loader/TypeOrmComparaisonLoader.ts @@ -64,6 +64,9 @@ type ComparaisonEJTypeOrm = Readonly<{ enveloppe_1: number; enveloppe_2: number; enveloppe_3: number; + fonds_de_roulement: number; + besoin_fonds_de_roulement: number; + tresorerie: number; }>; type ComparaisonSANTypeOrm = Readonly<{ @@ -89,6 +92,9 @@ type ComparaisonSANTypeOrm = Readonly<{ enveloppe_1: number; enveloppe_2: number; enveloppe_3: number; + fonds_de_roulement: number; + besoin_fonds_de_roulement: number; + tresorerie: number; }>; export class TypeOrmComparaisonLoader implements ComparaisonLoader { @@ -319,6 +325,9 @@ FROM ( bg.resultat_net_comptable_san, bg.taux_de_caf_nette_san, bg.ratio_dependance_financiere, + bg.fonds_de_roulement, + bg.besoin_fonds_de_roulement, + bg.tresorerie, acs.nombre_sejours_had, rhej.nombre_etp_pm, rhej.nombre_etp_pnm, @@ -350,6 +359,9 @@ FROM ( enveloppe_1, enveloppe_2, enveloppe_3, + bg.fonds_de_roulement, + bg.besoin_fonds_de_roulement, + bg.tresorerie, CASE WHEN bg.depenses_titre_i_global IS NULL AND bg.depenses_titre_ii_global IS NULL AND bg.depenses_titre_iii_global IS NULL AND bg.depenses_titre_iv_global IS NULL THEN NULL ELSE COALESCE(bg.depenses_titre_i_global, 0) + COALESCE(bg.depenses_titre_ii_global, 0) + COALESCE(bg.depenses_titre_iii_global, 0) + COALESCE(bg.depenses_titre_iv_global, 0) @@ -567,6 +579,8 @@ FROM ( const compareEtSanQueryBody = ` from recherche et LEFT JOIN activite_sanitaire acs on et.numero_finess = acs.numero_finess_etablissement_territorial and acs.annee = ${annee} + LEFT JOIN budget_et_finances_sanitaire bg + on et.numero_finess = bg.numero_finess_etablissement_territorial and bg.annee = ${annee} LEFT JOIN ${compareEnveloppe1} on et.numero_finess = ar1.numero_finess_etablissement_territorial LEFT JOIN ${compareEnveloppe2} on et.numero_finess = ar2.numero_finess_etablissement_territorial LEFT JOIN ${compareEnveloppe3} on et.numero_finess = ar3.numero_finess_etablissement_territorial @@ -611,7 +625,10 @@ FROM ( rhs.jours_absenteisme_pnm, enveloppe_1, enveloppe_2, - enveloppe_3 ${compareEtSanQueryBody}`; + enveloppe_3, + bg.fonds_de_roulement, + bg.besoin_fonds_de_roulement, + bg.tresorerie ${compareEtSanQueryBody}`; const limitForExport = forExport ? "" : `LIMIT ${this.NOMBRE_DE_RÉSULTATS_MAX_PAR_PAGE} OFFSET ${this.NOMBRE_DE_RÉSULTATS_MAX_PAR_PAGE * (page - 1)}`; const paginatedCompareETSanQuery = @@ -717,6 +734,9 @@ FROM ( resultatNetComptableEj: resultat.type === "Entité juridique" ? resultat.resultat_net_comptable_san : '', tauxCafEj: resultat.type === "Entité juridique" ? this.transformInRate(resultat.taux_de_caf_nette_san, 1) : '', ratioDependanceFinanciere: resultat.type === "Entité juridique" ? this.transformInRate(resultat.ratio_dependance_financiere, 1) : '', + fondsDeRoulement: resultat.type === "Entité juridique" ? this.makeNumberArrondi(resultat.fonds_de_roulement, 0) : '', + besoinFondsDeRoulement: resultat.type === "Entité juridique" ? this.makeNumberArrondi(resultat.besoin_fonds_de_roulement, 0) : '', + tresorerie: resultat.type === "Entité juridique" ? this.makeNumberArrondi(resultat.tresorerie, 0) : '', sejoursHad: resultat.type === "Entité juridique" ? resultat.nombre_sejours_had : '', nombreEtpPm: resultat.type === "Entité juridique" ? resultat.nombre_etp_pm : '', nombreEtpPnm: resultat.type === "Entité juridique" ? resultat.nombre_etp_pnm : '', @@ -753,7 +773,10 @@ FROM ( joursAbsenteismePnm: resultat.type === "Sanitaire" ? resultat.jours_absenteisme_pnm : '', enveloppe1: resultat.type === "Sanitaire" ? resultat.enveloppe_1 : '', enveloppe2: resultat.type === "Sanitaire" ? resultat.enveloppe_2 : '', - enveloppe3: resultat.type === "Sanitaire" ? resultat.enveloppe_3 : '' + enveloppe3: resultat.type === "Sanitaire" ? resultat.enveloppe_3 : '', + fondsDeRoulement: resultat.type === "Sanitaire" ? this.makeNumberArrondi(resultat.fonds_de_roulement, 0) : '', + besoinFondsDeRoulement: resultat.type === "Sanitaire" ? this.makeNumberArrondi(resultat.besoin_fonds_de_roulement, 0) : '', + tresorerie: resultat.type === "Sanitaire" ? this.makeNumberArrondi(resultat.tresorerie, 0) : '', }; }); } diff --git "a/src/backend/infrastructure/gateways/entit\303\251-juridique-loader/TypeORMEntit\303\251JuridiqueLoader.test.ts" "b/src/backend/infrastructure/gateways/entit\303\251-juridique-loader/TypeORMEntit\303\251JuridiqueLoader.test.ts" index 6a0101d04..e45a41b65 100644 --- "a/src/backend/infrastructure/gateways/entit\303\251-juridique-loader/TypeORMEntit\303\251JuridiqueLoader.test.ts" +++ "b/src/backend/infrastructure/gateways/entit\303\251-juridique-loader/TypeORMEntit\303\251JuridiqueLoader.test.ts" @@ -310,6 +310,9 @@ describe("Entité juridique loader", () => { budgetFinanceEntiteJuridique.resultatNetComptableSan = 0.1; budgetFinanceEntiteJuridique.tauxDeCafNetteSan = 0.2; budgetFinanceEntiteJuridique.ratioDependanceFinanciere = 0.3; + budgetFinanceEntiteJuridique.fondsDeRoulement = 0.4; + budgetFinanceEntiteJuridique.besoinFondsDeRoulement = 0.5; + budgetFinanceEntiteJuridique.tresorerie = 0.6; await budgetFinanceEntiteJuridiqueRepository.insert(budgetFinanceEntiteJuridique); @@ -363,6 +366,12 @@ describe("Entité juridique loader", () => { ratioDependanceFinanciere: 0.3, tauxDeCafNetSan: 0.2, + + fondsDeRoulement: 0.4, + + besoinFondsDeRoulement: 0.5, + + tresorerie: 0.6, } as EntitéJuridiqueBudgetFinance, ]); }); diff --git "a/src/backend/infrastructure/gateways/entit\303\251-juridique-loader/TypeOrmEntit\303\251JuridiqueLoader.ts" "b/src/backend/infrastructure/gateways/entit\303\251-juridique-loader/TypeOrmEntit\303\251JuridiqueLoader.ts" index d843b1d4d..1494e75bc 100644 --- "a/src/backend/infrastructure/gateways/entit\303\251-juridique-loader/TypeOrmEntit\303\251JuridiqueLoader.ts" +++ "b/src/backend/infrastructure/gateways/entit\303\251-juridique-loader/TypeOrmEntit\303\251JuridiqueLoader.ts" @@ -343,6 +343,9 @@ export class TypeOrmEntiteJuridiqueLoader implements EntitéJuridiqueLoader { resultatNetComptable: budget.resultatNetComptableSan, ratioDependanceFinanciere: budget.ratioDependanceFinanciere, tauxDeCafNetSan: budget.tauxDeCafNetteSan, + fondsDeRoulement: budget.fondsDeRoulement, + besoinFondsDeRoulement: budget.besoinFondsDeRoulement, + tresorerie: budget.tresorerie, })); } diff --git a/src/backend/infrastructure/gateways/utilisateur-loader/TypeOrmUtilisateurLoader.ts b/src/backend/infrastructure/gateways/utilisateur-loader/TypeOrmUtilisateurLoader.ts index fb26b9fd4..89307d9bf 100644 --- a/src/backend/infrastructure/gateways/utilisateur-loader/TypeOrmUtilisateurLoader.ts +++ b/src/backend/infrastructure/gateways/utilisateur-loader/TypeOrmUtilisateurLoader.ts @@ -391,6 +391,16 @@ export class TypeOrmUtilisateurLoader implements UtilisateurLoader { } } + async updateCgu(userCode: string, cguAccepted: boolean): Promise { + const user = await (await this.orm).getRepository(UtilisateurModel).findOne({ where: { code: userCode } }); + if (!user) { + throw new Error("User not found"); + } + + user.cgu = cguAccepted; + await (await this.orm).getRepository(UtilisateurModel).save(user); + } + async deleteUser(userCode: string): Promise { try { const user = await (await this.orm).getRepository(UtilisateurModel).findOne({ where: { code: userCode } }); diff --git "a/src/backend/infrastructure/gateways/\303\251tablissement-territorial-loader/TypeOrm\303\211tablissementTerritorialSanitaireLoader.test.ts" "b/src/backend/infrastructure/gateways/\303\251tablissement-territorial-loader/TypeOrm\303\211tablissementTerritorialSanitaireLoader.test.ts" index 5f5b841be..1692242c8 100644 --- "a/src/backend/infrastructure/gateways/\303\251tablissement-territorial-loader/TypeOrm\303\211tablissementTerritorialSanitaireLoader.test.ts" +++ "b/src/backend/infrastructure/gateways/\303\251tablissement-territorial-loader/TypeOrm\303\211tablissementTerritorialSanitaireLoader.test.ts" @@ -897,6 +897,10 @@ describe("Établissement territorial sanitaire loader", () => { budgetEtFinancesSanitaire.resultatNetComptableSan = 0.1; budgetEtFinancesSanitaire.tauxDeCafNetteSan = 0.2; budgetEtFinancesSanitaire.ratioDependanceFinanciere = 0.3; + budgetEtFinancesSanitaire.fondsDeRoulement = 0.4; + budgetEtFinancesSanitaire.besoinFondsDeRoulement = 0.5; + budgetEtFinancesSanitaire.tresorerie = 0.6; + await budgetEtFinancesSanitaireRepository.insert(budgetEtFinancesSanitaire); @@ -950,6 +954,10 @@ describe("Établissement territorial sanitaire loader", () => { ratioDependanceFinanciere: 0.3, tauxDeCafNetSan: 0.2, + + fondsDeRoulement: 0.4, + besoinFondsDeRoulement: 0.5, + tresorerie: 0.6, } as EntitéJuridiqueBudgetFinance, ]); }); diff --git "a/src/backend/infrastructure/gateways/\303\251tablissement-territorial-loader/TypeOrm\303\211tablissementTerritorialSanitaireLoader.ts" "b/src/backend/infrastructure/gateways/\303\251tablissement-territorial-loader/TypeOrm\303\211tablissementTerritorialSanitaireLoader.ts" index 0940d45a0..e717bd2ba 100644 --- "a/src/backend/infrastructure/gateways/\303\251tablissement-territorial-loader/TypeOrm\303\211tablissementTerritorialSanitaireLoader.ts" +++ "b/src/backend/infrastructure/gateways/\303\251tablissement-territorial-loader/TypeOrm\303\211tablissementTerritorialSanitaireLoader.ts" @@ -1151,6 +1151,9 @@ export class TypeOrmEtablissementTerritorialSanitaireLoader implements Établiss resultatNetComptable: budget.resultatNetComptableSan, ratioDependanceFinanciere: budget.ratioDependanceFinanciere, tauxDeCafNetSan: budget.tauxDeCafNetteSan, + fondsDeRoulement: budget.fondsDeRoulement, + besoinFondsDeRoulement: budget.besoinFondsDeRoulement, + tresorerie: budget.tresorerie, })); } diff --git "a/src/backend/m\303\251tier/entities/ResultatDeComparaison.ts" "b/src/backend/m\303\251tier/entities/ResultatDeComparaison.ts" index 400b878e1..96503a04f 100644 --- "a/src/backend/m\303\251tier/entities/ResultatDeComparaison.ts" +++ "b/src/backend/m\303\251tier/entities/ResultatDeComparaison.ts" @@ -14,6 +14,9 @@ export type ResultatEJ = Readonly<{ resultatNetComptableEj: number | null | string; tauxCafEj: number | null | string; ratioDependanceFinanciere: number | null | string; + fondsDeRoulement: number | null | string; + besoinFondsDeRoulement: number | null | string; + tresorerie: number | null | string; sejoursHad: number | null | string; nombreEtpPm: number | null | string; nombreEtpPnm: number | null | string; @@ -72,6 +75,9 @@ export type ResultatSAN = Readonly<{ depensesInterimPm: number | null | string; joursAbsenteismePm: number | null | string; joursAbsenteismePnm: number | null | string; + fondsDeRoulement: number | null | string; + besoinFondsDeRoulement: number | null | string; + tresorerie: number | null | string; enveloppe1: number | null | string; enveloppe2: number | null | string; enveloppe3: number | null | string; @@ -101,4 +107,3 @@ export type EnveloppesResult = { topEnveloppesEj: Enveloppes; topEnveloppesSan: Enveloppes; }; - diff --git "a/src/backend/m\303\251tier/entities/Utilisateur/Utilisateur.ts" "b/src/backend/m\303\251tier/entities/Utilisateur/Utilisateur.ts" index a2bf8e602..8844076f2 100644 --- "a/src/backend/m\303\251tier/entities/Utilisateur/Utilisateur.ts" +++ "b/src/backend/m\303\251tier/entities/Utilisateur/Utilisateur.ts" @@ -15,4 +15,5 @@ export type Utilisateur = { dateModification: Date; creePar: number; modifiePar: number; + cgu: boolean; }; diff --git "a/src/backend/m\303\251tier/entities/entit\303\251-juridique/Entit\303\251JuridiqueBudgetFinance.ts" "b/src/backend/m\303\251tier/entities/entit\303\251-juridique/Entit\303\251JuridiqueBudgetFinance.ts" index 5c69288c6..631e67824 100644 --- "a/src/backend/m\303\251tier/entities/entit\303\251-juridique/Entit\303\251JuridiqueBudgetFinance.ts" +++ "b/src/backend/m\303\251tier/entities/entit\303\251-juridique/Entit\303\251JuridiqueBudgetFinance.ts" @@ -42,4 +42,10 @@ export type EntitéJuridiqueBudgetFinance = { ratioDependanceFinanciere: number | null; tauxDeCafNetSan: number | null; + + fondsDeRoulement: number | null; + + besoinFondsDeRoulement: number | null; + + tresorerie: number | null; }; diff --git "a/src/backend/m\303\251tier/gateways/UtilisateurLoader.ts" "b/src/backend/m\303\251tier/gateways/UtilisateurLoader.ts" index dd3d5254d..aa575b0ae 100644 --- "a/src/backend/m\303\251tier/gateways/UtilisateurLoader.ts" +++ "b/src/backend/m\303\251tier/gateways/UtilisateurLoader.ts" @@ -26,6 +26,7 @@ export interface UtilisateurLoader { ): Promise; getUserByCode(code: string): Promise; updateUser(userCode: string, roleCode: string, institutionCode: string, profilsCode: string[], firstname: string, lastname: string): Promise; + updateCgu(userCode: string, cguAccepted: boolean): Promise; deleteUser(userCode: string): Promise; reactivateUser(userCode: string): Promise; } diff --git "a/src/backend/m\303\251tier/use-cases/UtilisateursUseCase.ts" "b/src/backend/m\303\251tier/use-cases/UtilisateursUseCase.ts" index e27aae58a..bbfe25da1 100644 --- "a/src/backend/m\303\251tier/use-cases/UtilisateursUseCase.ts" +++ "b/src/backend/m\303\251tier/use-cases/UtilisateursUseCase.ts" @@ -33,6 +33,10 @@ export class UtilisateursUseCase { return this.utilisateurLoader.updateUser(userCode, roleCode, institutionCode, profilsCode, firstname, lastname); } + async updateCgu(userCode: string, cguAccepted: boolean): Promise { + return this.utilisateurLoader.updateCgu(userCode, cguAccepted); + } + async deleteUser(userCode: string): Promise { return this.utilisateurLoader.deleteUser(userCode); } diff --git a/src/backend/profileFiltersHelper.juridique.test.ts b/src/backend/profileFiltersHelper.juridique.test.ts index f309692e4..40a14d006 100644 --- a/src/backend/profileFiltersHelper.juridique.test.ts +++ b/src/backend/profileFiltersHelper.juridique.test.ts @@ -121,6 +121,9 @@ function getBudgetFinanceJuridique(): EntitéJuridiqueBudgetFinance { resultatNetComptable: 30, ratioDependanceFinanciere: 31, tauxDeCafNetSan: 32, + fondsDeRoulement: 33, + besoinFondsDeRoulement: 34, + tresorerie: 35, } } @@ -203,6 +206,9 @@ function getBudgetFinanceProfile() { ratioDépendanceFinancière: "ok", tauxDeCafNette: "ok", allocationDeRessources: "ok", + fondsDeRoulement: "ok", + besoinFondsDeRoulement: "ok", + tresorerie: "ok", } } @@ -694,6 +700,63 @@ describe("Filtre les informations de budget et finance des etablissement juridiq expect(entiteJuridiqueResult.budgetFinance).toEqual([expectedBudgetAndFinance]); }) + it("retire les info sur les fonds de roulement si il n’y a pas les droits", () => { + // Given + const rawBudgetAndFinance = getBudgetFinanceJuridique(); + const expectedBudgetAndFinance = { + ...rawBudgetAndFinance, + fondsDeRoulement: "", + } + + let entiteJuridiqueResult = getFullEntiteJuridique(); + const profile = getFullProfile(); + profile.budgetEtFinance.fondsDeRoulement = "Ko"; + + // When + entiteJuridiqueResult = filterEntiteJuridique(entiteJuridiqueResult, profile); + + // Then + expect(entiteJuridiqueResult.budgetFinance).toEqual([expectedBudgetAndFinance]); + }) + + it("retire les info sur le besoin en fonds de roulement si il n’y a pas les droits", () => { + // Given + const rawBudgetAndFinance = getBudgetFinanceJuridique(); + const expectedBudgetAndFinance = { + ...rawBudgetAndFinance, + besoinFondsDeRoulement: "", + } + + let entiteJuridiqueResult = getFullEntiteJuridique(); + const profile = getFullProfile(); + profile.budgetEtFinance.besoinFondsDeRoulement = "Ko"; + + // When + entiteJuridiqueResult = filterEntiteJuridique(entiteJuridiqueResult, profile); + + // Then + expect(entiteJuridiqueResult.budgetFinance).toEqual([expectedBudgetAndFinance]); + }) + + it("retire les info sur la trésorerie si il n’y a pas les droits", () => { + // Given + const rawBudgetAndFinance = getBudgetFinanceJuridique(); + const expectedBudgetAndFinance = { + ...rawBudgetAndFinance, + tresorerie: "", + } + + let entiteJuridiqueResult = getFullEntiteJuridique(); + const profile = getFullProfile(); + profile.budgetEtFinance.tresorerie = "Ko"; + + // When + entiteJuridiqueResult = filterEntiteJuridique(entiteJuridiqueResult, profile); + + // Then + expect(entiteJuridiqueResult.budgetFinance).toEqual([expectedBudgetAndFinance]); + }) + it("retire les info d’allocation de ressource si il n’y a pas les droits", () => { // Given const rawAllocation = getAllocationRessource(); diff --git a/src/backend/profileFiltersHelper.sanitaire.test.ts b/src/backend/profileFiltersHelper.sanitaire.test.ts index d73abb568..601609f5d 100644 --- a/src/backend/profileFiltersHelper.sanitaire.test.ts +++ b/src/backend/profileFiltersHelper.sanitaire.test.ts @@ -134,6 +134,9 @@ function getBudgetFinanceSanitaire(): EntitéJuridiqueBudgetFinance { resultatNetComptable: 30, ratioDependanceFinanciere: 31, tauxDeCafNetSan: 32, + fondsDeRoulement: 33, + besoinFondsDeRoulement: 34, + tresorerie: 35, } } @@ -241,6 +244,9 @@ function getBudgetFinanceProfile() { ratioDépendanceFinancière: "ok", tauxDeCafNette: "ok", allocationDeRessources: "ok", + fondsDeRoulement: "ok", + besoinFondsDeRoulement: "ok", + tresorerie: "ok", } } @@ -865,6 +871,63 @@ describe("Filtre les informations de budget et finance des etablissement juridiq expect(etabSanitaireResult.budgetFinance).toEqual([expectedBudgetAndFinance]); }) + it("retire les info sur les fonds de roulement si il n’y a pas les droits", () => { + // Given + const rawBudgetAndFinance = getBudgetFinanceSanitaire(); + const expectedBudgetAndFinance = { + ...rawBudgetAndFinance, + fondsDeRoulement: "", + } + + let etabSanitaireResult = getFullSanitaire(); + const profile = getFullProfile(); + profile.budgetEtFinance.fondsDeRoulement = "Ko"; + + // When + etabSanitaireResult = filterEtablissementSanitaire(etabSanitaireResult, profile); + + // Then + expect(etabSanitaireResult.budgetFinance).toEqual([expectedBudgetAndFinance]); + }) + + it("retire les info sur le besoin en fonds de roulement si il n’y a pas les droits", () => { + // Given + const rawBudgetAndFinance = getBudgetFinanceSanitaire(); + const expectedBudgetAndFinance = { + ...rawBudgetAndFinance, + besoinFondsDeRoulement: "", + } + + let etabSanitaireResult = getFullSanitaire(); + const profile = getFullProfile(); + profile.budgetEtFinance.besoinFondsDeRoulement = "Ko"; + + // When + etabSanitaireResult = filterEtablissementSanitaire(etabSanitaireResult, profile); + + // Then + expect(etabSanitaireResult.budgetFinance).toEqual([expectedBudgetAndFinance]); + }) + + it("retire les info sur la trésorerie si il n’y a pas les droits", () => { + // Given + const rawBudgetAndFinance = getBudgetFinanceSanitaire(); + const expectedBudgetAndFinance = { + ...rawBudgetAndFinance, + tresorerie: "", + } + + let etabSanitaireResult = getFullSanitaire(); + const profile = getFullProfile(); + profile.budgetEtFinance.tresorerie = "Ko"; + + // When + etabSanitaireResult = filterEtablissementSanitaire(etabSanitaireResult, profile); + + // Then + expect(etabSanitaireResult.budgetFinance).toEqual([expectedBudgetAndFinance]); + }) + it("retire les info d’allocation de ressource si il n’y a pas les droits", () => { // Given const rawAllocation = getAllocationRessource(); diff --git a/src/backend/profileFiltersHelper.ts b/src/backend/profileFiltersHelper.ts index 95760a1ef..284ee5991 100644 --- a/src/backend/profileFiltersHelper.ts +++ b/src/backend/profileFiltersHelper.ts @@ -540,6 +540,9 @@ const filterBudgetFinanceEJ = (budgetFinance: any, profil: any) => { budget.resultatNetComptable = profil.résultatNetComptable === "ok" ? budget.resultatNetComptable : ""; budget.ratioDependanceFinanciere = profil.ratioDépendanceFinancière === "ok" ? budget.ratioDependanceFinanciere : ""; budget.tauxDeCafNetSan = profil.tauxDeCafNette === "ok" ? budget.tauxDeCafNetSan : ""; + budget.fondsDeRoulement = profil.fondsDeRoulement === "ok" ? budget.fondsDeRoulement : ""; + budget.besoinFondsDeRoulement = profil.besoinFondsDeRoulement === "ok" ? budget.besoinFondsDeRoulement : ""; + budget.tresorerie = profil.tresorerie === "ok" ? budget.tresorerie : ""; } return budgetFinance; diff --git a/src/frontend/configuration/Paths.ts b/src/frontend/configuration/Paths.ts index 6d6ba0908..50fc1ddad 100644 --- a/src/frontend/configuration/Paths.ts +++ b/src/frontend/configuration/Paths.ts @@ -9,6 +9,8 @@ export class Paths { readonly ÉTABLISSEMENT_TERRITORIAL_SANITAIRE: string = "/etablissement-territorial-sanitaire"; readonly RÉGION: string = "/region"; readonly ECO_CONCEPTION: string = "/eco-conception"; + readonly CGU: string = "/cgu"; + readonly VALIDER_CGU: string = "/valider-cgu"; readonly GESTION_DES_COOKIES: string = "/gestion-des-cookies"; readonly AIDE: string = "/aide"; readonly MES_LISTES: string = "/mes-listes" diff --git a/src/frontend/configuration/wording/Wording.tsx b/src/frontend/configuration/wording/Wording.tsx index e9d276b08..48f719551 100644 --- a/src/frontend/configuration/wording/Wording.tsx +++ b/src/frontend/configuration/wording/Wording.tsx @@ -37,6 +37,7 @@ export interface Wording { readonly TITRE_PAGE_DONNÉES_PERSONNELLES: string; readonly TITRE_PAGE_INACCESSIBLE: string; readonly TITRE_PAGE_ECO_CONCEPTION: string; + readonly TITRE_PAGE_CGU: string; readonly TITRE_PAGE_GESTION_COOKIES: string; // Breadcrumb @@ -313,6 +314,9 @@ export interface Wording { readonly GROUPE_III: string; readonly TAUX_DE_CAF: string; readonly FONDS_DE_ROULEMENT_NET_GLOBAL: string; + readonly FONDS_DE_ROULEMENT: string; + readonly BESOIN_FONDS_DE_ROULEMENT: string; + readonly TRESORERIE: string; // Bloc budget et finance entité juridique readonly TITRE_I: string; @@ -444,6 +448,7 @@ export interface Wording { readonly LICENCE_ETALAB: string; readonly NOUVELLE_FENÊTRE: string; readonly ECO_CONCEPTION: string; + readonly CGU: string; readonly COOKIES: string; // Accessibilité diff --git a/src/frontend/configuration/wording/WordingFr.tsx b/src/frontend/configuration/wording/WordingFr.tsx index 90e8c61b2..f5bd9df1d 100644 --- a/src/frontend/configuration/wording/WordingFr.tsx +++ b/src/frontend/configuration/wording/WordingFr.tsx @@ -47,6 +47,7 @@ export class WordingFr implements Wording { readonly TITRE_PAGE_GESTION_COOKIES: string = "Gestion des cookies - Helios"; readonly TITRE_PAGE_INACCESSIBLE: string = "Accès refusé - Helios"; readonly TITRE_PAGE_ECO_CONCEPTION: string = "Eco-Conception - Helios"; + readonly TITRE_PAGE_CGU: string = "Conditions Générales d’Utilisation - Helios"; // Breadcrumb readonly VOUS_ÊTES_ICI: string = "Vous êtes ici :"; @@ -369,6 +370,9 @@ export class WordingFr implements Wording { readonly GROUPE_III: string = "Groupe III"; readonly TAUX_DE_CAF: string = "Taux de CAF"; readonly FONDS_DE_ROULEMENT_NET_GLOBAL: string = "Fond de roulement net global"; + readonly FONDS_DE_ROULEMENT: string = "Fonds de roulement"; + readonly BESOIN_FONDS_DE_ROULEMENT: string = "Besoin en fonds de roulement"; + readonly TRESORERIE: string = "Trésorerie"; // Bloc Buget Finance Entite Juridique readonly TITRE_I: string = "Titre I"; @@ -533,6 +537,7 @@ export class WordingFr implements Wording { readonly LICENCE_ETALAB: string = "licence etalab-2.0"; readonly NOUVELLE_FENÊTRE: string = "nouvelle Fenêtre"; readonly ECO_CONCEPTION: string = "Eco-Conception"; + readonly CGU: string = "CGU"; readonly COOKIES: string = "Gestion des Cookies"; // Accessibilité diff --git a/src/frontend/ui/ValiderCguPage.test.tsx b/src/frontend/ui/ValiderCguPage.test.tsx new file mode 100644 index 000000000..4f6b5bedb --- /dev/null +++ b/src/frontend/ui/ValiderCguPage.test.tsx @@ -0,0 +1,57 @@ +import { fireEvent, screen, waitFor } from "@testing-library/react"; +import { signOut, useSession } from "next-auth/react"; +import mockRouter from "next-router-mock"; + +import ValiderCguPage from "../../pages/valider-cgu"; +import { renderFakeComponent } from "../test-helpers/testHelper"; + +jest.mock("next-auth/react", (): typeof import("next-auth/react") => ({ + ...jest.requireActual("next-auth/react"), + signOut: jest.fn(), + useSession: jest.fn(), +})); + +describe("Page CGU", () => { + let updateSessionMock: jest.Mock; + + beforeEach(() => { + // @ts-expect-error test mock + jest.spyOn(globalThis, "fetch").mockResolvedValue({ + json: jest.fn(), + ok: true, + }); + updateSessionMock = jest.fn().mockResolvedValue(undefined); + jest.mocked(useSession).mockReturnValue({ update: updateSessionMock, data: null, status: "loading" }); + mockRouter.push("/cgu"); + }); + + it("accepte les CGU, met à jour la session et redirige vers l’accueil", async () => { + renderFakeComponent(); + + fireEvent.click(screen.getByRole("button", { name: "Accepter" })); + + await waitFor(() => { + expect(globalThis.fetch).toHaveBeenCalledWith("/api/utilisateurs/cgu", { + body: JSON.stringify({ accepted: true }), + headers: { "Content-Type": "application/json" }, + method: "POST", + }); + }); + await waitFor(() => { + expect(updateSessionMock).toHaveBeenCalledWith({ cgu: true }); + }); + await waitFor(() => { + expect(mockRouter.pathname).toBe("/"); + }); + }); + + it("refuse les CGU et déclenche la déconnexion vers l’accueil", async () => { + renderFakeComponent(); + + fireEvent.click(screen.getByRole("button", { name: "Refuser" })); + + await waitFor(() => { + expect(signOut).toHaveBeenCalledWith({ callbackUrl: "/" }); + }); + }); +}); diff --git a/src/frontend/ui/cgu/PageCgu.tsx b/src/frontend/ui/cgu/PageCgu.tsx new file mode 100644 index 000000000..181325f18 --- /dev/null +++ b/src/frontend/ui/cgu/PageCgu.tsx @@ -0,0 +1,28 @@ +export const PageCgu = () => { + return ( + <> +

Conditions Générales d’Utilisation

+

+ Dernière mise à jour : 20 Mars 2026 +

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer ac ornare neque, eu finibus elit. Curabitur vitae sem euismod, consequat turpis eget, ultricies mi. Phasellus suscipit luctus orci ut rutrum. Duis nec dignissim odio. Morbi condimentum ut odio ut porta. Donec eu blandit justo, sollicitudin lobortis nibh. Cras ultrices ex luctus tortor congue, et varius massa dignissim. Phasellus euismod, lacus eu maximus volutpat, nunc nibh aliquam leo, id fringilla risus dui ut urna. Sed consectetur enim eu finibus lacinia. Proin porttitor varius magna accumsan auctor. In nec iaculis lectus. Pellentesque viverra diam vel arcu ullamcorper fringilla. +

+

+ Suspendisse mattis placerat nisi, in mollis ex. Aenean in commodo nisi. Sed vitae lacinia nisl, a gravida nisl. Pellentesque ac commodo justo. Morbi fermentum mauris sed turpis rutrum, a scelerisque sem molestie. Phasellus vitae facilisis lacus. Ut blandit lacus quis fermentum maximus. In non ligula porttitor, aliquam mi quis, lobortis diam. Curabitur eu massa sit amet sem pulvinar maximus sit amet sit amet nunc. Nulla facilisi. Curabitur ipsum urna, condimentum vel fringilla in, feugiat non mi. +

+

+ Mauris non velit a risus rutrum dignissim. Sed hendrerit est nec magna mollis, quis ornare arcu efficitur. Ut congue tellus a ante pretium, et congue nulla luctus. Vestibulum sem nisi, semper in egestas sed, tristique a turpis. In id maximus purus. Fusce ac pulvinar mauris. Cras pellentesque quam sit amet sodales ultricies. Curabitur lacinia, massa ut dictum lobortis, enim neque luctus diam, sed egestas felis velit vel sapien. In a ex tellus. Vestibulum consectetur malesuada diam, eget blandit neque vulputate id. Vivamus id congue mauris. Suspendisse quis sodales dui. +

+

Lorem ipsum :

+
    +
  1. + Ut id magna eget ante luctus venenatis vel a nisi. In hac habitasse platea dictumst. Aenean placerat tincidunt aliquet. Morbi posuere leo in malesuada venenatis. Donec eu odio purus. Pellentesque dictum massa at mattis tincidunt. Curabitur eget augue commodo, congue turpis ac, fermentum leo. Donec ornare massa sollicitudin est mollis ultricies. Ut mollis ac augue id posuere. Integer lobortis turpis in lorem pretium pretium. +
  2. +
  3. + Cras posuere ornare viverra. Nullam justo diam, euismod ac interdum sed, mollis eu sapien. Nulla ut ultrices purus. In eu volutpat dui. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Etiam posuere posuere nisi at volutpat. Maecenas dapibus sagittis massa imperdiet blandit. +
  4. +
+ + ); +}; diff --git a/src/frontend/ui/commun/Breadcrumb/Breadcrumb.test.tsx b/src/frontend/ui/commun/Breadcrumb/Breadcrumb.test.tsx index e96e1c689..37937e6fd 100644 --- a/src/frontend/ui/commun/Breadcrumb/Breadcrumb.test.tsx +++ b/src/frontend/ui/commun/Breadcrumb/Breadcrumb.test.tsx @@ -34,7 +34,8 @@ const mockSession = { institution: {}, institutionId: 1, codeRegion: 84, - codeProfiles: [""] + codeProfiles: [""], + cgu: true, }, expires: "1235" } diff --git a/src/frontend/ui/commun/Footer/Footer.tsx b/src/frontend/ui/commun/Footer/Footer.tsx index 4e3bb7a51..9acada4f0 100644 --- a/src/frontend/ui/commun/Footer/Footer.tsx +++ b/src/frontend/ui/commun/Footer/Footer.tsx @@ -85,6 +85,11 @@ export const Footer = () => { {wording.ECO_CONCEPTION} +
  • + + {wording.CGU} + +
  • {wording.MENTIONS_LÉGALES} diff --git a/src/frontend/ui/commun/Header/Header.test.tsx b/src/frontend/ui/commun/Header/Header.test.tsx index f8c367c14..5227e7464 100644 --- a/src/frontend/ui/commun/Header/Header.test.tsx +++ b/src/frontend/ui/commun/Header/Header.test.tsx @@ -16,7 +16,8 @@ const mockSession = { institution: {}, institutionId: 1, codeRegion: 84, - codeProfiles: [""] + codeProfiles: [""], + cgu: true, }, expires: "1235" } diff --git a/src/frontend/ui/comparaison/ExportExcel.tsx b/src/frontend/ui/comparaison/ExportExcel.tsx index 3a20958c7..e70f89f30 100644 --- a/src/frontend/ui/comparaison/ExportExcel.tsx +++ b/src/frontend/ui/comparaison/ExportExcel.tsx @@ -110,6 +110,9 @@ function transformData(data: any, favoris: UserListViewModel[] | undefined, type enveloppe1: (etab) => etab.enveloppe1 ?? "-", enveloppe2: (etab) => etab.enveloppe2 ?? "-", enveloppe3: (etab) => etab.enveloppe3 ?? "-", + fondsDeRoulement: (etab) => etab.fondsDeRoulement ?? "-", + besoinFondsDeRoulement: (etab) => etab.besoinFondsDeRoulement ?? "-", + tresorerie: (etab) => etab.tresorerie ?? "-", }; return data.resultat.map((etab: ResultatSAN) => @@ -143,6 +146,9 @@ function transformData(data: any, favoris: UserListViewModel[] | undefined, type enveloppe1: (etab) => etab.enveloppe1 ?? '-', enveloppe2: (etab) => etab.enveloppe2 ?? '-', enveloppe3: (etab) => etab.enveloppe3 ?? '-', + fondsDeRoulement: (etab) => etab.fondsDeRoulement ?? '-', + besoinFondsDeRoulement: (etab) => etab.besoinFondsDeRoulement ?? '-', + tresorerie: (etab) => etab.tresorerie ?? '-', }; return data.resultat.map((etab: ResultatEJ) => @@ -301,7 +307,10 @@ const getSanHeaders = (enabledIndicators: string[], enveloppes: string[]): strin ["joursAbsenteismePnm", "Jours d’absentéisme PNM"], ["enveloppe1", `Allocation de ressources: ${enveloppes[0] ? enveloppes[0] : 'Non renseigné'}`], ["enveloppe2", `Allocation de ressources: ${enveloppes[1] ? enveloppes[1] : 'Non renseigné'}`], - ["enveloppe3", `Allocation de ressources: ${enveloppes[2] ? enveloppes[2] : 'Non renseigné'}`] + ["enveloppe3", `Allocation de ressources: ${enveloppes[2] ? enveloppes[2] : 'Non renseigné'}`], + ["fondsDeRoulement", "Fonds de roulement"], + ["besoinFondsDeRoulement", "Besoin en fonds de roulement"], + ["tresorerie", "Trésorerie"], ]); return Array.from(headersSan.entries()) @@ -333,7 +342,10 @@ const getEJHeaders = (enabledIndicators: string[], enveloppes: string[]): string ["ratioDependanceFinanciere", "Ratio de dépendance financière"], ["enveloppe1", `Allocation de ressources: ${enveloppes[0] ? enveloppes[0] : 'Non renseigné'}`], ["enveloppe2", `Allocation de ressources: ${enveloppes[1] ? enveloppes[1] : 'Non renseigné'}`], - ["enveloppe3", `Allocation de ressources: ${enveloppes[2] ? enveloppes[2] : 'Non renseigné'}`] + ["enveloppe3", `Allocation de ressources: ${enveloppes[2] ? enveloppes[2] : 'Non renseigné'}`], + ["fondsDeRoulement", "Fonds de roulement"], + ["besoinFondsDeRoulement", "Besoin en fonds de roulement"], + ["tresorerie", "Trésorerie"], ]); return Array.from(headersEJ.entries()) diff --git a/src/frontend/ui/comparaison/useComparaison.tsx b/src/frontend/ui/comparaison/useComparaison.tsx index 6ab17b83e..451285145 100644 --- a/src/frontend/ui/comparaison/useComparaison.tsx +++ b/src/frontend/ui/comparaison/useComparaison.tsx @@ -4,8 +4,11 @@ import { DatesMisAjourSources } from "../../../backend/métier/entities/Resultat import { useDependencies } from "../commun/contexts/useDependencies"; import StringFormater from "../commun/StringFormater"; import { ContenuAllocationRessourcesEJ } from "../entité-juridique/bloc-budget-finance/allocation-ressources/ContenuAllocationRessourcesEJ"; +import { ContenuBesoinFondsDeRoulement } from "../entité-juridique/bloc-budget-finance/besoin-fonds-de-roulement/BesoinFondsDeRoulement"; import { ContenuCompteDeRésultatEJ } from "../entité-juridique/bloc-budget-finance/compte-de-resultat/ContenuCompteDeRésultatEJ"; +import { ContenuFondsDeRoulement } from "../entité-juridique/bloc-budget-finance/fonds-de-roulement/FondsDeRoulement"; import { ContenuRatioDependanceFinancière } from "../entité-juridique/bloc-budget-finance/ratio-dependance-financiere/RatioDependanceFinanciere"; +import { ContenuTresorerie } from "../entité-juridique/bloc-budget-finance/tresorerie/Tresorerie"; import { ContenuDeDepensesInterimPm } from "../entité-juridique/infoBulle/ContenuDeDepensesInterimPm"; import { ContenuDeJoursAbsenteismePm } from "../entité-juridique/infoBulle/ContenuDeJoursAbsenteismePm"; import { ContenuDeJoursAbsenteismePnm } from "../entité-juridique/infoBulle/ContenuDeJoursAbsenteismePnm"; @@ -305,6 +308,21 @@ export function useComparaison() { contenu: , titre: wording.RATIO_DEPENDANCE_FINANCIERE, }; + case "fondsDeRoulement": + return { + contenu: , + titre: wording.FONDS_DE_ROULEMENT, + }; + case "besoinFondsDeRoulement": + return { + contenu: , + titre: wording.BESOIN_FONDS_DE_ROULEMENT, + }; + case "tresorerie": + return { + contenu: , + titre: wording.TRESORERIE, + }; case "totalHosptMedecine": case "totalHosptChirurgie": case "totalHosptObstetrique": @@ -423,6 +441,10 @@ export function useComparaison() { { label: `Alloc. ressources: ${topEnveloppes[0] ? topEnveloppes[0] : "Non renseigné"}`, nomComplet: `Allocation de ressources: ${topEnveloppes[0] ? topEnveloppes[0] : "Non renseigné"}`, key: 'enveloppe1', info: true, sort: true, orderBy: "enveloppe_1" }, { label: `Alloc. ressources: ${topEnveloppes[1] ? topEnveloppes[1] : "Non renseigné"}`, nomComplet: `Allocation de ressources: ${topEnveloppes[1] ? topEnveloppes[1] : "Non renseigné"}`, key: 'enveloppe2', info: true, sort: true, orderBy: "enveloppe_2" }, { label: `Alloc. ressources: ${topEnveloppes[2] ? topEnveloppes[2] : "Non renseigné"}`, nomComplet: `Allocation de ressources: ${topEnveloppes[2] ? topEnveloppes[2] : "Non renseigné"}`, key: 'enveloppe3', info: true, sort: true, orderBy: "enveloppe_3" }, + { label: "Fonds de roulement", nomComplet: "Fonds de roulement", key: "fondsDeRoulement", info: true, sort: true, orderBy: "fonds_de_roulement" }, + { label: "BFR", nomComplet: "Besoin en fonds de roulement", key: "besoinFondsDeRoulement", info: true, sort: true, orderBy: "besoin_fonds_de_roulement" }, + { label: "Trésorerie", nomComplet: "Trésorerie", key: "tresorerie", info: true, sort: true, orderBy: "tresorerie" }, + ] } else @@ -448,6 +470,9 @@ export function useComparaison() { { label: `Alloc. ressources: ${topEnveloppes[0] ? topEnveloppes[0] : "Non renseigné"}`, nomComplet: `Allocation de ressources: ${topEnveloppes[0] ? topEnveloppes[0] : "Non renseigné"}`, key: 'enveloppe1', info: true, sort: true, orderBy: "enveloppe_1" }, { label: `Alloc. ressources: ${topEnveloppes[1] ? topEnveloppes[1] : "Non renseigné"}`, nomComplet: `Allocation de ressources: ${topEnveloppes[1] ? topEnveloppes[1] : "Non renseigné"}`, key: 'enveloppe2', info: true, sort: true, orderBy: "enveloppe_2" }, { label: `Alloc. ressources: ${topEnveloppes[2] ? topEnveloppes[2] : "Non renseigné"}`, nomComplet: `Allocation de ressources: ${topEnveloppes[2] ? topEnveloppes[2] : "Non renseigné"}`, key: 'enveloppe3', info: true, sort: true, orderBy: "enveloppe_3" }, + { label: "Fonds de roulement", nomComplet: "Fonds de roulement", key: "fondsDeRoulement", info: true, sort: true, orderBy: "fonds_de_roulement" }, + { label: "BFR", nomComplet: "Besoin en fonds de roulement", key: "besoinFondsDeRoulement", info: true, sort: true, orderBy: "besoin_fonds_de_roulement" }, + { label: "Trésorerie", nomComplet: "Trésorerie", key: "tresorerie", info: true, sort: true, orderBy: "tresorerie" }, ] }; diff --git a/src/frontend/ui/comparaison/useModalSelectionIndicateur.test.tsx b/src/frontend/ui/comparaison/useModalSelectionIndicateur.test.tsx new file mode 100644 index 000000000..a728d08eb --- /dev/null +++ b/src/frontend/ui/comparaison/useModalSelectionIndicateur.test.tsx @@ -0,0 +1,99 @@ +import { fireEvent, render, screen } from "@testing-library/react"; + +import { useModalSelectionIndicateur, getInitialIndicatorsState } from "./useModalSelectionIndicateur"; + +type TestComponentProps = Readonly<{ structure: string }>; +type IndicatorFixture = Readonly<{ label: string; columnName: string; enabledByDefault: boolean }>; + +type StructureFixture = Readonly<{ + structure: string; + indicators: readonly IndicatorFixture[]; +}>; + +const getIndicatorsByStructure = (): readonly StructureFixture[] => { + const initialIndicatorsState = getInitialIndicatorsState(); + + return Array.from(initialIndicatorsState.entries()).map(([structureKey, structureIndicators]) => { + // On map le nom de la structure dans l’indicateur au nom attendu par le composant + let structure: string; + switch (structureKey) { + case "medicoSocial": + structure = "Médico-social"; + break; + case "sanitaire": + structure = "Sanitaire"; + break; + case "entiteJuridique": + structure = "Entité juridique"; + break; + default: + structure = structureKey; + } + + // On transforme les indicateurs de chaque structure en un format plus simple pour les tests, on retire la notion de «groupe» d’indicateurs + const indicators = [...structureIndicators.values()].flatMap((categoryIndicators) => + categoryIndicators.map(({ displayName, columnName, enabled }) => ({ + label: displayName, + columnName, + enabledByDefault: enabled, + })) + ); + + return { structure, indicators }; + }); +}; + +const STRUCTURE_FIXTURES = getIndicatorsByStructure(); + +const TestComponent = ({ structure }: TestComponentProps) => { + const { enabledIndicators, generateModal, openIndicatorSelectionModal } = useModalSelectionIndicateur(structure); + + return ( + <> + +
    {enabledIndicators.join(",")}
    + {generateModal()} + + ); +}; + +const openModal = () => { + fireEvent.click(screen.getByText("Ouvrir modal")); +}; + +const validateSelection = () => { + fireEvent.click(screen.getByText("Valider")); +}; + +const getEnabledIndicators = (): string[] => { + const content = screen.getByTestId("enabled-indicators").textContent; + if (!content) return []; + return content.split(","); +}; + +describe("useModalSelectionIndicateur - Test de la desactivation/réactivation des indicateurs", () => { + it.each(STRUCTURE_FIXTURES)( + "test des indicateurs pour la structure «$structure», on valide les transitions true -> false puis false -> true", + ({ structure, indicators }) => { + render(); + + indicators.forEach(({ label, columnName, enabledByDefault }) => { + openModal(); + expect(getEnabledIndicators().includes(columnName)).toBe(enabledByDefault); + + fireEvent.click(screen.getByLabelText(label)); + validateSelection(); + + expect(getEnabledIndicators().includes(columnName)).toBe(!enabledByDefault); + + openModal(); + fireEvent.click(screen.getByLabelText(label)); + validateSelection(); + + expect(getEnabledIndicators().includes(columnName)).toBe(enabledByDefault); + }); + } + ); +}); diff --git a/src/frontend/ui/comparaison/useModalSelectionIndicateur.tsx b/src/frontend/ui/comparaison/useModalSelectionIndicateur.tsx index 8182904b1..6302b59cc 100644 --- a/src/frontend/ui/comparaison/useModalSelectionIndicateur.tsx +++ b/src/frontend/ui/comparaison/useModalSelectionIndicateur.tsx @@ -24,7 +24,7 @@ type IndicatorCategory = { type IndicatorsState = Map> -function getInitialIndicatorsState(): IndicatorsState { +export function getInitialIndicatorsState(): IndicatorsState { // ################################## Medico-social indicators ################################## const medicoSocialIndicators = new Map([]); @@ -106,6 +106,9 @@ function getInitialIndicatorsState(): IndicatorsState { { displayName: "Allocation de ressources: 1ᵉʳ enveloppe", columnName: 'enveloppe1', enabled: true }, { displayName: "Allocation de ressources: 2ᵉᵐᵉ enveloppe", columnName: 'enveloppe2', enabled: true }, { displayName: "Allocation de ressources: 3ᵉᵐᵉ enveloppe", columnName: 'enveloppe3', enabled: true }, + { displayName: "Fonds de roulement", columnName: "fondsDeRoulement", enabled: true }, + { displayName: "Besoin en fonds de roulement", columnName: "besoinFondsDeRoulement", enabled: true }, + { displayName: "Trésorerie", columnName: "tresorerie", enabled: true }, ]; sanitaireIndicators.set(budgetSanCategory, sanitaireIndicatorsBudgetEtFinances); // ########################################################################################## @@ -151,6 +154,9 @@ function getInitialIndicatorsState(): IndicatorsState { { displayName: "Allocation de ressources: 1ᵉʳ enveloppe", columnName: 'enveloppe1', enabled: true }, { displayName: "Allocation de ressources: 2ᵉᵐᵉ enveloppe", columnName: 'enveloppe2', enabled: true }, { displayName: "Allocation de ressources: 3ᵉᵐᵉ enveloppe", columnName: 'enveloppe3', enabled: true }, + { displayName: "Fonds de roulement", columnName: "fondsDeRoulement", enabled: true }, + { displayName: "Besoin en fonds de roulement", columnName: "besoinFondsDeRoulement", enabled: true }, + { displayName: "Trésorerie", columnName: "tresorerie", enabled: true }, ]; entiteJuridiqueIndicators.set(budgetEJCategory, entiteJuridiqueIndicatorsBudgetEtFinances); // ########################################################################################## @@ -373,4 +379,3 @@ export function useModalSelectionIndicateur(structure: string) { return { generateModal, enabledIndicators, openIndicatorSelectionModal }; } - diff --git "a/src/frontend/ui/entit\303\251-juridique/PageEntit\303\251Juridique.test.tsx" "b/src/frontend/ui/entit\303\251-juridique/PageEntit\303\251Juridique.test.tsx" index 06ede89cf..c307497bc 100644 --- "a/src/frontend/ui/entit\303\251-juridique/PageEntit\303\251Juridique.test.tsx" +++ "b/src/frontend/ui/entit\303\251-juridique/PageEntit\303\251Juridique.test.tsx" @@ -24,6 +24,7 @@ const mockSession = { institutionId: 1, codeRegion: 84, codeProfiles: [""], + cgu: true, }, expires: "1235", }; diff --git "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/BlocBudgetFinance.test.tsx" "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/BlocBudgetFinance.test.tsx" index fbad28177..44da2a632 100644 --- "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/BlocBudgetFinance.test.tsx" +++ "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/BlocBudgetFinance.test.tsx" @@ -20,6 +20,9 @@ const autorisationsMockData = { résultatNetComptable: "ok", ratioDépendanceFinancière: "ok", allocationDeRessources: "ok", + fondsDeRoulement: "ok", + besoinFondsDeRoulement: "ok", + tresorerie: "ok", }, budgetEtFinances: { tauxDeCafNette: "ok", @@ -115,4 +118,58 @@ describe("Bloc Budget et Finance", () => { const titre = screen.getAllByText(wording.TAUX_DE_CAF); expect(titre).not.toHaveLength(0); }); + + it("affiche l'indicateur fonds de roulement s'il y a des données", () => { + // GIVEN + const budgetFinance: EntitéJuridiqueBudgetFinance[] = [ + { + année: 2022, + fondsDeRoulement: 400, + } as EntitéJuridiqueBudgetFinance, + ]; + const viewModel = new EntitéJuridiqueBudgetFinanceViewModel(budgetFinance, allocationRessourceMockData, wording, autorisationsMockData); + + // WHEN + renderFakeComponent(); + + // THEN + const titre = screen.getAllByText(wording.FONDS_DE_ROULEMENT); + expect(titre).not.toHaveLength(0); + }); + + it("affiche l'indicateur besoin en fonds de roulement s'il y a des données", () => { + // GIVEN + const budgetFinance: EntitéJuridiqueBudgetFinance[] = [ + { + année: 2022, + besoinFondsDeRoulement: 500, + } as EntitéJuridiqueBudgetFinance, + ]; + const viewModel = new EntitéJuridiqueBudgetFinanceViewModel(budgetFinance, allocationRessourceMockData, wording, autorisationsMockData); + + // WHEN + renderFakeComponent(); + + // THEN + const titre = screen.getAllByText(wording.BESOIN_FONDS_DE_ROULEMENT); + expect(titre).not.toHaveLength(0); + }); + + it("affiche l'indicateur trésorerie s'il y a des données", () => { + // GIVEN + const budgetFinance: EntitéJuridiqueBudgetFinance[] = [ + { + année: 2022, + tresorerie: 800, + } as EntitéJuridiqueBudgetFinance, + ]; + const viewModel = new EntitéJuridiqueBudgetFinanceViewModel(budgetFinance, allocationRessourceMockData, wording, autorisationsMockData); + + // WHEN + renderFakeComponent(); + + // THEN + const titre = screen.getAllByText(wording.TRESORERIE); + expect(titre).not.toHaveLength(0); + }); }); diff --git "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/BlocBudgetFinance.tsx" "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/BlocBudgetFinance.tsx" index f6aa503af..94bc54e50 100644 --- "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/BlocBudgetFinance.tsx" +++ "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/BlocBudgetFinance.tsx" @@ -1,8 +1,11 @@ import { AllocationRessources } from "./allocation-ressources/AllocationRessources"; +import { BesoinFondsDeRoulement } from "./besoin-fonds-de-roulement/BesoinFondsDeRoulement"; import styles from "./BlocBudgetFinance.module.css"; import { CompteDeResultat } from "./compte-de-resultat/CompteDeResultat"; import { EntitéJuridiqueBudgetFinanceViewModel } from "./EntitéJuridiqueBudgetFinanceViewModel"; +import { FondsDeRoulement } from "./fonds-de-roulement/FondsDeRoulement"; import { RatioDependanceFinanciere } from "./ratio-dependance-financiere/RatioDependanceFinanciere"; +import { Tresorerie } from "./tresorerie/Tresorerie"; import { Bloc } from "../../commun/Bloc/Bloc"; import { useDependencies } from "../../commun/contexts/useDependencies"; import { BlocIndicateurVide } from "../../commun/IndicateurGraphique/BlocIndicateurVide"; @@ -39,7 +42,7 @@ export const BlocBudgetFinance = ({ etabTitle, etabFiness, entitéJuridiqueBudge return null; }; - const renderBudgetEtFinances = () => { + const renderBudgetEtFinancesFirstSection = () => { if (!estEntiteJuridiqueOuPnL) { return null; } @@ -100,11 +103,44 @@ export const BlocBudgetFinance = ({ etabTitle, etabFiness, entitéJuridiqueBudge ); }; + const renderBlocBudgetEtFinancesSecondSection = () => { + const peutAfficherFondsDeRoulement = entitéJuridiqueBudgetFinanceViewModel.fondsDeRoulement.auMoinsUnFondsDeRoulementRenseigné() && entitéJuridiqueBudgetFinanceViewModel.fondsDeRoulement.fondsDeRoulementEstIlAutorisé; + const peutAfficherBesoinFondsDeRoulement = entitéJuridiqueBudgetFinanceViewModel.besoinFondsDeRoulement.auMoinsUnBesoinFondsDeRoulementRenseigné() && entitéJuridiqueBudgetFinanceViewModel.besoinFondsDeRoulement.besoinFondsDeRoulementEstIlAutorisé; + const peutAfficherTresorerie = entitéJuridiqueBudgetFinanceViewModel.tresorerie.auMoinsUneTresorerieRenseignée() && entitéJuridiqueBudgetFinanceViewModel.tresorerie.tresorerieEstIlAutorisé; + + return ( +
      + {peutAfficherFondsDeRoulement && ( + + )} + {peutAfficherBesoinFondsDeRoulement && ( + + )} + {peutAfficherTresorerie && ( + + )} +
    + ); + } + return ( {renderBlocMessages()} - {renderBudgetEtFinances()} + {renderBudgetEtFinancesFirstSection()} {renderAllocationRessources()} + {renderBlocBudgetEtFinancesSecondSection()} ); }; diff --git "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/Entit\303\251JuridiqueBudgetFinanceViewModel.tsx" "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/Entit\303\251JuridiqueBudgetFinanceViewModel.tsx" index 806ed1507..ea8467c78 100644 --- "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/Entit\303\251JuridiqueBudgetFinanceViewModel.tsx" +++ "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/Entit\303\251JuridiqueBudgetFinanceViewModel.tsx" @@ -1,5 +1,8 @@ import { AllocationRessourcesViewModel } from "./AllocationRessourcesViewModel"; +import { BesoinFondsDeRoulementViewModel } from "./besoin-fonds-de-roulement/BesoinFondsDeRoulementViewModel"; +import { FondsDeRoulementViewModel } from "./fonds-de-roulement/FondsDeRoulementViewModel"; import { RatioDependanceFinanciereViewModel } from "./ratio-dependance-financiere/RatioDependanceFinanciereViewModel"; +import { TresorerieViewModel } from "./tresorerie/TresorerieViewModel"; import { IAllocationRessources } from "../../../../backend/métier/entities/entité-juridique/EntitéJuridiqueAllocationRessources"; import { EntitéJuridiqueBudgetFinance } from "../../../../backend/métier/entities/entité-juridique/EntitéJuridiqueBudgetFinance"; import { Wording } from "../../../configuration/wording/Wording"; @@ -20,6 +23,9 @@ export class EntitéJuridiqueBudgetFinanceViewModel { public NOMBRE_ANNEES = 5; public ratioDependanceFinanciere: RatioDependanceFinanciereViewModel; public tauxDeCafViewModel: TauxDeCafViewModel; + public fondsDeRoulement: FondsDeRoulementViewModel; + public besoinFondsDeRoulement: BesoinFondsDeRoulementViewModel; + public tresorerie: TresorerieViewModel; public autorisations: any; constructor(budgetFinance: EntitéJuridiqueBudgetFinance[], allocationRessources: IAllocationRessources, wording: Wording, autorisations: any) { @@ -31,6 +37,9 @@ export class EntitéJuridiqueBudgetFinanceViewModel { this.resultatNetComptable = new ResultatNetComptableViewModel(budgetFinance, autorisations); this.ratioDependanceFinanciere = new RatioDependanceFinanciereViewModel(budgetFinance, autorisations); this.tauxDeCafViewModel = TauxDeCafViewModel.fromBudgetFinanceEntiteJuridique(budgetFinance, autorisations, wording); + this.fondsDeRoulement = new FondsDeRoulementViewModel(budgetFinance, autorisations); + this.besoinFondsDeRoulement = new BesoinFondsDeRoulementViewModel(budgetFinance, autorisations); + this.tresorerie = new TresorerieViewModel(budgetFinance, autorisations); this.autorisations = autorisations; } @@ -51,7 +60,10 @@ export class EntitéJuridiqueBudgetFinanceViewModel { (this.compteDeResultatVide() && !this.resultatNetComptable.auMoinsUnResultatNetRenseigné() && !this.ratioDependanceFinanciere.auMoinsUnRatioRenseigné() && - !this.tauxDeCafViewModel.leTauxDeCafEstIlRenseigné) + !this.tauxDeCafViewModel.leTauxDeCafEstIlRenseigné && + !this.fondsDeRoulement.auMoinsUnFondsDeRoulementRenseigné() && + !this.besoinFondsDeRoulement.auMoinsUnBesoinFondsDeRoulementRenseigné() && + !this.tresorerie.auMoinsUneTresorerieRenseignée()) ); } @@ -65,6 +77,10 @@ export class EntitéJuridiqueBudgetFinanceViewModel { } if (this.allocationRessources.vide()) nonRenseignees.push(this.wording.ALLOCATION_DE_RESSOURCES); + if (!this.fondsDeRoulement.auMoinsUnFondsDeRoulementRenseigné()) nonRenseignees.push(this.wording.FONDS_DE_ROULEMENT); + if (!this.besoinFondsDeRoulement.auMoinsUnBesoinFondsDeRoulementRenseigné()) nonRenseignees.push(this.wording.BESOIN_FONDS_DE_ROULEMENT); + if (!this.tresorerie.auMoinsUneTresorerieRenseignée()) nonRenseignees.push(this.wording.TRESORERIE); + return nonRenseignees; } @@ -78,6 +94,9 @@ export class EntitéJuridiqueBudgetFinanceViewModel { } if (!this.allocationRessources.estIlAutorisé) nonAutorisés.push(this.wording.ALLOCATION_DE_RESSOURCES); + if (!this.fondsDeRoulement.fondsDeRoulementEstIlAutorisé) nonAutorisés.push(this.wording.FONDS_DE_ROULEMENT); + if (!this.besoinFondsDeRoulement.besoinFondsDeRoulementEstIlAutorisé) nonAutorisés.push(this.wording.BESOIN_FONDS_DE_ROULEMENT); + if (!this.tresorerie.tresorerieEstIlAutorisé) nonAutorisés.push(this.wording.TRESORERIE); return nonAutorisés; } diff --git "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/besoin-fonds-de-roulement/BesoinFondsDeRoulement.tsx" "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/besoin-fonds-de-roulement/BesoinFondsDeRoulement.tsx" new file mode 100644 index 000000000..3e71f4d45 --- /dev/null +++ "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/besoin-fonds-de-roulement/BesoinFondsDeRoulement.tsx" @@ -0,0 +1,82 @@ +import { ReactElement } from "react"; + +import { BesoinFondsDeRoulementViewModel } from "./BesoinFondsDeRoulementViewModel"; +import { annéesManquantes } from "../../../../utils/dateUtils"; +import { useDependencies } from "../../../commun/contexts/useDependencies"; +import { HistogrammeHorizontal } from "../../../commun/Graphique/HistogrammeHorizontal"; +import { IndicateurGraphique } from "../../../commun/IndicateurGraphique/IndicateurGraphique"; +import StringFormater from "../../../commun/StringFormater"; + +type BesoinFondsDeRoulementProps = Readonly<{ + etabFiness: string; + etabTitle: string; + besoinFondsDeRoulementViewModel: BesoinFondsDeRoulementViewModel; +}>; + +export function ContenuBesoinFondsDeRoulement(props: Readonly<{ dateDeMiseÀJour: any; source: ReactElement }>): ReactElement { + const { wording } = useDependencies(); + + return ( + <> +

    {wording.miseÀJourEtSource(props.source, props.dateDeMiseÀJour)}

    +
    +

    + Le Besoin en Fonds de Roulement (BFR) traduit le besoin de financement du cycle d’exploitation. +

    +
    +
    +

    + Fréquence : Quotidienne +

    +
    +
    +

    + Mode de calcul : +

    +

    Différence entre l’actif circulant et le passif circulant

    +
    +
    +

    + Source(s) : ANCRE (Application Nationale Compte financier Rapport infra-annuel Eprd) - Agence technique de l’information sur l’hospitalisation (ATIH)

    +
    +
    +

    + Hélios collecte ces données depuis le SI mutualisé des ARS DIAMANT « Décisionnel Inter-ARS pour la Maîtrise et l’Anticipation », outil décisionnel de pilotage centré sur la régulation de l’offre de soins, abordée sous les aspects des moyens humains, financiers, et productivité. +

    +
    + + ); +} + +export function BesoinFondsDeRoulement({ etabFiness, etabTitle, besoinFondsDeRoulementViewModel }: BesoinFondsDeRoulementProps): ReactElement { + const { wording } = useDependencies(); + const infoBulleContenu = ; + const sourceName = wording.ANCRE; + const anneesManquantes: number[] = annéesManquantes(besoinFondsDeRoulementViewModel.années, 5); + + return besoinFondsDeRoulementViewModel.auMoinsUnBesoinFondsDeRoulementRenseigné() ? ( + + + + ) : ( + <> + ); +} diff --git "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/besoin-fonds-de-roulement/BesoinFondsDeRoulementViewModel.tsx" "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/besoin-fonds-de-roulement/BesoinFondsDeRoulementViewModel.tsx" new file mode 100644 index 000000000..f510fcea8 --- /dev/null +++ "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/besoin-fonds-de-roulement/BesoinFondsDeRoulementViewModel.tsx" @@ -0,0 +1,72 @@ +import { EntitéJuridiqueBudgetFinance } from "../../../../../backend/métier/entities/entité-juridique/EntitéJuridiqueBudgetFinance"; +import { + couleurDuFond, + couleurDuFondHistogrammeSecondaire, + couleurErreur, + CouleurHistogramme, + couleurIdentifiant, + TaillePoliceTick, +} from "../../../commun/Graphique/couleursGraphique"; +import StringFormater from "../../../commun/StringFormater"; + +type BesoinFondsDeRoulement = { valeur: number | null; année: number; dateDeMiseÀJour: string }; + +export class BesoinFondsDeRoulementViewModel { + readonly NOMBRE_ANNEES = 5; + private besoinFondsDeRoulement: BesoinFondsDeRoulement[]; + private autorisations: any; + + constructor(budgetFinance: EntitéJuridiqueBudgetFinance[], autorisations: any) { + this.besoinFondsDeRoulement = budgetFinance + .filter((budget) => budget.besoinFondsDeRoulement !== null && budget.besoinFondsDeRoulement !== undefined && budget.besoinFondsDeRoulement !== 0) + .map((budget) => ({ + année: budget.année, + valeur: budget.besoinFondsDeRoulement, + dateDeMiseÀJour: budget.dateMiseÀJourSource, + })); + this.autorisations = autorisations; + } + + public get années(): number[] { + return this.besoinFondsDeRoulement.map((bfdr) => bfdr.année); + } + + public get dateMiseÀJour(): string { + return StringFormater.formatDate(this.besoinFondsDeRoulement[0]?.dateDeMiseÀJour); + } + + public auMoinsUnBesoinFondsDeRoulementRenseigné(): boolean { + return this.besoinFondsDeRoulement.some(this.valeurRemplie); + } + + public get besoinFondsDeRoulementEstIlAutorisé(): boolean { + return this.autorisations.budgetEtFinance?.besoinFondsDeRoulement === 'ok'; + } + + private valeurRemplie(bfdr: BesoinFondsDeRoulement): boolean { + return bfdr.valeur !== null; + } + + public get valeurs(): number[] { + return this.besoinFondsDeRoulement.map((bfdr) => bfdr.valeur as number); + } + + public construisLesLibellésDesTicks(): TaillePoliceTick[] { + return this.besoinFondsDeRoulement.map(() => "normal"); + } + + public construisLesCouleursDesLibelles(): string[] { + return this.besoinFondsDeRoulement.map(() => couleurIdentifiant); + } + + get couleursDeLHistogramme(): CouleurHistogramme[] { + return this.besoinFondsDeRoulement.map((bfdr) => ({ + premierPlan: this.valeurEstEnErreur(bfdr) ? couleurErreur : couleurDuFondHistogrammeSecondaire, + secondPlan: couleurDuFond, + })); + } + + private valeurEstEnErreur(bfdr: BesoinFondsDeRoulement): boolean { + return bfdr.valeur === null; + } +} diff --git "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/fonds-de-roulement/FondsDeRoulement.tsx" "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/fonds-de-roulement/FondsDeRoulement.tsx" new file mode 100644 index 000000000..ba59508ea --- /dev/null +++ "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/fonds-de-roulement/FondsDeRoulement.tsx" @@ -0,0 +1,83 @@ +import { ReactElement } from "react"; + +import { FondsDeRoulementViewModel } from "./FondsDeRoulementViewModel"; +import { annéesManquantes } from "../../../../utils/dateUtils"; +import { useDependencies } from "../../../commun/contexts/useDependencies"; +import { HistogrammeHorizontal } from "../../../commun/Graphique/HistogrammeHorizontal"; +import { IndicateurGraphique } from "../../../commun/IndicateurGraphique/IndicateurGraphique"; +import StringFormater from "../../../commun/StringFormater"; + +type FondsDeRoulementProps = Readonly<{ + etabFiness: string; + etabTitle: string; + fondsDeRoulementViewModel: FondsDeRoulementViewModel; +}>; + +export function ContenuFondsDeRoulement(props: Readonly<{ dateDeMiseÀJour: any; source: ReactElement }>): ReactElement { + const { wording } = useDependencies(); + + return ( + <> +

    {wording.miseÀJourEtSource(props.source, props.dateDeMiseÀJour)}

    +
    +

    + Le calcul du fonds de roulement permet d’apprécier l’adéquation des ressources aux emplois. +

    +
    +
    +

    + Fréquence : Quotidienne +

    +
    +
    +

    + Mode de calcul : +

    +

    Différence entre les ressources stables (passif long terme) et les emplois stables (actif long terme)

    +
    +
    +

    + Source(s) : ANCRE (Application Nationale Compte financier Rapport infra-annuel Eprd) - Agence technique de l’information sur l’hospitalisation (ATIH)

    +
    +
    +

    + Hélios collecte ces données depuis le SI mutualisé des ARS DIAMANT « Décisionnel Inter-ARS pour la Maîtrise et l’Anticipation », outil décisionnel de pilotage centré sur la régulation de l’offre de soins, abordée sous les aspects des moyens humains, financiers, et productivité. +

    +
    + + ); +} + +export function FondsDeRoulement({ etabFiness, etabTitle, fondsDeRoulementViewModel }: FondsDeRoulementProps): ReactElement { + const { wording } = useDependencies(); + const infoBulleContenu = ; + const sourceName = wording.ANCRE; + const anneesManquantes: number[] = annéesManquantes(fondsDeRoulementViewModel.années, 5); + + return fondsDeRoulementViewModel.auMoinsUnFondsDeRoulementRenseigné() ? ( + + + + ) : ( + <> + ); +} diff --git "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/fonds-de-roulement/FondsDeRoulementViewModel.tsx" "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/fonds-de-roulement/FondsDeRoulementViewModel.tsx" new file mode 100644 index 000000000..15a1d9ac8 --- /dev/null +++ "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/fonds-de-roulement/FondsDeRoulementViewModel.tsx" @@ -0,0 +1,72 @@ +import { EntitéJuridiqueBudgetFinance } from "../../../../../backend/métier/entities/entité-juridique/EntitéJuridiqueBudgetFinance"; +import { + couleurDuFond, + couleurDuFondHistogrammeSecondaire, + couleurErreur, + CouleurHistogramme, + couleurIdentifiant, + TaillePoliceTick, +} from "../../../commun/Graphique/couleursGraphique"; +import StringFormater from "../../../commun/StringFormater"; + +type FondsDeRoulement = { valeur: number | null | ''; année: number; dateDeMiseÀJour: string }; + +export class FondsDeRoulementViewModel { + readonly NOMBRE_ANNEES = 5; + private fondsDeRoulement: FondsDeRoulement[]; + private autorisations: any; + + constructor(budgetFinance: EntitéJuridiqueBudgetFinance[], autorisations: any) { + this.fondsDeRoulement = budgetFinance + .filter((budget) => budget.fondsDeRoulement !== null && budget.fondsDeRoulement !== undefined && budget.fondsDeRoulement !== 0) + .map((budget) => ({ + année: budget.année, + valeur: budget.fondsDeRoulement, + dateDeMiseÀJour: budget.dateMiseÀJourSource, + })); + this.autorisations = autorisations; + } + + public get années(): number[] { + return this.fondsDeRoulement.map((fdr) => fdr.année); + } + + public get dateMiseÀJour(): string { + return StringFormater.formatDate(this.fondsDeRoulement[0]?.dateDeMiseÀJour); + } + + public auMoinsUnFondsDeRoulementRenseigné(): boolean { + return this.fondsDeRoulement.some(this.valeurRemplie); + } + + public get fondsDeRoulementEstIlAutorisé(): boolean { + return this.autorisations.budgetEtFinance?.fondsDeRoulement === 'ok'; + } + + private valeurRemplie(fdr: FondsDeRoulement): boolean { + return fdr.valeur !== null && fdr.valeur !== ''; + } + + public get valeurs(): number[] { + return this.fondsDeRoulement.map((fdr) => fdr.valeur as number); + } + + public construisLesLibellésDesTicks(): TaillePoliceTick[] { + return this.fondsDeRoulement.map(() => "normal"); + } + + public construisLesCouleursDesLibelles(): string[] { + return this.fondsDeRoulement.map(() => couleurIdentifiant); + } + + get couleursDeLHistogramme(): CouleurHistogramme[] { + return this.fondsDeRoulement.map((fdr) => ({ + premierPlan: this.valeurEstEnErreur(fdr) ? couleurErreur : couleurDuFondHistogrammeSecondaire, + secondPlan: couleurDuFond, + })); + } + + private valeurEstEnErreur(fdr: FondsDeRoulement): boolean { + return fdr.valeur === null || fdr.valeur === ''; + } +} diff --git "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/tresorerie/Tresorerie.tsx" "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/tresorerie/Tresorerie.tsx" new file mode 100644 index 000000000..6139e6444 --- /dev/null +++ "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/tresorerie/Tresorerie.tsx" @@ -0,0 +1,82 @@ +import { ReactElement } from "react"; + +import { TresorerieViewModel } from "./TresorerieViewModel"; +import { annéesManquantes } from "../../../../utils/dateUtils"; +import { useDependencies } from "../../../commun/contexts/useDependencies"; +import { HistogrammeHorizontal } from "../../../commun/Graphique/HistogrammeHorizontal"; +import { IndicateurGraphique } from "../../../commun/IndicateurGraphique/IndicateurGraphique"; +import StringFormater from "../../../commun/StringFormater"; + +type TresorerieProps = Readonly<{ + etabFiness: string; + etabTitle: string; + tresorerieViewModel: TresorerieViewModel; +}>; + +export function ContenuTresorerie(props: Readonly<{ dateDeMiseÀJour: any; source: ReactElement }>): ReactElement { + const { wording } = useDependencies(); + + return ( + <> +

    {wording.miseÀJourEtSource(props.source, props.dateDeMiseÀJour)}

    +
    +

    + La trésorerie désigne les liquidités ou quasi liquidités dont dispose l’établissement à un moment donné. +

    +
    +
    +

    + Fréquence : Quotidienne +

    +
    +
    +

    + Mode de calcul : +

    +

    Trésorerie = Fonds de roulement - Besoin en fonds de roulement

    +
    +
    +

    + Source(s) : ANCRE (Application Nationale Compte financier Rapport infra-annuel Eprd) - Agence technique de l’information sur l’hospitalisation (ATIH)

    +
    +
    +

    + Hélios collecte ces données depuis le SI mutualisé des ARS DIAMANT « Décisionnel Inter-ARS pour la Maîtrise et l’Anticipation », outil décisionnel de pilotage centré sur la régulation de l’offre de soins, abordée sous les aspects des moyens humains, financiers, et productivité. +

    +
    + + ); +} + +export function Tresorerie({ etabFiness, etabTitle, tresorerieViewModel }: TresorerieProps): ReactElement { + const { wording } = useDependencies(); + const infoBulleContenu = ; + const sourceName = wording.ANCRE; + const anneesManquantes: number[] = annéesManquantes(tresorerieViewModel.années, 5); + + return tresorerieViewModel.auMoinsUneTresorerieRenseignée() ? ( + + + + ) : ( + <> + ); +} diff --git "a/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/tresorerie/TresorerieViewModel.tsx" "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/tresorerie/TresorerieViewModel.tsx" new file mode 100644 index 000000000..2dd560382 --- /dev/null +++ "b/src/frontend/ui/entit\303\251-juridique/bloc-budget-finance/tresorerie/TresorerieViewModel.tsx" @@ -0,0 +1,72 @@ +import { EntitéJuridiqueBudgetFinance } from "../../../../../backend/métier/entities/entité-juridique/EntitéJuridiqueBudgetFinance"; +import { + couleurDuFond, + couleurDuFondHistogrammeSecondaire, + couleurErreur, + CouleurHistogramme, + couleurIdentifiant, + TaillePoliceTick, +} from "../../../commun/Graphique/couleursGraphique"; +import StringFormater from "../../../commun/StringFormater"; + +type Tresorerie = { valeur: number | null; année: number; dateDeMiseÀJour: string }; + +export class TresorerieViewModel { + readonly NOMBRE_ANNEES = 5; + private tresorerie: Tresorerie[]; + private autorisations: any; + + constructor(budgetFinance: EntitéJuridiqueBudgetFinance[], autorisations: any) { + this.tresorerie = budgetFinance + .filter((budget) => budget.tresorerie !== null && budget.tresorerie !== undefined && budget.tresorerie !== 0) + .map((budget) => ({ + année: budget.année, + valeur: budget.tresorerie, + dateDeMiseÀJour: budget.dateMiseÀJourSource, + })); + this.autorisations = autorisations; + } + + public get années(): number[] { + return this.tresorerie.map((t) => t.année); + } + + public get dateMiseÀJour(): string { + return StringFormater.formatDate(this.tresorerie[0]?.dateDeMiseÀJour); + } + + public auMoinsUneTresorerieRenseignée(): boolean { + return this.tresorerie.some(this.valeurRemplie); + } + + public get tresorerieEstIlAutorisé(): boolean { + return this.autorisations.budgetEtFinance?.tresorerie === 'ok'; + } + + private valeurRemplie(t: Tresorerie): boolean { + return t.valeur !== null; + } + + public get valeurs(): number[] { + return this.tresorerie.map((t) => t.valeur as number); + } + + public construisLesLibellésDesTicks(): TaillePoliceTick[] { + return this.tresorerie.map(() => "normal"); + } + + public construisLesCouleursDesLibelles(): string[] { + return this.tresorerie.map(() => couleurIdentifiant); + } + + get couleursDeLHistogramme(): CouleurHistogramme[] { + return this.tresorerie.map((t) => ({ + premierPlan: this.valeurEstEnErreur(t) ? couleurErreur : couleurDuFondHistogrammeSecondaire, + secondPlan: couleurDuFond, + })); + } + + private valeurEstEnErreur(t: Tresorerie): boolean { + return t.valeur === null; + } +} diff --git a/src/frontend/ui/home/ComparaisonViewModel.tsx b/src/frontend/ui/home/ComparaisonViewModel.tsx index 8d638df25..e6623950e 100644 --- a/src/frontend/ui/home/ComparaisonViewModel.tsx +++ b/src/frontend/ui/home/ComparaisonViewModel.tsx @@ -47,6 +47,9 @@ export type ResultatComparaisonEJ = Readonly<{ resultatNetComptableEj: number | null | string; tauxCafEj: number | null | string; ratioDependanceFinanciere: number | null | string; + fondsDeRoulement: number | null | string; + besoinFondsDeRoulement: number | null | string; + tresorerie: number | null | string; sejoursHad: number | null | string; nombreEtpPm: number | null | string; nombreEtpPnm: number | null | string; @@ -77,6 +80,9 @@ export type ResultatComparaisonSAN = Readonly<{ depensesInterimPm: number | null | string; joursAbsenteismePm: number | null | string; joursAbsenteismePnm: number | null | string; + fondsDeRoulement: number | null | string; + besoinFondsDeRoulement: number | null | string; + tresorerie: number | null | string; enveloppe1: number | null | string; enveloppe2: number | null | string; enveloppe3: number | null | string; @@ -314,6 +320,18 @@ export class ComparaisonSANViewModel { return formatCurrency(this.comparaison.depensesInterimPm); } + public get fondsDeRoulement(): string { + return formatCurrency(this.comparaison.fondsDeRoulement); + } + + public get besoinFondsDeRoulement(): string { + return formatCurrency(this.comparaison.besoinFondsDeRoulement); + } + + public get tresorerie(): string { + return formatCurrency(this.comparaison.tresorerie); + } + public get joursAbsenteismePm(): string { return formatNumberValue(this.comparaison.joursAbsenteismePm); } @@ -382,6 +400,18 @@ export class ComparaisonEJViewModel { return formatRatio(this.comparaison.ratioDependanceFinanciere); } + public get fondsDeRoulement(): string { + return formatCurrency(this.comparaison.fondsDeRoulement); + } + + public get besoinFondsDeRoulement(): string { + return formatCurrency(this.comparaison.besoinFondsDeRoulement); + } + + public get tresorerie(): string { + return formatCurrency(this.comparaison.tresorerie); + } + public get sejoursHad(): number | string | null { if (this.comparaison.sejoursHad === '') return '' return this.comparaison.sejoursHad; diff --git a/src/frontend/ui/home/PageRecherche.test.tsx b/src/frontend/ui/home/PageRecherche.test.tsx index b46caebc9..fe38e830e 100644 --- a/src/frontend/ui/home/PageRecherche.test.tsx +++ b/src/frontend/ui/home/PageRecherche.test.tsx @@ -21,6 +21,7 @@ const mockSession = { institutionId: 1, codeRegion: 84, codeProfiles: [""], + cgu: true, }, expires: "1235", }; diff --git a/src/frontend/ui/search-history/SearchHistoryPage.integration.test.tsx b/src/frontend/ui/search-history/SearchHistoryPage.integration.test.tsx index 741a5e828..f5ea4a06a 100644 --- a/src/frontend/ui/search-history/SearchHistoryPage.integration.test.tsx +++ b/src/frontend/ui/search-history/SearchHistoryPage.integration.test.tsx @@ -16,6 +16,7 @@ const mockSession = { institutionId: 1, codeRegion: 84, codeProfiles: [""], + cgu: true, }, expires: "1235", }; diff --git "a/src/frontend/ui/\303\251tablissement-territorial-m\303\251dico-social/bloc-activit\303\251/Page\303\211tablissementTerritorialM\303\251dicoSocialActivit\303\251.test.tsx" "b/src/frontend/ui/\303\251tablissement-territorial-m\303\251dico-social/bloc-activit\303\251/Page\303\211tablissementTerritorialM\303\251dicoSocialActivit\303\251.test.tsx" index 7f96cd3bd..82bad85d1 100644 --- "a/src/frontend/ui/\303\251tablissement-territorial-m\303\251dico-social/bloc-activit\303\251/Page\303\211tablissementTerritorialM\303\251dicoSocialActivit\303\251.test.tsx" +++ "b/src/frontend/ui/\303\251tablissement-territorial-m\303\251dico-social/bloc-activit\303\251/Page\303\211tablissementTerritorialM\303\251dicoSocialActivit\303\251.test.tsx" @@ -19,7 +19,8 @@ const mockSession = { institution: {}, institutionId: 1, codeRegion: 84, - codeProfiles: [""] + codeProfiles: [""], + cgu: true, }, expires: "1235" } diff --git "a/src/frontend/ui/\303\251tablissement-territorial-m\303\251dico-social/bloc-identit\303\251/Page\303\211tablissementTerritorialM\303\251dicoSocialIdentit\303\251.test.tsx" "b/src/frontend/ui/\303\251tablissement-territorial-m\303\251dico-social/bloc-identit\303\251/Page\303\211tablissementTerritorialM\303\251dicoSocialIdentit\303\251.test.tsx" index c23bd8e21..c4c0cecd3 100644 --- "a/src/frontend/ui/\303\251tablissement-territorial-m\303\251dico-social/bloc-identit\303\251/Page\303\211tablissementTerritorialM\303\251dicoSocialIdentit\303\251.test.tsx" +++ "b/src/frontend/ui/\303\251tablissement-territorial-m\303\251dico-social/bloc-identit\303\251/Page\303\211tablissementTerritorialM\303\251dicoSocialIdentit\303\251.test.tsx" @@ -21,6 +21,7 @@ const mockSession = { institutionId: 1, codeRegion: 84, codeProfiles: [""], + cgu: true, }, expires: "1235", }; diff --git "a/src/frontend/ui/\303\251tablissement-territorial-sanitaire/bloc-identit\303\251/Page\303\211tablissementTerritorialSanitaireIdentit\303\251.test.tsx" "b/src/frontend/ui/\303\251tablissement-territorial-sanitaire/bloc-identit\303\251/Page\303\211tablissementTerritorialSanitaireIdentit\303\251.test.tsx" index 2fa3bdefc..cb0c0f860 100644 --- "a/src/frontend/ui/\303\251tablissement-territorial-sanitaire/bloc-identit\303\251/Page\303\211tablissementTerritorialSanitaireIdentit\303\251.test.tsx" +++ "b/src/frontend/ui/\303\251tablissement-territorial-sanitaire/bloc-identit\303\251/Page\303\211tablissementTerritorialSanitaireIdentit\303\251.test.tsx" @@ -24,6 +24,7 @@ const mockSession = { institutionId: 1, codeRegion: 84, codeProfiles: [""], + cgu: true, }, expires: "1235", }; diff --git "a/src/frontend/ui/\303\251tablissement-territorial-sanitaire/bloc-ressources-humaines/Page\303\211tablissementTerritorialSanitaireBlocRh.test.tsx" "b/src/frontend/ui/\303\251tablissement-territorial-sanitaire/bloc-ressources-humaines/Page\303\211tablissementTerritorialSanitaireBlocRh.test.tsx" index a7d8061eb..b9c52b363 100644 --- "a/src/frontend/ui/\303\251tablissement-territorial-sanitaire/bloc-ressources-humaines/Page\303\211tablissementTerritorialSanitaireBlocRh.test.tsx" +++ "b/src/frontend/ui/\303\251tablissement-territorial-sanitaire/bloc-ressources-humaines/Page\303\211tablissementTerritorialSanitaireBlocRh.test.tsx" @@ -24,6 +24,7 @@ const mockSession = { institutionId: 1, codeRegion: 84, codeProfiles: [""], + cgu: true, }, expires: "1235", }; diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts index 855da36dc..93531297a 100644 --- a/src/pages/api/auth/[...nextauth].ts +++ b/src/pages/api/auth/[...nextauth].ts @@ -32,6 +32,7 @@ export const authOptions: NextAuthOptions = { institutionId: utilisateur.institution.id, codeRegion: utilisateur.institution.codeGeo, codeProfiles: utilisateur.profils, + cgu: utilisateur.cgu, }; } catch { return null; @@ -41,7 +42,7 @@ export const authOptions: NextAuthOptions = { ], secret: process.env["NEXTAUTH_SECRET"], callbacks: { - async jwt({ token, user, account }) { + async jwt({ token, user, account, trigger, session }) { if (account && user) { return { ...token, @@ -53,6 +54,13 @@ export const authOptions: NextAuthOptions = { institutionId: user.institutionId, codeRegion: user.codeRegion, codeProfiles: user.codeProfiles, + cgu: user.cgu, + }; + } + if (trigger === "update" && typeof session?.cgu === "boolean") { + return { + ...token, + cgu: session.cgu, }; } return token; @@ -65,6 +73,7 @@ export const authOptions: NextAuthOptions = { session.user.institutionId = token["institutionId"] as number; session.user.codeRegion = token["codeRegion"] as number; session.user.codeProfiles = token["codeProfiles"] as string[]; + session.user.cgu = token["cgu"] as boolean; return session; }, }, diff --git a/src/pages/api/utilisateurs/cgu.ts b/src/pages/api/utilisateurs/cgu.ts new file mode 100644 index 000000000..9317caea7 --- /dev/null +++ b/src/pages/api/utilisateurs/cgu.ts @@ -0,0 +1,27 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { getServerSession } from "next-auth"; + +import { updateCguEndpoint } from "../../../backend/infrastructure/controllers/updateCguEndpoint"; +import { dependencies } from "../../../backend/infrastructure/dependencies"; +import { authOptions } from "../auth/[...nextauth]"; + +export default async function handler(request: NextApiRequest, response: NextApiResponse) { + if (request.method !== "POST") { + return response.status(405).send("Method not allowed"); + } + + const session = await getServerSession(request, response, authOptions); + const userCode = session?.user?.idUser; + + if (!userCode) { + return response.status(401).send("Unauthorized"); + } + + const { accepted } = request.body as { accepted?: boolean }; + if (typeof accepted !== "boolean") { + return response.status(400).send("Bad request"); + } + + await updateCguEndpoint(dependencies, userCode, accepted); + return response.status(200).json({}); +} diff --git a/src/pages/cgu.tsx b/src/pages/cgu.tsx new file mode 100644 index 000000000..1c2b373d4 --- /dev/null +++ b/src/pages/cgu.tsx @@ -0,0 +1,25 @@ +import Head from "next/head"; + +import { PageCgu } from "../frontend/ui/cgu/PageCgu"; +import { useDependencies } from "../frontend/ui/commun/contexts/useDependencies"; +import { useBreadcrumb } from "../frontend/ui/commun/hooks/useBreadcrumb"; + +export default function Cgu() { + const { wording } = useDependencies(); + + useBreadcrumb([ + { + label: wording.CGU, + path: "", + }, + ]); + + return ( +
    + + {wording.TITRE_PAGE_CGU} + + +
    + ); +} diff --git a/src/pages/valider-cgu.tsx b/src/pages/valider-cgu.tsx new file mode 100644 index 000000000..7c215d4bb --- /dev/null +++ b/src/pages/valider-cgu.tsx @@ -0,0 +1,55 @@ +import Head from "next/head"; +import { useRouter } from "next/router"; +import { signOut, useSession } from "next-auth/react"; + +import { PageCgu } from "../frontend/ui/cgu/PageCgu"; +import { useDependencies } from "../frontend/ui/commun/contexts/useDependencies"; +import { useBreadcrumb } from "../frontend/ui/commun/hooks/useBreadcrumb"; + +export default function ValiderCgu() { + const { wording } = useDependencies(); + const router = useRouter(); + const { update } = useSession(); + + useBreadcrumb([ + { + label: wording.CGU, + path: "", + }, + ]); + + return ( +
    + + {wording.TITRE_PAGE_CGU} + + +
    + + +
    +
    + ); +} diff --git a/src/proxy.ts b/src/proxy.ts index 96edc7843..6e3964af3 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,7 +1,11 @@ import { NextResponse } from "next/server"; import { withAuth } from "next-auth/middleware"; +import { Paths } from "./frontend/configuration/Paths"; import type { NextRequest } from "next/server"; +import type { NextRequestWithAuth } from "next-auth/middleware"; + +const PATHS = new Paths(); export default withAuth( function proxy(request: NextRequest) { @@ -16,6 +20,17 @@ export default withAuth( return NextResponse.rewrite(url); } + const token = (request as NextRequestWithAuth).nextauth.token as { cgu?: boolean } | null; + + const isCguPage = request.nextUrl.pathname === PATHS.VALIDER_CGU; + const hasAcceptedCgu = token?.cgu === true; + + if (!hasAcceptedCgu && !isCguPage) { + const url = request.nextUrl.clone(); + url.pathname = PATHS.VALIDER_CGU; + return NextResponse.redirect(url); + } + return NextResponse.next(); }, { @@ -39,6 +54,7 @@ export const config = { "/comparaison", "/history", "/change-mot-passe", + "/valider-cgu", "/liste/:path*", "/api/recherche", "/api/revalidate", diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index ee5ffc046..dc441ebba 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -11,6 +11,7 @@ declare module "next-auth" { institutionId: number; codeRegion: string; codeProfiles: string[]; + cgu: boolean; } interface Session { @@ -22,6 +23,7 @@ declare module "next-auth" { codeProfiles: string[]; institution; institutionId: number; + cgu: boolean; } & DefaultSession["user"]; } }