From 6ba698dab57532e380523c48f560bf6fe6b249e4 Mon Sep 17 00:00:00 2001 From: Fabien Mercier Date: Wed, 15 May 2024 16:54:11 +0200 Subject: [PATCH] Mise en place de la comparaison entre deux inventaires --- public/img/logo-ministere.svg | 2 +- src/app/(connecte)/(both)/inventaire/page.tsx | 2 +- .../(utilisateur)/tableau-comparatif/page.tsx | 75 +++ src/components/Cgu/Cgu.tsx | 291 ++++++++- .../IndicateursCles/IndicateursCles.test.tsx | 2 - .../IndicateursCles/IndicateursCles.tsx | 39 +- .../IndicateursCles/Transcription.tsx | 2 +- src/components/IndicateursCles/graphiques.ts | 4 +- src/components/Inventaire/Actions.tsx | 2 - src/components/Inventaire/Inventaire.test.tsx | 83 +++ .../Inventaires/Inventaires.module.css | 8 + .../Inventaires/Inventaires.test.tsx | 164 +++++- src/components/Inventaires/Inventaires.tsx | 32 +- .../Inventaires/InventairesLayout.tsx | 19 +- src/components/Inventaires/useInventaires.ts | 39 ++ .../ListeEquipements.test.tsx | 2 - .../TableauComparatif.module.css | 16 + .../TableauComparatif.test.tsx | 109 ++++ .../TableauComparatif/TableauComparatif.tsx | 550 ++++++++++++++++++ src/presenters/indicateursClesPresenter.ts | 85 +-- src/presenters/sharedPresenter.ts | 87 ++- src/presenters/tableauComparatifPresenter.ts | 50 ++ src/testShared.ts | 2 +- 23 files changed, 1527 insertions(+), 138 deletions(-) create mode 100644 src/app/(connecte)/(utilisateur)/tableau-comparatif/page.tsx create mode 100644 src/components/Inventaire/Inventaire.test.tsx create mode 100644 src/components/Inventaires/useInventaires.ts create mode 100644 src/components/TableauComparatif/TableauComparatif.module.css create mode 100644 src/components/TableauComparatif/TableauComparatif.test.tsx create mode 100644 src/components/TableauComparatif/TableauComparatif.tsx create mode 100644 src/presenters/tableauComparatifPresenter.ts diff --git a/public/img/logo-ministere.svg b/public/img/logo-ministere.svg index 4a55601..5dcdf1e 100644 --- a/public/img/logo-ministere.svg +++ b/public/img/logo-ministere.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/(connecte)/(both)/inventaire/page.tsx b/src/app/(connecte)/(both)/inventaire/page.tsx index 2d40adf..9f1ef5f 100644 --- a/src/app/(connecte)/(both)/inventaire/page.tsx +++ b/src/app/(connecte)/(both)/inventaire/page.tsx @@ -29,7 +29,7 @@ type PageProps = Readonly<{ searchParams?: SearchParams }> -export default async function Page({ searchParams }: PageProps): Promise { +export default async function PageInventaire({ searchParams }: PageProps): Promise { if (searchParams?.nomEtablissement === undefined || searchParams.nomInventaire === undefined) { notFound() } diff --git a/src/app/(connecte)/(utilisateur)/tableau-comparatif/page.tsx b/src/app/(connecte)/(utilisateur)/tableau-comparatif/page.tsx new file mode 100644 index 0000000..d7768c3 --- /dev/null +++ b/src/app/(connecte)/(utilisateur)/tableau-comparatif/page.tsx @@ -0,0 +1,75 @@ +import { Metadata } from 'next' +import { notFound } from 'next/navigation' +import { ReactElement } from 'react' + +import { getProfilAtih } from '../../../../authentification' +import Breadcrumb from '../../../../components/sharedComponents/Breadcrumb' +import TableauComparatif from '../../../../components/TableauComparatif/TableauComparatif' +import { tableauComparatifPresenter } from '../../../../presenters/tableauComparatifPresenter' +import { recupererLesIndicateursImpactsEquipementsRepository } from '../../../../repositories/indicateursRepository' +import { recupererLesModelesRepository } from '../../../../repositories/modelesRepository' + +const title = 'Tableau comparatif' +export const metadata: Metadata = { + title, +} + +type PageProps = Readonly<{ + searchParams?: Readonly<{ + inventaireCompare?: string + inventaireReference?: string + }> +}> + +export default async function PageTableauComparatif({ searchParams }: PageProps): Promise { + if (searchParams?.inventaireCompare === undefined || searchParams.inventaireReference === undefined) { + notFound() + } + + const profil = await getProfilAtih() + + if (profil.isAdmin) { + notFound() + } + + const indicateursImpactsEquipementsCompareModel = await recupererLesIndicateursImpactsEquipementsRepository( + profil.nomEtablissement, + searchParams.inventaireCompare + ) + + if (indicateursImpactsEquipementsCompareModel.length === 0) { + notFound() + } + + const indicateursImpactsEquipementsReferenceModel = await recupererLesIndicateursImpactsEquipementsRepository( + profil.nomEtablissement, + searchParams.inventaireReference + ) + + if (indicateursImpactsEquipementsReferenceModel.length === 0) { + notFound() + } + + const modelesCompareModel = await recupererLesModelesRepository(profil.nomEtablissement, searchParams.inventaireCompare) + const modelesReferenceModel = await recupererLesModelesRepository(profil.nomEtablissement, searchParams.inventaireReference) + + return ( + <> + + + + ) +} diff --git a/src/components/Cgu/Cgu.tsx b/src/components/Cgu/Cgu.tsx index 7f44978..225dc90 100644 --- a/src/components/Cgu/Cgu.tsx +++ b/src/components/Cgu/Cgu.tsx @@ -2,8 +2,293 @@ import { ReactElement } from 'react' export default function Cgu(): ReactElement { return ( -

- A rédiger -

+ <> +

+ Conditions Générales d’Utilisation Service de calcul d’impact environnemental du système d’information de santé, EvalCarbone SIH +

+

+ Article 1. Objet +

+

+ Le service de calcul EvalCarbone SIH désigne la plateforme qui permet aux établissements sanitaires + et médico-sociaux de mesurer l’impact environnemental de leur système d’information, ci-dessous dénommé « le Service ». +
+ Le système d’information, objet de l’évaluation par le Service, est dénommée ci-dessous par le terme “SIH” (Système d’information hospitalier). +
+ Le terme “Utilisateurs” désigne les personnels en établissements de santé ou médico-social titulaires d’un compte leur permettant d’accéder au Service. +
+ Le Ministère du travail de la santé et des solidarités assume la qualité d’éditeur du Service (ci-après « l’Editeur »), + au sens de la Loi n° 2004-575 du 21 juin 2004 pour la Confiance dans l’Economie Numérique modifiée, + pour les Contenus qu’elle détermine et à l’exclusion du Contenu fourni par le Partenaire Editeur. +
+ L’Agence du Numérique en Santé assure la mise en œuvre du Service (ci-après l’Opérateur »). +
+ Les présentes Conditions ont pour objet de : +

+
+
    +
  • + déterminer les conditions d’utilisation du Service ; +
  • +
  • + définir les obligations de l’Editeur, de l’Opérateur et des Utilisateurs dans le but de garantir la préservation des systèmes et des données ; +
  • +
  • + informer les Utilisateurs des traitements de données à caractère personnel réalisés pour leur permettre d’accéder au Service. +
  • +
+
+

+ Article 2. Accès au Service +

+

+ 2.2 Prérequis : création de compte dans le fournisseur d’identité ATIH +

+

+ L’accès au Service en tant qu’Utilisateur nécessite la création d’un compte auprès du fournisseur d’identité de l’ATIH + (Agence technique de l’information hospitalière), désigné ci-après « Fournisseur d’identité ». +
+ Le Fournisseur d’identité permet aux Utilisateurs de créer un compte pour accéder aux services numériques proposés par l’ATIH + en utilisant des identifiants uniques (nom d’utilisateur et mot de passe). +
+ Une fois qu’il s’est authentifié auprès du Fournisseur d’Identité et après en avoir pris connaissance des CGU, + l’Utilisateur peut utiliser le Service dans le respect des présentes Conditions. +

+

+ Article 3. Fonctionnement du Service +

+

+ 3.1 Prérequis : acceptation des présentes Conditions +

+

+ L’accès, la navigation ou l’utilisation du Service vaut acceptation des présentes Conditions. +
+ Ces Conditions sont susceptibles d’être modifiées à tout moment par l’Editeur. +

+

+ 3.2 Fonctionnalités du Service +

+

+ Le Service propose les fonctionnalités suivantes : +

+
+
    +
  • + Renseignement d’inventaires matériels répertoriant les équipements informatiques détenus ou utilisés par l’établissement. +
  • +
  • + Calcul des indicateurs d’impact environnemental des équipement inventoriés. +
  • +
  • + Consultation des indicateurs d’impact environnemental. +
  • +
  • + Accès à la documentation pour comprendre les résultats et aux questions fréquentes (FAQ) expliquant le fonctionnement et l’utilisation du Service. +
  • +
+
+

+ Article 4. Obligations des Utilisateurs +

+

+ Sauf mention contraire, expressément signalée lors de l’octroi de l’accès au Service, les identifiants de connexion sont délivrés + à titre personnel et confidentiel. Toute utilisation du Service se fait sous la responsabilité de l’Utilisateur titulaire du compte. +
+ L’Utilisateur reconnait agir au nom et pour le compte de l’établissement auquel il est rattaché. Il s’engage à utiliser + le Service de bonne foi et uniquement en vue d’obtenir l’impact environnemental du système d’information de l’établissement + auquel il est rattaché. L’Utilisateur est responsable de tout litige ou contentieux lié à une utilisation du Service non conforme + aux dispositions des présentes CGU. +
+ L’Utilisateur est responsable de la préservation de la sécurité et de la confidentialité de ses moyens d’authentification personnels. + L’Utilisateur s’engage à informer sans délai l’Editeur de toute modification de sa situation professionnelle déclarée + au moment de l’octroi des identifiants de connexion. +
+ L’Editeur ne saurait être tenu responsable d’un accès par un tiers, suite à une usurpation des moyens d’authentification de l’Utilisateur. + L’Utilisateur qui aurait connaissance d’un risque lié à l’utilisation de ses moyens d’authentification s’engage à en informer + sans délai l’Editeur, afin que celui-ci puisse prendre toute mesure nécessaire, notamment de bloquer l’accès à son compte personnel + jusqu’à délivrance de nouveaux moyens d’authentification. +

+

+ Article 5. Confidentialité des indicateurs d’impact environnemental fournis par le Service +

+

+ Les indicateurs produits par le Service ne sont pas rendus publics et sont uniquement accessibles par l’Editeur et l’Opérateur, + en leur qualité d’administrateurs. +

+

+ Article 6. Protection des données à caractère personnel +

+

+ 6.1 Responsabilité et finalité du traitement +

+

+ La mise en œuvre du Service entraine un traitement de données à caractère personnel encadré par la loi n°78-17 du 6 janvier 1978, + modifiée, relative à l’informatique, aux fichiers et aux libertés et le Règlement (UE) 2016/679 du Parlement européen et + du Conseil du 27 avril 2016 relatif à la protection des personnes physiques à l’égard du traitement des données à caractère personnel + et à la libre circulation de ces données (« RGPD »). +
+ Ce traitement est mis en œuvre sous la responsabilité de l’Editeur, représenté par la Délégation ministérielle au numérique en santé (DNS). +
+ L’Opérateur est le sous-traitant au sens de l’article 28 du RGPD. +
+ Ce traitement est fondé sur une mission d’intérêt public au sens de l’article 6 du RGPD. +

+

+ 6.2 Catégories des données +

+

+ Les catégories des données collectées et traitées par les Fournisseurs d’identité sont les données d’identification nécessaires + à la création du compte de l’Utilisateur et à son accès au Service (nom, prénom, adresse mail professionnelle). + Voir les mentions + {' '} + + Protection des données personnelles + + {' '} + du Fournisseur d’identité. +
+ Ces données à caractère personnel collectées par le Fournisseur d’identité ne sont pas récupérées par le Service. +
+ Hors Fournisseur d’identité, aucune donnée à caractère personnel n’est collectée ou traitée par le Service. +

+

+ 6.3 Destinataires des données +

+

+ Hors Fournisseur d’identité, aucune donnée à caractère personnel n’est collectée ou traitée par le Service. +

+

+ 6.4 Durée de conservation des données +

+

+ Hors Fournisseur d’identité, aucune donnée à caractère personnel n’est collectée ou traitée par le Service. +

+

+ 6.5 Cookies +

+

+ Le Service est conçu pour être particulièrement attentif aux besoins des Utilisateurs. À cet égard, il y est fait usage de cookies. +
+ Le dépôt de cookies, réalisé dans le cadre de l’utilisation du Service, permet d’enregistrer des informations relatives + à la navigation du terminal de l’Utilisateur qui pourront être lues lors de ses visites ultérieures. +
+ Les informations collectées sont à l’usage exclusif de l’Editeur et l’Opérateur ou de ses prestataires techniques, + et ne sont en aucun cas cédées à des tiers. +
+ Les cookies déposés par le Service sont les suivants : +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Intitulé des cookies + + Type + + Finalité + + Durée de conservation +
+ next-auth.callback-url + + Fonctionnement du site + + Authentification + + Jusqu’à expiration de la session Utilisateur +
+ next-auth.csrf-token + + Fonctionnement du site + + Authentification + + Jusqu’à expiration de la session Utilisateur +
+ next-auth.session-token + + Fonctionnement du site + + Mémoriser des informations liées à la session de l’utilisateur + + Un mois +
+

+ 6.6 Droit des personnes concernées +

+

+ Chaque Utilisateur est informé qu’il bénéficie, conformément au RGDP et à la Loi Informatique et Libertés modifiée, + d’un droit d’opposition, d’accès, de rectification, d’effacement, de limitation du traitement et de portabilité. +
+ Ces droits peuvent être exercés auprès de l’Opérateur par messagerie électronique, à l’adresse suivante : + {' '} + + dpo@esante.gouv.fr + + . +
+ Si la personne concernée estime que ses droits n’ont pas été respectés, + elle a la possibilité de saisir la Commission Nationale de l’Informatique et des Libertés (CNIL) d’une réclamation : + {' '} + + https://www.cnil.fr/fr/plaintes/CNIL + + {' '} + — Service des plaintes — 3 place Fontenoy — TSA 80715 - 75334 PARIS CEDEX 07. +

+

+ Article 7. Propriété intellectuelle +

+

+ Pour tous les contenus, textes et logos présentés sur le Service, tous droits d’auteur des œuvres sont réservés. + Sauf autorisation formelle écrite préalable, la reproduction ainsi que toute utilisation des œuvres, + autres que la consultation individuelle et privée sont interdites. +
+ Le Service réutilise le code du produit NumEcoEval mis à disposition en open source par le Ministère de la Transition Ecologique + et les facteurs d’impact du « Starter kit » publiés par la société Résilio. +

+

+ Article 8. Loi applicable et tribunaux compétents +

+

+ Les présentes Conditions sont régies par la loi française. Tout litige résultant de leur application relèvera de la compétence des tribunaux français. +

+ ) } diff --git a/src/components/IndicateursCles/IndicateursCles.test.tsx b/src/components/IndicateursCles/IndicateursCles.test.tsx index 372a224..fb26db2 100644 --- a/src/components/IndicateursCles/IndicateursCles.test.tsx +++ b/src/components/IndicateursCles/IndicateursCles.test.tsx @@ -15,8 +15,6 @@ describe('page des indicateurs clés', () => { // GIVEN jeSuisUnUtilisateur() - vi.spyOn(repositoryIndicateurs, 'recupererLesIndicateursImpactsEquipementsRepository').mockResolvedValueOnce([indicateurImpactEquipementModelFactory()]) - const queryParams = { searchParams: { nomEtablissement: 'Hopital de Bordeaux$$00000001J', diff --git a/src/components/IndicateursCles/IndicateursCles.tsx b/src/components/IndicateursCles/IndicateursCles.tsx index ebf17d7..dc6f5e1 100644 --- a/src/components/IndicateursCles/IndicateursCles.tsx +++ b/src/components/IndicateursCles/IndicateursCles.tsx @@ -8,7 +8,8 @@ import Astuce from './Astuce' import { donneesParCycleDeVie, donneesParTypeEquipement, donneesRepartitionParTypeEquipement, optionsCamembert, optionsHistogramme } from './graphiques' import Indicateur from './Indicateur' import Transcription from './Transcription' -import { EtapesAcv, IndicateursClesPresenter } from '../../presenters/indicateursClesPresenter' +import { IndicateursClesPresenter } from '../../presenters/indicateursClesPresenter' +import { EtapesAcv, formaterDeuxChiffresApresLaVirgule } from '../../presenters/sharedPresenter' import Accordeon from '../sharedComponents/Accordeon' import Actions from '../sharedComponents/Actions' import InfoBulle from '../sharedComponents/Infobulle' @@ -45,7 +46,7 @@ export default function IndicateursCles({
- {presenter.indicateursImpactsEquipements.empreinteCarbone} + {formaterDeuxChiffresApresLaVirgule(presenter.indicateursImpactsEquipements.empreinteCarbone)}
@@ -75,11 +76,9 @@ export default function IndicateursCles({ Fabrication
- {presenter.indicateursImpactsEquipements.fabrication} + {formaterDeuxChiffresApresLaVirgule(presenter.indicateursImpactsEquipements.fabrication)} {' '} - - tCO2 eq - + tCO2 eq

@@ -88,11 +87,9 @@ export default function IndicateursCles({ Distribution
- {presenter.indicateursImpactsEquipements.distribution} + {formaterDeuxChiffresApresLaVirgule(presenter.indicateursImpactsEquipements.distribution)} {' '} - - tCO2 eq - + tCO2 eq

@@ -101,11 +98,9 @@ export default function IndicateursCles({ Utilisation
- {presenter.indicateursImpactsEquipements.utilisation} + {formaterDeuxChiffresApresLaVirgule(presenter.indicateursImpactsEquipements.utilisation)} {' '} - - tCO2 eq - + tCO2 eq

@@ -114,11 +109,9 @@ export default function IndicateursCles({ Fin de vie
- {presenter.indicateursImpactsEquipements.finDeVie} + {formaterDeuxChiffresApresLaVirgule(presenter.indicateursImpactsEquipements.finDeVie)} {' '} - - tCO2 eq - + tCO2 eq
@@ -160,7 +153,7 @@ export default function IndicateursCles({

- RÉPARTITION DE l’EMPREINTE CARBONE PAR TYPE D’ÉQUIPEMENT + RÉPARTITION DE L’EMPREINTE CARBONE PAR TYPE D’ÉQUIPEMENT

L’empreinte est détaillée en kgCO2 équivalent selon la fabrication. @@ -325,7 +318,7 @@ export default function IndicateursCles({ kg U235 eq } - valeur={presenter.indicateursImpactsEquipements.radiationIonisantes} + valeur={formaterDeuxChiffresApresLaVirgule(presenter.indicateursImpactsEquipements.radiationIonisantes)} /> } - valeur={presenter.indicateursImpactsEquipements.epuisementDesRessources} + valeur={formaterDeuxChiffresApresLaVirgule(presenter.indicateursImpactsEquipements.epuisementDesRessources)} />

@@ -345,7 +338,7 @@ export default function IndicateursCles({ texteInfoBulle="Les particules fines (PM2,5) peuvent provenir du chauffage au bois, du trafic routier et des activités de chantier. Elles sont nocives pour la santé respiratoire et cardiovasculaire." titre="Émissions de particules fines" unite="Incidence de maladies" - valeur={presenter.indicateursImpactsEquipements.emissionsDeParticulesFines} + valeur={formaterDeuxChiffresApresLaVirgule(presenter.indicateursImpactsEquipements.emissionsDeParticulesFines)} /> } - valeur={presenter.indicateursImpactsEquipements.acidification} + valeur={formaterDeuxChiffresApresLaVirgule(presenter.indicateursImpactsEquipements.acidification)} />
diff --git a/src/components/IndicateursCles/Transcription.tsx b/src/components/IndicateursCles/Transcription.tsx index f5bd78d..5cb3bbe 100644 --- a/src/components/IndicateursCles/Transcription.tsx +++ b/src/components/IndicateursCles/Transcription.tsx @@ -37,7 +37,7 @@ export default function Transcription({ indicateursImpactsEquipementsSommes }: T {mettreEnBasDeCasse(indicateurImpactEquipementSomme.etapeAcv)} - {Number(indicateurImpactEquipementSomme.impact.toFixed(2)).toLocaleString()} + {Number(indicateurImpactEquipementSomme.impact.toFixed(2)).toLocaleString('fr-FR')} )) diff --git a/src/components/IndicateursCles/graphiques.ts b/src/components/IndicateursCles/graphiques.ts index 643048d..2fdf704 100644 --- a/src/components/IndicateursCles/graphiques.ts +++ b/src/components/IndicateursCles/graphiques.ts @@ -1,7 +1,7 @@ import { Chart, CategoryScale, LinearScale, BarElement, Tooltip, Legend, ArcElement, ChartData, ChartOptions, ChartDataset } from 'chart.js' -import { EtapesAcv, IndicateurImpactEquipementSomme } from '../../presenters/indicateursClesPresenter' -import { mettreEnBasDeCasse } from '../../presenters/sharedPresenter' +import { IndicateurImpactEquipementSomme } from '../../presenters/indicateursClesPresenter' +import { EtapesAcv, mettreEnBasDeCasse } from '../../presenters/sharedPresenter' Chart.register( ArcElement, diff --git a/src/components/Inventaire/Actions.tsx b/src/components/Inventaire/Actions.tsx index 8c7ecf3..ecdded9 100644 --- a/src/components/Inventaire/Actions.tsx +++ b/src/components/Inventaire/Actions.tsx @@ -1,5 +1,3 @@ -'use client' - import { ReactElement } from 'react' type ModaleProps = Readonly<{ diff --git a/src/components/Inventaire/Inventaire.test.tsx b/src/components/Inventaire/Inventaire.test.tsx new file mode 100644 index 0000000..8a8bb38 --- /dev/null +++ b/src/components/Inventaire/Inventaire.test.tsx @@ -0,0 +1,83 @@ +import { screen } from '@testing-library/react' + +import PageInventaire from '../../app/(connecte)/(both)/inventaire/page' +import * as repositoryModeles from '../../repositories/modelesRepository' +import * as repositoryTypesEquipements from '../../repositories/typesEquipementsRepository' +import { FrozenDate, jeSuisUnUtilisateur, modeleModelFactory, referentielTypeEquipementModelFactory, renderComponent } from '../../testShared' + +describe('page inventaires', () => { + describe('en tant qu’utilisateur', () => { + it('quand le nom d’établissement n’est pas le même que le mien dans l’URL alors je n’y ai pas accès', async () => { + // GIVEN + jeSuisUnUtilisateur() + + const queryParams = { + searchParams: { + nomEtablissement: 'Hopital de Bordeaux$$00000001J', + nomInventaire: 'Centre hospitalier', + }, + } + + // WHEN + const page = async () => renderComponent(await PageInventaire(queryParams)) + + // THEN + await expect(page).rejects.toThrow('NEXT_NOT_FOUND') + }) + + it('quand j’affiche la page alors j’affiche le formulaire', async () => { + // GIVEN + jeSuisUnUtilisateur() + + vi.stubGlobal('Date', FrozenDate) + vi.spyOn(repositoryModeles, 'recupererLesModelesRepository').mockResolvedValueOnce([modeleModelFactory()]) + vi.spyOn(repositoryTypesEquipements, 'recupererLesReferentielsTypesEquipementsRepository').mockResolvedValueOnce([referentielTypeEquipementModelFactory()]) + + // WHEN + renderComponent(await PageInventaire(queryParams())) + + // THEN + const titre = screen.getByRole('heading', { level: 2, name: 'Renseigner les équipements' }) + expect(titre).toBeInTheDocument() + }) + }) + + it('quand il n’y a pas le nom d’inventaire dans l’URL alors je n’y ai pas accès', async () => { + // GIVEN + const queryParams = { + searchParams: { + nomEtablissement: 'Hopital de Paris$$00000001K', + }, + } + + // WHEN + const page = async () => renderComponent(await PageInventaire(queryParams)) + + // THEN + await expect(page).rejects.toThrow('NEXT_NOT_FOUND') + }) + + it('quand il n’y a pas le nom d’établissement dans l’URL alors je n’y ai pas accès', async () => { + // GIVEN + const queryParams = { + searchParams: { + nomInventaire: 'Centre hospitalier', + }, + } + + // WHEN + const page = async () => renderComponent(await PageInventaire(queryParams)) + + // THEN + await expect(page).rejects.toThrow('NEXT_NOT_FOUND') + }) +}) + +function queryParams(): { searchParams: { nomEtablissement: string; nomInventaire: string } } { + return { + searchParams: { + nomEtablissement: 'Hopital de Paris$$00000001K', + nomInventaire: 'Centre hospitalier', + }, + } +} diff --git a/src/components/Inventaires/Inventaires.module.css b/src/components/Inventaires/Inventaires.module.css index 5c9d392..d1fe78c 100644 --- a/src/components/Inventaires/Inventaires.module.css +++ b/src/components/Inventaires/Inventaires.module.css @@ -12,3 +12,11 @@ .non_calculé { background-color: #369AEE; } + +.middle { + line-height: 3.5rem; +} + +.fix { + height: 6rem; +} diff --git a/src/components/Inventaires/Inventaires.test.tsx b/src/components/Inventaires/Inventaires.test.tsx index de7baee..00bc77a 100644 --- a/src/components/Inventaires/Inventaires.test.tsx +++ b/src/components/Inventaires/Inventaires.test.tsx @@ -119,14 +119,148 @@ describe('page inventaires', () => { expect(lienExporterLesInventaires).not.toBeInTheDocument() }) + it('quand j’affiche la page alors j’ai le bouton pour comparer deux inventaires mais il est désactivé', async () => { + // GIVEN + jeSuisUnUtilisateur() + + vi.spyOn(repositoryInventaires, 'recupererLesInventairesRepository').mockResolvedValueOnce([inventaireModelFactory()]) + + // WHEN + renderComponent(await PageInventaires({})) + + // THEN + const buttonComparerDeuxInventaires = screen.getByRole('button', { name: 'Comparer deux inventaires' }) + expect(buttonComparerDeuxInventaires).toBeDisabled() + }) + + it('quand j’affiche la page alors je peux cocher que les inventaires calculés', async () => { + // GIVEN + jeSuisUnUtilisateur() + + vi.spyOn(repositoryInventaires, 'recupererLesInventairesRepository').mockResolvedValueOnce([ + inventaireModelFactory({ + id: 1, + nomInventaire: 'mon premier inventaire', + statut: 'TRAITE', + }), + inventaireModelFactory({ + id: 2, + nomInventaire: 'mon deuxième inventaire', + statut: 'EN_ATTENTE', + }), + ]) + + // WHEN + renderComponent(await PageInventaires({})) + + // THEN + const checkboxInventaire1 = screen.getByLabelText('mon premier inventaire') + expect(checkboxInventaire1).toBeInTheDocument() + const checkboxInventaire2 = screen.queryByLabelText('mon deuxième inventaire') + expect(checkboxInventaire2).not.toBeInTheDocument() + }) + + it('quand je sélectionne un seul inventaire calculé pour comparaison alors le bouton pour comparer deux inventaires est toujours désactivé', async () => { + // GIVEN + jeSuisUnUtilisateur() + + vi.spyOn(repositoryInventaires, 'recupererLesInventairesRepository').mockResolvedValueOnce([ + inventaireModelFactory({ + statut: 'TRAITE', + }), + ]) + + renderComponent(await PageInventaires({})) + const checkboxInventaire1 = screen.getByLabelText('mon super inventaire') + + // WHEN + fireEvent.click(checkboxInventaire1) + + // THEN + const buttonComparerDeuxInventaires = screen.getByRole('button', { name: 'Comparer deux inventaires' }) + expect(buttonComparerDeuxInventaires).toBeDisabled() + }) + + it('quand je sélectionne deux inventaires calculés pour comparaison alors le bouton pour comparer deux inventaires est activé', async () => { + // GIVEN + const nomEtablissement = jeSuisUnUtilisateur() + + vi.spyOn(repositoryInventaires, 'recupererLesInventairesRepository').mockResolvedValueOnce([ + inventaireModelFactory({ + id: 1, + nomEtablissement, + nomInventaire: 'mon premier inventaire', + statut: 'TRAITE', + }), + inventaireModelFactory({ + id: 2, + nomEtablissement, + nomInventaire: 'mon deuxième inventaire', + statut: 'TRAITE', + }), + inventaireModelFactory({ + id: 3, + nomEtablissement, + nomInventaire: 'mon troisième inventaire', + statut: 'TRAITE', + }), + ]) + + renderComponent(await PageInventaires({})) + + // WHEN + const checkboxInventaire1 = screen.getByLabelText('mon premier inventaire') + fireEvent.click(checkboxInventaire1) + const checkboxInventaire3 = screen.getByLabelText('mon troisième inventaire') + fireEvent.click(checkboxInventaire3) + + // THEN + const buttonComparerDeuxInventaires = screen.getByRole('button', { name: 'Comparer deux inventaires' }) + expect(buttonComparerDeuxInventaires).toBeEnabled() + }) + + it('quand je clique sur le bouton comparer deux inventaires alors je vais sur la page de comparaison', async () => { + // GIVEN + const nomEtablissement = jeSuisUnUtilisateur() + + vi.spyOn(repositoryInventaires, 'recupererLesInventairesRepository').mockResolvedValueOnce([ + inventaireModelFactory({ + id: 1, + nomEtablissement, + nomInventaire: 'mon premier inventaire', + statut: 'TRAITE', + }), + inventaireModelFactory({ + id: 2, + nomEtablissement, + nomInventaire: 'mon deuxième inventaire', + statut: 'TRAITE', + }), + ]) + vi.spyOn(navigation, 'useRouter').mockReturnValue(spyNextNavigation.useRouter) + + renderComponent(await PageInventaires({})) + const checkboxInventaire1 = screen.getByLabelText('mon premier inventaire') + fireEvent.click(checkboxInventaire1) + const checkboxInventaire2 = screen.getByLabelText('mon deuxième inventaire') + fireEvent.click(checkboxInventaire2) + fireEvent.click(checkboxInventaire2) + fireEvent.click(checkboxInventaire2) + + // WHEN + const boutonComparerDeuxInventaires = screen.getByRole('button', { name: 'Comparer deux inventaires' }) + fireEvent.click(boutonComparerDeuxInventaires) + + // THEN + expect(spyNextNavigation.useRouter.push).toHaveBeenCalledWith('http://localhost:3000/tableau-comparatif?inventaireReference=mon+premier+inventaire&inventaireCompare=mon+deuxi%C3%A8me+inventaire') + }) + it('quand je clique pour supprimer un inventaire alors l’inventaire est supprimé et ne s’affiche plus', async () => { // GIVEN jeSuisUnUtilisateur() vi.spyOn(repositoryInventaires, 'recupererLesInventairesRepository').mockResolvedValueOnce([inventaireModelFactory()]) - vi.spyOn(navigation, 'useRouter') - .mockReturnValueOnce(spyNextNavigation.useRouter) - .mockReturnValueOnce(spyNextNavigation.useRouter) + vi.spyOn(navigation, 'useRouter').mockReturnValue(spyNextNavigation.useRouter) vi.spyOn(repositoryInventaires, 'supprimerUnInventaireRepository').mockResolvedValueOnce(new Date()) renderComponent(await PageInventaires({})) @@ -241,13 +375,7 @@ describe('page inventaires', () => { // GIVEN jeSuisUnAdmin() - vi.spyOn(repositoryInventaires, 'recupererLesInventairesPaginesRepository').mockResolvedValueOnce([ - inventaireModelFactory({ - id: 1, - nomEtablissement: 'Hopital A$$00000001K', - nomInventaire: 'mon inventaire A', - }), - ]) + vi.spyOn(repositoryInventaires, 'recupererLesInventairesPaginesRepository').mockResolvedValueOnce([inventaireModelFactory()]) vi.spyOn(repositoryInventaires, 'recupererLeTotalInventairesRepository').mockResolvedValueOnce(2) // WHEN @@ -263,6 +391,22 @@ describe('page inventaires', () => { expect(lienDupliquer).not.toBeInTheDocument() }) + it('quand j’affiche la page alors je n’ai pas accès à la comparaison de deux inventaires', async () => { + // GIVEN + jeSuisUnAdmin() + + vi.spyOn(repositoryInventaires, 'recupererLesInventairesRepository').mockResolvedValueOnce([inventaireModelFactory()]) + + // WHEN + renderComponent(await PageInventaires({})) + + // THEN + const buttonComparerDeuxInventaires = screen.queryByRole('button', { name: 'Comparer deux inventaires' }) + expect(buttonComparerDeuxInventaires).not.toBeInTheDocument() + const checkboxInventaire1 = screen.queryByLabelText('mon super inventaire') + expect(checkboxInventaire1).not.toBeInTheDocument() + }) + it('quand j’affiche la page alors je télécharge l’export CSV', async () => { // GIVEN jeSuisUnAdmin() diff --git a/src/components/Inventaires/Inventaires.tsx b/src/components/Inventaires/Inventaires.tsx index a2c3c39..b6ae09a 100644 --- a/src/components/Inventaires/Inventaires.tsx +++ b/src/components/Inventaires/Inventaires.tsx @@ -1,20 +1,27 @@ import Link from 'next/link' -import React, { ReactElement } from 'react' +import React, { FormEvent, ReactElement } from 'react' import ActionSupprimer from './ActionSupprimer' import styles from './Inventaires.module.css' import { InventairePresenter } from '../../presenters/inventairesPresenter' -import { formaterLeNomEtablissement } from '../../presenters/sharedPresenter' +import { StatutsInventaire, formaterLeNomEtablissement } from '../../presenters/sharedPresenter' import Pagination from '../sharedComponents/Pagination/Pagination' type InventairesProps = Readonly<{ inventaires: ReadonlyArray isAdmin: boolean + mettreAJourNombreInventaireCoche: (event: FormEvent) => void pageCourante: number totalInventaires: number }> -export default function Inventaires({ inventaires, isAdmin, pageCourante, totalInventaires }: InventairesProps): ReactElement { +export default function Inventaires({ + inventaires, + isAdmin, + mettreAJourNombreInventaireCoche, + pageCourante, + totalInventaires, +}: InventairesProps): ReactElement { return ( <> @@ -46,6 +53,25 @@ export default function Inventaires({ inventaires, isAdmin, pageCourante, totalI return (
+ { + !isAdmin && inventaire.statut === StatutsInventaire.TRAITE && ( + <> + + + {'  '} + + ) + } {inventaire.nomInventaire} diff --git a/src/components/Inventaires/InventairesLayout.tsx b/src/components/Inventaires/InventairesLayout.tsx index 26b4daa..697457d 100644 --- a/src/components/Inventaires/InventairesLayout.tsx +++ b/src/components/Inventaires/InventairesLayout.tsx @@ -1,8 +1,12 @@ +'use client' + import Link from 'next/link' import { ReactElement } from 'react' import Inventaires from './Inventaires' +import styles from './Inventaires.module.css' import InventairesVide from './InventairesVide' +import { useInventaires } from './useInventaires' import { InventairesPresenter } from '../../presenters/inventairesPresenter' import InfoBulle from '../sharedComponents/Infobulle' @@ -11,6 +15,8 @@ type InventairesLayoutProps = Readonly<{ }> export default function InventairesLayout({ presenter }: InventairesLayoutProps): ReactElement { + const { comparerDeuxInventaires, isDisabled, mettreAJourNombreInventaireCoche } = useInventaires() + return ( <>
@@ -25,9 +31,17 @@ export default function InventairesLayout({ presenter }: InventairesLayoutProps)
{ !presenter.isAdmin ? ( -
+
+ Créer un inventaire @@ -58,6 +72,7 @@ export default function InventairesLayout({ presenter }: InventairesLayoutProps) diff --git a/src/components/Inventaires/useInventaires.ts b/src/components/Inventaires/useInventaires.ts new file mode 100644 index 0000000..6645c72 --- /dev/null +++ b/src/components/Inventaires/useInventaires.ts @@ -0,0 +1,39 @@ +import { useRouter } from 'next/navigation' +import { FormEvent, useState } from 'react' + +type UseInventaires = Readonly<{ + comparerDeuxInventaires: () => void + isDisabled: boolean + mettreAJourNombreInventaireCoche: (event: FormEvent) => void +}> + +export function useInventaires(): UseInventaires { + const router = useRouter() + const [inventairesCoches, setInventairesCoches] = useState>([]) + const isDisabled = inventairesCoches.length < 2 || inventairesCoches.length > 2 + + function mettreAJourNombreInventaireCoche(event: FormEvent) { + if (event.currentTarget.checked) { + setInventairesCoches([ + ...inventairesCoches, + event.currentTarget.value, + ]) + } else { + setInventairesCoches(inventairesCoches.filter((inventaire): boolean => inventaire !== event.currentTarget.value)) + } + } + + function comparerDeuxInventaires() { + const url = new URL('/tableau-comparatif', document.location.href) + url.searchParams.append('inventaireReference', inventairesCoches[0]) + url.searchParams.append('inventaireCompare', inventairesCoches[1]) + + router.push(url.toString()) + } + + return { + comparerDeuxInventaires, + isDisabled, + mettreAJourNombreInventaireCoche, + } +} diff --git a/src/components/ListeEquipements/ListeEquipements.test.tsx b/src/components/ListeEquipements/ListeEquipements.test.tsx index b81b762..29d6e55 100644 --- a/src/components/ListeEquipements/ListeEquipements.test.tsx +++ b/src/components/ListeEquipements/ListeEquipements.test.tsx @@ -13,8 +13,6 @@ describe('page liste d’équipements', () => { // GIVEN jeSuisUnUtilisateur() - vi.spyOn(repositoryModeles, 'recupererLesModelesRepository').mockResolvedValueOnce([modeleModelFactory()]) - const queryParams = { searchParams: { nomEtablissement: 'Hopital de Bordeaux$$00000001J', diff --git a/src/components/TableauComparatif/TableauComparatif.module.css b/src/components/TableauComparatif/TableauComparatif.module.css new file mode 100644 index 0000000..d28ba11 --- /dev/null +++ b/src/components/TableauComparatif/TableauComparatif.module.css @@ -0,0 +1,16 @@ +.indicateur { + background-color: #EFF4FD; + color: var(--black); +} + +.differencePositive { + color: #017803 +} + +.differenceNegative { + color: #D20050 +} + +.hauteur { + height: 14rem; +} diff --git a/src/components/TableauComparatif/TableauComparatif.test.tsx b/src/components/TableauComparatif/TableauComparatif.test.tsx new file mode 100644 index 0000000..7167471 --- /dev/null +++ b/src/components/TableauComparatif/TableauComparatif.test.tsx @@ -0,0 +1,109 @@ +import { screen } from '@testing-library/react' + +import PageTableauComparatif from '../../app/(connecte)/(utilisateur)/tableau-comparatif/page' +import * as repositoryIndicateurs from '../../repositories/indicateursRepository' +import * as repositoryModeles from '../../repositories/modelesRepository' +import { indicateurImpactEquipementModelFactory, jeSuisUnAdmin, jeSuisUnUtilisateur, modeleModelFactory, renderComponent } from '../../testShared' + +describe('page tableau comparatif', () => { + describe('en tant qu’utilisateur', () => { + it('quand j’affiche la page alors j’y ai accès', async () => { + // GIVEN + jeSuisUnUtilisateur() + + vi.spyOn(repositoryIndicateurs, 'recupererLesIndicateursImpactsEquipementsRepository') + .mockResolvedValueOnce([indicateurImpactEquipementModelFactory()]) + .mockResolvedValueOnce([indicateurImpactEquipementModelFactory()]) + vi.spyOn(repositoryModeles, 'recupererLesModelesRepository') + .mockResolvedValueOnce([modeleModelFactory()]) + .mockResolvedValueOnce([modeleModelFactory()]) + + // WHEN + renderComponent(await PageTableauComparatif(queryParams())) + + // THEN + const titre = screen.getByRole('heading', { name: 'Tableau comparatif' }) + expect(titre).toBeInTheDocument() + }) + + it('quand le nom d’établissement comparé n’existe pas ou plus alors je n’y ai pas accès', async () => { + // GIVEN + jeSuisUnUtilisateur() + + vi.spyOn(repositoryIndicateurs, 'recupererLesIndicateursImpactsEquipementsRepository').mockResolvedValueOnce([]) + + // WHEN + const page = async () => renderComponent(await PageTableauComparatif(queryParams())) + + // THEN + await expect(page).rejects.toThrow('NEXT_NOT_FOUND') + }) + + it('quand le nom d’établissement référencé n’existe pas ou plus alors je n’y ai pas accès', async () => { + // GIVEN + jeSuisUnUtilisateur() + + vi.spyOn(repositoryIndicateurs, 'recupererLesIndicateursImpactsEquipementsRepository') + .mockResolvedValueOnce([indicateurImpactEquipementModelFactory()]) + .mockResolvedValueOnce([]) + + // WHEN + const page = async () => renderComponent(await PageTableauComparatif(queryParams())) + + // THEN + await expect(page).rejects.toThrow('NEXT_NOT_FOUND') + }) + }) + + describe('en tant qu’admin', () => { + it('quand j’affiche la page alors je n’y ai pas accès', async () => { + // GIVEN + jeSuisUnAdmin() + + // WHEN + const page = async () => renderComponent(await PageTableauComparatif(queryParams())) + + // THEN + await expect(page).rejects.toThrow('NEXT_NOT_FOUND') + }) + }) + + it('quand il n’y a pas de nom d’inventaire de référence dans l’url alors je n’y ai pas accès', async () => { + // GIVEN + const queryParams = { + searchParams: { + inventaireCompare: 'Centre hospitalier', + }, + } + + // WHEN + const page = async () => renderComponent(await PageTableauComparatif(queryParams)) + + // THEN + await expect(page).rejects.toThrow('NEXT_NOT_FOUND') + }) + + it('quand il n’y a pas de nom d’inventaire comparé dans l’url alors je n’y ai pas accès', async () => { + // GIVEN + const queryParams = { + searchParams: { + inventaireReference: 'Centre hospitalier', + }, + } + + // WHEN + const page = async () => renderComponent(await PageTableauComparatif(queryParams)) + + // THEN + await expect(page).rejects.toThrow('NEXT_NOT_FOUND') + }) +}) + +function queryParams() { + return { + searchParams: { + inventaireCompare: 'Centre hospitalier', + inventaireReference: 'Secteur infirmier', + }, + } +} diff --git a/src/components/TableauComparatif/TableauComparatif.tsx b/src/components/TableauComparatif/TableauComparatif.tsx new file mode 100644 index 0000000..a5d8384 --- /dev/null +++ b/src/components/TableauComparatif/TableauComparatif.tsx @@ -0,0 +1,550 @@ +import Link from 'next/link' +import { ReactElement } from 'react' + +import styles from './TableauComparatif.module.css' +import { formaterDeuxChiffresApresLaVirgule } from '../../presenters/sharedPresenter' +import { TableauComparatifPresenter } from '../../presenters/tableauComparatifPresenter' +import InfoBulle from '../sharedComponents/Infobulle' + +type TableauComparatifProps = Readonly<{ + presenterCompare: TableauComparatifPresenter + presenterReference: TableauComparatifPresenter +}> + +export default function TableauComparatif({ + presenterCompare, + presenterReference, +}: TableauComparatifProps): ReactElement { + const differenceEmpreinteCarbone = + presenterCompare.indicateursImpactsEquipements.empreinteCarbone - presenterReference.indicateursImpactsEquipements.empreinteCarbone + const classNameDifferenceEmpreinteCarbone = differenceEmpreinteCarbone <= 0 ? styles.differencePositive : styles.differenceNegative + const signeDifferenceEmpreinteCarbone = differenceEmpreinteCarbone > 0 ? '+' : '' + + const differenceQuantiteEquipement = (presenterCompare.quantiteTotale - presenterReference.quantiteTotale) / presenterReference.quantiteTotale * 100 + const classNameDifferenceQuantiteEquipement = differenceQuantiteEquipement <= 0 ? styles.differencePositive : styles.differenceNegative + const signeDifferenceQuantiteEquipement = differenceQuantiteEquipement > 0 ? '+' : '' + + const differenceDureeDeVieMoyenne = presenterCompare.dureeDeVieTotale - presenterReference.dureeDeVieTotale + const classNameDifferenceDureeDeVieMoyenne = differenceDureeDeVieMoyenne <= 0 ? styles.differenceNegative : styles.differencePositive + const signeDifferenceDureeDeVieMoyenne = differenceDureeDeVieMoyenne > 0 ? '+' : '' + + const differenceTauxUtilisationTotale = presenterCompare.tauxUtilisationTotale - presenterReference.tauxUtilisationTotale + const classNameDifferenceTauxUtilisationTotale = differenceTauxUtilisationTotale <= 0 ? styles.differencePositive : styles.differenceNegative + const signeDifferenceTauxUtilisationTotale = differenceTauxUtilisationTotale > 0 ? '+' : '' + + const differenceFabrication = presenterCompare.indicateursImpactsEquipements.fabrication - presenterReference.indicateursImpactsEquipements.fabrication + const classNameDifferenceFabrication = differenceFabrication <= 0 ? styles.differencePositive : styles.differenceNegative + const signeDifferenceFabrication = differenceFabrication > 0 ? '↗ +' : '↘ ' + + const differenceDistribution = presenterCompare.indicateursImpactsEquipements.distribution - presenterReference.indicateursImpactsEquipements.distribution + const classNameDifferenceDistribution = differenceDistribution <= 0 ? styles.differencePositive : styles.differenceNegative + const signeDifferenceDistribution = differenceDistribution > 0 ? '↗ +' : '↘ ' + + const differenceUtilisation = presenterCompare.indicateursImpactsEquipements.utilisation - presenterReference.indicateursImpactsEquipements.utilisation + const classNameDifferenceUtilisation = differenceUtilisation <= 0 ? styles.differencePositive : styles.differenceNegative + const signeDifferenceUtilisation = differenceUtilisation > 0 ? '↗ +' : '↘ ' + + const differenceFinDeVie = presenterCompare.indicateursImpactsEquipements.finDeVie - presenterReference.indicateursImpactsEquipements.finDeVie + const classNameDifferenceFinDeVie = differenceFinDeVie <= 0 ? styles.differencePositive : styles.differenceNegative + const signeDifferenceFinDeVie = differenceFinDeVie > 0 ? '↗ +' : '↘ ' + + return ( + <> +

+ Tableau comparatif +

+

+ En comparant ces deux inventaires, aucune copie du tableau ne sera créée. Une fois le tableau comparatif fermé, + si vous souhaitez revenir à la comparaison il faudra le recréer. +

+
+

+ Empreinte carbone par an + +

+
+
+
+
+ + + + + + + + + + + + + + + +
+
+

+ Inventaire de référence : + {' '} + {presenterReference.nomInventaire} +

+

+ {presenterReference.nomEtablissement} + {' '} + - + {' '} + {presenterReference.dateInventaire} +

+ + + Voir l’inventaire + +
+
+
+
+
+
+ {formaterDeuxChiffresApresLaVirgule(presenterReference.indicateursImpactsEquipements.empreinteCarbone)} +
+
+ + tCO2 eq + +
+
+
+
+
+ Soit autant d’émissions que : +
+
+ 🚗 + {' '} + + {presenterReference.indicateursImpactsEquipements.kilometresEnVoiture} + + {' '} + kilomètres en voiture +
+
+
+

+ Détail de l’empreinte carbone de l’inventaire +

+
+
+ Fabrication +
+
+ {formaterDeuxChiffresApresLaVirgule(presenterReference.indicateursImpactsEquipements.fabrication)} + {' '} + tCO2 eq +
+
+
+
+
+ Distribution +
+
+ {formaterDeuxChiffresApresLaVirgule(presenterReference.indicateursImpactsEquipements.distribution)} + {' '} + tCO2 eq +
+
+
+
+
+ Utilisation +
+
+ {formaterDeuxChiffresApresLaVirgule(presenterReference.indicateursImpactsEquipements.utilisation)} + {' '} + tCO2 eq +
+
+
+
+
+ Fin de vie +
+
+ {formaterDeuxChiffresApresLaVirgule(presenterReference.indicateursImpactsEquipements.finDeVie)} + {' '} + tCO2 eq +
+
+
+
+

+ Quantité d’équipements +

+
+
+
+ {presenterReference.quantiteTotale} +
+
+ équipements +
+
+
+
+
+

+ Durée de vie moyenne des équipements +

+
+
+
+ {formaterDeuxChiffresApresLaVirgule(presenterReference.dureeDeVieTotale)} +
+
+ ans +
+
+
+
+
+

+ Temps d’utilisation moyen par jour +

+
+
+
+ {formaterDeuxChiffresApresLaVirgule(presenterReference.tauxUtilisationTotale)} +
+
+ heures +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+

+ Inventaire comparé : + {' '} + {presenterCompare.nomInventaire} +

+

+ {presenterCompare.nomEtablissement} + {' '} + - + {' '} + {presenterCompare.dateInventaire} +

+ + + Voir l’inventaire + +
+
+
+
+
+
+ {formaterDeuxChiffresApresLaVirgule(presenterCompare.indicateursImpactsEquipements.empreinteCarbone)} +
+
+ + tCO2 eq + +
+
+
+

+ Soit : +

+

+ {signeDifferenceEmpreinteCarbone} + {formaterDeuxChiffresApresLaVirgule(differenceEmpreinteCarbone)} +

+
+ tCO2 eq par rapport à l’inventaire de référence +
+
+
+
+
+ Soit autant d’émissions que : +
+
+ 🚗 + {' '} + + {presenterCompare.indicateursImpactsEquipements.kilometresEnVoiture} + + {' '} + kilomètres en voiture +
+
+
+

+ Détail de l’empreinte carbone de l’inventaire +

+
+
+ Fabrication +
+
+ + {signeDifferenceFabrication} + {formaterDeuxChiffresApresLaVirgule(differenceFabrication)} + + {' – '} + {formaterDeuxChiffresApresLaVirgule(presenterCompare.indicateursImpactsEquipements.fabrication)} + {' '} + tCO2 eq +
+
+
+
+
+ Distribution +
+
+ + {signeDifferenceDistribution} + {formaterDeuxChiffresApresLaVirgule(differenceDistribution)} + + {' – '} + {formaterDeuxChiffresApresLaVirgule(presenterCompare.indicateursImpactsEquipements.distribution)} + {' '} + tCO2 eq +
+
+
+
+
+ Utilisation +
+
+ + {signeDifferenceUtilisation} + {formaterDeuxChiffresApresLaVirgule(differenceUtilisation)} + + {' – '} + {formaterDeuxChiffresApresLaVirgule(presenterCompare.indicateursImpactsEquipements.utilisation)} + {' '} + tCO2 eq +
+
+
+
+
+ Fin de vie +
+
+ + {signeDifferenceFinDeVie} + {formaterDeuxChiffresApresLaVirgule(differenceFinDeVie)} + + {' – '} + {formaterDeuxChiffresApresLaVirgule(presenterCompare.indicateursImpactsEquipements.finDeVie)} + {' '} + tCO2 eq +
+
+
+
+

+ Quantité d’équipements +

+
+
+
+ {presenterCompare.quantiteTotale} +
+
+ équipements +
+
+
+

+ Soit : +

+

+ {signeDifferenceQuantiteEquipement} + {formaterDeuxChiffresApresLaVirgule(differenceQuantiteEquipement)} + {' '} + % +

+
+ x% d’équipements par rapport à l’inventaire de référence +
+
+
+
+
+

+ Durée de vie moyenne des équipements +

+
+
+
+ {formaterDeuxChiffresApresLaVirgule(presenterCompare.dureeDeVieTotale)} +
+
+ ans +
+
+
+

+ Soit : +

+

+ {signeDifferenceDureeDeVieMoyenne} + {formaterDeuxChiffresApresLaVirgule(differenceDureeDeVieMoyenne)} +

+
+ années par rapport à l’inventaire de référence +
+
+
+
+
+

+ Temps d’utilisation moyen par jour +

+
+
+
+ {formaterDeuxChiffresApresLaVirgule(presenterCompare.tauxUtilisationTotale)} +
+
+ heures +
+
+
+

+ Soit : +

+

+ {signeDifferenceTauxUtilisationTotale} + {formaterDeuxChiffresApresLaVirgule(differenceTauxUtilisationTotale)} +

+
+ heures par rapport à l’inventaire de référence +
+
+
+
+
+
+
+ + ) +} diff --git a/src/presenters/indicateursClesPresenter.ts b/src/presenters/indicateursClesPresenter.ts index 0a47295..f632971 100644 --- a/src/presenters/indicateursClesPresenter.ts +++ b/src/presenters/indicateursClesPresenter.ts @@ -1,23 +1,10 @@ import { indicateurImpactEquipementModel } from '@prisma/client' -import { formaterDeuxChiffresApresLaVirgule, formaterLaDateEnFrancais } from './sharedPresenter' +import { EtapesAcv, IndicateursImpactsEquipements, formaterLaDateEnFrancais, indicateursImpactsEquipementsPresenter } from './sharedPresenter' import { ProfilAtih } from '../authentification' import { IndicateurImpactEquipementSommeModel } from '../repositories/indicateursRepository' import { ReferentielTypeEquipementModel } from '../repositories/typesEquipementsRepository' -type IndicateursImpactsEquipements = Readonly<{ - acidification: string - distribution: string - emissionsDeParticulesFines: string - empreinteCarbone: string - epuisementDesRessources: string - fabrication: string - finDeVie: string - radiationIonisantes: string - utilisation: string - kilometresEnVoiture: string -}> - export type IndicateurImpactEquipementSomme = Readonly<{ etapeAcv: `${EtapesAcv}` impact: number @@ -33,21 +20,6 @@ export type IndicateursClesPresenter = Readonly<{ referentielsTypesEquipements: ReadonlyArray }> -export enum EtapesAcv { - fabrication = 'FABRICATION', - distribution = 'DISTRIBUTION', - utilisation = 'UTILISATION', - finDeVie = 'FIN_DE_VIE', -} - -enum Criteres { - radiationIonisantes = 'Ionising radiation', - epuisementDesRessources = 'Resource use (minerals and metals)', - emissionsDeParticulesFines = 'Particulate matter and respiratory inorganics', - acidification = 'Acidification', - empreinteCarbone = 'Climate change', -} - export function indicateursClesPresenter( referentielsTypesEquipementsModel: ReadonlyArray, indicateursImpactsEquipementsSommesModel: ReadonlyArray, @@ -71,61 +43,6 @@ export function indicateursClesPresenter( } } -function indicateursImpactsEquipementsPresenter( - indicateursImpactsEquipementsModel: ReadonlyArray -): IndicateursImpactsEquipements { - let radiationIonisantes = 0 - let epuisementDesRessources = 0 - let emissionsDeParticulesFines = 0 - let acidification = 0 - let empreinteCarbone = 0 - let fabrication = 0 - let distribution = 0 - let utilisation = 0 - let finDeVie = 0 - const kilometresEquivalent1TonneCO2 = 5181 - - for (const indicateurImpactEquipementModel of indicateursImpactsEquipementsModel) { - const critere = indicateurImpactEquipementModel.critere as Criteres - const etapeacv = indicateurImpactEquipementModel.etapeAcv as EtapesAcv - - if (critere === Criteres.radiationIonisantes) { - radiationIonisantes += indicateurImpactEquipementModel.impactUnitaire - } else if (critere === Criteres.epuisementDesRessources) { - epuisementDesRessources += indicateurImpactEquipementModel.impactUnitaire - } else if (critere === Criteres.emissionsDeParticulesFines) { - emissionsDeParticulesFines += indicateurImpactEquipementModel.impactUnitaire - } else if (critere === Criteres.acidification) { - acidification += indicateurImpactEquipementModel.impactUnitaire - } else { - empreinteCarbone += indicateurImpactEquipementModel.impactUnitaire / 1000 - - if (etapeacv === EtapesAcv.fabrication) { - fabrication += indicateurImpactEquipementModel.impactUnitaire / 1000 - } else if (etapeacv === EtapesAcv.distribution) { - distribution += indicateurImpactEquipementModel.impactUnitaire / 1000 - } else if (etapeacv === EtapesAcv.utilisation) { - utilisation += indicateurImpactEquipementModel.impactUnitaire / 1000 - } else { - finDeVie += indicateurImpactEquipementModel.impactUnitaire / 1000 - } - } - } - - return { - acidification: formaterDeuxChiffresApresLaVirgule(acidification), - distribution: formaterDeuxChiffresApresLaVirgule(distribution), - emissionsDeParticulesFines: formaterDeuxChiffresApresLaVirgule(emissionsDeParticulesFines), - empreinteCarbone: formaterDeuxChiffresApresLaVirgule(empreinteCarbone), - epuisementDesRessources: formaterDeuxChiffresApresLaVirgule(epuisementDesRessources), - fabrication: formaterDeuxChiffresApresLaVirgule(fabrication), - finDeVie: formaterDeuxChiffresApresLaVirgule(finDeVie), - kilometresEnVoiture: Math.round(empreinteCarbone * kilometresEquivalent1TonneCO2).toLocaleString(), - radiationIonisantes: formaterDeuxChiffresApresLaVirgule(radiationIonisantes), - utilisation: formaterDeuxChiffresApresLaVirgule(utilisation), - } -} - function indicateursImpactsEquipementsSommesPresenter( indicateursImpactsEquipementsSommesModel: ReadonlyArray, referentielsEquipements: ReadonlyArray diff --git a/src/presenters/sharedPresenter.ts b/src/presenters/sharedPresenter.ts index 5430450..5ca6db5 100644 --- a/src/presenters/sharedPresenter.ts +++ b/src/presenters/sharedPresenter.ts @@ -1,10 +1,40 @@ +import { indicateurImpactEquipementModel } from '@prisma/client' + import { separator } from '../configuration' +export type IndicateursImpactsEquipements = Readonly<{ + acidification: number + distribution: number + emissionsDeParticulesFines: number + empreinteCarbone: number + epuisementDesRessources: number + fabrication: number + finDeVie: number + kilometresEnVoiture: string + radiationIonisantes: number + utilisation: number +}> + export enum StatutsInventaire { EN_ATTENTE = 'NON CALCULÉ', TRAITE = 'CALCULÉ', } +export enum Criteres { + radiationIonisantes = 'Ionising radiation', + epuisementDesRessources = 'Resource use (minerals and metals)', + emissionsDeParticulesFines = 'Particulate matter and respiratory inorganics', + acidification = 'Acidification', + empreinteCarbone = 'Climate change', +} + +export enum EtapesAcv { + fabrication = 'FABRICATION', + distribution = 'DISTRIBUTION', + utilisation = 'UTILISATION', + finDeVie = 'FIN_DE_VIE', +} + export function convertirLeTauxUtilisationEnHeureUtilisation(tauxUtilisation: number): number { return Math.round(tauxUtilisation * 24) } @@ -26,7 +56,7 @@ export function formaterEnIdentifiant(text: string): string { } export function formaterDeuxChiffresApresLaVirgule(chiffre: number): string { - return Number(chiffre.toFixed(2)).toLocaleString() + return Number(chiffre.toFixed(2)).toLocaleString('fr-FR') } export function genererUnIdentifiantUnique(): string { @@ -36,3 +66,58 @@ export function genererUnIdentifiantUnique(): string { export function formaterLeNomEtablissement(nomEtablissement: string): string { return nomEtablissement.split(separator)[0] } + +export function indicateursImpactsEquipementsPresenter( + indicateursImpactsEquipementsModel: ReadonlyArray +): IndicateursImpactsEquipements { + let radiationIonisantes = 0 + let epuisementDesRessources = 0 + let emissionsDeParticulesFines = 0 + let acidification = 0 + let empreinteCarbone = 0 + let fabrication = 0 + let distribution = 0 + let utilisation = 0 + let finDeVie = 0 + const kilometresEquivalent1TonneCO2 = 5181 + + for (const indicateurImpactEquipementModel of indicateursImpactsEquipementsModel) { + const critere = indicateurImpactEquipementModel.critere as Criteres + const etapeacv = indicateurImpactEquipementModel.etapeAcv as EtapesAcv + + if (critere === Criteres.radiationIonisantes) { + radiationIonisantes += indicateurImpactEquipementModel.impactUnitaire + } else if (critere === Criteres.epuisementDesRessources) { + epuisementDesRessources += indicateurImpactEquipementModel.impactUnitaire + } else if (critere === Criteres.emissionsDeParticulesFines) { + emissionsDeParticulesFines += indicateurImpactEquipementModel.impactUnitaire + } else if (critere === Criteres.acidification) { + acidification += indicateurImpactEquipementModel.impactUnitaire + } else { + empreinteCarbone += indicateurImpactEquipementModel.impactUnitaire / 1000 + + if (etapeacv === EtapesAcv.fabrication) { + fabrication += indicateurImpactEquipementModel.impactUnitaire / 1000 + } else if (etapeacv === EtapesAcv.distribution) { + distribution += indicateurImpactEquipementModel.impactUnitaire / 1000 + } else if (etapeacv === EtapesAcv.utilisation) { + utilisation += indicateurImpactEquipementModel.impactUnitaire / 1000 + } else { + finDeVie += indicateurImpactEquipementModel.impactUnitaire / 1000 + } + } + } + + return { + acidification, + distribution, + emissionsDeParticulesFines, + empreinteCarbone, + epuisementDesRessources, + fabrication, + finDeVie, + kilometresEnVoiture: Math.round(empreinteCarbone * kilometresEquivalent1TonneCO2).toLocaleString('fr-FR'), + radiationIonisantes, + utilisation, + } +} diff --git a/src/presenters/tableauComparatifPresenter.ts b/src/presenters/tableauComparatifPresenter.ts new file mode 100644 index 0000000..fd3a426 --- /dev/null +++ b/src/presenters/tableauComparatifPresenter.ts @@ -0,0 +1,50 @@ +import { indicateurImpactEquipementModel, modeleModel } from '@prisma/client' + +import { IndicateursImpactsEquipements, calculerLaDureeDeVie, convertirLeTauxUtilisationEnHeureUtilisation, formaterLaDateEnFrancais, formaterLeNomEtablissement, indicateursImpactsEquipementsPresenter } from './sharedPresenter' + +export type TableauComparatifPresenter = Readonly<{ + dateInventaire: string + dureeDeVieTotale: number + indicateursImpactsEquipements: IndicateursImpactsEquipements + lienIndicateursCles: string + nomEtablissement: string + nomInventaire: string + quantiteTotale: number + tauxUtilisationTotale: number +}> + +export function tableauComparatifPresenter( + indicateursImpactsEquipementsModel: ReadonlyArray, + modelesCompareModel: ReadonlyArray, + nomEtablissement: string, + nomInventaire: string +): TableauComparatifPresenter { + const dateInventaire = formaterLaDateEnFrancais(indicateursImpactsEquipementsModel[0].dateInventaire) + + const indicateursImpactsEquipements = indicateursImpactsEquipementsPresenter(indicateursImpactsEquipementsModel) + + const quantiteTotale = modelesCompareModel.reduce((quantiteAccumulee, modeleCompareModel): number => { + return quantiteAccumulee + modeleCompareModel.quantite + }, 0) + + const dureeDeVieTotale = modelesCompareModel.reduce((dureeDeVieAccumulee, modeleCompareModel): number => { + return dureeDeVieAccumulee + (modeleCompareModel.quantite * calculerLaDureeDeVie(modeleCompareModel.dateAchat) / quantiteTotale) + }, 0) + + const tauxUtilisationTotale = modelesCompareModel.reduce((tauxUtilisationAccumulee, modeleCompareModel): number => { + return tauxUtilisationAccumulee + + (modeleCompareModel.quantite * convertirLeTauxUtilisationEnHeureUtilisation(modeleCompareModel.tauxUtilisation) / quantiteTotale) + }, 0) + + return { + dateInventaire, + dureeDeVieTotale, + indicateursImpactsEquipements, + lienIndicateursCles: encodeURI(`/indicateurs-cles?nomEtablissement=${nomEtablissement}&nomInventaire=${nomInventaire}`), + nomEtablissement: formaterLeNomEtablissement(nomEtablissement), + nomInventaire, + quantiteTotale, + tauxUtilisationTotale, + } +} + diff --git a/src/testShared.ts b/src/testShared.ts index 9ba3d94..218f6b9 100644 --- a/src/testShared.ts +++ b/src/testShared.ts @@ -6,7 +6,7 @@ import { ReactElement } from 'react' import * as authentification from './authentification' import { separator } from './configuration' -import { EtapesAcv } from './presenters/indicateursClesPresenter' +import { EtapesAcv } from './presenters/sharedPresenter' import { IndicateurImpactEquipementSommeModel } from './repositories/indicateursRepository' import { ReferentielTypeEquipementModel } from './repositories/typesEquipementsRepository'