diff --git a/packages/api/src/api/rest/etapes.ts b/packages/api/src/api/rest/etapes.ts index 209ab488291c96afa0226dfb6d3530749c196e52..56b44029e768a8cce74821cbb62f3d1276de3352 100644 --- a/packages/api/src/api/rest/etapes.ts +++ b/packages/api/src/api/rest/etapes.ts @@ -1079,6 +1079,7 @@ export const deposeEtape: RestNewPutCall<'/rest/etapes/:etapeId/depot'> = (rootP { fields: { titre: { pointsEtape: { id: {} }, titulairesEtape: { id: {} }, amodiatairesEtape: { id: {} } }, + etapes: { id: {} }, }, }, userSuper diff --git a/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts b/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts index 5d4d48f48bead60b7d1ab1db0ba16691fdaa69c0..97e529467e2215494711b823dc82f52892f6618c 100644 --- a/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts +++ b/packages/api/src/business/validations/titre-demarche-etat-validate.test.ts @@ -672,6 +672,64 @@ describe('titreDemarcheUpdatedEtatValidate', () => { }) describe('getPossiblesEtapesTypes', () => { + test("bug prod edition d'une étape quand une étape de même type est en brouillon", () => { + const rcoId = newEtapeId('rcoId') + const etapes: Pick<ITitreEtape, 'typeId' | 'date' | 'isBrouillon' | 'id' | 'ordre' | 'statutId' | 'communes'>[] = [ + { typeId: 'rco', date: toCaminoDate('2025-04-10'), isBrouillon: ETAPE_IS_BROUILLON, id: newEtapeId(), ordre: 18, statutId: 'fai', communes: [] }, + { typeId: 'men', date: toCaminoDate('2018-12-28'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 3, statutId: 'fai', communes: [] }, + { typeId: 'spp', date: toCaminoDate('2019-02-12'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 4, statutId: 'fai', communes: [] }, + { typeId: 'mco', date: toCaminoDate('2019-04-29'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 5, statutId: 'fai', communes: [] }, + { + typeId: 'mfr', + date: toCaminoDate('2018-12-20'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId(), + ordre: 1, + statutId: 'fai', + communes: [{ id: toCommuneId('67001') }], + }, + { + typeId: 'mod', + date: toCaminoDate('2018-12-21'), + isBrouillon: ETAPE_IS_NOT_BROUILLON, + id: newEtapeId(), + ordre: 2, + statutId: 'fai', + communes: [{ id: toCommuneId('67001') }], + }, + { typeId: 'rco', date: toCaminoDate('2019-06-05'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: rcoId, ordre: 6, statutId: 'fai', communes: [] }, + { typeId: 'mcr', date: toCaminoDate('2019-07-10'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 7, statutId: 'fav', communes: [] }, + { typeId: 'asc', date: toCaminoDate('2019-07-17'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 8, statutId: 'fai', communes: [] }, + { typeId: 'adc', date: toCaminoDate('2019-07-17'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 9, statutId: 'fai', communes: [] }, + { typeId: 'anf', date: toCaminoDate('2019-07-19'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 10, statutId: 'ter', communes: [] }, + { typeId: 'apd', date: toCaminoDate('2020-01-13'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 11, statutId: 'fav', communes: [] }, + { typeId: 'app', date: toCaminoDate('2020-09-01'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 12, statutId: 'fav', communes: [] }, + { typeId: 'ppu', date: toCaminoDate('2020-10-05'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 13, statutId: 'ter', communes: [] }, + { typeId: 'rco', date: toCaminoDate('2023-09-25'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 14, statutId: 'fai', communes: [] }, + { typeId: 'ppu', date: toCaminoDate('2024-01-15'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 15, statutId: 'ter', communes: [] }, + { typeId: 'apd', date: toCaminoDate('2024-03-19'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 16, statutId: 'fre', communes: [] }, + { typeId: 'app', date: toCaminoDate('2024-04-08'), isBrouillon: ETAPE_IS_NOT_BROUILLON, id: newEtapeId(), ordre: 17, statutId: 'def', communes: [] }, + ] + const tested = getPossiblesEtapesTypes( + undefined, + TITRES_TYPES_IDS.PERMIS_EXCLUSIF_DE_RECHERCHES_METAUX, + DEMARCHES_TYPES_IDS.Octroi, + ETAPES_TYPES.receptionDeComplements, + rcoId, + toCaminoDate('2019-06-06'), + etapes + ) + + expect(tested.rco).toMatchInlineSnapshot(` + { + "etapeStatutIds": [ + "fai", + ], + "mainStep": false, + } + `) + }) + test('peut déplacer le Rapport et avis de la DREAL avant la Consultation du public pour les Octrois de Permis exclusif de recherches ', () => { const apdId = newEtapeId('apiId') const etapes: TitreEtapeForMachine[] = [ diff --git a/packages/api/src/business/validations/titre-demarche-etat-validate.ts b/packages/api/src/business/validations/titre-demarche-etat-validate.ts index 611b6d24b6f5597b1a3f2ad4d57d571bc26186a8..74b86d705aee475355e3350e0bcb8dc795da34f1 100644 --- a/packages/api/src/business/validations/titre-demarche-etat-validate.ts +++ b/packages/api/src/business/validations/titre-demarche-etat-validate.ts @@ -5,7 +5,7 @@ import type { ITitreEtape } from '../../types' import { Etape, TitreEtapeForMachine, titreEtapeForMachineValidator, toMachineEtapes } from '../rules-demarches/machine-common' import { demarcheEnregistrementDemandeDateFind, DemarcheId } from 'camino-common/src/demarche' import { DemarchesTypes, DemarcheTypeId } from 'camino-common/src/static/demarchesTypes' -import { ETAPE_IS_BROUILLON, EtapeId, EtapeTypeEtapeStatutWithMainStep } from 'camino-common/src/etape' +import { ETAPE_IS_BROUILLON, ETAPE_IS_NOT_BROUILLON, EtapeId, EtapeTypeEtapeStatutWithMainStep } from 'camino-common/src/etape' import { TitreTypeId } from 'camino-common/src/static/titresTypes' import { CaminoMachines, machineFind } from '../rules-demarches/machines' import { CaminoDate } from 'camino-common/src/date' @@ -180,7 +180,19 @@ export const getPossiblesEtapesTypes = ( } // On ne peut pas avoir 2 fois le même type d'étape en brouillon - const etapeTypeIdsInBrouillon = demarcheEtapes.filter(({ isBrouillon, id }) => id !== etapeId && isBrouillon === ETAPE_IS_BROUILLON).map(({ typeId }) => typeId) + const etapeTypeIdsInBrouillon = demarcheEtapes + .filter(({ isBrouillon, id }) => { + if (isNullOrUndefined(etapeId)) { + return isBrouillon === ETAPE_IS_BROUILLON + } else { + const etape = demarcheEtapes.find(myEtape => myEtape.id === etapeId) + if (etape?.isBrouillon === ETAPE_IS_NOT_BROUILLON) { + return false + } + return id !== etapeId && isBrouillon === ETAPE_IS_BROUILLON + } + }) + .map(({ typeId }) => typeId) for (const etapeTypeIdInBrouillon of etapeTypeIdsInBrouillon) { delete etapesTypes[etapeTypeIdInBrouillon] diff --git a/packages/common/src/titres.test.ts b/packages/common/src/titres.test.ts index 63284fbbcd7a7e2499bff6d063a25409d31f6632..bfcb78a21ff56d822e2785efbaccf37cb7c3ab11 100644 --- a/packages/common/src/titres.test.ts +++ b/packages/common/src/titres.test.ts @@ -1,12 +1,17 @@ import { describe, expect, test } from 'vitest' -import { TitrePropTitreEtapeFindDemarcheEtape, createAutomaticallyEtapeWhenCreatingTitre, getMostRecentValuePropFromEtapeFondamentaleValide } from './titres' +import { + TitrePropTitreEtapeFindDemarcheEtape, + createAutomaticallyEtapeWhenCreatingTitre, + getMostRecentValuePropFromEtapeFondamentale, + getMostRecentValuePropFromEtapeFondamentaleValide, +} from './titres' import { toCaminoDate } from './date' import { entrepriseIdValidator } from './entreprise' import { ETAPE_IS_BROUILLON, ETAPE_IS_NOT_BROUILLON } from './etape' import { testBlankUser } from './tests-utils' import { ETAPES_TYPES } from './static/etapesTypes' import { ETAPES_STATUTS } from './static/etapesStatuts' -describe('getMostRecentValuePropFromEtapeFondamentaleValide', () => { +describe('getMostRecentValuePropFromEtapeFondamentaleValide et non valide', () => { test("retourne le titulaire de la demande même si elle est en brouillon, si elle est l'unique étape", () => { const acg: TitrePropTitreEtapeFindDemarcheEtape = { etape_type_id: ETAPES_TYPES.saisinesEtAvisCGE_AE, @@ -29,15 +34,21 @@ describe('getMostRecentValuePropFromEtapeFondamentaleValide', () => { is_brouillon: ETAPE_IS_BROUILLON, ordre: 1, } - - expect( - getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [ + const testedValide = getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [ + { + etapes: [acg, mfr], + ordre: 1, + }, + ]) + expect(testedValide).toStrictEqual(mfr.fondamentale.titulaireIds) + expect(testedValide).toStrictEqual( + getMostRecentValuePropFromEtapeFondamentale('titulaireIds', [ { etapes: [acg, mfr], ordre: 1, }, ]) - ).toStrictEqual(mfr.fondamentale.titulaireIds) + ) }) test('retourne le dernier titulaire même si les étapes ne sont pas dans le bon ordre', () => { const dpu: TitrePropTitreEtapeFindDemarcheEtape = { @@ -72,8 +83,19 @@ describe('getMostRecentValuePropFromEtapeFondamentaleValide', () => { ordre: 1, } - expect( - getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [ + const testedValid = getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [ + { + etapes: [dpu, dex], + ordre: 1, + }, + { + etapes: [], + ordre: 2, + }, + ]) + expect(testedValid).toStrictEqual(dpu.fondamentale.titulaireIds) + expect(testedValid).toStrictEqual( + getMostRecentValuePropFromEtapeFondamentale('titulaireIds', [ { etapes: [dpu, dex], ordre: 1, @@ -83,20 +105,28 @@ describe('getMostRecentValuePropFromEtapeFondamentaleValide', () => { ordre: 2, }, ]) - ).toStrictEqual(dpu.fondamentale.titulaireIds) + ) - expect( - getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [ + const testedValidInverse = getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [ + { + etapes: [dex, dpu], + ordre: 1, + }, + ]) + expect(testedValidInverse).toStrictEqual(dpu.fondamentale.titulaireIds) + expect(testedValidInverse).toStrictEqual( + getMostRecentValuePropFromEtapeFondamentale('titulaireIds', [ { etapes: [dex, dpu], ordre: 1, }, ]) - ).toStrictEqual(dpu.fondamentale.titulaireIds) + ) }) test("retourne null s'il n'y a aucune étape fondamentale", () => { expect(getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [])).toBe(null) + expect(getMostRecentValuePropFromEtapeFondamentale('titulaireIds', [])).toBe(null) }) test('ignore une étape si sa propriété est null', () => { @@ -140,6 +170,14 @@ describe('getMostRecentValuePropFromEtapeFondamentaleValide', () => { }, ]) ).toStrictEqual(240) + expect( + getMostRecentValuePropFromEtapeFondamentale('duree', [ + { + etapes: [dpu, dex], + ordre: 1, + }, + ]) + ).toStrictEqual(null) }) }) diff --git a/packages/common/src/titres.ts b/packages/common/src/titres.ts index fa593498ffc5299952565da9e7a49360e3d47d1f..860550f80f4058e4e47c16b59fb9e6bff9998796 100644 --- a/packages/common/src/titres.ts +++ b/packages/common/src/titres.ts @@ -153,6 +153,27 @@ export const getMostRecentValuePropFromEtapeFondamentaleValide = < return null } +export const getMostRecentValuePropFromEtapeFondamentale = < + P extends 'titulaireIds' | 'amodiataireIds' | 'perimetre' | 'substances' | 'duree', + F extends Pick<DemarcheEtapeFondamentale, 'etape_statut_id' | 'etape_type_id' | 'ordre' | 'fondamentale' | 'is_brouillon'>, + NF extends Pick<DemarcheEtapeNonFondamentale, 'etape_statut_id' | 'etape_type_id' | 'ordre' | 'is_brouillon'>, +>( + propId: P, + titreDemarches: TitrePropTitreEtapeFindDemarche<F | NF>[] +): DemarcheEtapeFondamentale['fondamentale'][P] | null => { + const titreDemarchesDesc: TitrePropTitreEtapeFindDemarche<F | NF>[] = [...titreDemarches].sort((a, b) => b.ordre - a.ordre) + + for (const titreDemarche of titreDemarchesDesc) { + const titreEtapeDesc = [...titreDemarche.etapes].sort((a, b) => b.ordre - a.ordre).filter((etape): etape is F => 'fondamentale' in etape) + for (const titreEtape of titreEtapeDesc) { + if (isFondamentalesStatutOk(titreEtape.etape_statut_id) && (titreEtape.is_brouillon === ETAPE_IS_NOT_BROUILLON || titreEtapeDesc.length === 1)) { + return titreEtape.fondamentale[propId] + } + } + } + + return null +} export const getDemarcheByIdOrSlugValidator = z.object({ demarche_id: demarcheIdValidator, diff --git a/packages/ui/src/components/demarche/demarche-etape.tsx b/packages/ui/src/components/demarche/demarche-etape.tsx index ce2539487f069f9dc9acd4e4138cfe587a3b59f1..073fdde0a12a11913d46bc56d4800b68c33dece5 100644 --- a/packages/ui/src/components/demarche/demarche-etape.tsx +++ b/packages/ui/src/components/demarche/demarche-etape.tsx @@ -30,7 +30,7 @@ import { RemoveEtapePopup } from './remove-etape-popup' import { SDOMZoneId } from 'camino-common/src/static/sdom' import { DeposeEtapePopup } from './depose-etape-popup' import { ApiClient } from '@/api/api-client' -import { TitreGetDemarche, getMostRecentValuePropFromEtapeFondamentaleValide } from 'camino-common/src/titres' +import { TitreGetDemarche, getMostRecentValuePropFromEtapeFondamentale } from 'camino-common/src/titres' import { Unites } from 'camino-common/src/static/unites' import { EntrepriseId, Entreprise } from 'camino-common/src/entreprise' import { Badge } from '../_ui/badge' @@ -148,12 +148,11 @@ export const DemarcheEtape = defineComponent<Props>(props => { ) const isDeposable = computed<boolean>(() => { - const titulaireIds = getMostRecentValuePropFromEtapeFondamentaleValide('titulaireIds', [{ ...props.demarche, ordre: 0 }]) - const amodiataireIds = getMostRecentValuePropFromEtapeFondamentaleValide('amodiataireIds', [{ ...props.demarche, ordre: 0 }]) - const perimetre = getMostRecentValuePropFromEtapeFondamentaleValide('perimetre', [{ ...props.demarche, ordre: 0 }]) - const substances = getMostRecentValuePropFromEtapeFondamentaleValide('substances', [{ ...props.demarche, ordre: 0 }]) - const duree = getMostRecentValuePropFromEtapeFondamentaleValide('duree', [{ ...props.demarche, ordre: 0 }]) - + const titulaireIds = getMostRecentValuePropFromEtapeFondamentale('titulaireIds', [{ ...props.demarche, ordre: 0 }]) + const amodiataireIds = getMostRecentValuePropFromEtapeFondamentale('amodiataireIds', [{ ...props.demarche, ordre: 0 }]) + const perimetre = getMostRecentValuePropFromEtapeFondamentale('perimetre', [{ ...props.demarche, ordre: 0 }]) + const substances = getMostRecentValuePropFromEtapeFondamentale('substances', [{ ...props.demarche, ordre: 0 }]) + const duree = getMostRecentValuePropFromEtapeFondamentale('duree', [{ ...props.demarche, ordre: 0 }]) const sections = getSections(props.titre.typeId, props.demarche.demarche_type_id, props.etape.etape_type_id) const sortedEtapes = [...props.demarche.etapes].sort((a, b) => b.ordre - a.ordre) const contenu: FlattenEtape['contenu'] = {}