From e65f881eb48101328fe7dba0d87464909fd2c686 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?BITARD=20Micha=C3=ABl?= <michael.bitard@beta.gouv.fr>
Date: Mon, 7 Apr 2025 13:50:59 +0000
Subject: [PATCH] chore(api): passe en effect la route pour supprimer un
 utilisateur (pub/pnm-public/camino!1694)

---
 .../api/graphql/resolvers/titres-activites.ts |   2 +-
 .../api/src/api/rest/activites.queries.ts     |  55 ++--
 .../src/api/rest/activites.queries.types.ts   |  20 ++
 .../api/rest/activites.test.integration.ts    | 150 +++++++++++
 packages/api/src/api/rest/activites.ts        | 190 +++++++++-----
 .../src/api/rest/etapes.test.integration.ts   |   4 +-
 .../api/rest/utilisateurs.test.integration.ts |  41 ++-
 packages/api/src/api/rest/utilisateurs.ts     | 185 ++++++-------
 ...es-etapes-consentement.test.integration.ts |   4 +-
 .../utilisateur-updation-validate.test.ts     | 248 ++++++++++--------
 .../utilisateur-updation-validate.ts          |  94 ++++---
 .../database/queries/utilisateurs.queries.ts  |  58 ++--
 packages/api/src/server/rest.ts               |  30 +--
 packages/api/src/tools/fp-tools.ts            |  12 +-
 packages/common/src/activite.ts               |  12 +
 packages/common/src/rest.ts                   |  15 +-
 packages/ui/src/api/client-rest.ts            |   9 -
 .../components/activite-edition.stories.tsx   |   4 +-
 .../ui/src/components/activite-edition.tsx    |  27 +-
 .../activite/activite-api-client.ts           |   7 +-
 packages/ui/src/components/dashboard.tsx      |  77 ++++--
 .../dashboard/dashboard-api-client.ts         |   3 +
 .../pure-super-dashboard.stories.tsx          | 140 +++++++++-
 ...rd.stories_snapshots_LoadingActivites.html |  29 ++
 ...rd.stories_snapshots_LoadingBrouillon.html |  29 ++
 ...ories_snapshots_TableauPleinActivites.html |  69 +++++
 ...ories_snapshots_TableauPleinBrouillon.html |  76 ++++++
 ...tories_snapshots_TableauVideActivites.html |  25 ++
 ...tories_snapshots_TableauVideBrouillon.html |  25 ++
 ....stories_snapshots_WithErrorActivites.html |  31 +++
 ....stories_snapshots_WithErrorBrouillon.html |  31 +++
 .../dashboard/pure-super-dashboard.tsx        | 204 +++++++++++---
 .../ui/src/components/utilisateur.stories.tsx |   4 +-
 packages/ui/src/components/utilisateur.tsx    |  28 +-
 .../utilisateur/permission-edit.stories.tsx   |   8 +-
 .../utilisateur/permission-edit.tsx           |  36 ++-
 .../utilisateur/remove-popup.stories.tsx      |  11 +-
 .../components/utilisateur/remove-popup.tsx   |  16 +-
 .../utilisateur/utilisateur-api-client.ts     |  10 +-
 39 files changed, 1507 insertions(+), 512 deletions(-)
 create mode 100644 packages/api/src/api/rest/activites.test.integration.ts
 create mode 100644 packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_LoadingActivites.html
 create mode 100644 packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_LoadingBrouillon.html
 create mode 100644 packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauPleinActivites.html
 create mode 100644 packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauPleinBrouillon.html
 create mode 100644 packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauVideActivites.html
 create mode 100644 packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauVideBrouillon.html
 create mode 100644 packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_WithErrorActivites.html
 create mode 100644 packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_WithErrorBrouillon.html

diff --git a/packages/api/src/api/graphql/resolvers/titres-activites.ts b/packages/api/src/api/graphql/resolvers/titres-activites.ts
index a0edd0fd9..b09adfc65 100644
--- a/packages/api/src/api/graphql/resolvers/titres-activites.ts
+++ b/packages/api/src/api/graphql/resolvers/titres-activites.ts
@@ -168,7 +168,7 @@ export const activiteDeposer = async ({ id }: { id: ActiviteId }, { user, pool }
 
     const activite = await callAndExit(getActiviteById(id, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires))
 
-    const activitesDocuments = await getActiviteDocumentsByActiviteId(id, pool)
+    const activitesDocuments = await callAndExit(getActiviteDocumentsByActiviteId(id, pool))
     const sectionsWithValue = getSectionsWithValue(activite.sections, activite.contenu)
     if (!(await isActiviteDeposable(user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, { ...activite, sections_with_value: sectionsWithValue }, activitesDocuments))) {
       throw new Error('droits insuffisants')
diff --git a/packages/api/src/api/rest/activites.queries.ts b/packages/api/src/api/rest/activites.queries.ts
index c3b29e96b..d03e594cf 100644
--- a/packages/api/src/api/rest/activites.queries.ts
+++ b/packages/api/src/api/rest/activites.queries.ts
@@ -11,15 +11,18 @@ import {
   IInsertActiviteDocumentInternalQuery,
   IUpdateActiviteDbQuery,
   IActiviteDeleteDbQuery,
+  IGetActivitesSuperDbQuery,
 } from './activites.queries.types'
 import {
   ActiviteDocument,
   ActiviteDocumentId,
   ActiviteId,
   ActiviteIdOrSlug,
+  ActiviteSuper,
   activiteDocumentIdValidator,
   activiteDocumentValidator,
   activiteIdValidator,
+  activiteSuperValidator,
   activiteValidator,
 } from 'camino-common/src/activite'
 import { Pool } from 'pg'
@@ -53,7 +56,7 @@ export const titreTypeIdByActiviteId = (activiteId: ActiviteIdOrSlug, pool: Pool
   )
 
 const miseAJourActiviteInterdite = `Interdiction d'éditer une activité` as const
-type UpdateActiviteQueryErrors = EffectDbQueryAndValidateErrors | typeof miseAJourActiviteInterdite
+export type UpdateActiviteQueryErrors = EffectDbQueryAndValidateErrors | typeof miseAJourActiviteInterdite
 export const updateActiviteQuery = (
   pool: Pool,
   user: User,
@@ -119,7 +122,7 @@ const dbActiviteValidator = activiteValidator
 
 const activiteInterdite = `Lecture de l'activité impossible` as const
 const activiteIntrouvable = `Pas d'activité trouvée` as const
-type GetActiviteByIdErrors = EffectDbQueryAndValidateErrors | typeof activiteInterdite | typeof activiteIntrouvable
+export type GetActiviteByIdErrors = EffectDbQueryAndValidateErrors | typeof activiteInterdite | typeof activiteIntrouvable
 export type DbActivite = z.infer<typeof dbActiviteValidator>
 export const getActiviteById = (
   activiteId: ActiviteIdOrSlug,
@@ -204,8 +207,8 @@ delete from activites_documents
 where activite_id = $ activiteId !
 `
 
-export const getActiviteDocumentsByActiviteId = async (activiteId: ActiviteId, pool: Pool): Promise<ActiviteDocument[]> => {
-  return dbQueryAndValidate(
+export const getActiviteDocumentsByActiviteId = (activiteId: ActiviteId, pool: Pool): Effect.Effect<ActiviteDocument[], CaminoError<EffectDbQueryAndValidateErrors>> =>
+  effectDbQueryAndValidate(
     getActiviteDocumentsInternal,
     {
       activiteId,
@@ -213,7 +216,6 @@ export const getActiviteDocumentsByActiviteId = async (activiteId: ActiviteId, p
     pool,
     activiteDocumentValidator
   )
-}
 
 export const administrationsLocalesByActiviteId = async (activiteId: ActiviteIdOrSlug, pool: Pool): Promise<AdministrationId[]> => {
   const admins = await dbQueryAndValidate(getAdministrationsLocalesByActiviteId, { activiteId }, pool, administrationsLocalesValidator)
@@ -285,26 +287,30 @@ where
     d.activite_id = $ activiteId !
 `
 
-export const deleteActiviteDocument = async (
+const droitsInsuffisanstPourSupprimerLeDocument = "droits insuffisants pour supprimer un document d'activité" as const
+export type DeleteActiviteDocumentErrors = EffectDbQueryAndValidateErrors | typeof droitsInsuffisanstPourSupprimerLeDocument
+export const deleteActiviteDocument = (
   id: ActiviteDocumentId,
   activiteDocumentTypeId: ActiviteDocumentTypeId,
   activiteTypeId: ActivitesTypesId,
   activiteStatutId: ActivitesStatutId,
   pool: Pool
-): Promise<void[]> => {
-  if (!canDeleteActiviteDocument(activiteDocumentTypeId, activiteTypeId, activiteStatutId)) {
-    throw new Error('droits insuffisants')
-  }
-
-  return dbQueryAndValidate(deleteActiviteDocumentQuery, { id }, pool, z.void())
-}
+): Effect.Effect<true, CaminoError<DeleteActiviteDocumentErrors>> =>
+  Effect.Do.pipe(
+    Effect.filterOrFail(
+      () => canDeleteActiviteDocument(activiteDocumentTypeId, activiteTypeId, activiteStatutId),
+      () => ({ message: droitsInsuffisanstPourSupprimerLeDocument })
+    ),
+    Effect.flatMap(() => effectDbQueryAndValidate(deleteActiviteDocumentQuery, { id }, pool, z.void())),
+    Effect.map(() => true as const)
+  )
 
 const deleteActiviteDocumentQuery = sql<Redefine<IDeleteActiviteDocumentQueryQuery, { id: ActiviteDocumentId }, void>>`
 delete from activites_documents
 where id = $ id !
 `
 
-export const insertActiviteDocument = async (
+export const insertActiviteDocument = (
   pool: Pool,
   params: {
     id: ActiviteDocumentId
@@ -314,7 +320,8 @@ export const insertActiviteDocument = async (
     description: string
     largeobject_id: number
   }
-): Promise<{ id: ActiviteDocumentId }[]> => dbQueryAndValidate(insertActiviteDocumentInternal, params, pool, z.object({ id: activiteDocumentIdValidator }))
+): Effect.Effect<{ id: ActiviteDocumentId }[], CaminoError<EffectDbQueryAndValidateErrors>> =>
+  effectDbQueryAndValidate(insertActiviteDocumentInternal, params, pool, z.object({ id: activiteDocumentIdValidator }))
 
 const insertActiviteDocumentInternal = sql<
   Redefine<
@@ -375,3 +382,21 @@ where
     d.id = $ activiteDocumentId !
 LIMIT 1
 `
+
+export const getActivitesSuper = (pool: Pool): Effect.Effect<ActiviteSuper[], CaminoError<EffectDbQueryAndValidateErrors>> =>
+  Effect.Do.pipe(Effect.flatMap(() => effectDbQueryAndValidate(getActivitesSuperDb, {}, pool, activiteSuperValidator)))
+
+const getActivitesSuperDb = sql<Redefine<IGetActivitesSuperDbQuery, {}, z.infer<typeof activiteSuperValidator>>>`
+select
+    t.nom as titre_nom,
+    t.type_id as titre_type_id,
+    ta.id,
+    ta.annee,
+    ta.type_id,
+    ta.periode_id,
+    ta.activite_statut_id
+from titres_activites ta
+left join titres t on ta.titre_id = t.id
+where ta.suppression is true
+order by t.nom asc, ta.annee asc, ta.periode_id asc
+`
diff --git a/packages/api/src/api/rest/activites.queries.types.ts b/packages/api/src/api/rest/activites.queries.types.ts
index d55d52363..5e6a94c7d 100644
--- a/packages/api/src/api/rest/activites.queries.types.ts
+++ b/packages/api/src/api/rest/activites.queries.types.ts
@@ -197,3 +197,23 @@ export interface IGetLargeobjectIdByActiviteDocumentIdInternalQuery {
   result: IGetLargeobjectIdByActiviteDocumentIdInternalResult;
 }
 
+/** 'GetActivitesSuperDb' parameters type */
+export type IGetActivitesSuperDbParams = void;
+
+/** 'GetActivitesSuperDb' return type */
+export interface IGetActivitesSuperDbResult {
+  activite_statut_id: string;
+  annee: number;
+  id: string;
+  periode_id: number;
+  titre_nom: string;
+  titre_type_id: string;
+  type_id: string;
+}
+
+/** 'GetActivitesSuperDb' query type */
+export interface IGetActivitesSuperDbQuery {
+  params: IGetActivitesSuperDbParams;
+  result: IGetActivitesSuperDbResult;
+}
+
diff --git a/packages/api/src/api/rest/activites.test.integration.ts b/packages/api/src/api/rest/activites.test.integration.ts
new file mode 100644
index 000000000..cdd8a4c92
--- /dev/null
+++ b/packages/api/src/api/rest/activites.test.integration.ts
@@ -0,0 +1,150 @@
+/* eslint-disable sql/no-unsafe-query */
+import { restCall, restNewPutCall } from '../../../tests/_utils/index'
+import { dbManager } from '../../../tests/db-manager'
+import { test, describe, afterAll, beforeAll, vi, expect } from 'vitest'
+import type { Pool } from 'pg'
+import { toCaminoDate } from 'camino-common/src/date'
+import { insertTitreGraph, ITitreInsert } from '../../../tests/integration-test-helper'
+import { ETAPE_IS_NOT_BROUILLON } from 'camino-common/src/etape'
+import { newTitreId, newDemarcheId, newEtapeId, idGenerate } from '../../database/models/_format/id-create'
+import { TITRES_TYPES_IDS } from 'camino-common/src/static/titresTypes'
+import { DEMARCHES_TYPES_IDS } from 'camino-common/src/static/demarchesTypes'
+import { DemarchesStatutsIds } from 'camino-common/src/static/demarchesStatuts'
+import { TitresStatutIds } from 'camino-common/src/static/titresStatuts'
+import { Knex } from 'knex'
+import { ACTIVITES_TYPES_IDS } from 'camino-common/src/static/activitesTypes'
+import { ACTIVITES_STATUTS_IDS } from 'camino-common/src/static/activitesStatuts'
+import { ETAPES_TYPES } from 'camino-common/src/static/etapesTypes'
+import { ETAPES_STATUTS } from 'camino-common/src/static/etapesStatuts'
+import { Activite, activiteDocumentIdValidator, activiteIdValidator } from 'camino-common/src/activite'
+import { userSuper } from '../../database/user-super'
+import { DOCUMENTS_TYPES_IDS } from 'camino-common/src/static/documentsTypes'
+import { copyFileSync, mkdirSync } from 'node:fs'
+import { tempDocumentNameValidator } from 'camino-common/src/document'
+import { SectionWithValue } from 'camino-common/src/sections'
+
+const dir = `${process.cwd()}/files/tmp/`
+
+console.info = vi.fn()
+console.error = vi.fn()
+let dbPool: Pool
+let knex: Knex
+beforeAll(async () => {
+  const { knex: knexInstance, pool } = await dbManager.populateDb()
+  dbPool = pool
+  knex = knexInstance
+})
+
+afterAll(async () => {
+  await dbManager.closeKnex()
+})
+
+describe('updateActivite', () => {
+  test('met à jour une activité', async () => {
+    const titreId = newTitreId('titre-id')
+    const titre: ITitreInsert = {
+      id: titreId,
+      nom: 'mon titre',
+      typeId: TITRES_TYPES_IDS.AUTORISATION_DE_RECHERCHE_METAUX,
+      titreStatutId: TitresStatutIds.Valide,
+      publicLecture: true,
+      propsTitreEtapesIds: { points: 'titre-id-demarche-id-dpu' },
+      demarches: [
+        {
+          id: newDemarcheId('titre-id-demarche-id'),
+          titreId: titreId,
+          typeId: DEMARCHES_TYPES_IDS.Octroi,
+          statutId: DemarchesStatutsIds.Accepte,
+          publicLecture: true,
+          etapes: [
+            {
+              id: newEtapeId('titre-id-demarche-id-dpu'),
+              typeId: ETAPES_TYPES.publicationDeDecisionAuJORF,
+              ordre: 0,
+              titreDemarcheId: newDemarcheId('titre-id-demarche-id'),
+              statutId: ETAPES_STATUTS.ACCEPTE,
+              date: toCaminoDate('2020-02-02'),
+              administrationsLocales: ['dea-guyane-01'],
+              isBrouillon: ETAPE_IS_NOT_BROUILLON,
+            },
+          ],
+        },
+      ],
+    }
+
+    await insertTitreGraph(titre)
+
+    const activiteId = activiteIdValidator.parse('activite1Id')
+    await knex.raw(
+      `INSERT INTO titres_activites (  id, titre_id, utilisateur_id, "date", date_saisie, contenu, type_id, activite_statut_id, annee, periode_id, sections, suppression, slug) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,?,?,?,?)`,
+      [
+        activiteId,
+        titreId,
+        null,
+        toCaminoDate('2021-01-01'),
+        null,
+        null,
+        ACTIVITES_TYPES_IDS["rapport trimestriel d'exploitation d'or en Guyane"],
+        ACTIVITES_STATUTS_IDS.ABSENT,
+        2023,
+        1,
+        [JSON.stringify({ id: 'id', elements: [{ id: 'element', type: 'number', optionnel: true }] })],
+        false,
+        'slug',
+      ]
+    )
+
+    const activite1DocumentId = activiteDocumentIdValidator.parse('activiteDocumentId1')
+    const activite2DocumentId = activiteDocumentIdValidator.parse('activiteDocumentId2')
+    await knex.raw('INSERT INTO activites_documents(id,activite_document_type_id,"date",activite_id,description,largeobject_id) VALUES (?, ?,?,?,?,?), (?, ?,?,?,?,?)', [
+      activite1DocumentId,
+      DOCUMENTS_TYPES_IDS.rapportAnnuelDExploitation,
+      toCaminoDate('2021-01-01'),
+      activiteId,
+      '',
+      54300,
+      activite2DocumentId,
+      DOCUMENTS_TYPES_IDS.rapportAnnuelDExploitation,
+      toCaminoDate('2023-01-01'),
+      activiteId,
+      '',
+      54300,
+    ])
+
+    let tested = await restNewPutCall(dbPool, '/rest/activites/:activiteId', { activiteId: activiteId }, undefined, { sectionsWithValue: [], activiteDocumentIds: [], newTempDocuments: [] })
+
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Accès interdit",
+        "status": 403,
+      }
+    `)
+    const fileName = `existing_temp_file_${idGenerate()}`
+    mkdirSync(dir, { recursive: true })
+    copyFileSync(`./src/tools/small.pdf`, `${dir}/${fileName}`)
+
+    const sectionWithValue: SectionWithValue[] = [{ id: 'id', elements: [{ id: 'element', type: 'number', value: 12, optionnel: true }] }]
+    tested = await restNewPutCall(dbPool, '/rest/activites/:activiteId', { activiteId: activiteId }, userSuper, {
+      sectionsWithValue: sectionWithValue,
+      activiteDocumentIds: [activite1DocumentId],
+      newTempDocuments: [
+        { activite_document_type_id: DOCUMENTS_TYPES_IDS.rapportEnvironnementalDExploration, description: 'Une jolie description', tempDocumentName: tempDocumentNameValidator.parse(fileName) },
+      ],
+    })
+
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "id": "activite1Id",
+      }
+    `)
+
+    tested = await restCall(dbPool, '/rest/activites/:activiteId', { activiteId }, userSuper)
+    const activite: Activite = tested.body
+    expect(activite.activite_statut_id).toStrictEqual(ACTIVITES_STATUTS_IDS.EN_CONSTRUCTION)
+    expect(activite.sections_with_value).toStrictEqual(sectionWithValue)
+    const newDocumentIds = activite.activite_documents.map(document => document.id)
+    expect(newDocumentIds).not.toContain(activite2DocumentId)
+    expect(newDocumentIds).toContain(activite1DocumentId)
+    expect(activite.activite_documents).toHaveLength(2)
+  })
+})
diff --git a/packages/api/src/api/rest/activites.ts b/packages/api/src/api/rest/activites.ts
index 7010e6dbc..b84b5b613 100644
--- a/packages/api/src/api/rest/activites.ts
+++ b/packages/api/src/api/rest/activites.ts
@@ -1,13 +1,14 @@
 import { CaminoRequest, CustomResponse } from './express-type'
 import { HTTP_STATUS } from 'camino-common/src/http'
 import { Pool } from 'pg'
-import { Activite, activiteDocumentIdValidator, activiteEditionValidator, activiteIdOrSlugValidator, activiteIdValidator } from 'camino-common/src/activite'
+import { Activite, activiteDocumentIdValidator, ActiviteId, activiteIdOrSlugValidator, activiteIdValidator, ActiviteSuper } from 'camino-common/src/activite'
 import {
   Contenu,
   administrationsLocalesByActiviteId,
   deleteActiviteDocument,
   entreprisesTitulairesOuAmoditairesByActiviteId,
   getActiviteById,
+  GetActiviteByIdErrors,
   getActiviteDocumentsByActiviteId,
   getLargeobjectIdByActiviteDocumentId,
   insertActiviteDocument,
@@ -15,22 +16,29 @@ import {
   updateActiviteQuery,
   DbActivite,
   activiteDeleteQuery,
+  UpdateActiviteQueryErrors,
+  DeleteActiviteDocumentErrors,
+  getActivitesSuper,
 } from './activites.queries'
 import { NewDownload } from './fichiers'
-import { SimplePromiseFn, isNonEmptyArray, isNullOrUndefined, memoize } from 'camino-common/src/typescript-tools'
+import { SimplePromiseFn, isNonEmptyArray, memoize } from 'camino-common/src/typescript-tools'
 import { canEditActivite, isActiviteDeposable } from 'camino-common/src/permissions/activites'
 import { SectionWithValue } from 'camino-common/src/sections'
 import { Section, getSectionsWithValue } from 'camino-common/src/static/titresTypes_demarchesTypes_etapesTypes/sections'
 import { newActiviteDocumentId } from '../../database/models/_format/id-create'
 import { ACTIVITES_STATUTS_IDS } from 'camino-common/src/static/activitesStatuts'
 import { Unites } from 'camino-common/src/static/unites'
-import { User } from 'camino-common/src/roles'
+import { isSuper, User } from 'camino-common/src/roles'
 import { TitreTypeId } from 'camino-common/src/static/titresTypes'
 import { AdministrationId } from 'camino-common/src/static/administrations'
 import { EntrepriseId } from 'camino-common/src/entreprise'
 import { getCurrent } from 'camino-common/src/date'
-import { createLargeObject } from '../../database/largeobjects'
+import { createLargeObject, CreateLargeObjectError } from '../../database/largeobjects'
 import { callAndExit } from '../../tools/fp-tools'
+import { RestNewGetCall, RestNewPutCall } from '../../server/rest'
+import { CaminoApiError } from '../../types'
+import { Effect, Match, Option } from 'effect'
+import { EffectDbQueryAndValidateErrors } from '../../pg-database'
 
 const extractContenuFromSectionWithValue = (sections: Section[], sectionsWithValue: SectionWithValue[]): Contenu => {
   const contenu: Contenu = {}
@@ -58,74 +66,91 @@ const extractContenuFromSectionWithValue = (sections: Section[], sectionsWithVal
   return contenu
 }
 
-export const updateActivite =
-  (pool: Pool) =>
-  async (req: CaminoRequest, res: CustomResponse<void>): Promise<void> => {
-    const activiteIdParsed = activiteIdOrSlugValidator.safeParse(req.params.activiteId)
-    const user = req.auth
-
-    if (!activiteIdParsed.success) {
-      res.sendStatus(HTTP_STATUS.BAD_REQUEST)
-    } else if (isNullOrUndefined(user)) {
-      res.sendStatus(HTTP_STATUS.BAD_REQUEST)
-    } else {
-      try {
-        const titreTypeId = memoize(() => callAndExit(titreTypeIdByActiviteId(activiteIdParsed.data, pool)))
-        const administrationsLocales = memoize(() => administrationsLocalesByActiviteId(activiteIdParsed.data, pool))
-        const entreprisesTitulairesOuAmodiataires = memoize(() => entreprisesTitulairesOuAmoditairesByActiviteId(activiteIdParsed.data, pool))
-
-        const result = await callAndExit(getActiviteById(activiteIdParsed.data, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires))
+const canEditActiviteError = "Impossible de vérifier si on peut éditer l'activité" as const
+const editionDeLActiviteImpossible = "Droit insuffisants pour éditer l'activité" as const
+type UpdateActiviteErrors =
+  | GetActiviteByIdErrors
+  | typeof canEditActiviteError
+  | typeof editionDeLActiviteImpossible
+  | UpdateActiviteQueryErrors
+  | DeleteActiviteDocumentErrors
+  | CreateLargeObjectError
+export const updateActivite: RestNewPutCall<'/rest/activites/:activiteId'> = (rootPipe): Effect.Effect<{ id: ActiviteId }, CaminoApiError<UpdateActiviteErrors>> =>
+  rootPipe.pipe(
+    Effect.let('titreTypeId', ({ params, pool }) => memoize(() => callAndExit(titreTypeIdByActiviteId(params.activiteId, pool)))),
+    Effect.let('administrationsLocales', ({ params, pool }) => memoize(() => administrationsLocalesByActiviteId(params.activiteId, pool))),
+    Effect.let('entreprisesTitulairesOuAmodiataires', ({ params, pool }) => memoize(() => entreprisesTitulairesOuAmoditairesByActiviteId(params.activiteId, pool))),
+    Effect.bind('result', ({ params, pool, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, user }) =>
+      getActiviteById(params.activiteId, pool, user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires)
+    ),
+    Effect.tap(({ user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, result }) =>
+      Effect.tryPromise({
+        try: () => canEditActivite(user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, result.activite_statut_id),
+        catch: e => ({ message: canEditActiviteError, extra: e }),
+      }).pipe(
+        Effect.filterOrFail(
+          canRead => canRead,
+          () => ({ message: editionDeLActiviteImpossible })
+        )
+      )
+    ),
+    Effect.let('contenu', ({ result, body }) => extractContenuFromSectionWithValue(result.sections, body.sectionsWithValue)),
+    Effect.tap(({ pool, user, result, contenu, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires }) =>
+      updateActiviteQuery(pool, user, result.id, contenu, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires)
+    ),
+    Effect.bind('oldActiviteDocuments', ({ pool, result }) => getActiviteDocumentsByActiviteId(result.id, pool)),
+    Effect.tap(({ body, oldActiviteDocuments, pool, result }) => {
+      const alreadyExistingDocumentIds = body.activiteDocumentIds
 
-        if (!(await canEditActivite(user, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires, result.activite_statut_id))) {
-          res.sendStatus(HTTP_STATUS.FORBIDDEN)
-        } else {
-          const parsed = activiteEditionValidator.safeParse(req.body)
-
-          if (!parsed.success) {
-            res.sendStatus(HTTP_STATUS.BAD_REQUEST)
-          } else {
-            const contenu = extractContenuFromSectionWithValue(result.sections, parsed.data.sectionsWithValue)
-            await callAndExit(updateActiviteQuery(pool, user, result.id, contenu, titreTypeId, administrationsLocales, entreprisesTitulairesOuAmodiataires))
-
-            const activiteDocumentsToCreate = parsed.data.newTempDocuments
-            const alreadyExistingDocumentIds = parsed.data.activiteDocumentIds
-            const oldActiviteDocuments = await getActiviteDocumentsByActiviteId(result.id, pool)
-
-            if (isNonEmptyArray(oldActiviteDocuments)) {
-              // supprime les anciens documents ou ceux qui n'ont pas de fichier
-              for (const oldActiviteDocument of oldActiviteDocuments) {
-                const documentId = alreadyExistingDocumentIds.find(id => id === oldActiviteDocument.id)
-
-                if (!documentId) {
-                  await deleteActiviteDocument(oldActiviteDocument.id, oldActiviteDocument.activite_document_type_id, result.type_id, ACTIVITES_STATUTS_IDS.EN_CONSTRUCTION, pool)
-                }
-              }
-            }
-
-            for (const document of activiteDocumentsToCreate) {
-              const loid = await callAndExit(createLargeObject(pool, document.tempDocumentName))
-
-              const date = getCurrent()
-
-              await insertActiviteDocument(pool, {
-                id: newActiviteDocumentId(date, document.activite_document_type_id),
-                activite_document_type_id: document.activite_document_type_id,
-                description: document.description ?? '',
-                date,
-                largeobject_id: loid,
-                activite_id: result.id,
-              })
-            }
-
-            res.sendStatus(HTTP_STATUS.NO_CONTENT)
+      if (isNonEmptyArray(oldActiviteDocuments)) {
+        return Effect.forEach(oldActiviteDocuments, oldActiviteDocument => {
+          const documentId = alreadyExistingDocumentIds.find(id => id === oldActiviteDocument.id)
+          if (!documentId) {
+            return deleteActiviteDocument(oldActiviteDocument.id, oldActiviteDocument.activite_document_type_id, result.type_id, ACTIVITES_STATUTS_IDS.EN_CONSTRUCTION, pool)
           }
-        }
-      } catch (e: any) {
-        console.error(e)
-        res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR)
+          return Effect.succeed(true)
+        })
       }
-    }
-  }
+      return Effect.succeed(Option.none)
+    }),
+    Effect.tap(({ body, pool, result }) =>
+      Effect.forEach(body.newTempDocuments, document => {
+        return Effect.Do.pipe(
+          Effect.flatMap(() => createLargeObject(pool, document.tempDocumentName)),
+          Effect.flatMap(loid => {
+            const date = getCurrent()
+            return insertActiviteDocument(pool, {
+              id: newActiviteDocumentId(date, document.activite_document_type_id),
+              activite_document_type_id: document.activite_document_type_id,
+              description: document.description ?? '',
+              date,
+              largeobject_id: loid,
+              activite_id: result.id,
+            })
+          })
+        )
+      })
+    ),
+    Effect.map(({ result }) => ({ id: result.id })),
+    Effect.mapError(caminoError =>
+      Match.value(caminoError.message).pipe(
+        Match.when("Pas d'activité trouvée", () => ({ ...caminoError, status: HTTP_STATUS.NOT_FOUND })),
+        Match.whenOr("Droit insuffisants pour éditer l'activité", "Interdiction d'éditer une activité", "droits insuffisants pour supprimer un document d'activité", () => ({
+          ...caminoError,
+          status: HTTP_STATUS.FORBIDDEN,
+        })),
+        Match.whenOr(
+          "Impossible d'exécuter la requête dans la base de données",
+          "Impossible de vérifier si on peut éditer l'activité",
+          "Lecture de l'activité impossible",
+          'Les données en base ne correspondent pas à ce qui est attendu',
+          "impossible d'insérer un fichier en base",
+          () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR })
+        ),
+        Match.exhaustive
+      )
+    )
+  )
 
 const formatActivite = async (
   dbActivite: DbActivite,
@@ -137,7 +162,7 @@ const formatActivite = async (
 ): Promise<Activite> => {
   const sectionsWithValue: SectionWithValue[] = getSectionsWithValue(dbActivite.sections, dbActivite.contenu)
 
-  const activiteDocuments = await getActiviteDocumentsByActiviteId(dbActivite.id, pool)
+  const activiteDocuments = await callAndExit(getActiviteDocumentsByActiviteId(dbActivite.id, pool))
   const deposable = await isActiviteDeposable(
     user,
     titreTypeId,
@@ -220,3 +245,28 @@ export const activiteDocumentDownload: NewDownload = async (params, user, pool)
 
   return { loid: activiteDocumentLargeObjectId, fileName: activiteDocumentId }
 }
+
+const permissionsInsuffisantes = 'Permissions insuffisantes' as const
+type GetActivitesSuperErrors = EffectDbQueryAndValidateErrors | typeof permissionsInsuffisantes
+
+export const getActivitesForTDBSuper: RestNewGetCall<'/rest/activitesSuper'> = (rootPipe): Effect.Effect<ActiviteSuper[], CaminoApiError<GetActivitesSuperErrors>> =>
+  rootPipe.pipe(
+    Effect.filterOrFail(
+      ({ user }) => isSuper(user),
+      () => ({ message: permissionsInsuffisantes })
+    ),
+    Effect.flatMap(({ pool }) => getActivitesSuper(pool)),
+    Effect.mapError(caminoError =>
+      Match.value(caminoError.message).pipe(
+        Match.whenOr('Permissions insuffisantes', () => ({
+          ...caminoError,
+          status: HTTP_STATUS.FORBIDDEN,
+        })),
+        Match.whenOr("Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
+          ...caminoError,
+          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
+        })),
+        Match.exhaustive
+      )
+    )
+  )
diff --git a/packages/api/src/api/rest/etapes.test.integration.ts b/packages/api/src/api/rest/etapes.test.integration.ts
index f68ea39b8..2ada5d99e 100644
--- a/packages/api/src/api/rest/etapes.test.integration.ts
+++ b/packages/api/src/api/rest/etapes.test.integration.ts
@@ -438,9 +438,7 @@ describe('getEtapeAvis', () => {
     await expect(callAndExit(insertEtapeAvisWithLargeObjectId(dbPool, etapeId, { ...avis, description: '' }, etapeAvisIdValidator.parse('avisId'), largeObjectIdValidator.parse(42)))).rejects
       .toThrowErrorMatchingInlineSnapshot(`
       [Error: Impossible d'exécuter la requête dans la base de données
-       extra: new row for relation "etape_avis" violates check constraint "etape_avis_description_required"
-      detail: undefined
-       zod: undefined]
+       extra: new row for relation "etape_avis" violates check constraint "etape_avis_description_required"]
     `)
 
     await callAndExit(insertEtapeAvisWithLargeObjectId(dbPool, etapeId, avis, etapeAvisIdValidator.parse('avisId'), largeObjectIdValidator.parse(42)))
diff --git a/packages/api/src/api/rest/utilisateurs.test.integration.ts b/packages/api/src/api/rest/utilisateurs.test.integration.ts
index ff81c9bc5..b48ba5cf9 100644
--- a/packages/api/src/api/rest/utilisateurs.test.integration.ts
+++ b/packages/api/src/api/rest/utilisateurs.test.integration.ts
@@ -1,4 +1,4 @@
-import { restCall, restNewCall, restNewPostCall, restPostCall, userGenerate } from '../../../tests/_utils/index'
+import { restNewCall, restNewPostCall, userGenerate } from '../../../tests/_utils/index'
 import { dbManager } from '../../../tests/db-manager'
 import { Knex } from 'knex'
 import { expect, test, describe, afterAll, beforeAll, vi, beforeEach } from 'vitest'
@@ -67,9 +67,14 @@ describe('utilisateurModifier', () => {
       entrepriseIds: [],
       administrationId: null,
     }
-    const tested = await restPostCall(dbPool, '/rest/utilisateurs/:id/permission', { id: utilisateurToEdit.id }, undefined, utilisateurToEdit)
+    const tested = await restNewPostCall(dbPool, '/rest/utilisateurs/:id/permission', { id: utilisateurToEdit.id }, undefined, utilisateurToEdit)
 
-    expect(tested.statusCode).toBe(403)
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "message": "Accès interdit",
+        "status": 403,
+      }
+    `)
   })
 
   test("peut modifier le rôle d'un compte utilisateur", async () => {
@@ -81,7 +86,7 @@ describe('utilisateurModifier', () => {
       entrepriseIds: [],
       administrationId: 'aut-97300-01',
     }
-    const tested = await restPostCall(
+    const tested = await restNewPostCall(
       dbPool,
       '/rest/utilisateurs/:id/permission',
       { id: userToEdit.id },
@@ -91,17 +96,21 @@ describe('utilisateurModifier', () => {
       utilisateurToEdit
     )
 
-    expect(tested.statusCode).toBe(204)
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "id": "defaut-user",
+      }
+    `)
   })
 })
 
 describe('utilisateurSupprimer', () => {
   test('ne peut pas supprimer un compte (utilisateur anonyme)', async () => {
-    const tested = await restCall(dbPool, '/rest/utilisateurs/:id/delete', { id: utilisateurIdValidator.parse('test') }, undefined)
-    expect(tested.statusCode).toBe(500)
+    const tested = await restNewCall(dbPool, '/rest/utilisateurs/:id/delete', { id: utilisateurIdValidator.parse('test') }, undefined)
     expect(tested.body).toMatchInlineSnapshot(`
       {
-        "error": "droits insuffisants",
+        "message": "Droits insuffisants pour supprimer l'utilisateur",
+        "status": 403,
       }
     `)
   })
@@ -112,7 +121,7 @@ describe('utilisateurSupprimer', () => {
     renewConfig()
     const user = await userGenerate(dbPool, { role: 'defaut' })
 
-    const tested = await restCall(dbPool, '/rest/utilisateurs/:id/delete', { id: user.id }, { role: 'defaut' })
+    const tested = await restNewCall(dbPool, '/rest/utilisateurs/:id/delete', { id: user.id }, { role: 'defaut' })
     expect(tested.statusCode).toBe(302)
     expect(tested.header.location).toBe(`${OAUTH_URL}/oauth2/sign_out`)
   })
@@ -129,16 +138,20 @@ describe('utilisateurSupprimer', () => {
       keycloakId: idUserKeycloakRecognised,
     })
 
-    const tested = await restCall(dbPool, '/rest/utilisateurs/:id/delete', { id }, { role: 'super' })
-    expect(tested.statusCode).toBe(204)
+    const tested = await restNewCall(dbPool, '/rest/utilisateurs/:id/delete', { id }, { role: 'super' })
+    expect(tested.body).toMatchInlineSnapshot(`
+      {
+        "id": "user-todelete",
+      }
+    `)
   })
 
   test('ne peut pas supprimer un utilisateur inexistant (utilisateur super)', async () => {
-    const tested = await restCall(dbPool, '/rest/utilisateurs/:id/delete', { id: utilisateurIdValidator.parse('not-existing') }, { role: 'super' })
-    expect(tested.statusCode).toBe(500)
+    const tested = await restNewCall(dbPool, '/rest/utilisateurs/:id/delete', { id: utilisateurIdValidator.parse('not-existing') }, { role: 'super' })
     expect(tested.body).toMatchInlineSnapshot(`
       {
-        "error": "aucun utilisateur avec cet id ou droits insuffisants pour voir cet utilisateur",
+        "message": "Impossible de trouver l'utilisateur",
+        "status": 400,
       }
     `)
   })
diff --git a/packages/api/src/api/rest/utilisateurs.ts b/packages/api/src/api/rest/utilisateurs.ts
index d6a30267b..e0b8d5e36 100644
--- a/packages/api/src/api/rest/utilisateurs.ts
+++ b/packages/api/src/api/rest/utilisateurs.ts
@@ -1,13 +1,12 @@
-import { CaminoRequest, CustomResponse } from './express-type'
 import { CaminoApiError } from '../../types'
 import { HTTP_STATUS } from 'camino-common/src/http'
-import { User, UserNotNull, utilisateurIdValidator } from 'camino-common/src/roles'
+import { User, UserNotNull, UtilisateurId } from 'camino-common/src/roles'
 import { utilisateursFormatTable } from './format/utilisateurs'
 import { tableConvert } from './_convert'
 import { fileNameCreate } from '../../tools/file-name-create'
-import { QGISTokenRest, qgisTokenValidator, utilisateursSearchParamsValidator, UtilisateursTable, utilisateurToEdit } from 'camino-common/src/utilisateur'
+import { QGISTokenRest, qgisTokenValidator, utilisateursSearchParamsValidator, UtilisateursTable } from 'camino-common/src/utilisateur'
 import { idGenerate } from '../../database/models/_format/id-create'
-import { utilisateurUpdationValidate } from '../../business/validations/utilisateur-updation-validate'
+import { utilisateurUpdationValidate, UtilisateurUpdationValidateErrors } from '../../business/validations/utilisateur-updation-validate'
 import { canDeleteUtilisateur } from 'camino-common/src/permissions/utilisateurs'
 import { Pool } from 'pg'
 import { isNotNullNorUndefined, isNullOrUndefined } from 'camino-common/src/typescript-tools'
@@ -16,50 +15,51 @@ import { Effect, Match } from 'effect'
 import { RestNewGetCall, RestNewPostCall } from '../../server/rest'
 import {
   getKeycloakIdByUserId,
-  getUtilisateurById,
+  GetKeycloakIdByUserIdErrors,
   GetUtilisateurByIdErrors,
   getUtilisateursFilteredAndSorted,
+  GetUtilisateursFilteredAndSortedErrors,
   newGetUtilisateurById,
   softDeleteUtilisateur,
   updateUtilisateurRole,
 } from '../../database/queries/utilisateurs.queries'
 import { EffectDbQueryAndValidateErrors } from '../../pg-database'
-import { callAndExit, shortCircuitError, zodParseEffect, ZodUnparseable } from '../../tools/fp-tools'
+import { callAndExit, shortCircuitError, zodParseEffectTyped } from '../../tools/fp-tools'
 import { z } from 'zod'
 import { getEntreprises } from './entreprises.queries'
 import { fetch } from 'undici'
 import { updateQgisToken } from './utilisateurs.queries'
 
-export const updateUtilisateurPermission =
-  (pool: Pool) =>
-  async (req: CaminoRequest, res: CustomResponse<void>): Promise<void> => {
-    const user = req.auth
-
-    if (!req.params.id) {
-      res.sendStatus(HTTP_STATUS.FORBIDDEN)
-    } else {
-      const userId = utilisateurIdValidator.parse(req.params.id)
-      const utilisateurOld = await getUtilisateurById(pool, userId, user)
-
-      if (!user || !utilisateurOld) {
-        res.sendStatus(HTTP_STATUS.FORBIDDEN)
-      } else {
-        try {
-          const utilisateur = utilisateurToEdit.parse(req.body)
-
-          utilisateurUpdationValidate(user, utilisateur, utilisateurOld)
-
-          await updateUtilisateurRole(pool, utilisateur)
-
-          res.sendStatus(HTTP_STATUS.NO_CONTENT)
-        } catch (e) {
-          console.error(e)
-
-          res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR)
-        }
-      }
-    }
-  }
+type UpdateUtilisateurPermissionErrors = EffectDbQueryAndValidateErrors | GetUtilisateurByIdErrors | UtilisateurUpdationValidateErrors
+export const updateUtilisateurPermission: RestNewPostCall<'/rest/utilisateurs/:id/permission'> = (rootPipe): Effect.Effect<{ id: UtilisateurId }, CaminoApiError<UpdateUtilisateurPermissionErrors>> =>
+  rootPipe.pipe(
+    Effect.bind('utilisateurOld', ({ params, pool, user }) => newGetUtilisateurById(pool, params.id, user)),
+    Effect.tap(({ user, body, utilisateurOld }) => utilisateurUpdationValidate(user, body, utilisateurOld)),
+    Effect.tap(({ body, pool }) => updateUtilisateurRole(pool, body)),
+    Effect.map(({ params }) => ({ id: params.id })),
+    Effect.mapError(caminoError =>
+      Match.value(caminoError.message).pipe(
+        Match.when("L'utilisateur est invalide", () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR })),
+        Match.whenOr(
+          'droits insuffisants',
+          "Impossible d'exécuter la requête dans la base de données",
+          'Les données en base ne correspondent pas à ce qui est attendu',
+          'utilisateur incorrect',
+          "l'utilisateur n'existe pas",
+          'droits insuffisants',
+          'impossible de modifier son propre rôle',
+          'droits insuffisants pour modifier les rôles',
+          'droits insuffisants pour modifier les administrations',
+          'droits insuffisants pour modifier les entreprises',
+          () => ({
+            ...caminoError,
+            status: HTTP_STATUS.BAD_REQUEST,
+          })
+        ),
+        Match.exhaustive
+      )
+    )
+  )
 
 export type KeycloakAccessTokenResponse = { access_token: string }
 
@@ -96,54 +96,58 @@ const getKeycloakApiToken = async (): Promise<string> => {
   }
 }
 
-export const deleteUtilisateur =
-  (pool: Pool) =>
-  async (req: CaminoRequest, res: CustomResponse<void>): Promise<void> => {
-    const user = req.auth
-
-    if (!req.params.id) {
-      res.sendStatus(HTTP_STATUS.FORBIDDEN)
-    } else {
-      try {
-        const utilisateurId = utilisateurIdValidator.parse(req.params.id)
-
-        if (!canDeleteUtilisateur(user, utilisateurId)) {
-          throw new Error('droits insuffisants')
-        }
-
-        const utilisateurKeycloakId = await getKeycloakIdByUserId(pool, utilisateurId)
-        if (isNullOrUndefined(utilisateurKeycloakId)) {
-          throw new Error('aucun utilisateur avec cet id ou droits insuffisants pour voir cet utilisateur')
-        }
-
-        const authorizationToken = await getKeycloakApiToken()
-
-        const deleteFromKeycloak = await fetch(`${config().KEYCLOAK_URL}/admin/realms/Camino/users/${utilisateurKeycloakId}`, {
-          method: 'DELETE',
-          headers: {
-            authorization: `Bearer ${authorizationToken}`,
-          },
-        })
-        if (!deleteFromKeycloak.ok) {
-          throw new Error(`une erreur est apparue durant la suppression de l'utilisateur sur keycloak`)
-        }
-
-        await softDeleteUtilisateur(pool, utilisateurId)
+const droitsInsuffisantsPourSupprimerLUtilisateur = "Droits insuffisants pour supprimer l'utilisateur" as const
+const erreurLorsDeLaSuppressionDeLUtilisateur = "Erreur lors de la suppression de l'utilisateur" as const
 
-        if (isNotNullNorUndefined(user) && user.id === req.params.id) {
-          const uiUrl = config().OAUTH_URL
-          const oauthLogoutUrl = new URL(`${uiUrl}/oauth2/sign_out`)
-          res.redirect(oauthLogoutUrl.href)
-        } else {
-          res.sendStatus(HTTP_STATUS.NO_CONTENT)
-        }
-      } catch (e: any) {
-        console.error(e)
+type DeleteUtilisateurErrors = GetKeycloakIdByUserIdErrors | typeof droitsInsuffisantsPourSupprimerLUtilisateur | typeof erreurLorsDeLaSuppressionDeLUtilisateur
+export const deleteUtilisateur: RestNewGetCall<'/rest/utilisateurs/:id/delete'> = (rootPipe): Effect.Effect<{ id: UtilisateurId }, CaminoApiError<DeleteUtilisateurErrors>> =>
+  rootPipe.pipe(
+    Effect.let('utilisateurId', ({ params }) => params.id),
+    Effect.filterOrFail(
+      ({ user, utilisateurId }) => canDeleteUtilisateur(user, utilisateurId),
+      () => ({ message: droitsInsuffisantsPourSupprimerLUtilisateur })
+    ),
+    Effect.bind('keycloakId', ({ pool, utilisateurId }) => getKeycloakIdByUserId(pool, utilisateurId)),
+    Effect.tap(({ keycloakId }) =>
+      Effect.tryPromise({
+        try: async () => {
+          const authorizationToken = await getKeycloakApiToken()
+
+          const deleteFromKeycloak = await fetch(`${config().KEYCLOAK_URL}/admin/realms/Camino/users/${keycloakId}`, {
+            method: 'DELETE',
+            headers: {
+              authorization: `Bearer ${authorizationToken}`,
+            },
+          })
+          if (!deleteFromKeycloak.ok) {
+            throw new Error(`une erreur est apparue durant la suppression de l'utilisateur sur keycloak`)
+          }
+        },
+        catch: e => ({ message: erreurLorsDeLaSuppressionDeLUtilisateur, extra: e }),
+      })
+    ),
+    Effect.tap(({ pool, utilisateurId }) => softDeleteUtilisateur(pool, utilisateurId)),
 
-        res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send({ error: e.message ?? `Une erreur s'est produite` })
+    Effect.tap(({ utilisateurId, redirect, user }) => {
+      if (isNotNullNorUndefined(user) && user.id === utilisateurId) {
+        const uiUrl = config().OAUTH_URL
+        const oauthLogoutUrl = new URL(`${uiUrl}/oauth2/sign_out`)
+        redirect(oauthLogoutUrl.href)
       }
-    }
-  }
+    }),
+    Effect.map(({ utilisateurId }) => ({ id: utilisateurId })),
+    Effect.mapError(caminoError =>
+      Match.value(caminoError.message).pipe(
+        Match.when("Droits insuffisants pour supprimer l'utilisateur", () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
+        Match.when("Erreur lors de la suppression de l'utilisateur", () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR })),
+        Match.whenOr("Impossible de trouver l'utilisateur", "Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
+          ...caminoError,
+          status: HTTP_STATUS.BAD_REQUEST,
+        })),
+        Match.exhaustive
+      )
+    )
+  )
 
 export const moi: RestNewGetCall<'/moi'> = (rootPipe): Effect.Effect<User, CaminoApiError<GetUtilisateurByIdErrors>> => {
   return rootPipe.pipe(
@@ -160,7 +164,7 @@ export const moi: RestNewGetCall<'/moi'> = (rootPipe): Effect.Effect<User, Camin
       Match.value(caminoError.message).pipe(
         Match.when('droits insuffisants', () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
         Match.when('Les données en base ne correspondent pas à ce qui est attendu', () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR })),
-        Match.whenOr("Impossible d'exécuter la requête dans la base de données", 'Problème de validation de données', () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })),
+        Match.whenOr("Impossible d'exécuter la requête dans la base de données", "L'utilisateur est invalide", () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })),
 
         Match.exhaustive
       )
@@ -168,14 +172,17 @@ export const moi: RestNewGetCall<'/moi'> = (rootPipe): Effect.Effect<User, Camin
   )
 }
 
-export const generateQgisToken: RestNewPostCall<'/rest/utilisateur/generateQgisToken'> = (rootPipe): Effect.Effect<QGISTokenRest, CaminoApiError<EffectDbQueryAndValidateErrors | ZodUnparseable>> =>
+const impossibleDeGenererUnTokenQgis = 'impossible de générer un token Qgis' as const
+export const generateQgisToken: RestNewPostCall<'/rest/utilisateur/generateQgisToken'> = (
+  rootPipe
+): Effect.Effect<QGISTokenRest, CaminoApiError<EffectDbQueryAndValidateErrors | typeof impossibleDeGenererUnTokenQgis>> =>
   rootPipe.pipe(
-    Effect.bind('token', () => zodParseEffect(qgisTokenValidator, idGenerate())),
+    Effect.bind('token', () => zodParseEffectTyped(qgisTokenValidator, idGenerate(), impossibleDeGenererUnTokenQgis)),
     Effect.tap(({ token, pool, user }) => updateQgisToken(pool, user, token)),
     Effect.map(({ token, user }) => ({ token, url: `https://${user.email.replace('@', '%40')}:${token}@${config().API_HOST}/titres_qgis?` })),
     Effect.mapError(caminoError =>
       Match.value(caminoError.message).pipe(
-        Match.whenOr("Impossible d'exécuter la requête dans la base de données", 'Problème de validation de données', 'Les données en base ne correspondent pas à ce qui est attendu', () => ({
+        Match.whenOr("Impossible d'exécuter la requête dans la base de données", 'Les données en base ne correspondent pas à ce qui est attendu', 'impossible de générer un token Qgis', () => ({
           ...caminoError,
           status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
         })),
@@ -211,7 +218,7 @@ export const utilisateurs =
       : null
   }
 
-type GetUtilisateursError = EffectDbQueryAndValidateErrors | ZodUnparseable | "Impossible d'accéder à la liste des utilisateurs" | 'droits insuffisants'
+type GetUtilisateursError = GetUtilisateursFilteredAndSortedErrors
 
 export const getUtilisateurs: RestNewGetCall<'/rest/utilisateurs'> = (rootPipe): Effect.Effect<UtilisateursTable, CaminoApiError<GetUtilisateursError>> => {
   return rootPipe.pipe(
@@ -229,14 +236,14 @@ export const getUtilisateurs: RestNewGetCall<'/rest/utilisateurs'> = (rootPipe):
           status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
         })),
         Match.when('droits insuffisants', () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
-        Match.when('Problème de validation de données', () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })),
+        Match.when("L'utilisateur est invalide", () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })),
         Match.exhaustive
       )
     )
   )
 }
 
-type GetUtilisateurError = EffectDbQueryAndValidateErrors | ZodUnparseable | 'droits insuffisants'
+type GetUtilisateurError = GetUtilisateurByIdErrors | 'droits insuffisants'
 export const getUtilisateur: RestNewGetCall<'/rest/utilisateurs/:id'> = (rootPipe): Effect.Effect<UserNotNull, CaminoApiError<GetUtilisateurError>> => {
   return rootPipe.pipe(
     Effect.flatMap(({ pool, params, user }) => newGetUtilisateurById(pool, params.id, user)),
@@ -247,7 +254,7 @@ export const getUtilisateur: RestNewGetCall<'/rest/utilisateurs/:id'> = (rootPip
           status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
         })),
         Match.when('droits insuffisants', () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
-        Match.when('Problème de validation de données', () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })),
+        Match.when("L'utilisateur est invalide", () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })),
         Match.exhaustive
       )
     )
diff --git a/packages/api/src/business/processes/titres-etapes-consentement.test.integration.ts b/packages/api/src/business/processes/titres-etapes-consentement.test.integration.ts
index 4cf90bbb7..85475834d 100644
--- a/packages/api/src/business/processes/titres-etapes-consentement.test.integration.ts
+++ b/packages/api/src/business/processes/titres-etapes-consentement.test.integration.ts
@@ -34,9 +34,7 @@ describe('etapeConsentementUpdate', () => {
     const etapeId = newEtapeId()
     await expect(callAndExit(etapeConsentementUpdate(dbPool, etapeId))).rejects.toThrowErrorMatchingInlineSnapshot(`
       [Error: Élément non trouvé dans la base de données
-       extra: [object Object]
-      detail: undefined
-       zod: undefined]
+       extra: [object Object]]
     `)
   })
 
diff --git a/packages/api/src/business/validations/utilisateur-updation-validate.test.ts b/packages/api/src/business/validations/utilisateur-updation-validate.test.ts
index 2cfad9d23..508b629c8 100644
--- a/packages/api/src/business/validations/utilisateur-updation-validate.test.ts
+++ b/packages/api/src/business/validations/utilisateur-updation-validate.test.ts
@@ -2,9 +2,10 @@ import { newEntrepriseId } from 'camino-common/src/entreprise'
 import { Role, UserNotNull } from 'camino-common/src/roles'
 import { AdministrationId } from 'camino-common/src/static/administrations'
 import { testBlankUser } from 'camino-common/src/tests-utils'
-import { test, expect } from 'vitest'
+import { test, expect, vi } from 'vitest'
 import { utilisateurUpdationValidate } from './utilisateur-updation-validate'
 import { newUtilisateurId } from '../../database/models/_format/id-create'
+import { callAndExit } from '../../tools/fp-tools'
 
 const users: Record<Role, UserNotNull> = {
   super: { ...testBlankUser, role: 'super' },
@@ -30,124 +31,161 @@ const users: Record<Role, UserNotNull> = {
 
 const fakeAdministrationId = 'fakeAdminId' as AdministrationId
 
-test('utilisateurUpdationValidate privilege escalation forbidden', () => {
-  expect(() => utilisateurUpdationValidate(users.defaut, { ...users.defaut, role: 'super', administrationId: null, entrepriseIds: [] }, users.defaut)).toThrowErrorMatchingInlineSnapshot(
-    `[Error: droits insuffisants]`
-  )
-  expect(() => utilisateurUpdationValidate(users.admin, { ...users.admin, role: 'super', entrepriseIds: [], administrationId: null }, users.admin)).toThrowErrorMatchingInlineSnapshot(
-    `[Error: droits insuffisants]`
-  )
-  expect(() => utilisateurUpdationValidate(users.lecteur, { ...users.lecteur, role: 'super', entrepriseIds: [], administrationId: null }, users.lecteur)).toThrowErrorMatchingInlineSnapshot(
-    `[Error: droits insuffisants]`
-  )
-  expect(() => utilisateurUpdationValidate(users.editeur, { ...users.editeur, role: 'super', entrepriseIds: [], administrationId: null }, users.editeur)).toThrowErrorMatchingInlineSnapshot(
-    `[Error: droits insuffisants]`
-  )
-  expect(() => utilisateurUpdationValidate(users.entreprise, { ...users.entreprise, role: 'super', entrepriseIds: [], administrationId: null }, users.entreprise)).toThrowErrorMatchingInlineSnapshot(
-    `[Error: droits insuffisants]`
-  )
-  expect(() =>
-    utilisateurUpdationValidate(users["bureau d'études"], { ...users["bureau d'études"], role: 'super', entrepriseIds: [], administrationId: null }, users.entreprise)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+console.error = vi.fn()
+test('utilisateurUpdationValidate privilege escalation forbidden', async () => {
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.defaut, { ...users.defaut, role: 'super', administrationId: null, entrepriseIds: [] }, users.defaut))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.admin, { ...users.admin, role: 'super', entrepriseIds: [], administrationId: null }, users.admin))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.lecteur, { ...users.lecteur, role: 'super', entrepriseIds: [], administrationId: null }, users.lecteur))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.editeur, { ...users.editeur, role: 'super', entrepriseIds: [], administrationId: null }, users.editeur))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.entreprise, { ...users.entreprise, role: 'super', entrepriseIds: [], administrationId: null }, users.entreprise))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users["bureau d'études"], { ...users["bureau d'études"], role: 'super', entrepriseIds: [], administrationId: null }, users.entreprise))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
 
-  expect(() =>
-    utilisateurUpdationValidate(users.editeur, { ...users.editeur, role: 'entreprise', administrationId: null, entrepriseIds: [newEntrepriseId('id')] }, users.editeur)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
-  expect(() =>
-    utilisateurUpdationValidate(users.defaut, { ...users.defaut, role: 'entreprise', administrationId: null, entrepriseIds: [newEntrepriseId('id')] }, users.defaut)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.editeur, { ...users.editeur, role: 'entreprise', administrationId: null, entrepriseIds: [newEntrepriseId('id')] }, users.editeur))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.defaut, { ...users.defaut, role: 'entreprise', administrationId: null, entrepriseIds: [newEntrepriseId('id')] }, users.defaut))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
 })
 
-test('utilisateurUpdationValidate incorrect users throw error', () => {
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'super', administrationId: null, entrepriseIds: [] }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: l'utilisateur n'existe pas]`)
+test('utilisateurUpdationValidate incorrect users throw error', async () => {
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'super', administrationId: null, entrepriseIds: [] }, undefined))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: l'utilisateur n'existe pas]`)
 
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'super', entrepriseIds: [newEntrepriseId('entrepriseId')], administrationId: null }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'super', administrationId: 'aut-97300-01', entrepriseIds: [] }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'super', entrepriseIds: [newEntrepriseId('entrepriseId')], administrationId: null }, undefined))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'super', administrationId: 'aut-97300-01', entrepriseIds: [] }, undefined))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
 
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'defaut', administrationId: null, entrepriseIds: [] }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: l'utilisateur n'existe pas]`)
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'defaut', entrepriseIds: [newEntrepriseId('entrepriseId')], administrationId: null }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'defaut', administrationId: 'aut-97300-01', entrepriseIds: [] }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'defaut', administrationId: null, entrepriseIds: [] }, undefined))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: l'utilisateur n'existe pas]`)
+  await expect(
+    callAndExit(
+      utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'defaut', entrepriseIds: [newEntrepriseId('entrepriseId')], administrationId: null }, undefined)
+    )
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'defaut', administrationId: 'aut-97300-01', entrepriseIds: [] }, undefined))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
 
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'admin', administrationId: null, entrepriseIds: [] }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'admin', entrepriseIds: [newEntrepriseId('entrepriseId')], administrationId: null }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'admin', administrationId: fakeAdministrationId, entrepriseIds: [] }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'admin', administrationId: null, entrepriseIds: [] }, undefined))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'admin', entrepriseIds: [newEntrepriseId('entrepriseId')], administrationId: null }, undefined))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'admin', administrationId: fakeAdministrationId, entrepriseIds: [] }, undefined))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
 
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'entreprise', administrationId: null, entrepriseIds: [] }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'entreprise', administrationId: null, entrepriseIds: [] }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
-  expect(() =>
-    utilisateurUpdationValidate(
-      users.super,
-      { id: newUtilisateurId('utilisateurId'), role: 'entreprise', administrationId: fakeAdministrationId, entrepriseIds: [newEntrepriseId('entrepriseId')] },
-      undefined
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'entreprise', administrationId: null, entrepriseIds: [] }, undefined))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'entreprise', administrationId: null, entrepriseIds: [] }, undefined))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
+  await expect(
+    callAndExit(
+      utilisateurUpdationValidate(
+        users.super,
+        { id: newUtilisateurId('utilisateurId'), role: 'entreprise', administrationId: fakeAdministrationId, entrepriseIds: [newEntrepriseId('entrepriseId')] },
+        undefined
+      )
     )
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: utilisateur incorrect]`)
 
-  expect(() =>
-    utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'super', entrepriseIds: [], administrationId: null }, undefined)
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: l'utilisateur n'existe pas]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.super, { id: newUtilisateurId('utilisateurId'), role: 'super', entrepriseIds: [], administrationId: null }, undefined))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: l'utilisateur n'existe pas]`)
 
-  expect(() =>
-    utilisateurUpdationValidate(
-      users.admin,
-      { id: newUtilisateurId('utilisateurId'), role: 'editeur', administrationId: 'aut-97300-01', entrepriseIds: [] },
-      { ...testBlankUser, id: newUtilisateurId('fakeId'), role: 'admin', administrationId: 'aut-97300-01' }
+  await expect(
+    callAndExit(
+      utilisateurUpdationValidate(
+        users.admin,
+        { id: newUtilisateurId('utilisateurId'), role: 'editeur', administrationId: 'aut-97300-01', entrepriseIds: [] },
+        { ...testBlankUser, id: newUtilisateurId('fakeId'), role: 'admin', administrationId: 'aut-97300-01' }
+      )
     )
-  ).not.toThrowError()
-  expect(() =>
-    utilisateurUpdationValidate(
-      users.admin,
-      { id: newUtilisateurId('utilisateurId'), role: 'admin', administrationId: 'aut-97300-01', entrepriseIds: [] },
-      { ...testBlankUser, id: newUtilisateurId('fakeId'), role: 'editeur', administrationId: 'aut-97300-01' }
+  ).resolves.toMatchInlineSnapshot(`
+    {
+      "administrationId": "aut-97300-01",
+      "email": "email@gmail.com",
+      "id": "fakeId",
+      "nom": "nom",
+      "prenom": "prenom",
+      "role": "admin",
+      "telephone_fixe": null,
+      "telephone_mobile": null,
+    }
+  `)
+  await expect(
+    callAndExit(
+      utilisateurUpdationValidate(
+        users.admin,
+        { id: newUtilisateurId('utilisateurId'), role: 'admin', administrationId: 'aut-97300-01', entrepriseIds: [] },
+        { ...testBlankUser, id: newUtilisateurId('fakeId'), role: 'editeur', administrationId: 'aut-97300-01' }
+      )
     )
-  ).not.toThrowError()
-  expect(() =>
-    utilisateurUpdationValidate(
-      users.admin,
-      { id: newUtilisateurId('utilisateurId'), role: 'editeur', administrationId: 'aut-mrae-guyane-01', entrepriseIds: [] },
-      { ...testBlankUser, id: newUtilisateurId('fakeId'), role: 'editeur', administrationId: 'aut-97300-01' }
+  ).resolves.toMatchInlineSnapshot(`
+    {
+      "administrationId": "aut-97300-01",
+      "email": "email@gmail.com",
+      "id": "fakeId",
+      "nom": "nom",
+      "prenom": "prenom",
+      "role": "editeur",
+      "telephone_fixe": null,
+      "telephone_mobile": null,
+    }
+  `)
+  await expect(
+    callAndExit(
+      utilisateurUpdationValidate(
+        users.admin,
+        { id: newUtilisateurId('utilisateurId'), role: 'editeur', administrationId: 'aut-mrae-guyane-01', entrepriseIds: [] },
+        { ...testBlankUser, id: newUtilisateurId('fakeId'), role: 'editeur', administrationId: 'aut-97300-01' }
+      )
     )
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
-  expect(() =>
-    utilisateurUpdationValidate(
-      users.admin,
-      { id: newUtilisateurId('utilisateurId'), role: 'editeur', administrationId: 'aut-97300-01', entrepriseIds: [] },
-      { ...testBlankUser, id: newUtilisateurId('fakeId'), role: 'editeur', administrationId: 'aut-mrae-guyane-01' }
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  await expect(
+    callAndExit(
+      utilisateurUpdationValidate(
+        users.admin,
+        { id: newUtilisateurId('utilisateurId'), role: 'editeur', administrationId: 'aut-97300-01', entrepriseIds: [] },
+        { ...testBlankUser, id: newUtilisateurId('fakeId'), role: 'editeur', administrationId: 'aut-mrae-guyane-01' }
+      )
     )
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
 
-  expect(() => utilisateurUpdationValidate(users.editeur, { ...users.editeur, administrationId: 'dea-reunion-01', entrepriseIds: [] }, { ...users.editeur })).toThrowErrorMatchingInlineSnapshot(
-    `[Error: droits insuffisants]`
-  )
-  expect(() => utilisateurUpdationValidate(users.lecteur, { ...users.lecteur, administrationId: 'dea-reunion-01', entrepriseIds: [] }, { ...users.lecteur })).toThrowErrorMatchingInlineSnapshot(
-    `[Error: droits insuffisants]`
-  )
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.editeur, { ...users.editeur, administrationId: 'dea-reunion-01', entrepriseIds: [] }, { ...users.editeur }))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.lecteur, { ...users.lecteur, administrationId: 'dea-reunion-01', entrepriseIds: [] }, { ...users.lecteur }))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
 
-  expect(() =>
-    utilisateurUpdationValidate(users.entreprise, { ...users.entreprise, administrationId: null, entrepriseIds: [newEntrepriseId('newEntreprise')] }, { ...users.entreprise })
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
-  expect(() =>
-    utilisateurUpdationValidate(users["bureau d'études"], { ...users["bureau d'études"], administrationId: null, entrepriseIds: [newEntrepriseId('newEntreprise')] }, { ...users["bureau d'études"] })
-  ).toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  await expect(
+    callAndExit(utilisateurUpdationValidate(users.entreprise, { ...users.entreprise, administrationId: null, entrepriseIds: [newEntrepriseId('newEntreprise')] }, { ...users.entreprise }))
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
+  await expect(
+    callAndExit(
+      utilisateurUpdationValidate(users["bureau d'études"], { ...users["bureau d'études"], administrationId: null, entrepriseIds: [newEntrepriseId('newEntreprise')] }, { ...users["bureau d'études"] })
+    )
+  ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: droits insuffisants]`)
 })
diff --git a/packages/api/src/business/validations/utilisateur-updation-validate.ts b/packages/api/src/business/validations/utilisateur-updation-validate.ts
index ee5144ab8..7d6ef5102 100644
--- a/packages/api/src/business/validations/utilisateur-updation-validate.ts
+++ b/packages/api/src/business/validations/utilisateur-updation-validate.ts
@@ -15,6 +15,9 @@ import { canEditPermission, getAssignableRoles } from 'camino-common/src/permiss
 import { equalStringArrays } from '../../tools/index'
 import { isAdministrationId } from 'camino-common/src/static/administrations'
 import { UtilisateurToEdit } from 'camino-common/src/utilisateur'
+import { CaminoError } from 'camino-common/src/zod-tools'
+import { Effect, Option } from 'effect'
+import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
 
 const userIsCorrect = (utilisateur: UtilisateurToEdit): boolean => {
   if (!isRole(utilisateur.role)) {
@@ -34,34 +37,65 @@ const userIsCorrect = (utilisateur: UtilisateurToEdit): boolean => {
   return false
 }
 
-export const utilisateurUpdationValidate = (user: UserNotNull, utilisateur: UtilisateurToEdit, utilisateurOld: User): void => {
-  if (!userIsCorrect(utilisateur)) {
-    throw new Error('utilisateur incorrect')
-  }
-
-  if (!utilisateurOld) {
-    throw new Error("l'utilisateur n'existe pas")
-  }
-
-  if (!canEditPermission(user, utilisateurOld) || !canEditPermission(user, utilisateur as unknown as UserNotNull)) {
-    throw new Error('droits insuffisants')
-  }
-
-  if (utilisateur.role !== utilisateurOld.role) {
-    if (user.id === utilisateur.id) {
-      throw new Error('impossible de modifier son propre rôle')
-    } else if (!getAssignableRoles(user).includes(utilisateur.role)) {
-      throw new Error('droits insuffisants pour modifier les rôles')
-    }
-  }
-
-  if (!isSuper(user)) {
-    if (isAdministration(utilisateurOld) && utilisateur.administrationId && utilisateur.administrationId !== utilisateurOld.administrationId) {
-      throw new Error('droits insuffisants pour modifier les administrations')
-    }
-
-    if (!isAdministrationAdmin(user) && isEntrepriseOrBureauDEtude(utilisateurOld) && !equalStringArrays(utilisateurOld.entrepriseIds.toSorted(), utilisateur.entrepriseIds.toSorted())) {
-      throw new Error('droits insuffisants pour modifier les entreprises')
-    }
-  }
+const utilisateurIncorrect = 'utilisateur incorrect' as const
+const utilisateurNonExistant = "l'utilisateur n'existe pas" as const
+const droitsInsuffisants = 'droits insuffisants' as const
+const impossibleDeModifierSonRole = 'impossible de modifier son propre rôle' as const
+const impossibleDeModifierLesRoles = 'droits insuffisants pour modifier les rôles' as const
+const droitsInsuffisantsPourModifierAdministration = 'droits insuffisants pour modifier les administrations' as const
+const droitsInsuffisantPourModifierEntreprises = 'droits insuffisants pour modifier les entreprises' as const
+export type UtilisateurUpdationValidateErrors =
+  | typeof utilisateurIncorrect
+  | typeof utilisateurNonExistant
+  | typeof droitsInsuffisants
+  | typeof impossibleDeModifierLesRoles
+  | typeof droitsInsuffisantsPourModifierAdministration
+  | typeof impossibleDeModifierSonRole
+  | typeof droitsInsuffisantPourModifierEntreprises
+export const utilisateurUpdationValidate = (user: UserNotNull, newUtilisateur: UtilisateurToEdit, utilisateurOld: User): Effect.Effect<void, CaminoError<UtilisateurUpdationValidateErrors>> => {
+  return Effect.Do.pipe(
+    Effect.map(() => utilisateurOld),
+    Effect.filterOrFail(
+      () => userIsCorrect(newUtilisateur),
+      () => ({ message: utilisateurIncorrect })
+    ),
+    Effect.filterOrFail(
+      (oldUser): oldUser is UserNotNull => isNotNullNorUndefined(oldUser),
+      () => ({ message: utilisateurNonExistant })
+    ),
+    Effect.filterOrFail(
+      oldUser => canEditPermission(user, oldUser) && canEditPermission(user, newUtilisateur as unknown as UserNotNull),
+      () => ({ message: droitsInsuffisants })
+    ),
+    Effect.tap(oldUser => {
+      if (newUtilisateur.role !== oldUser.role) {
+        return Effect.Do.pipe(
+          Effect.filterOrFail(
+            () => user.id !== newUtilisateur.id,
+            () => ({ message: impossibleDeModifierSonRole })
+          ),
+          Effect.filterOrFail(
+            () => getAssignableRoles(user).includes(newUtilisateur.role),
+            () => ({ message: impossibleDeModifierLesRoles })
+          )
+        )
+      }
+      return Effect.succeed(oldUser)
+    }),
+    Effect.tap(oldUser => {
+      if (!isSuper(user)) {
+        return Effect.Do.pipe(
+          Effect.filterOrFail(
+            () => !isAdministration(oldUser) || newUtilisateur.administrationId === oldUser.administrationId,
+            () => ({ message: droitsInsuffisantsPourModifierAdministration })
+          ),
+          Effect.filterOrFail(
+            () => isAdministrationAdmin(user) || (isEntrepriseOrBureauDEtude(utilisateurOld) && equalStringArrays(utilisateurOld.entrepriseIds.toSorted(), newUtilisateur.entrepriseIds.toSorted())),
+            () => ({ message: droitsInsuffisantPourModifierEntreprises })
+          )
+        )
+      }
+      return Effect.succeed(Option.none)
+    })
+  )
 }
diff --git a/packages/api/src/database/queries/utilisateurs.queries.ts b/packages/api/src/database/queries/utilisateurs.queries.ts
index 93408db21..33d86b318 100644
--- a/packages/api/src/database/queries/utilisateurs.queries.ts
+++ b/packages/api/src/database/queries/utilisateurs.queries.ts
@@ -1,5 +1,5 @@
 import { sql } from '@pgtyped/runtime'
-import { Effect, pipe } from 'effect'
+import { Effect, Option, pipe } from 'effect'
 import { EffectDbQueryAndValidateErrors, Redefine, dbQueryAndValidate, effectDbQueryAndValidate } from '../../pg-database'
 import {
   AdminUserNotNull,
@@ -34,7 +34,7 @@ import {
   IUpdateUtilisateurDbQuery,
   IUpdateUtilisateurRoleDbQuery,
 } from './utilisateurs.queries.types'
-import { ZodUnparseable, callAndExit, zodParseEffect } from '../../tools/fp-tools'
+import { callAndExit, zodParseEffectTyped } from '../../tools/fp-tools'
 import { NonEmptyArray, Nullable, isNotNullNorUndefinedNorEmpty, isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools'
 import { EntrepriseId, entrepriseIdValidator } from 'camino-common/src/entreprise'
 import { CaminoDate } from 'camino-common/src/date'
@@ -53,7 +53,7 @@ const getUtilisateursValidator = z.object({
 })
 export type GetUtilisateur = z.infer<typeof getUtilisateursValidator>
 
-type GetUtilisateursFilteredAndSortedErrors = EffectDbQueryAndValidateErrors | ZodUnparseable | 'droits insuffisants'
+export type GetUtilisateursFilteredAndSortedErrors = EffectDbQueryAndValidateErrors | 'droits insuffisants' | typeof utilisateurInvalid
 export const getUtilisateursFilteredAndSorted = (pool: Pool, user: User, searchParams: UtilisateursSearchParams): Effect.Effect<UserNotNull[], CaminoError<GetUtilisateursFilteredAndSortedErrors>> => {
   return Effect.Do.pipe(
     Effect.filterOrFail(
@@ -69,7 +69,7 @@ export const getUtilisateursFilteredAndSorted = (pool: Pool, user: User, searchP
     Effect.flatMap(utilisateurs => {
       return Effect.forEach(utilisateurs, u => {
         return pipe(
-          zodParseEffect(userNotNullValidator, userDbToUser(u)),
+          zodParseEffectTyped(userNotNullValidator, userDbToUser(u) as UserNotNull, utilisateurInvalid),
           Effect.mapError(error => {
             return { ...error, extra: { email: u.email } }
           })
@@ -167,20 +167,21 @@ const userDbToUser = (
   return { ...user, prenom: user.prenom ?? '', entrepriseIds: user.entreprise_ids ?? [], administrationId: user.administration_id }
 }
 
-export type GetUtilisateurByIdErrors = 'droits insuffisants' | EffectDbQueryAndValidateErrors | ZodUnparseable
+const utilisateurInvalid = "L'utilisateur est invalide" as const
+export type GetUtilisateurByIdErrors = 'droits insuffisants' | EffectDbQueryAndValidateErrors | typeof utilisateurInvalid
 export const newGetUtilisateurById = (pool: Pool, id: UtilisateurId, user: User): Effect.Effect<UserNotNull, CaminoError<GetUtilisateurByIdErrors>> => {
   return pipe(
     effectDbQueryAndValidate(getUtilisateurByIdDb, { id }, pool, getUtilisateursValidator),
     Effect.filterOrFail(
       utilisateurs => isNotNullNorUndefinedNorEmpty(utilisateurs),
-      () => ({ message: 'droits insuffisants' as const })
+      () => ({ message: 'droits insuffisants' as const, detail: 'La liste des utilisateurs retournée par la base est vide' })
     ),
     Effect.flatMap(utilisateurs => {
-      return zodParseEffect(userNotNullValidator, userDbToUser(utilisateurs[0]))
+      return zodParseEffectTyped(userNotNullValidator, userDbToUser(utilisateurs[0]) as UserNotNull, utilisateurInvalid)
     }),
     Effect.filterOrFail(
       utilisateur => canReadUtilisateur(user, utilisateur),
-      () => ({ message: 'droits insuffisants' as const })
+      () => ({ message: 'droits insuffisants' as const, detail: 'Permissions insuffisantes pour accéder aux détails de cet utilisateur' })
     )
   )
 }
@@ -202,11 +203,17 @@ const getUtilisateurByIdDb = sql<Redefine<IGetUtilisateurByIdDbQuery, { id: Util
 
 const getKeycloakIdByUserIdValidator = z.object({ keycloak_id: z.string() })
 type GetKeycloakIdByUser = z.infer<typeof getKeycloakIdByUserIdValidator>
-export const getKeycloakIdByUserId = async (pool: Pool, utilisateurId: UtilisateurId): Promise<string | null> => {
-  const result = await dbQueryAndValidate(getKeycloakIdByUserIdDb, { id: utilisateurId }, pool, getKeycloakIdByUserIdValidator)
 
-  return isNullOrUndefinedOrEmpty(result) ? null : result[0].keycloak_id
-}
+const utilisateurNonTrouve = "Impossible de trouver l'utilisateur" as const
+export type GetKeycloakIdByUserIdErrors = EffectDbQueryAndValidateErrors | typeof utilisateurNonTrouve
+export const getKeycloakIdByUserId = (pool: Pool, utilisateurId: UtilisateurId): Effect.Effect<string, CaminoError<GetKeycloakIdByUserIdErrors>> =>
+  effectDbQueryAndValidate(getKeycloakIdByUserIdDb, { id: utilisateurId }, pool, getKeycloakIdByUserIdValidator).pipe(
+    Effect.filterOrFail(
+      result => isNotNullNorUndefinedNorEmpty(result),
+      () => ({ message: utilisateurNonTrouve })
+    ),
+    Effect.map(result => result[0].keycloak_id)
+  )
 
 const getKeycloakIdByUserIdDb = sql<Redefine<IGetKeycloakIdByUserIdDbQuery, { id: UtilisateurId }, GetKeycloakIdByUser>>`
   select
@@ -343,25 +350,24 @@ const updateUtilisateurDb = sql<Redefine<IUpdateUtilisateurDbQuery, Pick<UserNot
   `
 
 type UpdateUtilisateurRole = Pick<UserNotNull, 'id' | 'role'> & Nullable<Pick<AdminUserNotNull, 'administrationId'>> & Pick<EntrepriseUserNotNull, 'entrepriseIds'>
-export const updateUtilisateurRole = async (pool: Pool, user: UpdateUtilisateurRole): Promise<void> => {
-  await dbQueryAndValidate(updateUtilisateurRoleDb, user, pool, z.void())
-
-  await dbQueryAndValidate(deleteUtilisateurEntrepriseDb, { utilisateur_id: user.id }, pool, z.void())
-
-  if (isEntrepriseOrBureauDetudeRole(user.role)) {
-    for (const entreprise_id of user.entrepriseIds) {
-      await dbQueryAndValidate(createUtilisateurEntrepriseDb, { utilisateur_id: user.id, entreprise_id }, pool, z.void())
-    }
-  }
-}
+export const updateUtilisateurRole = (pool: Pool, user: UpdateUtilisateurRole): Effect.Effect<void, CaminoError<EffectDbQueryAndValidateErrors>> =>
+  Effect.Do.pipe(
+    Effect.tap(() => effectDbQueryAndValidate(updateUtilisateurRoleDb, user, pool, z.void())),
+    Effect.tap(() => effectDbQueryAndValidate(deleteUtilisateurEntrepriseDb, { utilisateur_id: user.id }, pool, z.void())),
+    Effect.tap(() => {
+      if (isEntrepriseOrBureauDetudeRole(user.role)) {
+        return Effect.forEach(user.entrepriseIds, entreprise_id => effectDbQueryAndValidate(createUtilisateurEntrepriseDb, { utilisateur_id: user.id, entreprise_id }, pool, z.void()))
+      }
+      return Effect.succeed(Option.none)
+    })
+  )
 
 const updateUtilisateurRoleDb = sql<Redefine<IUpdateUtilisateurRoleDbQuery, Pick<UserNotNull, 'id' | 'role'> & Nullable<Pick<AdminUserNotNull, 'administrationId'>>, void>>`
   update utilisateurs set role = $role!, administration_id = $administrationId! where id = $id!
   `
 
-export const softDeleteUtilisateur = async (pool: Pool, id: UtilisateurId): Promise<void> => {
-  await dbQueryAndValidate(softDeleteUtilisateurDb, { id }, pool, z.void())
-}
+export const softDeleteUtilisateur = (pool: Pool, id: UtilisateurId): Effect.Effect<void, CaminoError<EffectDbQueryAndValidateErrors>> =>
+  effectDbQueryAndValidate(softDeleteUtilisateurDb, { id }, pool, z.void())
 
 const softDeleteUtilisateurDb = sql<Redefine<ISoftDeleteUtilisateurDbQuery, { id: UtilisateurId }, void>>`
   update utilisateurs set keycloak_id = null, email = null where id = $id!
diff --git a/packages/api/src/server/rest.ts b/packages/api/src/server/rest.ts
index 6b8f2c1ed..9d82737a4 100644
--- a/packages/api/src/server/rest.ts
+++ b/packages/api/src/server/rest.ts
@@ -28,7 +28,6 @@ import {
   contentTypes,
   GetRestRoutes,
   PostRestRoutes,
-  PutRestRoutes,
   DeleteRestRoutes,
   isCaminoRestRoute,
   DownloadRestRoutes,
@@ -46,7 +45,7 @@ import { createEtape, deleteEtape, deposeEtape, getEtape, getEtapeAvis, getEtape
 import { ZodType, z } from 'zod'
 import { getCommunes } from '../api/rest/communes'
 import { SendFileOptions } from 'express-serve-static-core'
-import { activiteDocumentDownload, getActivite, updateActivite, deleteActivite } from '../api/rest/activites'
+import { activiteDocumentDownload, getActivite, updateActivite, deleteActivite, getActivitesForTDBSuper } from '../api/rest/activites'
 import { isNotNullNorUndefined, isNullOrUndefined } from 'camino-common/src/typescript-tools'
 import { getDemarcheByIdOrSlugApi, demarcheSupprimer, demarcheCreer, getDemarchesEnConcurrence, getResultatEnConcurrence } from '../api/rest/demarches'
 import { geojsonImport, geojsonImportPoints, geojsonImportForages, getPerimetreInfosByDemarche, getPerimetreInfosByEtape } from '../api/rest/perimetre'
@@ -120,6 +119,7 @@ export type RestNewGetCall<Route extends NewGetRestRoutes> = (
       params: z.infer<CaminoRestRoutesType[Route]['params']>
       searchParams: SearchParams<Route>
       cookie: CookieParams
+      redirect: (value: string) => void
     },
     never,
     never
@@ -140,7 +140,6 @@ export type RestNewDeleteCall<Route extends NewDeleteRestRoutes> = (
 ) => Effect.Effect<Option.Option<never>, CaminoApiError<string>>
 
 type RestPostCall<Route extends PostRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<z.infer<CaminoRestRoutesType[Route]['post']['output']>>) => Promise<void>
-type RestPutCall<Route extends PutRestRoutes> = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<z.infer<CaminoRestRoutesType[Route]['put']['output']>>) => Promise<void>
 type RestDeleteCall = (pool: Pool) => (req: CaminoRequest, res: CustomResponse<void | Error>) => Promise<void>
 type RestDownloadCall = (pool: Pool) => IRestResolver
 
@@ -149,7 +148,6 @@ type Transform<Route> = (Route extends GetRestRoutes ? { getCall: RestGetCall<Ro
   (Route extends PostRestRoutes ? { postCall: RestPostCall<Route> } : {}) &
   (Route extends NewPostRestRoutes ? { newPostCall: RestNewPostCall<Route> } : {}) &
   (Route extends NewPutRestRoutes ? { newPutCall: RestNewPutCall<Route> } : {}) &
-  (Route extends PutRestRoutes ? { putCall: RestPutCall<Route> } : {}) &
   (Route extends DeleteRestRoutes ? { deleteCall: RestDeleteCall } : {}) &
   (Route extends NewDeleteRestRoutes ? { newDeleteCall: RestNewDeleteCall<Route> } : {}) &
   (Route extends NewDownloadRestRoutes ? { newDownloadCall: NewDownload } : {}) &
@@ -210,8 +208,8 @@ const restRouteImplementations: Readonly<{ [key in CaminoRestRoute]: Transform<k
   '/rest/demarches/:demarcheId/miseEnConcurrence': { newGetCall: getDemarchesEnConcurrence, ...CaminoRestRoutes['/rest/demarches/:demarcheId/miseEnConcurrence'] },
   '/rest/demarches/:demarcheId/resultatMiseEnConcurrence': { newGetCall: getResultatEnConcurrence, ...CaminoRestRoutes['/rest/demarches/:demarcheId/resultatMiseEnConcurrence'] },
   '/rest/utilisateur/generateQgisToken': { newPostCall: generateQgisToken, ...CaminoRestRoutes['/rest/utilisateur/generateQgisToken'] },
-  '/rest/utilisateurs/:id/permission': { postCall: updateUtilisateurPermission, ...CaminoRestRoutes['/rest/utilisateurs/:id/permission'] },
-  '/rest/utilisateurs/:id/delete': { getCall: deleteUtilisateur, ...CaminoRestRoutes['/rest/utilisateurs/:id/delete'] },
+  '/rest/utilisateurs/:id/permission': { newPostCall: updateUtilisateurPermission, ...CaminoRestRoutes['/rest/utilisateurs/:id/permission'] },
+  '/rest/utilisateurs/:id/delete': { newGetCall: deleteUtilisateur, ...CaminoRestRoutes['/rest/utilisateurs/:id/delete'] },
   '/rest/utilisateurs/:id': { newGetCall: getUtilisateur, ...CaminoRestRoutes['/rest/utilisateurs/:id'] },
   '/rest/utilisateurs': { newGetCall: getUtilisateurs, ...CaminoRestRoutes['/rest/utilisateurs'] },
   '/rest/entreprises/:entrepriseId/fiscalite/:annee': { getCall: fiscalite, ...CaminoRestRoutes['/rest/entreprises/:entrepriseId/fiscalite/:annee'] }, // UNTESTED YET
@@ -240,7 +238,8 @@ const restRouteImplementations: Readonly<{ [key in CaminoRestRoute]: Transform<k
   '/rest/etapes/:etapeId/entrepriseDocuments': { newGetCall: getEtapeEntrepriseDocuments, ...CaminoRestRoutes['/rest/etapes/:etapeId/entrepriseDocuments'] },
   '/rest/etapes/:etapeId/etapeDocuments': { newGetCall: getEtapeDocuments, ...CaminoRestRoutes['/rest/etapes/:etapeId/etapeDocuments'] },
   '/rest/etapes/:etapeId/etapeAvis': { newGetCall: getEtapeAvis, ...CaminoRestRoutes['/rest/etapes/:etapeId/etapeAvis'] },
-  '/rest/activites/:activiteId': { getCall: getActivite, putCall: updateActivite, deleteCall: deleteActivite, ...CaminoRestRoutes['/rest/activites/:activiteId'] },
+  '/rest/activites/:activiteId': { getCall: getActivite, newPutCall: updateActivite, deleteCall: deleteActivite, ...CaminoRestRoutes['/rest/activites/:activiteId'] },
+  '/rest/activitesSuper': { newGetCall: getActivitesForTDBSuper, ...CaminoRestRoutes['/rest/activitesSuper'] },
   '/rest/communes': { newGetCall: getCommunes, ...CaminoRestRoutes['/rest/communes'] },
   '/rest/geojson/import/:geoSystemeId': { newPostCall: geojsonImport, ...CaminoRestRoutes['/rest/geojson/import/:geoSystemeId'] },
   '/rest/geojson_points/import/:geoSystemeId': { newPostCall: geojsonImportPoints, ...CaminoRestRoutes['/rest/geojson_points/import/:geoSystemeId'] },
@@ -287,7 +286,8 @@ export const restWithPool = (dbPool: Pool): Router => {
                     Effect.let('cookie', () => ({
                       clearConnectedCookie: () => res.clearCookie('shouldBeConnected'),
                       addConnectedCookie: () => res.cookie('shouldBeConnected', 'anyValueIsGood, We just check the presence of this cookie'),
-                    }))
+                    })),
+                    Effect.let('redirect', () => (redirect: string) => res.redirect(redirect))
                   )
                 )
               }),
@@ -303,10 +303,12 @@ export const restWithPool = (dbPool: Pool): Router => {
                   res.status(caminoError.status).json(caminoError)
                 },
                 onSuccess: ({ parsedResult }) => {
-                  if (isNullOrUndefined(parsedResult)) {
-                    res.sendStatus(HTTP_STATUS.NO_CONTENT)
-                  } else {
-                    res.json(parsedResult)
+                  if (!res.writableEnded) {
+                    if (isNullOrUndefined(parsedResult)) {
+                      res.sendStatus(HTTP_STATUS.NO_CONTENT)
+                    } else {
+                      res.json(parsedResult)
+                    }
                   }
                 },
               }),
@@ -473,10 +475,6 @@ export const restWithPool = (dbPool: Pool): Router => {
           }
         })
       }
-      if ('putCall' in maRoute) {
-        console.info(`PUT ${route}`)
-        rest.put(route, restCatcherWithMutation('put', maRoute.putCall(dbPool), dbPool)) // eslint-disable-line @typescript-eslint/no-misused-promises
-      }
 
       if ('deleteCall' in maRoute) {
         console.info(`delete ${route}`)
diff --git a/packages/api/src/tools/fp-tools.ts b/packages/api/src/tools/fp-tools.ts
index 73202de02..5ced5c460 100644
--- a/packages/api/src/tools/fp-tools.ts
+++ b/packages/api/src/tools/fp-tools.ts
@@ -52,7 +52,17 @@ export const callAndExit = async <A>(toCall: Effect.Effect<A, CaminoError<string
   } else {
     if (Cause.isFailType(pipeline.cause)) {
       console.error(pipeline.cause.error)
-      throw new Error(`${pipeline.cause.error.message}\n extra: ${pipeline.cause.error.extra}\ndetail: ${pipeline.cause.error.detail}\n zod: ${pipeline.cause.error.zodErrorReadableMessage}`)
+      let errorMessage = pipeline.cause.error.message
+      if (isNotNullNorUndefined(pipeline.cause.error.extra)) {
+        errorMessage += `\n extra: ${pipeline.cause.error.extra}`
+      }
+      if (isNotNullNorUndefined(pipeline.cause.error.detail)) {
+        errorMessage += `\n detail: ${pipeline.cause.error.detail}`
+      }
+      if (isNotNullNorUndefined(pipeline.cause.error.zodErrorReadableMessage)) {
+        errorMessage += `\n zod: ${pipeline.cause.error.zodErrorReadableMessage}`
+      }
+      throw new Error(errorMessage)
     } else {
       throw new Error(`Unexpected error ${pipeline.cause}`)
     }
diff --git a/packages/common/src/activite.ts b/packages/common/src/activite.ts
index 624090445..475cde543 100644
--- a/packages/common/src/activite.ts
+++ b/packages/common/src/activite.ts
@@ -5,6 +5,7 @@ import { sectionWithValueValidator } from './sections'
 import { activiteStatutIdValidator } from './static/activitesStatuts'
 import { activiteTypeIdValidator } from './static/activitesTypes'
 import { tempDocumentNameValidator } from './document'
+import { titreTypeIdValidator } from './static/titresTypes'
 
 export const activiteSlugValidator = z.string().brand<'ActiviteSlug'>()
 
@@ -54,3 +55,14 @@ export const activiteEditionValidator = z.object({
   activiteDocumentIds: z.array(activiteDocumentIdValidator),
   newTempDocuments: z.array(tempActiviteDocumentValidator),
 })
+
+export const activiteSuperValidator = z.object({
+  titre_nom: z.string(),
+  titre_type_id: titreTypeIdValidator,
+  id: activiteIdValidator,
+  annee: caminoAnneeValidator,
+  type_id: activiteTypeIdValidator,
+  periode_id: z.number(),
+  activite_statut_id: activiteStatutIdValidator,
+})
+export type ActiviteSuper = z.infer<typeof activiteSuperValidator>
diff --git a/packages/common/src/rest.ts b/packages/common/src/rest.ts
index 96a20d901..78f248921 100644
--- a/packages/common/src/rest.ts
+++ b/packages/common/src/rest.ts
@@ -53,7 +53,7 @@ import { fiscaliteValidator } from './validators/fiscalite'
 import { caminoConfigValidator } from './static/config'
 import { communeIdValidator, communeValidator } from './static/communes'
 import { Expect, isFalse, isNotNullNorUndefined, isTrue } from './typescript-tools'
-import { activiteDocumentIdValidator, activiteEditionValidator, activiteIdOrSlugValidator, activiteValidator } from './activite'
+import { activiteDocumentIdValidator, activiteEditionValidator, activiteIdOrSlugValidator, activiteIdValidator, activiteSuperValidator, activiteValidator } from './activite'
 import { geoSystemeIdValidator } from './static/geoSystemes'
 import {
   geojsonImportBodyValidator,
@@ -74,7 +74,6 @@ type CaminoRoute<T extends string> = { params: ZodObjectParsUrlParams<T> } & {
   newGet?: { output: ZodType; searchParams?: ZodType }
   post?: { input: ZodType; output: ZodType }
   newPost?: { input: ZodType; output: ZodType }
-  put?: { input: ZodType; output: ZodType }
   newPut?: { input: ZodType; output: ZodType }
   newDelete?: true
   delete?: true
@@ -126,6 +125,7 @@ const IDS = [
   '/rest/etapes/:etapeId/depot',
   '/rest/etapes',
   '/rest/activites/:activiteId',
+  '/rest/activitesSuper',
   '/rest/geojson/import/:geoSystemeId',
   '/rest/geojson_points/import/:geoSystemeId',
   '/rest/geojson_forages/import/:geoSystemeId',
@@ -165,8 +165,8 @@ export const CaminoRestRoutes = {
   '/moi': { params: noParamsValidator, newGet: { output: userValidator } },
   '/rest/utilisateurs/:id': { params: utilisateurIdParamsValidator, newGet: { output: userNotNullValidator } },
   // On passe par un http get plutot qu'un http delete car nous terminons par une redirection vers la deconnexion de oauth2, qui se traduit mal sur certains navigateurs et essaie de faire un delete sur une route get
-  '/rest/utilisateurs/:id/delete': { params: utilisateurIdParamsValidator, get: { output: z.void() } },
-  '/rest/utilisateurs/:id/permission': { params: utilisateurIdParamsValidator, post: { input: utilisateurToEdit, output: z.void() } },
+  '/rest/utilisateurs/:id/delete': { params: utilisateurIdParamsValidator, newGet: { output: z.object({ id: utilisateurIdValidator }) } },
+  '/rest/utilisateurs/:id/permission': { params: utilisateurIdParamsValidator, newPost: { input: utilisateurToEdit, output: z.object({ id: utilisateurIdValidator }) } },
   '/rest/utilisateurs': { params: noParamsValidator, newGet: { output: utilisateursTableValidator, searchParams: utilisateursSearchParamsValidator } },
   '/rest/statistiques/minerauxMetauxMetropole': { params: noParamsValidator, get: { output: statistiquesMinerauxMetauxMetropoleValidator } },
   '/rest/statistiques/guyane': { params: noParamsValidator, get: { output: statistiquesGuyaneDataValidator } },
@@ -237,9 +237,13 @@ export const CaminoRestRoutes = {
   '/rest/activites/:activiteId': {
     params: z.object({ activiteId: activiteIdOrSlugValidator }),
     get: { output: activiteValidator },
-    put: { input: activiteEditionValidator, output: z.void() },
+    newPut: { input: activiteEditionValidator, output: z.object({ id: activiteIdValidator }) },
     delete: true,
   },
+  '/rest/activitesSuper': {
+    newGet: { output: z.array(activiteSuperValidator) },
+    params: noParamsValidator,
+  },
   '/rest/communes': { params: noParamsValidator, newGet: { output: z.array(communeValidator), searchParams: z.object({ ids: z.array(communeIdValidator).nonempty() }) } },
   '/rest/geojson/import/:geoSystemeId': {
     params: geoSystemIdParamsValidator,
@@ -313,7 +317,6 @@ export type DeleteRestRoutes = CaminoRestRouteList<typeof IDS, 'delete'>[number]
 export type NewDeleteRestRoutes = CaminoRestRouteList<typeof IDS, 'newDelete'>[number]
 export type DownloadRestRoutes = CaminoRestRouteList<typeof IDS, 'download'>[number]
 export type NewDownloadRestRoutes = CaminoRestRouteList<typeof IDS, 'newDownload'>[number]
-export type PutRestRoutes = CaminoRestRouteList<typeof IDS, 'put'>[number]
 
 export type CaminoRestParams<Route extends CaminoRestRoute> = z.infer<(typeof CaminoRestRoutes)[Route]['params']>
 
diff --git a/packages/ui/src/api/client-rest.ts b/packages/ui/src/api/client-rest.ts
index b5300bfec..1128d1874 100644
--- a/packages/ui/src/api/client-rest.ts
+++ b/packages/ui/src/api/client-rest.ts
@@ -9,7 +9,6 @@ import {
   getRestRoute,
   GetRestRoutes,
   PostRestRoutes,
-  PutRestRoutes,
   NewPostRestRoutes,
   NewGetRestRoutes,
   NewPutRestRoutes,
@@ -231,14 +230,6 @@ export const newPostWithJson = async <T extends NewPostRestRoutes>(
     return { ...errorMessage, extra: e }
   }
 }
-/**
- * @deprecated use newPutWithJson
- **/
-export const putWithJson = async <T extends PutRestRoutes>(
-  path: T,
-  params: CaminoRestParams<T>,
-  body: z.infer<(typeof CaminoRestRoutes)[T]['put']['input']>
-): Promise<z.infer<(typeof CaminoRestRoutes)[T]['put']['output']>> => callFetch(path, params, 'put', {}, body)
 
 export const newPutWithJson = async <T extends NewPutRestRoutes>(
   path: T,
diff --git a/packages/ui/src/components/activite-edition.stories.tsx b/packages/ui/src/components/activite-edition.stories.tsx
index c43a118df..cf12d3c65 100644
--- a/packages/ui/src/components/activite-edition.stories.tsx
+++ b/packages/ui/src/components/activite-edition.stories.tsx
@@ -169,8 +169,8 @@ const activite: Activite = {
 }
 
 const apiClient: Props['apiClient'] = {
-  updateActivite(_activiteId, _sectionsWithValue, _activiteDocumentIds, _newTempDocuments): Promise<void> {
-    return Promise.resolve(undefined)
+  updateActivite(activiteId, _sectionsWithValue, _activiteDocumentIds, _newTempDocuments) {
+    return Promise.resolve({ id: activiteId })
   },
   getActivite: activiteId => {
     getActiviteAction(activiteId)
diff --git a/packages/ui/src/components/activite-edition.tsx b/packages/ui/src/components/activite-edition.tsx
index de6cc2f37..e9a65908d 100644
--- a/packages/ui/src/components/activite-edition.tsx
+++ b/packages/ui/src/components/activite-edition.tsx
@@ -101,23 +101,22 @@ export const PureActiviteEdition = defineComponent<Props>(props => {
 
   const save = async (goBack: boolean) => {
     if (data.value.status === 'LOADED') {
-      try {
-        await props.apiClient.updateActivite(
-          data.value.value.id,
-          data.value.value.type_id,
-          sectionsComplete.value.sectionsWithValue,
-          documentsComplete.value.activiteDocumentIds,
-          documentsComplete.value.tempsDocuments
-        )
+      const result = await props.apiClient.updateActivite(
+        data.value.value.id,
+        data.value.value.type_id,
+        sectionsComplete.value.sectionsWithValue,
+        documentsComplete.value.activiteDocumentIds,
+        documentsComplete.value.tempsDocuments
+      )
+      if ('message' in result) {
+        data.value = {
+          status: 'NEW_ERROR',
+          error: result,
+        }
+      } else {
         if (goBack) {
           props.goBack(data.value.value.id)
         }
-      } catch (e: any) {
-        console.error('error', e)
-        data.value = {
-          status: 'ERROR',
-          message: e.message ?? "Une erreur s'est produite",
-        }
       }
     }
   }
diff --git a/packages/ui/src/components/activite/activite-api-client.ts b/packages/ui/src/components/activite/activite-api-client.ts
index ca9de4ddf..32f8daa1c 100644
--- a/packages/ui/src/components/activite/activite-api-client.ts
+++ b/packages/ui/src/components/activite/activite-api-client.ts
@@ -4,9 +4,10 @@ import { CaminoAnnee } from 'camino-common/src/date'
 import { ActivitesStatutId } from 'camino-common/src/static/activitesStatuts'
 import { ActivitesTypesId } from 'camino-common/src/static/activitesTypes'
 import gql from 'graphql-tag'
-import { deleteWithJson, getWithJson, putWithJson } from '../../api/client-rest'
+import { deleteWithJson, getWithJson, newPutWithJson } from '../../api/client-rest'
 import { SectionWithValue } from 'camino-common/src/sections'
 import { EntrepriseId } from 'camino-common/src/entreprise'
+import { CaminoError } from 'camino-common/src/zod-tools'
 
 export interface UiGraphqlActivite {
   id: string
@@ -33,7 +34,7 @@ export interface ActiviteApiClient {
     sectionsWithValue: SectionWithValue[],
     activiteDocumentIds: ActiviteDocumentId[],
     newTempDocuments: TempActiviteDocument[]
-  ) => Promise<void>
+  ) => Promise<{ id: ActiviteId } | CaminoError<string>>
 }
 
 type GetActivitesParams = {
@@ -125,7 +126,7 @@ export const activiteApiClient: ActiviteApiClient = {
     activiteDocumentIds: ActiviteDocumentId[],
     newTempDocuments: TempActiviteDocument[]
   ) => {
-    return putWithJson(
+    return newPutWithJson(
       '/rest/activites/:activiteId',
       {
         activiteId,
diff --git a/packages/ui/src/components/dashboard.tsx b/packages/ui/src/components/dashboard.tsx
index 727b6bbef..1c3871328 100644
--- a/packages/ui/src/components/dashboard.tsx
+++ b/packages/ui/src/components/dashboard.tsx
@@ -1,9 +1,11 @@
-import { FunctionalComponent, defineAsyncComponent, defineComponent, inject, onMounted, ref } from 'vue'
+import { defineAsyncComponent, defineComponent, inject, onMounted, ref } from 'vue'
 import { useRouter } from 'vue-router'
 import { dashboardApiClient } from './dashboard/dashboard-api-client'
-import { User, isAdministration, isEntrepriseOrBureauDEtude, isSuper } from 'camino-common/src/roles'
+import { AdminUserNotNull, EntrepriseUserNotNull, User, isAdministration, isEntrepriseOrBureauDEtude, isSuper } from 'camino-common/src/roles'
 import { entreprisesKey, userKey } from '@/moi'
 import { Entreprise } from 'camino-common/src/entreprise'
+import { CaminoRouteLocation } from '@/router/routes'
+import { CaminoRouter } from '@/typings/vue-router'
 
 export const Dashboard = defineComponent({
   setup() {
@@ -18,37 +20,60 @@ export const Dashboard = defineComponent({
       }
     })
 
-    return () => <PureDashboard user={user} entreprises={entreprises.value} />
+    return () => <PureDashboard user={user} entreprises={entreprises.value} router={router} route={router.currentRoute.value} />
   },
 })
 
-const PureDashboard: FunctionalComponent<{ user: User; entreprises: Entreprise[] }> = props => {
-  if (isEntrepriseOrBureauDEtude(props.user)) {
-    const PureEntrepriseDashboard = defineAsyncComponent(async () => {
-      const { PureEntrepriseDashboard } = await import('@/components/dashboard/pure-entreprise-dashboard')
+type PureDashboardProps = { user: User; entreprises: Entreprise[]; route: CaminoRouteLocation; router: CaminoRouter }
+const PureDashboard = defineComponent((props: PureDashboardProps) => {
+  return () => (
+    <>
+      {isEntrepriseOrBureauDEtude(props.user) ? <PureEntrepriseDashboard user={props.user} entreprises={props.entreprises} /> : null}
+      {isAdministration(props.user) ? <PureAdministrationDashboard user={props.user} entreprises={props.entreprises} /> : null}
+      {isSuper(props.user) ? <PureSuperDashboard {...props} /> : null}
+    </>
+  )
+})
 
-      return PureEntrepriseDashboard
-    })
-    const entrepriseIds = props.user.entrepriseIds ?? []
+const PureEntrepriseDashboard = defineComponent((props: Pick<PureDashboardProps, 'entreprises'> & { user: EntrepriseUserNotNull }) => {
+  const PureEntrepriseDashboardComponent = defineAsyncComponent(async () => {
+    const { PureEntrepriseDashboard } = await import('@/components/dashboard/pure-entreprise-dashboard')
 
-    return <PureEntrepriseDashboard apiClient={dashboardApiClient} user={props.user} entrepriseIds={entrepriseIds} allEntreprises={props.entreprises} />
-  } else if (isAdministration(props.user)) {
-    const PureAdministrationDashboard = defineAsyncComponent(async () => {
-      const { PureAdministrationDashboard } = await import('@/components/dashboard/pure-administration-dashboard')
+    return PureEntrepriseDashboard
+  })
+  const entrepriseIds = props.user.entrepriseIds ?? []
 
-      return PureAdministrationDashboard
-    })
+  return () => <PureEntrepriseDashboardComponent apiClient={dashboardApiClient} user={props.user} entrepriseIds={entrepriseIds} allEntreprises={props.entreprises} />
+})
 
-    return <PureAdministrationDashboard apiClient={dashboardApiClient} user={props.user} entreprises={props.entreprises} />
-  } else if (isSuper(props.user)) {
-    const PureSuperDashboard = defineAsyncComponent(async () => {
-      const { PureSuperDashboard } = await import('@/components/dashboard/pure-super-dashboard')
+const PureSuperDashboard = defineComponent((props: PureDashboardProps) => {
+  const PureSuperDashboardComponent = defineAsyncComponent(async () => {
+    const { PureSuperDashboard } = await import('@/components/dashboard/pure-super-dashboard')
 
-      return PureSuperDashboard
-    })
+    return PureSuperDashboard
+  })
+
+  return () => <PureSuperDashboardComponent apiClient={dashboardApiClient} user={props.user} route={props.route} router={props.router} />
+})
+
+const PureAdministrationDashboard = defineComponent((props: Pick<PureDashboardProps, 'entreprises'> & { user: AdminUserNotNull }) => {
+  const PureAdministrationDashboardComponent = defineAsyncComponent(async () => {
+    const { PureAdministrationDashboard } = await import('@/components/dashboard/pure-administration-dashboard')
+
+    return PureAdministrationDashboard
+  })
+
+  return () => <PureAdministrationDashboardComponent apiClient={dashboardApiClient} user={props.user} entreprises={props.entreprises} />
+})
+
+// @ts-ignore waiting for https://github.com/vuejs/core/issues/7833
+PureDashboard.props = ['user', 'entreprises', 'route', 'router']
+
+// @ts-ignore waiting for https://github.com/vuejs/core/issues/7833
+PureSuperDashboard.props = ['user', 'entreprises', 'route', 'router']
 
-    return <PureSuperDashboard apiClient={dashboardApiClient} user={props.user} />
-  }
+// @ts-ignore waiting for https://github.com/vuejs/core/issues/7833
+PureAdministrationDashboard.props = ['user', 'entreprises', 'route', 'router']
 
-  return null
-}
+// @ts-ignore waiting for https://github.com/vuejs/core/issues/7833
+PureEntrepriseDashboard.props = ['user', 'entreprises', 'route', 'router']
diff --git a/packages/ui/src/components/dashboard/dashboard-api-client.ts b/packages/ui/src/components/dashboard/dashboard-api-client.ts
index e94ba5b7e..deebe9b65 100644
--- a/packages/ui/src/components/dashboard/dashboard-api-client.ts
+++ b/packages/ui/src/components/dashboard/dashboard-api-client.ts
@@ -1,5 +1,6 @@
 import { apiGraphQLFetch } from '@/api/_client'
 import { getWithJson, newGetWithJson } from '@/api/client-rest'
+import { ActiviteSuper } from 'camino-common/src/activite'
 import { EntrepriseId, TitreEntreprise } from 'camino-common/src/entreprise'
 import { StatistiquesDGTM } from 'camino-common/src/statistiques'
 import { CommonTitreAdministration, SuperTitre } from 'camino-common/src/titres'
@@ -11,6 +12,7 @@ export interface DashboardApiClient {
   getDgtmStats: () => Promise<StatistiquesDGTM>
   getEntreprisesTitres: (entreprisesIds: EntrepriseId[]) => Promise<TitreEntreprise[]>
   getTitresAvecEtapeEnBrouillon: () => Promise<SuperTitre[] | CaminoError<string>>
+  getActivitesSuper: () => Promise<ActiviteSuper[] | CaminoError<string>>
 }
 const titres = apiGraphQLFetch(gql`
   query Titres(
@@ -80,4 +82,5 @@ export const dashboardApiClient: DashboardApiClient = {
     return (await titres({ entreprisesIds })).elements
   },
   getTitresAvecEtapeEnBrouillon: async (): Promise<SuperTitre[] | CaminoError<string>> => newGetWithJson('/rest/titresSuper', {}),
+  getActivitesSuper: () => newGetWithJson('/rest/activitesSuper', {}),
 }
diff --git a/packages/ui/src/components/dashboard/pure-super-dashboard.stories.tsx b/packages/ui/src/components/dashboard/pure-super-dashboard.stories.tsx
index b665f212f..a3b7dfb34 100644
--- a/packages/ui/src/components/dashboard/pure-super-dashboard.stories.tsx
+++ b/packages/ui/src/components/dashboard/pure-super-dashboard.stories.tsx
@@ -4,8 +4,14 @@ import { titreSlugValidator } from 'camino-common/src/validators/titres'
 import { PureSuperDashboard } from './pure-super-dashboard'
 import { SuperTitre } from 'camino-common/src/titres'
 import { demarcheSlugValidator } from 'camino-common/src/demarche'
-import { caminoDateValidator } from 'camino-common/src/date'
+import { caminoDateValidator, toCaminoAnnee } from 'camino-common/src/date'
 import { etapeSlugValidator } from 'camino-common/src/etape'
+import { activiteIdValidator, ActiviteSuper } from 'camino-common/src/activite'
+import { CaminoRouteLocation } from '@/router/routes'
+import { CaminoRouter } from '@/typings/vue-router'
+import { ACTIVITES_TYPES_IDS } from 'camino-common/src/static/activitesTypes'
+import { ACTIVITES_STATUTS_IDS } from 'camino-common/src/static/activitesStatuts'
+import { TITRES_TYPES_IDS } from 'camino-common/src/static/titresTypes'
 
 const meta: Meta = {
   title: 'Components/Dashboard/Super',
@@ -14,6 +20,26 @@ const meta: Meta = {
 }
 export default meta
 
+const activites: ActiviteSuper[] = [
+  {
+    activite_statut_id: ACTIVITES_STATUTS_IDS.DEPOSE,
+    annee: toCaminoAnnee(2025),
+    id: activiteIdValidator.parse('id1'),
+    periode_id: 1,
+    titre_nom: 'Nom du titre',
+    titre_type_id: TITRES_TYPES_IDS.AUTORISATION_D_EXPLOITATION_METAUX,
+    type_id: ACTIVITES_TYPES_IDS["rapport trimestriel d'exploitation d'or en Guyane"],
+  },
+  {
+    activite_statut_id: ACTIVITES_STATUTS_IDS.CLOTURE,
+    annee: toCaminoAnnee(2024),
+    id: activiteIdValidator.parse('id2'),
+    periode_id: 3,
+    titre_nom: 'Nom du titre 2',
+    titre_type_id: TITRES_TYPES_IDS.PERMIS_D_EXPLOITATION_METAUX,
+    type_id: ACTIVITES_TYPES_IDS["rapport d'intensité d'exploration"],
+  },
+]
 const titres: SuperTitre[] = [
   {
     titre_nom: 'Aachen',
@@ -39,10 +65,112 @@ const titres: SuperTitre[] = [
   },
 ]
 
-export const TableauVide: StoryFn = () => <PureSuperDashboard user={{ role: 'super', ...testBlankUser }} apiClient={{ getTitresAvecEtapeEnBrouillon: () => Promise.resolve([]) }} />
-export const TableauPlein: StoryFn = () => <PureSuperDashboard user={{ role: 'super', ...testBlankUser }} apiClient={{ getTitresAvecEtapeEnBrouillon: () => Promise.resolve(titres) }} />
-export const Loading: StoryFn = () => <PureSuperDashboard user={{ role: 'super', ...testBlankUser }} apiClient={{ getTitresAvecEtapeEnBrouillon: () => new Promise<SuperTitre[]>(_resolve => {}) }} />
+const router: Pick<CaminoRouter, 'push'> = {
+  push: _ => {
+    return Promise.resolve()
+  },
+}
+
+const routeBrouillon: CaminoRouteLocation = {
+  name: 'dashboard',
+  params: {},
+  query: { vueId: 'brouillons' },
+}
+
+const routeActivite: CaminoRouteLocation = {
+  name: 'dashboard',
+  params: {},
+  query: { vueId: 'activites' },
+}
+
+export const TableauVideBrouillon: StoryFn = () => (
+  <PureSuperDashboard
+    user={{ role: 'super', ...testBlankUser }}
+    apiClient={{
+      getTitresAvecEtapeEnBrouillon: () => Promise.resolve([]),
+      getActivitesSuper: () => Promise.resolve([]),
+    }}
+    route={routeBrouillon}
+    router={router}
+  />
+)
+export const TableauVideActivites: StoryFn = () => (
+  <PureSuperDashboard
+    user={{ role: 'super', ...testBlankUser }}
+    apiClient={{
+      getTitresAvecEtapeEnBrouillon: () => Promise.resolve([]),
+      getActivitesSuper: () => Promise.resolve([]),
+    }}
+    route={routeActivite}
+    router={router}
+  />
+)
+
+export const TableauPleinBrouillon: StoryFn = () => (
+  <PureSuperDashboard
+    user={{ role: 'super', ...testBlankUser }}
+    apiClient={{
+      getTitresAvecEtapeEnBrouillon: () => Promise.resolve(titres),
+      getActivitesSuper: () => Promise.resolve(activites),
+    }}
+    route={routeBrouillon}
+    router={router}
+  />
+)
+export const TableauPleinActivites: StoryFn = () => (
+  <PureSuperDashboard
+    user={{ role: 'super', ...testBlankUser }}
+    apiClient={{
+      getTitresAvecEtapeEnBrouillon: () => Promise.resolve(titres),
+      getActivitesSuper: () => Promise.resolve(activites),
+    }}
+    route={routeActivite}
+    router={router}
+  />
+)
+export const LoadingBrouillon: StoryFn = () => (
+  <PureSuperDashboard
+    user={{ role: 'super', ...testBlankUser }}
+    apiClient={{
+      getTitresAvecEtapeEnBrouillon: () => new Promise<SuperTitre[]>(_resolve => {}),
+      getActivitesSuper: () => Promise.resolve([]),
+    }}
+    route={routeBrouillon}
+    router={router}
+  />
+)
+export const LoadingActivites: StoryFn = () => (
+  <PureSuperDashboard
+    user={{ role: 'super', ...testBlankUser }}
+    apiClient={{
+      getTitresAvecEtapeEnBrouillon: () => Promise.resolve([]),
+      getActivitesSuper: () => new Promise<ActiviteSuper[]>(_resolve => {}),
+    }}
+    route={routeActivite}
+    router={router}
+  />
+)
+
+export const WithErrorBrouillon: StoryFn = () => (
+  <PureSuperDashboard
+    user={{ role: 'super', ...testBlankUser }}
+    apiClient={{
+      getTitresAvecEtapeEnBrouillon: () => Promise.resolve({ message: 'Une erreur' }),
+      getActivitesSuper: () => Promise.resolve([]),
+    }}
+    route={routeBrouillon}
+    router={router}
+  />
+)
 
-export const WithError: StoryFn = () => (
-  <PureSuperDashboard user={{ role: 'super', ...testBlankUser }} apiClient={{ getTitresAvecEtapeEnBrouillon: () => Promise.resolve({ message: 'Une erreur' }) }} />
+export const WithErrorActivites: StoryFn = () => (
+  <PureSuperDashboard
+    user={{ role: 'super', ...testBlankUser }}
+    apiClient={{
+      getTitresAvecEtapeEnBrouillon: () => Promise.resolve([]),
+      getActivitesSuper: () => Promise.resolve({ message: 'Une erreur' }),
+    }}
+    route={routeActivite}
+    router={router}
+  />
 )
diff --git a/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_LoadingActivites.html b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_LoadingActivites.html
new file mode 100644
index 000000000..12c8a3946
--- /dev/null
+++ b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_LoadingActivites.html
@@ -0,0 +1,29 @@
+<div>
+  <div class="fr-grid-row">
+    <div class="fr-col-12 fr-col-md-6">
+      <h1>Tableau de bord</h1>
+    </div>
+    <div class="fr-col-12 fr-col-md-6" style="display: flex; flex-direction: row; align-items: flex-start; justify-content: flex-end;">
+      <!---->
+      <!---->
+    </div>
+  </div>
+  <div>
+    <div class="fr-tabs" style="--tabs-height: 0px;">
+      <ul class="fr-tabs__list" role="tablist" aria-label="Affichage des titres contenant un brouillon, ou des activités supprimable">
+        <li role="presentation"><button id="tabpanel-brouillons-tdb_super_vues" class="fr-tabs__tab fr-icon-draft-line fr-tabs__tab--icon-left" tabindex="-1" role="tab" aria-label="Brouillons" aria-selected="false" aria-controls="tabpanel-brouillons-tdb_super_vues-panel">Brouillons</button></li>
+        <li role="presentation"><button id="tabpanel-activites-tdb_super_vues" class="fr-tabs__tab fr-icon-delete-bin-line fr-tabs__tab--icon-left" tabindex="0" role="tab" aria-label="Activités" aria-selected="true" aria-controls="tabpanel-activites-tdb_super_vues-panel">Activités</button></li>
+      </ul>
+      <div id="tabpanel-brouillons-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--direction-start" role="tabpanel" aria-labelledby="tabpanel-brouillons-tdb_super_vues" tabindex="0">
+        <!---->
+      </div>
+      <div id="tabpanel-activites-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--selected" role="tabpanel" aria-labelledby="tabpanel-activites-tdb_super_vues" tabindex="0">
+        <div class="_top-level_3306d0" style="display: flex; justify-content: center;">
+          <!---->
+          <!---->
+          <div class="_spinner_3306d0"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_LoadingBrouillon.html b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_LoadingBrouillon.html
new file mode 100644
index 000000000..d6b4bac9b
--- /dev/null
+++ b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_LoadingBrouillon.html
@@ -0,0 +1,29 @@
+<div>
+  <div class="fr-grid-row">
+    <div class="fr-col-12 fr-col-md-6">
+      <h1>Tableau de bord</h1>
+    </div>
+    <div class="fr-col-12 fr-col-md-6" style="display: flex; flex-direction: row; align-items: flex-start; justify-content: flex-end;">
+      <!---->
+      <!---->
+    </div>
+  </div>
+  <div>
+    <div class="fr-tabs" style="--tabs-height: 0px;">
+      <ul class="fr-tabs__list" role="tablist" aria-label="Affichage des titres contenant un brouillon, ou des activités supprimable">
+        <li role="presentation"><button id="tabpanel-brouillons-tdb_super_vues" class="fr-tabs__tab fr-icon-draft-line fr-tabs__tab--icon-left" tabindex="0" role="tab" aria-label="Brouillons" aria-selected="true" aria-controls="tabpanel-brouillons-tdb_super_vues-panel">Brouillons</button></li>
+        <li role="presentation"><button id="tabpanel-activites-tdb_super_vues" class="fr-tabs__tab fr-icon-delete-bin-line fr-tabs__tab--icon-left" tabindex="-1" role="tab" aria-label="Activités" aria-selected="false" aria-controls="tabpanel-activites-tdb_super_vues-panel">Activités</button></li>
+      </ul>
+      <div id="tabpanel-brouillons-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--selected" role="tabpanel" aria-labelledby="tabpanel-brouillons-tdb_super_vues" tabindex="0">
+        <div class="_top-level_3306d0" style="display: flex; justify-content: center;">
+          <!---->
+          <!---->
+          <div class="_spinner_3306d0"></div>
+        </div>
+      </div>
+      <div id="tabpanel-activites-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--direction-end" role="tabpanel" aria-labelledby="tabpanel-activites-tdb_super_vues" tabindex="0">
+        <!---->
+      </div>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauPleinActivites.html b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauPleinActivites.html
new file mode 100644
index 000000000..ae367c77b
--- /dev/null
+++ b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauPleinActivites.html
@@ -0,0 +1,69 @@
+<div>
+  <div class="fr-grid-row">
+    <div class="fr-col-12 fr-col-md-6">
+      <h1>Tableau de bord</h1>
+    </div>
+    <div class="fr-col-12 fr-col-md-6" style="display: flex; flex-direction: row; align-items: flex-start; justify-content: flex-end;">
+      <!---->
+      <!---->
+    </div>
+  </div>
+  <div>
+    <div class="fr-tabs" style="--tabs-height: 0px;">
+      <ul class="fr-tabs__list" role="tablist" aria-label="Affichage des titres contenant un brouillon, ou des activités supprimable">
+        <li role="presentation"><button id="tabpanel-brouillons-tdb_super_vues" class="fr-tabs__tab fr-icon-draft-line fr-tabs__tab--icon-left" tabindex="-1" role="tab" aria-label="Brouillons" aria-selected="false" aria-controls="tabpanel-brouillons-tdb_super_vues-panel">Brouillons</button></li>
+        <li role="presentation"><button id="tabpanel-activites-tdb_super_vues" class="fr-tabs__tab fr-icon-delete-bin-line fr-tabs__tab--icon-left" tabindex="0" role="tab" aria-label="Activités" aria-selected="true" aria-controls="tabpanel-activites-tdb_super_vues-panel">Activités</button></li>
+      </ul>
+      <div id="tabpanel-brouillons-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--direction-start" role="tabpanel" aria-labelledby="tabpanel-brouillons-tdb_super_vues" tabindex="0">
+        <!---->
+      </div>
+      <div id="tabpanel-activites-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--selected" role="tabpanel" aria-labelledby="tabpanel-activites-tdb_super_vues" tabindex="0">
+        <div>
+          <div class="fr-table fr-table--no-scroll" style="overflow: auto;">
+            <div class="fr-table__wrapper" style="width: auto;">
+              <div class="fr-table__container">
+                <div class="fr-table__content">
+                  <table style="display: table; width: 100%;">
+                    <caption>Activités supprimables</caption>
+                    <thead>
+                      <tr>
+                        <th scope="col">Nom</th>
+                        <th scope="col">Type</th>
+                        <th scope="col">Année</th>
+                        <th scope="col">Période</th>
+                        <th scope="col">Type de rapport</th>
+                        <th scope="col">Statut</th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      <tr class="fr-label--error">
+                        <td><a href="/mocked-href" title="Nom du titre" aria-label="Nom du titre">Nom du titre</a></td>
+                        <td><span class="small bold">Autorisation d'exploitation</span></td>
+                        <td><span class="">2025</span></td>
+                        <td><span class="">1er trimestre</span></td>
+                        <td><span class="">rapport trimestriel d'exploitation d'or en Guyane</span></td>
+                        <td>
+                          <p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="fr-badge fr-badge--md fr-badge--green-bourgeon" title="déposé" aria-label="déposé">déposé</p>
+                        </td>
+                      </tr>
+                      <tr>
+                        <td><a href="/mocked-href" title="Nom du titre 2" aria-label="Nom du titre 2">Nom du titre 2</a></td>
+                        <td><span class="small bold">Permis d'exploitation</span></td>
+                        <td><span class="">2024</span></td>
+                        <td><span class=""></span></td>
+                        <td><span class="">rapport d'intensité d'exploration</span></td>
+                        <td>
+                          <p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="fr-badge fr-badge--md fr-badge--beige-gris-galet" title="cloturé" aria-label="cloturé">cloturé</p>
+                        </td>
+                      </tr>
+                    </tbody>
+                  </table>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauPleinBrouillon.html b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauPleinBrouillon.html
new file mode 100644
index 000000000..91ad57211
--- /dev/null
+++ b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauPleinBrouillon.html
@@ -0,0 +1,76 @@
+<div>
+  <div class="fr-grid-row">
+    <div class="fr-col-12 fr-col-md-6">
+      <h1>Tableau de bord</h1>
+    </div>
+    <div class="fr-col-12 fr-col-md-6" style="display: flex; flex-direction: row; align-items: flex-start; justify-content: flex-end;">
+      <!---->
+      <!---->
+    </div>
+  </div>
+  <div>
+    <div class="fr-tabs" style="--tabs-height: 0px;">
+      <ul class="fr-tabs__list" role="tablist" aria-label="Affichage des titres contenant un brouillon, ou des activités supprimable">
+        <li role="presentation"><button id="tabpanel-brouillons-tdb_super_vues" class="fr-tabs__tab fr-icon-draft-line fr-tabs__tab--icon-left" tabindex="0" role="tab" aria-label="Brouillons" aria-selected="true" aria-controls="tabpanel-brouillons-tdb_super_vues-panel">Brouillons</button></li>
+        <li role="presentation"><button id="tabpanel-activites-tdb_super_vues" class="fr-tabs__tab fr-icon-delete-bin-line fr-tabs__tab--icon-left" tabindex="-1" role="tab" aria-label="Activités" aria-selected="false" aria-controls="tabpanel-activites-tdb_super_vues-panel">Activités</button></li>
+      </ul>
+      <div id="tabpanel-brouillons-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--selected" role="tabpanel" aria-labelledby="tabpanel-brouillons-tdb_super_vues" tabindex="0">
+        <div>
+          <div class="fr-table fr-table--no-scroll" style="overflow: auto;">
+            <div class="fr-table__wrapper" style="width: auto;">
+              <div class="fr-table__container">
+                <div class="fr-table__content">
+                  <table style="display: table; width: 100%;">
+                    <caption>Titres avec une étape en brouillon</caption>
+                    <thead>
+                      <tr>
+                        <th scope="col">Nom</th>
+                        <th scope="col">Type de démarche</th>
+                        <th scope="col">Type de titre</th>
+                        <th scope="col">-</th>
+                        <th scope="col">Statut du titre</th>
+                        <th scope="col">Étape en brouillon</th>
+                        <th scope="col">Date de l'étape</th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      <tr>
+                        <td><a href="/mocked-href" title="Aachen" aria-label="Aachen">Aachen</a></td>
+                        <td><span class="">Octroi</span></td>
+                        <td><span class="small bold">Concession</span></td>
+                        <td>
+                          <p class="fr-tag fr-tag--md mono" title="Domaine minéraux et métaux" aria-label="Domaine minéraux et métaux" style="min-width: 2rem; background-color: var(--background-contrast-blue-ecume); color: black;">M</p>
+                        </td>
+                        <td>
+                          <p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="fr-badge fr-badge--md fr-badge--beige-gris-galet" title="échu" aria-label="échu">échu</p>
+                        </td>
+                        <td><span class="">Demande</span></td>
+                        <td><span class="">1810-01-01</span></td>
+                      </tr>
+                      <tr>
+                        <td><a href="/mocked-href" title="Amadis 5" aria-label="Amadis 5">Amadis 5</a></td>
+                        <td><span class="">Octroi</span></td>
+                        <td><span class="small bold">Permis d'exploitation</span></td>
+                        <td>
+                          <p class="fr-tag fr-tag--md mono" title="Domaine géothermie" aria-label="Domaine géothermie" style="min-width: 2rem; background-color: var(--background-contrast-pink-tuile); color: black;">G</p>
+                        </td>
+                        <td>
+                          <p style="z-index: unset; margin-bottom: 0px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="fr-badge fr-badge--md fr-badge--green-bourgeon" title="valide" aria-label="valide">valide</p>
+                        </td>
+                        <td><span class="">Avis des services et commissions consultatives</span></td>
+                        <td><span class="">2022-01-01</span></td>
+                      </tr>
+                    </tbody>
+                  </table>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div id="tabpanel-activites-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--direction-end" role="tabpanel" aria-labelledby="tabpanel-activites-tdb_super_vues" tabindex="0">
+        <!---->
+      </div>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauVideActivites.html b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauVideActivites.html
new file mode 100644
index 000000000..69ad48dfc
--- /dev/null
+++ b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauVideActivites.html
@@ -0,0 +1,25 @@
+<div>
+  <div class="fr-grid-row">
+    <div class="fr-col-12 fr-col-md-6">
+      <h1>Tableau de bord</h1>
+    </div>
+    <div class="fr-col-12 fr-col-md-6" style="display: flex; flex-direction: row; align-items: flex-start; justify-content: flex-end;">
+      <!---->
+      <!---->
+    </div>
+  </div>
+  <div>
+    <div class="fr-tabs" style="--tabs-height: 0px;">
+      <ul class="fr-tabs__list" role="tablist" aria-label="Affichage des titres contenant un brouillon, ou des activités supprimable">
+        <li role="presentation"><button id="tabpanel-brouillons-tdb_super_vues" class="fr-tabs__tab fr-icon-draft-line fr-tabs__tab--icon-left" tabindex="-1" role="tab" aria-label="Brouillons" aria-selected="false" aria-controls="tabpanel-brouillons-tdb_super_vues-panel">Brouillons</button></li>
+        <li role="presentation"><button id="tabpanel-activites-tdb_super_vues" class="fr-tabs__tab fr-icon-delete-bin-line fr-tabs__tab--icon-left" tabindex="0" role="tab" aria-label="Activités" aria-selected="true" aria-controls="tabpanel-activites-tdb_super_vues-panel">Activités</button></li>
+      </ul>
+      <div id="tabpanel-brouillons-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--direction-start" role="tabpanel" aria-labelledby="tabpanel-brouillons-tdb_super_vues" tabindex="0">
+        <!---->
+      </div>
+      <div id="tabpanel-activites-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--selected" role="tabpanel" aria-labelledby="tabpanel-activites-tdb_super_vues" tabindex="0">
+        <p>Aucune activité supprimable</p>
+      </div>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauVideBrouillon.html b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauVideBrouillon.html
new file mode 100644
index 000000000..3eaa5ac02
--- /dev/null
+++ b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_TableauVideBrouillon.html
@@ -0,0 +1,25 @@
+<div>
+  <div class="fr-grid-row">
+    <div class="fr-col-12 fr-col-md-6">
+      <h1>Tableau de bord</h1>
+    </div>
+    <div class="fr-col-12 fr-col-md-6" style="display: flex; flex-direction: row; align-items: flex-start; justify-content: flex-end;">
+      <!---->
+      <!---->
+    </div>
+  </div>
+  <div>
+    <div class="fr-tabs" style="--tabs-height: 0px;">
+      <ul class="fr-tabs__list" role="tablist" aria-label="Affichage des titres contenant un brouillon, ou des activités supprimable">
+        <li role="presentation"><button id="tabpanel-brouillons-tdb_super_vues" class="fr-tabs__tab fr-icon-draft-line fr-tabs__tab--icon-left" tabindex="0" role="tab" aria-label="Brouillons" aria-selected="true" aria-controls="tabpanel-brouillons-tdb_super_vues-panel">Brouillons</button></li>
+        <li role="presentation"><button id="tabpanel-activites-tdb_super_vues" class="fr-tabs__tab fr-icon-delete-bin-line fr-tabs__tab--icon-left" tabindex="-1" role="tab" aria-label="Activités" aria-selected="false" aria-controls="tabpanel-activites-tdb_super_vues-panel">Activités</button></li>
+      </ul>
+      <div id="tabpanel-brouillons-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--selected" role="tabpanel" aria-labelledby="tabpanel-brouillons-tdb_super_vues" tabindex="0">
+        <p>Aucune étape en brouillon</p>
+      </div>
+      <div id="tabpanel-activites-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--direction-end" role="tabpanel" aria-labelledby="tabpanel-activites-tdb_super_vues" tabindex="0">
+        <!---->
+      </div>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_WithErrorActivites.html b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_WithErrorActivites.html
new file mode 100644
index 000000000..38c87d3a0
--- /dev/null
+++ b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_WithErrorActivites.html
@@ -0,0 +1,31 @@
+<div>
+  <div class="fr-grid-row">
+    <div class="fr-col-12 fr-col-md-6">
+      <h1>Tableau de bord</h1>
+    </div>
+    <div class="fr-col-12 fr-col-md-6" style="display: flex; flex-direction: row; align-items: flex-start; justify-content: flex-end;">
+      <!---->
+      <!---->
+    </div>
+  </div>
+  <div>
+    <div class="fr-tabs" style="--tabs-height: 0px;">
+      <ul class="fr-tabs__list" role="tablist" aria-label="Affichage des titres contenant un brouillon, ou des activités supprimable">
+        <li role="presentation"><button id="tabpanel-brouillons-tdb_super_vues" class="fr-tabs__tab fr-icon-draft-line fr-tabs__tab--icon-left" tabindex="-1" role="tab" aria-label="Brouillons" aria-selected="false" aria-controls="tabpanel-brouillons-tdb_super_vues-panel">Brouillons</button></li>
+        <li role="presentation"><button id="tabpanel-activites-tdb_super_vues" class="fr-tabs__tab fr-icon-delete-bin-line fr-tabs__tab--icon-left" tabindex="0" role="tab" aria-label="Activités" aria-selected="true" aria-controls="tabpanel-activites-tdb_super_vues-panel">Activités</button></li>
+      </ul>
+      <div id="tabpanel-brouillons-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--direction-start" role="tabpanel" aria-labelledby="tabpanel-brouillons-tdb_super_vues" tabindex="0">
+        <!---->
+      </div>
+      <div id="tabpanel-activites-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--selected" role="tabpanel" aria-labelledby="tabpanel-activites-tdb_super_vues" tabindex="0">
+        <div class="" style="display: flex; justify-content: center;">
+          <!---->
+          <div class="fr-alert fr-alert--error fr-alert--sm" role="alert">
+            <p>Une erreur</p>
+          </div>
+          <!---->
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_WithErrorBrouillon.html b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_WithErrorBrouillon.html
new file mode 100644
index 000000000..21af986a8
--- /dev/null
+++ b/packages/ui/src/components/dashboard/pure-super-dashboard.stories_snapshots_WithErrorBrouillon.html
@@ -0,0 +1,31 @@
+<div>
+  <div class="fr-grid-row">
+    <div class="fr-col-12 fr-col-md-6">
+      <h1>Tableau de bord</h1>
+    </div>
+    <div class="fr-col-12 fr-col-md-6" style="display: flex; flex-direction: row; align-items: flex-start; justify-content: flex-end;">
+      <!---->
+      <!---->
+    </div>
+  </div>
+  <div>
+    <div class="fr-tabs" style="--tabs-height: 0px;">
+      <ul class="fr-tabs__list" role="tablist" aria-label="Affichage des titres contenant un brouillon, ou des activités supprimable">
+        <li role="presentation"><button id="tabpanel-brouillons-tdb_super_vues" class="fr-tabs__tab fr-icon-draft-line fr-tabs__tab--icon-left" tabindex="0" role="tab" aria-label="Brouillons" aria-selected="true" aria-controls="tabpanel-brouillons-tdb_super_vues-panel">Brouillons</button></li>
+        <li role="presentation"><button id="tabpanel-activites-tdb_super_vues" class="fr-tabs__tab fr-icon-delete-bin-line fr-tabs__tab--icon-left" tabindex="-1" role="tab" aria-label="Activités" aria-selected="false" aria-controls="tabpanel-activites-tdb_super_vues-panel">Activités</button></li>
+      </ul>
+      <div id="tabpanel-brouillons-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--selected" role="tabpanel" aria-labelledby="tabpanel-brouillons-tdb_super_vues" tabindex="0">
+        <div class="" style="display: flex; justify-content: center;">
+          <!---->
+          <div class="fr-alert fr-alert--error fr-alert--sm" role="alert">
+            <p>Une erreur</p>
+          </div>
+          <!---->
+        </div>
+      </div>
+      <div id="tabpanel-activites-tdb_super_vues-panel" class="fr-tabs__panel fr-tabs__panel--direction-end" role="tabpanel" aria-labelledby="tabpanel-activites-tdb_super_vues" tabindex="0">
+        <!---->
+      </div>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/packages/ui/src/components/dashboard/pure-super-dashboard.tsx b/packages/ui/src/components/dashboard/pure-super-dashboard.tsx
index 993d95bb0..ec83dd855 100644
--- a/packages/ui/src/components/dashboard/pure-super-dashboard.tsx
+++ b/packages/ui/src/components/dashboard/pure-super-dashboard.tsx
@@ -1,6 +1,6 @@
 import { defineComponent, onMounted, ref } from 'vue'
 import { Column, TableSimple } from '../_ui/table-simple'
-import { nomColumn, statutCell, typeCell, domaineColumn, domaineCell } from '@/components/titres/table-utils'
+import { nomColumn, statutCell, typeCell, domaineColumn, domaineCell, typeColumn } from '@/components/titres/table-utils'
 import { SuperTitre } from 'camino-common/src/titres'
 import { LoadingElement } from '@/components/_ui/functional-loader'
 import { AsyncData } from '@/api/client-rest'
@@ -8,18 +8,31 @@ import { ComponentColumnData, JSXElementColumnData, TableRow, TextColumnData } f
 import { DashboardApiClient } from './dashboard-api-client'
 import { User } from 'camino-common/src/roles'
 import { PageContentHeader } from '../_common/page-header-content'
-import { isNotNullNorUndefinedNorEmpty } from 'camino-common/src/typescript-tools'
-import { CaminoRouterLink } from '@/router/camino-router-link'
+import { exhaustiveCheck, isNotNullNorUndefinedNorEmpty } from 'camino-common/src/typescript-tools'
+import { CaminoRouterLink, routerQueryToString } from '@/router/camino-router-link'
 import { DemarchesTypes } from 'camino-common/src/static/demarchesTypes'
 import { capitalize } from 'camino-common/src/strings'
 import { EtapesTypes } from 'camino-common/src/static/etapesTypes'
 import { getDomaineId } from 'camino-common/src/static/titresTypes'
+import { computed } from 'vue'
+import { Tab, Tabs } from '../_ui/tabs'
+import { CaminoRouteLocation } from '@/router/routes'
+import { CaminoRouter } from '@/typings/vue-router'
+import { ActiviteSuper } from 'camino-common/src/activite'
+import { activitesColonneIdAnnee } from '../activites'
+import { getPeriode } from 'camino-common/src/static/frequence'
+import { ActivitesTypes } from 'camino-common/src/static/activitesTypes'
+import { ActiviteStatut } from '../_common/activite-statut'
+import { fr } from '@codegouvfr/react-dsfr'
+import { ACTIVITES_STATUTS_IDS } from 'camino-common/src/static/activitesStatuts'
 
 interface Props {
-  apiClient: Pick<DashboardApiClient, 'getTitresAvecEtapeEnBrouillon'>
+  apiClient: Pick<DashboardApiClient, 'getTitresAvecEtapeEnBrouillon' | 'getActivitesSuper'>
   user: User
+  route: CaminoRouteLocation
+  router: Pick<CaminoRouter, 'push'>
 }
-const columns = [
+const brouillonsColumns = [
   nomColumn,
   { id: 'demarche_type', contentTitle: 'Type de démarche' },
   { id: 'titre_type', contentTitle: 'Type de titre' },
@@ -28,17 +41,70 @@ const columns = [
   { id: 'etape_brouillon', contentTitle: 'Étape en brouillon' },
   { id: 'etape_date', contentTitle: "Date de l'étape" },
 ] as const satisfies Column[]
-type ColumnId = (typeof columns)[number]['id']
+type BrouillonColumnId = (typeof brouillonsColumns)[number]['id']
+
+const activitesColumns = [
+  nomColumn,
+  typeColumn,
+  { id: activitesColonneIdAnnee, contentTitle: 'Année' },
+  { id: 'periode', contentTitle: 'Période' },
+  { id: 'activite_type', contentTitle: 'Type de rapport' },
+  { id: 'statut', contentTitle: 'Statut' },
+] as const satisfies Column[]
+type ActiviteColumnId = (typeof activitesColumns)[number]['id']
+
+const tabs = ['brouillons', 'activites'] as const
+type TabId = (typeof tabs)[number]
 
 export const PureSuperDashboard = defineComponent<Props>(props => {
-  const data = ref<AsyncData<TableRow<ColumnId>[]>>({ status: 'LOADING' })
+  const brouillonsData = ref<AsyncData<TableRow<BrouillonColumnId>[]>>({ status: 'LOADING' })
+  const activitesData = ref<AsyncData<TableRow<ActiviteColumnId>[]>>({ status: 'LOADING' })
+
+  type BrouillonColumns = (typeof brouillonsColumns)[number]['id']
 
-  type Columns = (typeof columns)[number]['id']
+  const tabId = computed<TabId>(() => routerQueryToString(props.route.query.vueId, 'brouillons') as TabId)
 
-  const titresLignesBuild = (titres: SuperTitre[]): TableRow<Columns>[] => {
+  const vues = [
+    {
+      id: 'brouillons',
+      icon: 'fr-icon-draft-line',
+      title: 'Brouillons',
+      renderContent: () => (
+        <LoadingElement
+          data={brouillonsData.value}
+          renderItem={item => {
+            if (isNotNullNorUndefinedNorEmpty(item)) {
+              return <TableSimple caption={{ value: 'Titres avec une étape en brouillon', visible: true }} columns={brouillonsColumns} rows={item} />
+            }
+
+            return <p>Aucune étape en brouillon</p>
+          }}
+        />
+      ),
+    },
+    {
+      id: 'activites',
+      icon: 'fr-icon-delete-bin-line',
+      title: 'Activités',
+      renderContent: () => (
+        <LoadingElement
+          data={activitesData.value}
+          renderItem={item => {
+            if (isNotNullNorUndefinedNorEmpty(item)) {
+              return <TableSimple caption={{ value: 'Activités supprimables', visible: true }} columns={activitesColumns} rows={item} />
+            }
+
+            return <p>Aucune activité supprimable</p>
+          }}
+        />
+      ),
+    },
+  ] as const satisfies readonly Tab<TabId>[]
+
+  const titresLignesBuild = (titres: SuperTitre[]): TableRow<BrouillonColumns>[] => {
     return titres.map(titre => {
       const columns: {
-        [key in Columns]: JSXElementColumnData | ComponentColumnData | TextColumnData
+        [key in BrouillonColumns]: JSXElementColumnData | ComponentColumnData | TextColumnData
       } = {
         nom: {
           type: 'jsx',
@@ -62,38 +128,116 @@ export const PureSuperDashboard = defineComponent<Props>(props => {
 
       return {
         id: titre.titre_slug,
-        link: { name: 'titre', params: { id: titre.titre_slug } },
+        link: null,
+        columns,
+      }
+    })
+  }
+  const activitesLignesBuild = (activites: ActiviteSuper[]): TableRow<ActiviteColumnId>[] => {
+    return activites.map(activite => {
+      const columns: {
+        [key in ActiviteColumnId]: JSXElementColumnData | ComponentColumnData | TextColumnData
+      } = {
+        nom: {
+          type: 'jsx',
+          jsxElement: (
+            <CaminoRouterLink to={{ name: 'activite', params: { activiteId: activite.id } }} isDisabled={false} title={activite.titre_nom}>
+              {capitalize(activite.titre_nom)}
+            </CaminoRouterLink>
+          ),
+          value: activite.titre_nom,
+        },
+        type: typeCell(activite.titre_type_id),
+        periode: {
+          type: 'text',
+          value: getPeriode(ActivitesTypes[activite.type_id].frequenceId, activite.periode_id),
+        },
+        annee: {
+          type: 'text',
+          value: activite.annee,
+        },
+        activite_type: {
+          type: 'text',
+          value: ActivitesTypes[activite.type_id].nom,
+        },
+        statut: {
+          type: 'jsx',
+          jsxElement: <ActiviteStatut activiteStatutId={activite.activite_statut_id} />,
+          value: activite.activite_statut_id,
+        },
+      }
+
+      return {
+        class: activite.activite_statut_id === ACTIVITES_STATUTS_IDS.DEPOSE ? [fr.cx('fr-label--error')] : undefined,
+        id: activite.id,
+        link: null,
         columns,
       }
     })
   }
 
-  onMounted(async () => {
-    const titres = await props.apiClient.getTitresAvecEtapeEnBrouillon()
-    if ('message' in titres) {
-      data.value = {
-        status: 'NEW_ERROR',
-        error: titres,
+  const loadBrouillons = async () => {
+    if (brouillonsData.value.status !== 'LOADED') {
+      const titres = await props.apiClient.getTitresAvecEtapeEnBrouillon()
+      if ('message' in titres) {
+        brouillonsData.value = {
+          status: 'NEW_ERROR',
+          error: titres,
+        }
+      } else {
+        brouillonsData.value = {
+          status: 'LOADED',
+          value: titresLignesBuild(titres),
+        }
       }
-    } else {
-      data.value = {
-        status: 'LOADED',
-        value: titresLignesBuild(titres),
+    }
+  }
+  const loadActivites = async () => {
+    if (activitesData.value.status !== 'LOADED') {
+      const activites = await props.apiClient.getActivitesSuper()
+      if ('message' in activites) {
+        activitesData.value = {
+          status: 'NEW_ERROR',
+          error: activites,
+        }
+      } else {
+        activitesData.value = {
+          status: 'LOADED',
+          value: activitesLignesBuild(activites),
+        }
       }
     }
+  }
+
+  const reload = async (tabId: TabId) => {
+    switch (tabId) {
+      case 'brouillons':
+        await loadBrouillons()
+        break
+      case 'activites':
+        await loadActivites()
+        break
+      default:
+        exhaustiveCheck(tabId)
+    }
+  }
+  onMounted(async () => {
+    await reload(tabId.value)
   })
 
   return () => (
     <div>
       <PageContentHeader nom="Tableau de bord" download={null} renderButton={null} />
-      <LoadingElement
-        data={data.value}
-        renderItem={item => {
-          if (isNotNullNorUndefinedNorEmpty(item)) {
-            return <TableSimple caption={{ value: 'Titres avec une étape en brouillon', visible: true }} columns={columns} rows={item} />
-          }
-
-          return <p>Aucune étape en brouillon</p>
+
+      <Tabs
+        id="tdb_super_vues"
+        initTab={tabId.value}
+        tabs={vues}
+        tabsTitle={'Affichage des titres contenant un brouillon, ou des activités supprimable'}
+        tabClicked={async newTabId => {
+          const query: CaminoRouteLocation['query'] = { ...props.route.query, vueId: newTabId }
+          await props.router.push({ name: props.route.name ?? undefined, query, params: props.route.params })
+          await reload(newTabId)
         }}
       />
     </div>
@@ -101,4 +245,4 @@ export const PureSuperDashboard = defineComponent<Props>(props => {
 })
 
 // @ts-ignore waiting for https://github.com/vuejs/core/issues/7833
-PureSuperDashboard.props = ['apiClient', 'user']
+PureSuperDashboard.props = ['apiClient', 'user', 'route', 'router']
diff --git a/packages/ui/src/components/utilisateur.stories.tsx b/packages/ui/src/components/utilisateur.stories.tsx
index d6a7d85ca..d78efd8f0 100644
--- a/packages/ui/src/components/utilisateur.stories.tsx
+++ b/packages/ui/src/components/utilisateur.stories.tsx
@@ -31,12 +31,12 @@ const apiClientMock: Props['apiClient'] = {
   removeUtilisateur: params => {
     deleteUtilisateur(params)
 
-    return Promise.resolve()
+    return Promise.resolve({ id: params })
   },
   updateUtilisateur: params => {
     updateUtilisateur(params)
 
-    return Promise.resolve()
+    return Promise.resolve({ id: params.id })
   },
   getQGISToken: () => new Promise(resolve => setTimeout(() => resolve({ token: qgisTokenValidator.parse('token123'), url: 'https://google.fr' }), 1000)),
 }
diff --git a/packages/ui/src/components/utilisateur.tsx b/packages/ui/src/components/utilisateur.tsx
index c171c9f75..99a837b2f 100644
--- a/packages/ui/src/components/utilisateur.tsx
+++ b/packages/ui/src/components/utilisateur.tsx
@@ -30,14 +30,16 @@ export const Utilisateur = defineComponent({
       if (isMe) {
         // TODO 2023-10-23 type window.location pour s'appuyer sur nos routes rest et pas sur n'importe quoi
         window.location.replace(`/apiUrl/rest/utilisateurs/${userId}/delete`)
+        return { id: userId }
       } else {
-        await utilisateurApiClient.removeUtilisateur(userId)
+        const value = await utilisateurApiClient.removeUtilisateur(userId)
+        if ('message' in value) {
+          return value
+        }
         router.push({ name: 'utilisateurs', params: {} })
+        return value
       }
     }
-    const updateUtilisateur = async (utilisateur: UtilisateurToEdit) => {
-      await utilisateurApiClient.updateUtilisateur(utilisateur)
-    }
     const passwordUpdate = () => {
       window.location.replace('/apiUrl/changerMotDePasse')
     }
@@ -57,7 +59,7 @@ export const Utilisateur = defineComponent({
         {utilisateurId.value ? (
           <PureUtilisateur
             passwordUpdate={passwordUpdate}
-            apiClient={{ ...utilisateurApiClient, updateUtilisateur, removeUtilisateur: deleteUtilisateur }}
+            apiClient={{ ...utilisateurApiClient, removeUtilisateur: deleteUtilisateur }}
             utilisateurId={utilisateurId.value}
             user={user}
             entreprises={entreprises.value}
@@ -110,8 +112,12 @@ export const PureUtilisateur = defineComponent<Props>(props => {
   }
 
   const updateUtilisateur = async (utilisateur: UtilisateurToEdit) => {
-    await props.apiClient.updateUtilisateur(utilisateur)
+    const result = await props.apiClient.updateUtilisateur(utilisateur)
+    if ('message' in result) {
+      return result
+    }
     await get()
+    return result
   }
 
   return () => (
@@ -162,15 +168,7 @@ export const PureUtilisateur = defineComponent<Props>(props => {
       />
 
       {removePopup.value && utilisateur.value.status === 'LOADED' ? (
-        <RemovePopup
-          close={() => (removePopup.value = !removePopup.value)}
-          utilisateur={utilisateur.value.value}
-          deleteUser={async () => {
-            if (utilisateur.value.status === 'LOADED') {
-              await props.apiClient.removeUtilisateur(utilisateur.value.value.id)
-            }
-          }}
-        />
+        <RemovePopup close={() => (removePopup.value = !removePopup.value)} utilisateur={utilisateur.value.value} apiClient={props.apiClient} />
       ) : null}
     </div>
   )
diff --git a/packages/ui/src/components/utilisateur/permission-edit.stories.tsx b/packages/ui/src/components/utilisateur/permission-edit.stories.tsx
index 5196442b9..ae84f7d78 100644
--- a/packages/ui/src/components/utilisateur/permission-edit.stories.tsx
+++ b/packages/ui/src/components/utilisateur/permission-edit.stories.tsx
@@ -23,7 +23,7 @@ export const Default: StoryFn = () => (
         new Promise(resolve =>
           setTimeout(() => {
             update(user)
-            resolve()
+            resolve({ id: user.id })
           }, 1000)
         ),
     }}
@@ -39,7 +39,7 @@ export const Administration: StoryFn = () => (
         new Promise(resolve =>
           setTimeout(() => {
             update(user)
-            resolve()
+            resolve({ id: user.id })
           }, 1000)
         ),
     }}
@@ -63,7 +63,7 @@ export const Entreprise: StoryFn = () => (
         new Promise(resolve =>
           setTimeout(() => {
             update(user)
-            resolve()
+            resolve({ id: user.id })
           }, 1000)
         ),
     }}
@@ -80,7 +80,7 @@ export const UserAdminCanEditDefautIntoLecteur: StoryFn = () => (
         new Promise(resolve =>
           setTimeout(() => {
             update(user)
-            resolve()
+            resolve({ id: user.id })
           }, 1000)
         ),
     }}
diff --git a/packages/ui/src/components/utilisateur/permission-edit.tsx b/packages/ui/src/components/utilisateur/permission-edit.tsx
index 1b9114bf1..f9f4667e1 100644
--- a/packages/ui/src/components/utilisateur/permission-edit.tsx
+++ b/packages/ui/src/components/utilisateur/permission-edit.tsx
@@ -1,4 +1,4 @@
-import { isEntrepriseOrBureauDetudeRole, Role, User, UserNotNull, isAdministration, isSuper, isEntrepriseOrBureauDEtude, isAdministrationRole } from 'camino-common/src/roles'
+import { isEntrepriseOrBureauDetudeRole, Role, User, UserNotNull, isAdministration, isSuper, isEntrepriseOrBureauDEtude, isAdministrationRole, UtilisateurId } from 'camino-common/src/roles'
 import { computed, defineComponent, ref } from 'vue'
 import { AdministrationId, Administrations, sortedAdministrations } from 'camino-common/src/static/administrations'
 import { Entreprise, EntrepriseId } from 'camino-common/src/entreprise'
@@ -12,6 +12,10 @@ import { DsfrSelect } from '../_ui/dsfr-select'
 import { DsfrButton, DsfrButtonIcon } from '../_ui/dsfr-button'
 import { LabelWithValue } from '../_ui/label-with-value'
 import { DsfrTag } from '../_ui/tag'
+import { CaminoError } from 'camino-common/src/zod-tools'
+import { AsyncData } from '@/api/client-rest'
+import { LoadingElement } from '../_ui/functional-loader'
+import { fr } from '@codegouvfr/react-dsfr'
 
 interface Props {
   user: User
@@ -24,8 +28,12 @@ export const PermissionDisplay = defineComponent<Props>(props => {
   const mode = ref<'read' | 'edit'>('read')
 
   const updateUtilisateur = async (utilisateur: UtilisateurToEdit) => {
-    await props.apiClient.updateUtilisateur(utilisateur)
+    const value = await props.apiClient.updateUtilisateur(utilisateur)
+    if ('message' in value) {
+      return value
+    }
     mode.value = 'read'
+    return value
   }
 
   return () => (
@@ -48,7 +56,7 @@ export const PermissionDisplay = defineComponent<Props>(props => {
             <LabelWithValue
               title={`Entreprise${props.utilisateur.entrepriseIds.length > 0 ? 's' : ''}`}
               item={
-                <ul class="fr-tags-group">
+                <ul class={fr.cx('fr-tags-group')}>
                   {isEntrepriseOrBureauDEtude(props.utilisateur) &&
                     props.utilisateur.entrepriseIds.map(entrepriseId => {
                       const e = props.entreprises.find(({ id }) => id === entrepriseId)
@@ -89,11 +97,12 @@ type PermissionEditProps = {
   user: User
   utilisateur: UserNotNull
   entreprises: Entreprise[]
-  updateUtilisateur: (utilisateur: UtilisateurToEdit) => Promise<void>
+  updateUtilisateur: (utilisateur: UtilisateurToEdit) => Promise<{ id: UtilisateurId } | CaminoError<string>>
   cancelEdition: () => void
 }
 
 const PermissionEdit = defineComponent<PermissionEditProps>(props => {
+  const asyncData = ref<AsyncData<{ id: UtilisateurId }>>({ status: 'LOADED', value: { id: props.utilisateur.id } })
   const updatingUtilisateur = ref<UtilisateurToEdit>({
     id: props.utilisateur.id,
     role: props.utilisateur.role,
@@ -127,6 +136,7 @@ const PermissionEdit = defineComponent<PermissionEditProps>(props => {
 
   const save = async () => {
     if (complete.value) {
+      asyncData.value = { status: 'LOADING' }
       if (!isAdministrationRole(updatingUtilisateur.value.role)) {
         updatingUtilisateur.value.administrationId = null
       }
@@ -135,7 +145,12 @@ const PermissionEdit = defineComponent<PermissionEditProps>(props => {
         updatingUtilisateur.value.entrepriseIds = []
       }
 
-      await props.updateUtilisateur(updatingUtilisateur.value)
+      const result = await props.updateUtilisateur(updatingUtilisateur.value)
+      if ('message' in result) {
+        asyncData.value = { status: 'NEW_ERROR', error: result }
+      } else {
+        asyncData.value = { status: 'LOADED', value: result }
+      }
     }
   }
 
@@ -153,10 +168,10 @@ const PermissionEdit = defineComponent<PermissionEditProps>(props => {
           <LabelWithValue
             title="Rôles"
             item={
-              <ul class="fr-tags-group">
+              <ul class={fr.cx('fr-tags-group')}>
                 {assignableRoles.map(role => (
                   <li>
-                    <DsfrButton class="fr-tag" onClick={() => roleToggle(role)} aria-pressed={updatingUtilisateur.value.role === role} title={capitalize(role)} />
+                    <DsfrButton class={fr.cx('fr-tag')} onClick={() => roleToggle(role)} aria-pressed={updatingUtilisateur.value.role === role} title={capitalize(role)} />
                   </li>
                 ))}
               </ul>
@@ -200,9 +215,10 @@ const PermissionEdit = defineComponent<PermissionEditProps>(props => {
           <LabelWithValue
             title=""
             item={
-              <div>
-                <DsfrButton title="Annuler" buttonType="secondary" onClick={props.cancelEdition} />
-                <DsfrButton class="fr-ml-2w" title="Enregistrer" buttonType="primary" onClick={save} disabled={!complete.value} />
+              <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
+                <DsfrButton title="Annuler" buttonType="secondary" onClick={props.cancelEdition} disabled={asyncData.value.status === 'LOADING'} />
+                <DsfrButton title="Enregistrer" buttonType="primary" onClick={save} disabled={!complete.value || asyncData.value.status === 'LOADING'} />
+                {asyncData.value.status !== 'LOADED' ? <LoadingElement data={asyncData.value} renderItem={_ => null} /> : null}
               </div>
             }
           />
diff --git a/packages/ui/src/components/utilisateur/remove-popup.stories.tsx b/packages/ui/src/components/utilisateur/remove-popup.stories.tsx
index c4847e81f..934735712 100644
--- a/packages/ui/src/components/utilisateur/remove-popup.stories.tsx
+++ b/packages/ui/src/components/utilisateur/remove-popup.stories.tsx
@@ -1,6 +1,7 @@
 import { action } from '@storybook/addon-actions'
 import { Meta, StoryFn } from '@storybook/vue3'
 import { RemovePopup } from './remove-popup'
+import { utilisateurIdValidator } from 'camino-common/src/roles'
 
 const meta: Meta = {
   title: 'Components/Utilisateur/RemovePopup',
@@ -13,11 +14,13 @@ const close = action('close')
 
 export const Default: StoryFn = () => (
   <RemovePopup
-    utilisateur={{ nom: 'Nom', prenom: 'Prénom' }}
-    deleteUser={() => {
-      deleteUser()
+    utilisateur={{ id: utilisateurIdValidator.parse('id'), nom: 'Nom', prenom: 'Prénom' }}
+    apiClient={{
+      removeUtilisateur: utilisateurId => {
+        deleteUser(utilisateurId)
 
-      return Promise.resolve()
+        return Promise.resolve({ id: utilisateurId })
+      },
     }}
     close={close}
   />
diff --git a/packages/ui/src/components/utilisateur/remove-popup.tsx b/packages/ui/src/components/utilisateur/remove-popup.tsx
index 24e216cc9..3fa9da80e 100644
--- a/packages/ui/src/components/utilisateur/remove-popup.tsx
+++ b/packages/ui/src/components/utilisateur/remove-popup.tsx
@@ -1,10 +1,12 @@
 import { FunctionalComponent } from 'vue'
 import { FunctionalPopup } from '../_ui/functional-popup'
 import { Alert } from '@/components/_ui/alert'
+import { UtilisateurApiClient } from './utilisateur-api-client'
+import { UtilisateurId } from 'camino-common/src/roles'
 interface Props {
-  utilisateur: { nom: string; prenom: string }
+  utilisateur: { id: UtilisateurId; nom: string; prenom: string }
   close: () => void
-  deleteUser: () => Promise<void>
+  apiClient: Pick<UtilisateurApiClient, 'removeUtilisateur'>
 }
 
 export const RemovePopup: FunctionalComponent<Props> = props => {
@@ -24,5 +26,13 @@ export const RemovePopup: FunctionalComponent<Props> = props => {
     />
   )
 
-  return <FunctionalPopup title={`Suppression du compte utilisateur`} content={content} close={props.close} validate={{ action: props.deleteUser, text: 'Supprimer' }} canValidate={true} />
+  return (
+    <FunctionalPopup
+      title={`Suppression du compte utilisateur`}
+      content={content}
+      close={props.close}
+      validate={{ action: () => props.apiClient.removeUtilisateur(props.utilisateur.id), text: 'Supprimer' }}
+      canValidate={true}
+    />
+  )
 }
diff --git a/packages/ui/src/components/utilisateur/utilisateur-api-client.ts b/packages/ui/src/components/utilisateur/utilisateur-api-client.ts
index 56322beb5..9600aad97 100644
--- a/packages/ui/src/components/utilisateur/utilisateur-api-client.ts
+++ b/packages/ui/src/components/utilisateur/utilisateur-api-client.ts
@@ -1,13 +1,13 @@
 import { QGISTokenRest, UtilisateurToEdit, UtilisateursSearchParamsInput, UtilisateursTable } from 'camino-common/src/utilisateur'
 
-import { getWithJson, newGetWithJson, newPostWithJson, postWithJson } from '../../api/client-rest'
+import { newGetWithJson, newPostWithJson } from '../../api/client-rest'
 import { UserNotNull, UtilisateurId } from 'camino-common/src/roles'
 import { CaminoError } from 'camino-common/src/zod-tools'
 
 export interface UtilisateurApiClient {
   getUtilisateur: (userId: UtilisateurId) => Promise<CaminoError<string> | UserNotNull>
-  removeUtilisateur: (userId: UtilisateurId) => Promise<void>
-  updateUtilisateur: (user: UtilisateurToEdit) => Promise<void>
+  removeUtilisateur: (userId: UtilisateurId) => Promise<{ id: UtilisateurId } | CaminoError<string>>
+  updateUtilisateur: (user: UtilisateurToEdit) => Promise<{ id: UtilisateurId } | CaminoError<string>>
   getQGISToken: () => Promise<CaminoError<string> | QGISTokenRest>
   getUtilisateurs: (params: UtilisateursSearchParamsInput) => Promise<CaminoError<string> | UtilisateursTable>
 }
@@ -19,7 +19,7 @@ export const utilisateurApiClient: UtilisateurApiClient = {
   getUtilisateur: async (userId: UtilisateurId) => {
     return newGetWithJson('/rest/utilisateurs/:id', { id: userId })
   },
-  removeUtilisateur: async (userId: UtilisateurId) => getWithJson('/rest/utilisateurs/:id/delete', { id: userId }),
-  updateUtilisateur: async (utilisateur: UtilisateurToEdit) => postWithJson('/rest/utilisateurs/:id/permission', { id: utilisateur.id }, utilisateur),
+  removeUtilisateur: async (userId: UtilisateurId) => newGetWithJson('/rest/utilisateurs/:id/delete', { id: userId }),
+  updateUtilisateur: async (utilisateur: UtilisateurToEdit) => newPostWithJson('/rest/utilisateurs/:id/permission', { id: utilisateur.id }, utilisateur),
   getQGISToken: async () => newPostWithJson('/rest/utilisateur/generateQgisToken', {}, {}),
 }
-- 
GitLab