From 74c56f3acc8f99e5ab768b977207d476c39daf3e Mon Sep 17 00:00:00 2001 From: Fred <98240+farnoux@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:22:34 +0100 Subject: [PATCH] Liste les actions et statuts des actions depuis le backend et le snapshot courant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stoppe l'utilisation de la vue PG `action_statuts` pour ces cas d'actions liées --- .../actionStatut/ActionStatutDetaillee.tsx | 2 +- .../HistoriqueItemActionStatut.tsx | 2 +- .../Indicateurs/detail/ActionsLiees.tsx | 20 +-- .../ExportPdf/ExportFicheActionButton.tsx | 16 +-- .../ExportPdf/FicheActionPdf/ActionsLiees.tsx | 13 +- .../FicheActionPdf/FicheActionPdf.tsx | 5 +- .../ActionsLiees/ActionsLieesListe.tsx | 16 ++- .../ActionsLiees/ActionsLieesTab.tsx | 2 +- .../data/options/useActionListe.ts | 46 ------ .../referentiels/DetailTaches/CellStatut.tsx | 4 +- .../DetailTaches/FiltreStatut.tsx | 4 +- .../referentiels/DetailTaches/useTableData.ts | 2 +- .../action-statut.badge.stories.tsx | 0 .../action-statut.badge.tsx | 0 .../action-statut.select.stories.tsx | 0 .../action-statut.select.tsx | 0 .../action-statut}/use-action-statut.ts | 8 +- .../actions/action.linked-card.tsx | 17 +-- .../actions/sub-action-statut.dropdown.tsx | 23 +-- .../sub-action.detail/ScoreAutoModal.tsx | 2 +- .../actions/sub-action/sub-action.card.tsx | 2 +- .../referentiels/actions/use-list-actions.ts | 35 +++++ .../src/referentiels/utils.ts | 43 ------ .../ActionsReferentielsDropdown.tsx | 14 +- .../useActionsReferentielsListe.ts | 24 ---- .../components/Badge/BadgeStatutAction.tsx | 2 +- .../referentiels-scoring.controller.ts | 2 +- .../referentiels-scoring.service.spec.ts | 2 +- .../referentiels-scoring.service.ts | 22 ++- .../export-referentiel-score.service.spec.ts | 2 +- .../export-referentiel-score.service.ts | 2 +- .../list-action-definitions.service.ts | 131 ++++++++++++++++++ .../list-actions.router.e2e-spec.ts | 118 ++++++++++++++++ .../list-actions/list-actions.router.ts | 81 +++++++++++ .../models/referentiel-id.enum.ts | 15 +- .../src/referentiels/referentiels.module.ts | 28 ++-- .../src/referentiels/referentiels.router.ts | 3 + .../src/referentiels/referentiels.utils.ts | 84 ++++++++++- .../referentiels-scoring-snapshots.service.ts | 18 +-- .../snapshots/score-snaphots.router.ts | 4 +- .../referentiels/snapshots/snapshots.utils.ts | 58 ++++++++ 41 files changed, 637 insertions(+), 235 deletions(-) delete mode 100644 app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/data/options/useActionListe.ts rename app.territoiresentransitions.react/src/referentiels/actions/{ => action-statut}/action-statut.badge.stories.tsx (100%) rename app.territoiresentransitions.react/src/referentiels/actions/{ => action-statut}/action-statut.badge.tsx (100%) rename app.territoiresentransitions.react/src/referentiels/actions/{ => action-statut}/action-statut.select.stories.tsx (100%) rename app.territoiresentransitions.react/src/referentiels/actions/{ => action-statut}/action-statut.select.tsx (100%) rename app.territoiresentransitions.react/src/referentiels/{ => actions/action-statut}/use-action-statut.ts (94%) create mode 100644 app.territoiresentransitions.react/src/referentiels/actions/use-list-actions.ts delete mode 100644 app.territoiresentransitions.react/src/ui/dropdownLists/ActionsReferentielsDropdown/useActionsReferentielsListe.ts create mode 100644 backend/src/referentiels/list-action-definitions/list-action-definitions.service.ts create mode 100644 backend/src/referentiels/list-actions/list-actions.router.e2e-spec.ts create mode 100644 backend/src/referentiels/list-actions/list-actions.router.ts create mode 100644 backend/src/referentiels/snapshots/snapshots.utils.ts diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/Historique/actionStatut/ActionStatutDetaillee.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/Historique/actionStatut/ActionStatutDetaillee.tsx index 70fa72e2ad..0aed441312 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/Historique/actionStatut/ActionStatutDetaillee.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/Historique/actionStatut/ActionStatutDetaillee.tsx @@ -1,4 +1,4 @@ -import ActionStatutBadge from '@/app/referentiels/actions/action-statut.badge'; +import ActionStatutBadge from '@/app/referentiels/actions/action-statut/action-statut.badge'; export const PrecedenteActionStatutDetaille = ({ avancementDetaille, diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/Historique/actionStatut/HistoriqueItemActionStatut.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/Historique/actionStatut/HistoriqueItemActionStatut.tsx index cb20275bdf..ef71d08129 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/Historique/actionStatut/HistoriqueItemActionStatut.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/Historique/actionStatut/HistoriqueItemActionStatut.tsx @@ -7,7 +7,7 @@ import { NouvelleActionStatutDetaille, PrecedenteActionStatutDetaille, } from '@/app/app/pages/collectivite/Historique/actionStatut/ActionStatutDetaillee'; -import ActionStatutBadge from '@/app/referentiels/actions/action-statut.badge'; +import ActionStatutBadge from '@/app/referentiels/actions/action-statut/action-statut.badge'; import { THistoriqueItemProps } from '../types'; import { getItemActionProps } from './getItemActionProps'; diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/Indicateurs/detail/ActionsLiees.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/Indicateurs/detail/ActionsLiees.tsx index 6bc7cddf31..fd9980bb98 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/Indicateurs/detail/ActionsLiees.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/Indicateurs/detail/ActionsLiees.tsx @@ -9,13 +9,17 @@ type Props = { const ActionsLiees = ({ actionsIds }: Props) => { const isEmpty = actionsIds.length === 0; - return isEmpty ? ( - } - title="Aucune action des référentiels n'est liée !" - size="xs" - /> - ) : ( + if (isEmpty) { + return ( + } + title="Aucune action des référentiels n'est liée !" + size="xs" + /> + ); + } + + return (
@@ -23,7 +27,7 @@ const ActionsLiees = ({ actionsIds }: Props) => {
diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/ExportFicheActionButton.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/ExportFicheActionButton.tsx index 7472cf605a..9f9dd6fe5a 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/ExportFicheActionButton.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/ExportFicheActionButton.tsx @@ -3,12 +3,12 @@ import { useGetEtapes } from '@/app/app/pages/collectivite/PlansActions/FicheAct import ExportPDFButton from '@/app/ui/export-pdf/ExportPDFButton'; import { createElement, useEffect, useState } from 'react'; import { useIndicateurDefinitions } from '../../Indicateurs/Indicateur/useIndicateurDefinition'; -import { useActionListe } from '../FicheAction/data/options/useActionListe'; import { useAnnexesFicheActionInfos } from '../FicheAction/data/useAnnexesFicheActionInfos'; import { useFicheActionNotesSuivi } from '../FicheAction/data/useFicheActionNotesSuivi'; import { useFichesActionLiees } from '../FicheAction/data/useFichesActionLiees'; import { useFicheActionChemins } from '../PlanAction/data/usePlanActionChemin'; import FicheActionPdf from './FicheActionPdf/FicheActionPdf'; +import { useListActionsWithStatuts } from '@/app/referentiels/actions/use-list-actions'; type FicheActionPdfContentProps = { fiche: FicheAction; @@ -29,8 +29,10 @@ export const FicheActionPdfContent = ({ const { data: fichesLiees, isLoading: isLoadingFichesLiees } = useFichesActionLiees(fiche.id); - const { data: actionListe, isLoading: isLoadignActionsListe } = - useActionListe(); + const { data: actionsLiees, isLoading: isLoadignActionsListe } = + useListActionsWithStatuts({ + actionIds: fiche?.actions?.map((action) => action.id) ?? [], + }); const { data: annexes, isLoading: isLoadingAnnexes } = useAnnexesFicheActionInfos(fiche.id); @@ -53,12 +55,6 @@ export const FicheActionPdfContent = ({ useEffect(() => { if (!isLoading) { - const { actions } = fiche; - const actionsIds = (actions ?? []).map((action) => action.id); - const actionsLiees = (actionListe ?? []).filter((action) => - actionsIds.some((id) => id === action.action_id) - ); - generateContent( createElement(FicheActionPdf, { fiche, @@ -68,7 +64,7 @@ export const FicheActionPdfContent = ({ indicateursListe, etapes, fichesLiees, - actionsLiees, + actionsLiees: actionsLiees ?? [], annexes, notesSuivi, }) diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/FicheActionPdf/ActionsLiees.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/FicheActionPdf/ActionsLiees.tsx index 38c7d46cc8..8f9f678750 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/FicheActionPdf/ActionsLiees.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/FicheActionPdf/ActionsLiees.tsx @@ -1,6 +1,5 @@ import { referentielToName } from '@/app/app/labels'; -import { getActionStatut } from '@/app/referentiels/utils'; -import { TActionStatutsRow } from '@/app/types/alias'; +import { ActionWithStatut } from '@/app/referentiels/actions/use-list-actions'; import { BadgeStatutAction, Card, @@ -8,20 +7,18 @@ import { Stack, Title, } from '@/app/ui/export-pdf/components'; -import { objectToCamel } from 'ts-case-convert'; type ActionLieeCardProps = { - action: TActionStatutsRow; + action: ActionWithStatut; }; const ActionLieeCard = ({ action }: ActionLieeCardProps) => { const { identifiant, nom, referentiel } = action; - const statut = getActionStatut(objectToCamel(action)); return ( {/* Avancement */} - + {/* Référentiel associé */} @@ -39,7 +36,7 @@ const ActionLieeCard = ({ action }: ActionLieeCardProps) => { }; type ActionsLieesProps = { - actionsLiees: TActionStatutsRow[]; + actionsLiees: Action[]; }; const ActionsLiees = ({ actionsLiees }: ActionsLieesProps) => { @@ -53,7 +50,7 @@ const ActionsLiees = ({ actionsLiees }: ActionsLieesProps) => { {actionsLiees.length > 0 && ( {actionsLiees.map((action) => ( - + ))} )} diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/FicheActionPdf/FicheActionPdf.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/FicheActionPdf/FicheActionPdf.tsx index b5b9b6fdf5..b6f88d815f 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/FicheActionPdf/FicheActionPdf.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/ExportPdf/FicheActionPdf/FicheActionPdf.tsx @@ -1,5 +1,5 @@ import { FicheAction, FicheActionNote, FicheResume } from '@/api/plan-actions'; -import { TActionStatutsRow, TAxeRow } from '@/app/types/alias'; +import { TAxeRow } from '@/app/types/alias'; import { Divider, Stack, Title } from '@/app/ui/export-pdf/components'; import { AnnexeInfo } from '../../FicheAction/data/useAnnexesFicheActionInfos'; @@ -19,6 +19,7 @@ import Notes from './Notes'; import NotesDeSuivi from './NotesDeSuivi'; import Pilotes from './Pilotes'; import Planning from './Planning'; +import { ActionWithStatut } from '@/app/referentiels/actions/use-list-actions'; export type FicheActionPdfProps = { fiche: FicheAction; @@ -29,7 +30,7 @@ export type FicheActionPdfExtendedProps = FicheActionPdfProps & { indicateursListe: TIndicateurDefinition[] | undefined | null; etapes?: RouterOutput['plans']['fiches']['etapes']['list']; fichesLiees: FicheResume[]; - actionsLiees: TActionStatutsRow[]; + actionsLiees: ActionWithStatut[]; annexes: AnnexeInfo[] | undefined; notesSuivi: FicheActionNote[] | undefined; }; diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/ActionsLiees/ActionsLieesListe.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/ActionsLiees/ActionsLieesListe.tsx index deca4cebec..ce0cfa1889 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/ActionsLiees/ActionsLieesListe.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/ActionsLiees/ActionsLieesListe.tsx @@ -1,12 +1,12 @@ +import ActionLinkedCard from '@/app/referentiels/actions/action.linked-card'; +import { useListActionsWithStatuts } from '@/app/referentiels/actions/use-list-actions'; import SpinnerLoader from '@/app/ui/shared/SpinnerLoader'; import classNames from 'classnames'; import { useEffect } from 'react'; -import ActionLinkedCard from '../../../../../../referentiels/actions/action.linked-card'; -import { useActionListe } from '../data/options/useActionListe'; type ActionsLieesListeProps = { isReadonly?: boolean; - actionsIds: string[]; + actionIds: string[]; className?: string; onLoad?: (isLoading: boolean) => void; onUnlink?: (actionId: string) => void; @@ -14,12 +14,14 @@ type ActionsLieesListeProps = { const ActionsLieesListe = ({ isReadonly, - actionsIds, + actionIds, className, onLoad, onUnlink, }: ActionsLieesListeProps) => { - const { data: actionsLiees, isLoading } = useActionListe(actionsIds); + const { data: actionsLiees, isLoading } = useListActionsWithStatuts({ + actionIds, + }); useEffect(() => onLoad?.(isLoading), [isLoading]); @@ -40,10 +42,10 @@ const ActionsLieesListe = ({ > {actionsLiees.map((action) => ( onUnlink(action.action_id) : undefined} + onUnlink={onUnlink ? () => onUnlink(action.actionId) : undefined} openInNewTab /> ))} diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/ActionsLiees/ActionsLieesTab.tsx b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/ActionsLiees/ActionsLieesTab.tsx index 1eaca43c47..40db8e9dc5 100644 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/ActionsLiees/ActionsLieesTab.tsx +++ b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/ActionsLiees/ActionsLieesTab.tsx @@ -67,7 +67,7 @@ const ActionsLieesTab = ({ {/* Liste des actions des référentiels liées */} action.id)} + actionIds={actions?.map((action) => action.id)} className="sm:grid-cols-2 md:grid-cols-3" onLoad={setIsLoading} onUnlink={(actionsLieeId) => diff --git a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/data/options/useActionListe.ts b/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/data/options/useActionListe.ts deleted file mode 100644 index 1db5300320..0000000000 --- a/app.territoiresentransitions.react/src/app/pages/collectivite/PlansActions/FicheAction/data/options/useActionListe.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useQuery } from 'react-query'; - -import { DBClient } from '@/api'; -import { DISABLE_AUTO_REFETCH } from '@/api/utils/react-query/query-options'; -import { useSupabase } from '@/api/utils/supabase/use-supabase'; -import { useCollectiviteId } from '@/app/core-logic/hooks/params'; -import { TActionStatutsRow } from '@/app/types/alias'; - -const fetchActionListe = async ( - supabase: DBClient, - collectivite_id: number, - actionsIds?: string[] -) => { - const query = supabase - .from('action_statuts') - .select( - 'action_id, referentiel, nom, identifiant, avancement, avancement_descendants, desactive, concerne' - ) - .eq('collectivite_id', collectivite_id) - .in('type', ['action', 'sous-action']); - - if (actionsIds?.length) { - query.in('action_id', actionsIds); - } - - query.order('action_id', { ascending: true }); - - const { error, data } = await query; - - if (error) { - throw new Error(error.message); - } - - return data as TActionStatutsRow[]; -}; - -export const useActionListe = (actionsIds?: string[]) => { - const collectivite_id = useCollectiviteId(); - const supabase = useSupabase(); - - return useQuery( - ['actions_referentiels', collectivite_id, actionsIds], - () => fetchActionListe(supabase, collectivite_id!, actionsIds), - DISABLE_AUTO_REFETCH - ); -}; diff --git a/app.territoiresentransitions.react/src/referentiels/DetailTaches/CellStatut.tsx b/app.territoiresentransitions.react/src/referentiels/DetailTaches/CellStatut.tsx index 74e148cc66..192b42d609 100644 --- a/app.territoiresentransitions.react/src/referentiels/DetailTaches/CellStatut.tsx +++ b/app.territoiresentransitions.react/src/referentiels/DetailTaches/CellStatut.tsx @@ -1,5 +1,5 @@ -import { SelectActionStatut } from '@/app/referentiels/actions/action-statut.select'; -import { useEditActionStatutIsDisabled } from '@/app/referentiels/use-action-statut'; +import { SelectActionStatut } from '@/app/referentiels/actions/action-statut/action-statut.select'; +import { useEditActionStatutIsDisabled } from '@/app/referentiels/actions/action-statut/use-action-statut'; import { statutAvancementEnumSchema } from '@/domain/referentiels'; import { useCallback } from 'react'; import { TCellProps } from './DetailTacheTable'; diff --git a/app.territoiresentransitions.react/src/referentiels/DetailTaches/FiltreStatut.tsx b/app.territoiresentransitions.react/src/referentiels/DetailTaches/FiltreStatut.tsx index 9381902f61..4e89e46aae 100644 --- a/app.territoiresentransitions.react/src/referentiels/DetailTaches/FiltreStatut.tsx +++ b/app.territoiresentransitions.react/src/referentiels/DetailTaches/FiltreStatut.tsx @@ -1,10 +1,10 @@ -import ActionStatutBadge from '@/app/referentiels/actions/action-statut.badge'; +import ActionStatutBadge from '@/app/referentiels/actions/action-statut/action-statut.badge'; import { MultiSelectFilter, MultiSelectFilterTitle, } from '@/app/ui/shared/select/MultiSelectFilter'; -import { DEFAULT_OPTIONS } from '@/app/referentiels/actions/action-statut.select'; +import { DEFAULT_OPTIONS } from '@/app/referentiels/actions/action-statut/action-statut.select'; import { ITEM_ALL } from '@/app/ui/shared/filters/commons'; import { StatutAvancementIncludingNonConcerne } from '@/domain/referentiels'; import { TFiltreProps } from './filters'; diff --git a/app.territoiresentransitions.react/src/referentiels/DetailTaches/useTableData.ts b/app.territoiresentransitions.react/src/referentiels/DetailTaches/useTableData.ts index a1564fc4fd..a77e160d98 100644 --- a/app.territoiresentransitions.react/src/referentiels/DetailTaches/useTableData.ts +++ b/app.territoiresentransitions.react/src/referentiels/DetailTaches/useTableData.ts @@ -10,6 +10,7 @@ import { import { intersection } from 'es-toolkit'; import { useMutation, useQuery, useQueryClient } from 'react-query'; import { TableOptions } from 'react-table'; +import { useSaveActionStatut } from '../actions/action-statut/use-action-statut'; import { actionNewToDeprecated, ProgressionRow, @@ -19,7 +20,6 @@ import { NEW_useTable, useReferentiel, } from '../ReferentielTable/useReferentiel'; -import { useSaveActionStatut } from '../use-action-statut'; import { useSnapshotFlagEnabled } from '../use-snapshot'; import { initialFilters, nameToShortNames, TFilters } from './filters'; import { diff --git a/app.territoiresentransitions.react/src/referentiels/actions/action-statut.badge.stories.tsx b/app.territoiresentransitions.react/src/referentiels/actions/action-statut/action-statut.badge.stories.tsx similarity index 100% rename from app.territoiresentransitions.react/src/referentiels/actions/action-statut.badge.stories.tsx rename to app.territoiresentransitions.react/src/referentiels/actions/action-statut/action-statut.badge.stories.tsx diff --git a/app.territoiresentransitions.react/src/referentiels/actions/action-statut.badge.tsx b/app.territoiresentransitions.react/src/referentiels/actions/action-statut/action-statut.badge.tsx similarity index 100% rename from app.territoiresentransitions.react/src/referentiels/actions/action-statut.badge.tsx rename to app.territoiresentransitions.react/src/referentiels/actions/action-statut/action-statut.badge.tsx diff --git a/app.territoiresentransitions.react/src/referentiels/actions/action-statut.select.stories.tsx b/app.territoiresentransitions.react/src/referentiels/actions/action-statut/action-statut.select.stories.tsx similarity index 100% rename from app.territoiresentransitions.react/src/referentiels/actions/action-statut.select.stories.tsx rename to app.territoiresentransitions.react/src/referentiels/actions/action-statut/action-statut.select.stories.tsx diff --git a/app.territoiresentransitions.react/src/referentiels/actions/action-statut.select.tsx b/app.territoiresentransitions.react/src/referentiels/actions/action-statut/action-statut.select.tsx similarity index 100% rename from app.territoiresentransitions.react/src/referentiels/actions/action-statut.select.tsx rename to app.territoiresentransitions.react/src/referentiels/actions/action-statut/action-statut.select.tsx diff --git a/app.territoiresentransitions.react/src/referentiels/use-action-statut.ts b/app.territoiresentransitions.react/src/referentiels/actions/action-statut/use-action-statut.ts similarity index 94% rename from app.territoiresentransitions.react/src/referentiels/use-action-statut.ts rename to app.territoiresentransitions.react/src/referentiels/actions/action-statut/use-action-statut.ts index 7103ee4034..5efad95395 100644 --- a/app.territoiresentransitions.react/src/referentiels/use-action-statut.ts +++ b/app.territoiresentransitions.react/src/referentiels/actions/action-statut/use-action-statut.ts @@ -9,14 +9,14 @@ import { import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { omit } from 'es-toolkit'; import { objectToCamel, objectToSnake } from 'ts-case-convert'; -import { useCollectiviteId } from '../collectivites/collectivite-context'; -import { useCurrentCollectivite } from '../core-logic/hooks/useCurrentCollectivite'; -import { useActionScore } from './DEPRECATED_score-hooks'; +import { useCollectiviteId } from '../../../collectivites/collectivite-context'; +import { useCurrentCollectivite } from '../../../core-logic/hooks/useCurrentCollectivite'; +import { useActionScore } from '../../DEPRECATED_score-hooks'; import { useScore, useSnapshotComputeAndUpdate, useSnapshotFlagEnabled, -} from './use-snapshot'; +} from '../../use-snapshot'; /** * Charge le statut d'une action diff --git a/app.territoiresentransitions.react/src/referentiels/actions/action.linked-card.tsx b/app.territoiresentransitions.react/src/referentiels/actions/action.linked-card.tsx index bbe2cad6f5..58de998d57 100644 --- a/app.territoiresentransitions.react/src/referentiels/actions/action.linked-card.tsx +++ b/app.territoiresentransitions.react/src/referentiels/actions/action.linked-card.tsx @@ -1,15 +1,13 @@ import { referentielToName } from '@/app/app/labels'; import { makeReferentielTacheUrl } from '@/app/app/paths'; -import { useCollectiviteId } from '@/app/core-logic/hooks/params'; -import ActionStatutBadge from '@/app/referentiels/actions/action-statut.badge'; -import { getActionStatut } from '@/app/referentiels/utils'; -import { TActionStatutsRow } from '@/app/types/alias'; +import { useCollectiviteId } from '@/app/collectivites/collectivite-context'; +import ActionStatutBadge from '@/app/referentiels/actions/action-statut/action-statut.badge'; +import { ActionWithStatut } from '@/app/referentiels/actions/use-list-actions'; import { Button, Card } from '@/ui'; -import { objectToCamel } from 'ts-case-convert'; type ActionCardProps = { isReadonly?: boolean; - action: TActionStatutsRow; + action: ActionWithStatut; openInNewTab?: boolean; onUnlink?: () => void; }; @@ -20,9 +18,8 @@ const ActionLinkedCard = ({ openInNewTab = false, onUnlink, }: ActionCardProps) => { - const collectiviteId = useCollectiviteId()!; - const { action_id: actionId, identifiant, nom, referentiel } = action; - const statut = getActionStatut(objectToCamel(action)); + const collectiviteId = useCollectiviteId(); + const { actionId, identifiant, nom, referentiel, statut } = action; const link = makeReferentielTacheUrl({ collectiviteId, @@ -46,7 +43,7 @@ const ActionLinkedCard = ({ { - // affiche le statut "non concerné" quand l'action est désactivée par la - // personnalisation ou que l'option "non concerné" a été sélectionnée - // explicitement par l'utilisateur - if (desactive || concerne === false) { - return 'non_concerne'; - } else if (avancement === undefined) { - return 'non_renseigne'; - } - return avancement; -}; - -/** - * Détermine le statut "étendu" d'une action : inclus le "non concerné" et prend - * en compte l'avancement des tâches pour déterminer le statut à la sous-action - */ -export const getActionStatut = (action: { - avancement: StatutAvancement | undefined; - desactive: boolean | undefined; - concerne: boolean | undefined; - avancementDescendants: StatutAvancement[] | undefined; -}) => { - const avancementExt = getAvancementExt(action); - - return (!avancementExt || avancementExt === 'non_renseigne') && - action.avancementDescendants?.find((av) => !!av && av !== 'non_renseigne') - ? // Une sous-action "non renseigné" mais avec au moins une tâche renseignée a - // le statut "détaillé" - 'detaille' - : avancementExt || 'non_renseigne'; -}; - /** * Renvoie le statut en fonction de l'index dans le tableau avancement détaillé */ diff --git a/app.territoiresentransitions.react/src/ui/dropdownLists/ActionsReferentielsDropdown/ActionsReferentielsDropdown.tsx b/app.territoiresentransitions.react/src/ui/dropdownLists/ActionsReferentielsDropdown/ActionsReferentielsDropdown.tsx index c963ca89d9..bb3df2c441 100644 --- a/app.territoiresentransitions.react/src/ui/dropdownLists/ActionsReferentielsDropdown/ActionsReferentielsDropdown.tsx +++ b/app.territoiresentransitions.react/src/ui/dropdownLists/ActionsReferentielsDropdown/ActionsReferentielsDropdown.tsx @@ -1,6 +1,6 @@ -import { TActionRelationInsert, TActionStatutsRow } from '@/app/types/alias'; +import { useListActions } from '@/app/referentiels/actions/use-list-actions'; +import { TActionRelationInsert } from '@/app/types/alias'; import { OptionValue, SelectFilter, SelectMultipleProps } from '@/ui'; -import { useActionsReferentielsListe } from './useActionsReferentielsListe'; type ActionsReferentielsDropdownProps = Omit< SelectMultipleProps, @@ -20,16 +20,16 @@ const ActionsReferentielsDropdown = ({ ...props }: ActionsReferentielsDropdownProps) => { // Liste de toutes les actions - const { data: actionListe } = useActionsReferentielsListe(); + const { data: actionListe } = useListActions(); // Formattage des valeurs sélectionnées pour les renvoyer au composant parent const getSelectedActions = (values?: OptionValue[]) => { - const selectedActions = (actionListe ?? []).filter( - (action: TActionStatutsRow) => values?.some((v) => v === action.action_id) + const selectedActions = (actionListe ?? []).filter((action) => + values?.some((v) => v === action.actionId) ); const formatedActions: TActionRelationInsert[] = selectedActions.map( (action) => ({ - id: action.action_id, + id: action.actionId, referentiel: action.referentiel, }) ); @@ -38,7 +38,7 @@ const ActionsReferentielsDropdown = ({ // Calcul de la liste des options pour le select const options = (actionListe ?? []).map((action) => ({ - value: action.action_id, + value: action.actionId, label: `${action.referentiel} ${action.identifiant} - ${action.nom}`, })); diff --git a/app.territoiresentransitions.react/src/ui/dropdownLists/ActionsReferentielsDropdown/useActionsReferentielsListe.ts b/app.territoiresentransitions.react/src/ui/dropdownLists/ActionsReferentielsDropdown/useActionsReferentielsListe.ts deleted file mode 100644 index e55ed902cc..0000000000 --- a/app.territoiresentransitions.react/src/ui/dropdownLists/ActionsReferentielsDropdown/useActionsReferentielsListe.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useSupabase } from '@/api/utils/supabase/use-supabase'; -import { useCollectiviteId } from '@/app/core-logic/hooks/params'; -import { TActionStatutsRow } from '@/app/types/alias'; -import { useQuery } from 'react-query'; - -export const useActionsReferentielsListe = () => { - const collectivite_id = useCollectiviteId()!; - const supabase = useSupabase(); - - return useQuery(['actions_referentiels', collectivite_id], async () => { - const { error, data } = await supabase - .from('action_statuts') - .select( - 'action_id, referentiel, nom, identifiant, avancement, avancement_descendants, desactive, concerne' - ) - .eq('collectivite_id', collectivite_id) - .in('type', ['action', 'sous-action']) - .order('action_id', { ascending: true }); - - if (error) throw new Error(error.message); - - return data as TActionStatutsRow[]; - }); -}; diff --git a/app.territoiresentransitions.react/src/ui/export-pdf/components/Badge/BadgeStatutAction.tsx b/app.territoiresentransitions.react/src/ui/export-pdf/components/Badge/BadgeStatutAction.tsx index cb5f88970b..a3785fb55e 100644 --- a/app.territoiresentransitions.react/src/ui/export-pdf/components/Badge/BadgeStatutAction.tsx +++ b/app.territoiresentransitions.react/src/ui/export-pdf/components/Badge/BadgeStatutAction.tsx @@ -1,5 +1,5 @@ import { avancementToLabel } from '@/app/app/labels'; -import { statusToState } from '@/app/referentiels/actions/action-statut.badge'; +import { statusToState } from '@/app/referentiels/actions/action-statut/action-statut.badge'; import { StatutAvancementIncludingNonConcerne } from '@/domain/referentiels'; import classNames from 'classnames'; import { Badge } from './Badge'; diff --git a/backend/src/referentiels/compute-score/referentiels-scoring.controller.ts b/backend/src/referentiels/compute-score/referentiels-scoring.controller.ts index 82cf8d5eb2..d061a402ac 100644 --- a/backend/src/referentiels/compute-score/referentiels-scoring.controller.ts +++ b/backend/src/referentiels/compute-score/referentiels-scoring.controller.ts @@ -33,7 +33,7 @@ import { SNAPSHOT_REF_PARAM_KEY, } from '../models/referentiel-api.constants'; import { ReferentielId } from '../models/referentiel-id.enum'; -import ReferentielsScoringSnapshotsService from '../snapshots/referentiels-scoring-snapshots.service'; +import { ReferentielsScoringSnapshotsService } from '../snapshots/referentiels-scoring-snapshots.service'; import { actionStatutsByActionIdSchema } from './action-statuts-by-action-id.dto'; import { getReferentielScoresResponseSchema, diff --git a/backend/src/referentiels/compute-score/referentiels-scoring.service.spec.ts b/backend/src/referentiels/compute-score/referentiels-scoring.service.spec.ts index 23cfd6ac34..1688e62e4b 100644 --- a/backend/src/referentiels/compute-score/referentiels-scoring.service.spec.ts +++ b/backend/src/referentiels/compute-score/referentiels-scoring.service.spec.ts @@ -28,7 +28,7 @@ import { caeReferentiel } from '../models/samples/cae-referentiel'; import { deeperReferentiel } from '../models/samples/deeper-referentiel'; import { eciReferentiel } from '../models/samples/eci-referentiel'; import { simpleReferentiel } from '../models/samples/simple-referentiel'; -import ReferentielsScoringSnapshotsService from '../snapshots/referentiels-scoring-snapshots.service'; +import {ReferentielsScoringSnapshotsService} from '../snapshots/referentiels-scoring-snapshots.service'; import { ActionStatutsByActionId } from './action-statuts-by-action-id.dto'; import ReferentielsScoringService from './referentiels-scoring.service'; import { ScoreFields, ScoreWithOnlyPoints } from './score.dto'; diff --git a/backend/src/referentiels/compute-score/referentiels-scoring.service.ts b/backend/src/referentiels/compute-score/referentiels-scoring.service.ts index cc93c38ed9..eb99fe5fd8 100644 --- a/backend/src/referentiels/compute-score/referentiels-scoring.service.ts +++ b/backend/src/referentiels/compute-score/referentiels-scoring.service.ts @@ -73,7 +73,7 @@ import { postAuditScoresTable } from '../models/post-audit-scores.table'; import { preAuditScoresTable } from '../models/pre-audit-scores.table'; import { ReferentielId } from '../models/referentiel-id.enum'; import { getParentIdFromActionId } from '../referentiels.utils'; -import ReferentielsScoringSnapshotsService from '../snapshots/referentiels-scoring-snapshots.service'; +import { ReferentielsScoringSnapshotsService } from '../snapshots/referentiels-scoring-snapshots.service'; import { SnapshotJalon } from '../snapshots/snapshot-jalon.enum'; import { ActionStatutsByActionId } from './action-statuts-by-action-id.dto'; import { GetReferentielScoresResponseType } from './get-referentiel-scores.response'; @@ -114,14 +114,24 @@ export default class ReferentielsScoringService { referentielId: ReferentielId, forceRecalculScoreCourant?: boolean ) { - let currentScore = forceRecalculScoreCourant - ? null - : await this.referentielsScoringSnapshotsService.get( + let currentScore = null; + + if (!forceRecalculScoreCourant) { + try { + currentScore = await this.referentielsScoringSnapshotsService.get( collectiviteId, referentielId, - ReferentielsScoringSnapshotsService.SCORE_COURANT_SNAPSHOT_REF, - true + ReferentielsScoringSnapshotsService.SCORE_COURANT_SNAPSHOT_REF ); + } catch (e) { + if (e instanceof NotFoundException) { + // Snapshot not found, we will compute the score, do nothing + } else { + throw e; + } + } + } + if (!currentScore) { currentScore = await this.computeScoreForCollectivite( referentielId, diff --git a/backend/src/referentiels/export-score/export-referentiel-score.service.spec.ts b/backend/src/referentiels/export-score/export-referentiel-score.service.spec.ts index 7464610917..78a33daad4 100644 --- a/backend/src/referentiels/export-score/export-referentiel-score.service.spec.ts +++ b/backend/src/referentiels/export-score/export-referentiel-score.service.spec.ts @@ -3,7 +3,7 @@ import ReferentielsScoringService from '../compute-score/referentiels-scoring.se import { GetReferentielService } from '../get-referentiel/get-referentiel.service'; import { deeperReferentielScoring } from '../models/samples/deeper-referentiel-scoring.sample'; import { simpleReferentielScoring } from '../models/samples/simple-referentiel-scoring.sample'; -import ReferentielsScoringSnapshotsService from '../snapshots/referentiels-scoring-snapshots.service'; +import { ReferentielsScoringSnapshotsService } from '../snapshots/referentiels-scoring-snapshots.service'; import ExportReferentielScoreService from './export-referentiel-score.service'; describe('ExportReferentielScoreService', () => { diff --git a/backend/src/referentiels/export-score/export-referentiel-score.service.ts b/backend/src/referentiels/export-score/export-referentiel-score.service.ts index cc06af3f7e..80e5e2a0bc 100644 --- a/backend/src/referentiels/export-score/export-referentiel-score.service.ts +++ b/backend/src/referentiels/export-score/export-referentiel-score.service.ts @@ -25,7 +25,7 @@ import { getAxeFromActionId, getLevelFromActionId, } from '../referentiels.utils'; -import ReferentielsScoringSnapshotsService from '../snapshots/referentiels-scoring-snapshots.service'; +import { ReferentielsScoringSnapshotsService } from '../snapshots/referentiels-scoring-snapshots.service'; type ActionDefinitionFields = ActionDefinitionEssential & Partial>; diff --git a/backend/src/referentiels/list-action-definitions/list-action-definitions.service.ts b/backend/src/referentiels/list-action-definitions/list-action-definitions.service.ts new file mode 100644 index 0000000000..482cf53a2e --- /dev/null +++ b/backend/src/referentiels/list-action-definitions/list-action-definitions.service.ts @@ -0,0 +1,131 @@ +import { DatabaseService } from '@/backend/utils'; +import { Injectable } from '@nestjs/common'; +import { and, asc, eq, getTableColumns, inArray, sql } from 'drizzle-orm'; +import z from 'zod'; +import { actionDefinitionTable, actionTypeSchema } from '../index-domain'; +import { referentielDefinitionTable } from '../models/referentiel-definition.table'; + +export const inputSchema = z.object({ + actionIds: z.string().array().optional(), + actionTypes: actionTypeSchema.array().optional(), +}); + +type Input = z.infer; + +export type ActionDefinition = Awaited< + ReturnType +>[0]; + +@Injectable() +export class ListActionDefinitionsService { + constructor(private readonly databaseService: DatabaseService) {} + + private db = this.databaseService.db; + + async listActionDefinitions({ actionIds, actionTypes }: Input) { + const subQuery = this.db + .$with('action_definition_with_depth_and_type') + .as(this.listWithDepthAndType()); + + const request = this.db.with(subQuery).select().from(subQuery); + + // const request = this.db + // .select({ + // ...pick(getTableColumns(actionDefinitionTable), [ + // 'actionId', + // 'nom', + // 'referentiel', + // 'identifiant', + // 'points', + // ]), + // }) + // .from(actionDefinitionTable); + + // .leftJoin( + // actionStatutTable, + // and( + // eq(actionStatutTable.collectiviteId, collectiviteId), + // eq(actionStatutTable.actionId, actionDefinitionTable.actionId) + // ) + // ) + + const filters = []; + + if (actionIds?.length) { + filters.push(inArray(subQuery.actionId, actionIds)); + } + + if (actionTypes?.length) { + filters.push(inArray(subQuery.actionType, actionTypes)); + } + + if (filters.length) { + request.where(and(...filters)); + } + + request.orderBy(asc(subQuery.actionId)); + + return request; + } + + private listWithDepth() { + return this.db + .select({ + ...getTableColumns(actionDefinitionTable), + + // Add a column with the depth of the action depending on the number of dots in the identifiant + // Ex: 1.1 => depth 2 + // 1.1.1 => depth 3 + depth: sql`CASE + WHEN ${actionDefinitionTable.identifiant} IS NULL OR ${actionDefinitionTable.identifiant} LIKE '' THEN 0 + ELSE REGEXP_COUNT(${actionDefinitionTable.identifiant}, '\\.') + 1 + END`.as('depth'), + }) + .from(actionDefinitionTable); + } + + private listWithDepthAndType() { + const subQuery = this.db + .$with('action_definition_with_depth') + .as(this.listWithDepth()); + + return this.db + .with(subQuery) + .select({ + modifiedAt: subQuery.modifiedAt, + actionId: subQuery.actionId, + referentiel: subQuery.referentiel, + identifiant: subQuery.identifiant, + nom: subQuery.nom, + description: subQuery.description, + contexte: subQuery.contexte, + exemples: subQuery.exemples, + ressources: subQuery.ressources, + reductionPotentiel: subQuery.reductionPotentiel, + perimetreEvaluation: subQuery.perimetreEvaluation, + preuve: subQuery.preuve, + points: subQuery.points, + pourcentage: subQuery.pourcentage, + categorie: subQuery.categorie, + referentielId: subQuery.referentielId, + referentielVersion: subQuery.referentielVersion, + + depth: subQuery.depth, + + // Add the action type from the `referentiel_definition.hierarchie` array + // Ex: 'axe', 'sous-axe', etc + actionType: + sql`${referentielDefinitionTable.hierarchie}[${subQuery.depth} + 1]`.as( + 'actionType' + ), + }) + .from(subQuery) + .innerJoin( + referentielDefinitionTable, + and( + eq(referentielDefinitionTable.id, subQuery.referentielId), + eq(referentielDefinitionTable.version, subQuery.referentielVersion) + ) + ); + } +} diff --git a/backend/src/referentiels/list-actions/list-actions.router.e2e-spec.ts b/backend/src/referentiels/list-actions/list-actions.router.e2e-spec.ts new file mode 100644 index 0000000000..dcee163147 --- /dev/null +++ b/backend/src/referentiels/list-actions/list-actions.router.e2e-spec.ts @@ -0,0 +1,118 @@ +import { inferProcedureInput } from '@trpc/server'; +import { YOLO_DODO } from 'backend/test/test-users.samples'; +import { getTestRouter } from '../../../test/app-utils'; +import { getAnonUser, getAuthUser } from '../../../test/auth-utils'; +import { AuthenticatedUser } from '../../auth/models/auth.models'; +import { type AppRouter, TrpcRouter } from '../../utils/trpc/trpc.router'; +import { ReferentielIdEnum } from '../index-domain'; + +type Input = inferProcedureInput< + AppRouter['referentiels']['actions']['listActions'] +>; + +describe('ActionStatutListRouter', () => { + let router: TrpcRouter; + let yoloDodoUser: AuthenticatedUser; + + beforeAll(async () => { + router = await getTestRouter(); + yoloDodoUser = await getAuthUser(); + + const caller = router.createCaller({ user: yoloDodoUser }); + + await caller.referentiels.scores.computeScore({ + referentielId: ReferentielIdEnum.CAE, + collectiviteId: YOLO_DODO.collectiviteId.edition, + parameters: { + snapshot: true, + }, + }); + + await caller.referentiels.scores.computeScore({ + referentielId: ReferentielIdEnum.ECI, + collectiviteId: YOLO_DODO.collectiviteId.edition, + parameters: { + snapshot: true, + }, + }); + }); + + test('not authenticated = no access', async () => { + const caller = router.createCaller({ user: getAnonUser() }); + + const input: Input = { + collectiviteId: 1, + actionIds: ['cae_1.1.1'], + }; + + await expect( + caller.referentiels.actions.listActions(input) + ).rejects.toThrow(); + }); + + test('List a single action', async () => { + const caller = router.createCaller({ user: yoloDodoUser }); + + const input = { + collectiviteId: 1, + actionIds: ['cae_1.1.1'], + } satisfies Input; + + const result = await caller.referentiels.actions.listActions(input); + expect(result.length).toEqual(input.actionIds.length); + + for (const action of result) { + expect(input.actionIds).toContain(action.actionId); + + expect(action.referentiel).toBeDefined(); + expect(action.nom).toBeDefined(); + expect(action.identifiant).toBeDefined(); + expect(action.depth).toBeDefined(); + expect(action.actionType).toBeDefined(); + + expect(action).not.toHaveProperty('statut'); + } + }); + + test('List actions from CAE & ECI at the same time', async () => { + const caller = router.createCaller({ user: yoloDodoUser }); + + const input = { + collectiviteId: 1, + actionIds: ['cae_1.1.1', 'eci_1.3.2'], + } satisfies Input; + + const result = await caller.referentiels.actions.listActions(input); + expect(result.length).toEqual(input.actionIds.length); + + for (const action of result) { + expect(input.actionIds).toContain(action.actionId); + } + }); + + test('List actions with statuts', async () => { + const caller = router.createCaller({ user: yoloDodoUser }); + + const input = { + collectiviteId: 1, + actionIds: ['cae_1.1.1', 'eci_1.3.2'], + } satisfies Input; + + const result = await caller.referentiels.actions.listActionsWithStatuts( + input + ); + expect(result.length).toEqual(input.actionIds.length); + + for (const action of result) { + expect(input.actionIds).toContain(action.actionId); + + expect(action.depth).toBeDefined(); + expect(action.actionType).toBeDefined(); + + // Specific fields when `withStatuts` is true + expect(action.statut).toBeDefined(); + expect(action.desactive).toBeDefined(); + expect(action.concerne).toBeDefined(); + } + }); +}); diff --git a/backend/src/referentiels/list-actions/list-actions.router.ts b/backend/src/referentiels/list-actions/list-actions.router.ts new file mode 100644 index 0000000000..ec4b08402f --- /dev/null +++ b/backend/src/referentiels/list-actions/list-actions.router.ts @@ -0,0 +1,81 @@ +import { PermissionService } from '@/backend/auth/authorizations/permission.service'; +import { TrpcService } from '@/backend/utils/trpc/trpc.service'; +import { PermissionOperation, ResourceType } from '@/domain/auth'; +import { Injectable } from '@nestjs/common'; +import z from 'zod'; +import { ListActionDefinitionsService } from '../list-action-definitions/list-action-definitions.service'; +import { ReferentielsScoringSnapshotsService } from '../snapshots/referentiels-scoring-snapshots.service'; +import { getExtendActionWithComputedStatutsFields } from '../snapshots/snapshots.utils'; +import { ActionTypeEnum, actionTypeSchema } from './../models/action-type.enum'; + +export const inputSchema = z.object({ + collectiviteId: z.number(), + actionIds: z.string().array().optional(), + actionTypes: actionTypeSchema + .array() + .optional() + .default([ActionTypeEnum.ACTION, ActionTypeEnum.SOUS_ACTION]), +}); + +@Injectable() +export class ListActionsRouter { + constructor( + private readonly trpc: TrpcService, + private readonly permissions: PermissionService, + private readonly snapshotService: ReferentielsScoringSnapshotsService, + private readonly listActionDefinitionsService: ListActionDefinitionsService + ) {} + + router = this.trpc.router({ + listActions: this.trpc.authedProcedure + .input(inputSchema) + .query( + async ({ + input: { collectiviteId, actionIds, actionTypes }, + ctx: { user }, + }) => { + await this.permissions.isAllowed( + user, + PermissionOperation.REFERENTIELS_LECTURE, + ResourceType.COLLECTIVITE, + collectiviteId + ); + + return this.listActionDefinitionsService.listActionDefinitions({ + actionIds, + actionTypes, + }); + } + ), + + listActionsWithStatuts: this.trpc.authedProcedure + .input(inputSchema) + .query( + async ({ + input: { collectiviteId, actionIds, actionTypes }, + ctx: { user }, + }) => { + await this.permissions.isAllowed( + user, + PermissionOperation.REFERENTIELS_LECTURE, + ResourceType.COLLECTIVITE, + collectiviteId + ); + + const actionDefinitions = + await this.listActionDefinitionsService.listActionDefinitions({ + actionIds, + actionTypes, + }); + + const extendActionWithScores = + getExtendActionWithComputedStatutsFields( + collectiviteId, + this.snapshotService.get.bind(this.snapshotService) + ); + + return Promise.all(actionDefinitions.map(extendActionWithScores)); + } + ), + }); +} diff --git a/backend/src/referentiels/models/referentiel-id.enum.ts b/backend/src/referentiels/models/referentiel-id.enum.ts index 88c42bb964..3d07a160d1 100644 --- a/backend/src/referentiels/models/referentiel-id.enum.ts +++ b/backend/src/referentiels/models/referentiel-id.enum.ts @@ -1,14 +1,15 @@ +import { getEnumValues } from '@/domain/utils'; import { pgEnum } from 'drizzle-orm/pg-core'; import z from 'zod'; -// export enum ReferentielId { -// ECI = 'eci', -// CAE = 'cae', -// TE = 'te', -// TE_TEST = 'te-test', -// } +export const ReferentielIdEnum = { + CAE: 'cae', + ECI: 'eci', + TE: 'te', + TE_TEST: 'te-test', +} as const; -export const referentielIdEnumValues = ['cae', 'eci', 'te', 'te-test'] as const; +export const referentielIdEnumValues = getEnumValues(ReferentielIdEnum); export const referentielIdEnumSchema = z.enum(referentielIdEnumValues); diff --git a/backend/src/referentiels/referentiels.module.ts b/backend/src/referentiels/referentiels.module.ts index 137275f3bf..f64fb7788e 100644 --- a/backend/src/referentiels/referentiels.module.ts +++ b/backend/src/referentiels/referentiels.module.ts @@ -1,5 +1,4 @@ import { Module } from '@nestjs/common'; -import { AuthModule } from '../auth/auth.module'; import { CollectivitesModule } from '../collectivites/collectivites.module'; import { PersonnalisationsModule } from '../personnalisations/personnalisations.module'; import { SheetModule } from '../utils/google-sheets/sheet.module'; @@ -12,38 +11,41 @@ import { GetReferentielService } from './get-referentiel/get-referentiel.service import { ImportReferentielController } from './import-referentiel/import-referentiel.controller'; import ImportReferentielService from './import-referentiel/import-referentiel.service'; import { LabellisationService } from './labellisation.service'; -import ReferentielsScoringSnapshotsService from './snapshots/referentiels-scoring-snapshots.service'; +import { ListActionDefinitionsService } from './list-action-definitions/list-action-definitions.service'; +import { ListActionsRouter } from './list-actions/list-actions.router'; +import { ReferentielsRouter } from './referentiels.router'; +import { ReferentielsScoringSnapshotsService } from './snapshots/referentiels-scoring-snapshots.service'; import { ScoreSnapshotsRouter } from './snapshots/score-snaphots.router'; import { UpdateActionStatutRouter } from './update-action-statut/update-action-statut.router'; import { UpdateActionStatutService } from './update-action-statut/update-action-statut.service'; @Module({ - imports: [ - AuthModule, - CollectivitesModule, - SheetModule, - PersonnalisationsModule, - ], + imports: [CollectivitesModule, SheetModule, PersonnalisationsModule], providers: [ GetReferentielService, + + ListActionDefinitionsService, + ListActionsRouter, + + UpdateActionStatutService, + UpdateActionStatutRouter, + LabellisationService, ReferentielsScoringSnapshotsService, + ReferentielsScoringService, - UpdateActionStatutService, - UpdateActionStatutRouter, ComputeScoreRouter, ScoreSnapshotsRouter, + ReferentielsRouter, ExportReferentielScoreService, ImportReferentielService, ], exports: [ + ReferentielsRouter, LabellisationService, ReferentielsScoringSnapshotsService, ReferentielsScoringService, UpdateActionStatutService, - UpdateActionStatutRouter, - ComputeScoreRouter, - ScoreSnapshotsRouter, ExportReferentielScoreService, ], controllers: [ diff --git a/backend/src/referentiels/referentiels.router.ts b/backend/src/referentiels/referentiels.router.ts index edc41ccf22..9f0a5db1e6 100644 --- a/backend/src/referentiels/referentiels.router.ts +++ b/backend/src/referentiels/referentiels.router.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { TrpcService } from '../utils/trpc/trpc.service'; import { ComputeScoreRouter } from './compute-score/compute-score.router'; +import { ListActionsRouter } from './list-actions/list-actions.router'; import { ScoreSnapshotsRouter } from './snapshots/score-snaphots.router'; import { UpdateActionStatutRouter } from './update-action-statut/update-action-statut.router'; @@ -9,6 +10,7 @@ export class ReferentielsRouter { constructor( private readonly trpc: TrpcService, private readonly updateActionStatutRouter: UpdateActionStatutRouter, + private readonly listActionStatutRouter: ListActionsRouter, private readonly scoreSnapshotsRouter: ScoreSnapshotsRouter, private readonly computeScoreRouter: ComputeScoreRouter ) {} @@ -16,6 +18,7 @@ export class ReferentielsRouter { router = this.trpc.router({ actions: this.trpc.mergeRouters( this.updateActionStatutRouter.router, + this.listActionStatutRouter.router ), snapshots: this.scoreSnapshotsRouter.router, scores: this.computeScoreRouter.router, diff --git a/backend/src/referentiels/referentiels.utils.ts b/backend/src/referentiels/referentiels.utils.ts index bf2599d822..6e8d588e87 100644 --- a/backend/src/referentiels/referentiels.utils.ts +++ b/backend/src/referentiels/referentiels.utils.ts @@ -1,5 +1,10 @@ +import { memoize } from 'es-toolkit'; +import { StatutAvancement, StatutAvancementEnum } from './index-domain'; import { ActionTypeIncludingExemple } from './models/action-type.enum'; -import { referentielIdEnumSchema } from './models/referentiel-id.enum'; +import { + ReferentielId, + referentielIdEnumSchema, +} from './models/referentiel-id.enum'; export class ReferentielException extends Error { constructor(message: string) { @@ -93,6 +98,63 @@ export function getParentIdFromActionId(actionId: string): string | null { } } +/** + * Détermine le statut d'avancement d'une action (inclus le "non concerné") + * en fonction des autres propriétés provenant du score calculé de l'action. + * C'est à dire après le calcul des points tenant compte de la personnalisation. + */ +export function getStatutAvancement({ + avancement, + desactive, + concerne, +}: { + avancement?: StatutAvancement; + desactive: boolean | undefined; + concerne: boolean | undefined; +}) { + // statut "non concerné" quand l'action est désactivée par la personnalisation + // ou que l'option "non concerné" a été sélectionnée explicitement par l'utilisateur + if (desactive || concerne === false) { + return StatutAvancementEnum.NON_CONCERNE; + } + + if (avancement === undefined) { + return StatutAvancementEnum.NON_RENSEIGNE; + } + + return avancement; +} + +/** + * Détermine le statut d'avancement d'une action en incluant le statut "non concerné" + * et en fonction des avancements des actions enfants. + */ +export const getStatutAvancementBasedOnChildren = ( + action: { + avancement?: StatutAvancement; + desactive: boolean; + concerne: boolean; + }, + childrenStatuts: StatutAvancement[] | undefined +) => { + const statutEtendu = getStatutAvancement(action); + + const hasAtLeastOneChildWithStatutRenseigne = childrenStatuts?.some( + (statut) => statut && statut !== StatutAvancementEnum.NON_RENSEIGNE + ); + + const isStatutNonRenseigne = + !statutEtendu || statutEtendu === StatutAvancementEnum.NON_RENSEIGNE; + + // Une sous-action "non renseigné" mais avec au moins une tâche renseignée a + // le statut "détaillé" + if (hasAtLeastOneChildWithStatutRenseigne && isStatutNonRenseigne) { + return StatutAvancementEnum.DETAILLE; + } + + return statutEtendu ?? StatutAvancementEnum.NON_RENSEIGNE; +}; + /** * Equivalent to a `reduce` function but for a list of actions and their children. */ @@ -120,3 +182,23 @@ export function flatMapActionsEnfants( action, ]); } + +export function findActionInTree( + actions: A[], + predicate: (action: A) => boolean +): A | undefined { + for (const action of actions) { + if (predicate(action)) { + return action; + } + + if (action.actionsEnfant) { + const foundAction = findActionInTree(action.actionsEnfant, predicate); + if (foundAction) { + return foundAction; + } + } + } + + return undefined; +} diff --git a/backend/src/referentiels/snapshots/referentiels-scoring-snapshots.service.ts b/backend/src/referentiels/snapshots/referentiels-scoring-snapshots.service.ts index 611276167a..45cbbf64a8 100644 --- a/backend/src/referentiels/snapshots/referentiels-scoring-snapshots.service.ts +++ b/backend/src/referentiels/snapshots/referentiels-scoring-snapshots.service.ts @@ -34,7 +34,7 @@ import { } from './snapshot.table'; @Injectable() -export default class ReferentielsScoringSnapshotsService { +export class ReferentielsScoringSnapshotsService { static SCORE_COURANT_SNAPSHOT_REF = 'score-courant'; static SCORE_COURANT_SNAPSHOT_NOM = 'Score courant'; static PRE_AUDIT_SNAPSHOT_REF_PREFIX = 'pre-audit-'; @@ -458,9 +458,8 @@ export default class ReferentielsScoringSnapshotsService { async get( collectiviteId: number, referentielId: ReferentielId, - snapshotRef: string, - doNotThrowIfNotFound?: boolean - ): Promise { + snapshotRef: string = ReferentielsScoringSnapshotsService.SCORE_COURANT_SNAPSHOT_REF + ): Promise { const result = (await this.databaseService.db .select() .from(scoreSnapshotTable) @@ -473,13 +472,9 @@ export default class ReferentielsScoringSnapshotsService { )) as ScoreSnapshotType[]; if (!result.length) { - if (!doNotThrowIfNotFound) { - throw new NotFoundException( - `Aucun snapshot de score avec la référence ${snapshotRef} n'a été trouvé pour la collectivité ${collectiviteId} et le referentiel ${referentielId}` - ); - } else { - return null; - } + throw new NotFoundException( + `Aucun snapshot de score avec la référence ${snapshotRef} n'a été trouvé pour la collectivité ${collectiviteId} et le referentiel ${referentielId}` + ); } const fullScores = result[0].referentielScores; @@ -491,6 +486,7 @@ export default class ReferentielsScoringSnapshotsService { modifiedAt: result[0].modifiedAt, modifiedBy: result[0].modifiedBy, }; + return fullScores; } diff --git a/backend/src/referentiels/snapshots/score-snaphots.router.ts b/backend/src/referentiels/snapshots/score-snaphots.router.ts index e540406dc0..445c1389cd 100644 --- a/backend/src/referentiels/snapshots/score-snaphots.router.ts +++ b/backend/src/referentiels/snapshots/score-snaphots.router.ts @@ -3,11 +3,11 @@ import { Injectable } from '@nestjs/common'; import z from 'zod'; import ReferentielsScoringService from '../compute-score/referentiels-scoring.service'; import { ComputeScoreMode } from '../models/compute-scores-mode.enum'; +import { DEFAULT_SNAPSHOT_JALONS } from '../models/get-score-snapshots.request'; import { referentielIdEnumSchema } from '../models/referentiel-id.enum'; -import ReferentielsScoringSnapshotsService from './referentiels-scoring-snapshots.service'; +import { ReferentielsScoringSnapshotsService } from './referentiels-scoring-snapshots.service'; import { SnapshotJalon } from './snapshot-jalon.enum'; import { upsertSnapshotRequestSchema } from './upsert-snapshot.request'; -import { DEFAULT_SNAPSHOT_JALONS } from '../models/get-score-snapshots.request'; export const getScoreSnapshotInfosTrpcRequestSchema = z.object({ referentielId: referentielIdEnumSchema, diff --git a/backend/src/referentiels/snapshots/snapshots.utils.ts b/backend/src/referentiels/snapshots/snapshots.utils.ts new file mode 100644 index 0000000000..513fc45176 --- /dev/null +++ b/backend/src/referentiels/snapshots/snapshots.utils.ts @@ -0,0 +1,58 @@ +import { memoize } from 'es-toolkit'; +import { + findActionInTree, + flatMapActionsEnfants, + getStatutAvancementBasedOnChildren, + ReferentielException, +} from '../referentiels.utils'; +import { ReferentielId } from './../models/referentiel-id.enum'; +import { ReferentielsScoringSnapshotsService } from './referentiels-scoring-snapshots.service'; + +/** + * @returns a function that takes an action and returns the action with additional computed status fields + * This returned function aims to be used in actions.map() + */ +export function getExtendActionWithComputedStatutsFields( + collectiviteId: number, + getSnapshot: ( + collectiviteId: number, + referentielId: ReferentielId + ) => ReturnType +) { + const getCurrentSnapshot = memoize((referentielId: ReferentielId) => + getSnapshot(collectiviteId, referentielId) + ); + + return async ( + action: A + ) => { + const { scores: scoresTree } = await getCurrentSnapshot(action.referentiel); + + const actionWithScore = findActionInTree( + [scoresTree], + (a) => a.actionId === action.actionId + ); + + if (!actionWithScore) { + throw new ReferentielException( + `Action ${action.actionId} not found in current score` + ); + } + + const { score } = actionWithScore; + + return { + ...action, + + desactive: score.desactive, + concerne: score.concerne, + + statut: getStatutAvancementBasedOnChildren( + score, + flatMapActionsEnfants(actionWithScore) + .map((a) => a.score.avancement) + .filter((a) => a !== undefined) + ), + }; + }; +}