From a52434ca2bc3dcc6ac10926c6440c2068ff28216 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bitard=20Micha=C3=ABl?= <bitard.michael@gmail.com>
Date: Wed, 2 Apr 2025 11:42:05 +0200
Subject: [PATCH 1/3] quick access titre

---
 packages/ui/src/components/page/quick-access-titre.tsx | 10 ++++++----
 packages/ui/src/components/titre/titre-api-client.ts   |  7 ++++++-
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/packages/ui/src/components/page/quick-access-titre.tsx b/packages/ui/src/components/page/quick-access-titre.tsx
index bd35f8f31..99f112d57 100644
--- a/packages/ui/src/components/page/quick-access-titre.tsx
+++ b/packages/ui/src/components/page/quick-access-titre.tsx
@@ -9,10 +9,11 @@ import { capitalize } from 'camino-common/src/strings'
 import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
 import { CaminoAnnee, getAnnee } from 'camino-common/src/date'
 import { TypeAheadSingle } from '../_ui/typeahead-single'
+import { useState } from '@/utils/vue-tsx-utils'
 
 export const QuickAccessTitre = defineComponent<{ id: string; onSelectTitre: () => void }>(props => {
   const router = useRouter()
-  const titres = ref<TitreForTitresRerchercherByNom[]>([])
+  const [titres, setTitres] = useState<TitreForTitresRerchercherByNom[]>([])
 
   const search = async (searchTerm: string): Promise<void> => {
     const intervalle = 10
@@ -25,7 +26,7 @@ export const QuickAccessTitre = defineComponent<{ id: string; onSelectTitre: ()
         references: searchTerm,
       })
     }
-    titres.value.splice(0, titres.value.length, ...searchTitres.elements)
+    setTitres(searchTitres.elements)
   }
 
   const onSelectedTitre = (titre: TitreForTitresRerchercherByNom | undefined) => {
@@ -53,8 +54,9 @@ interface DisplayTitreProps {
 }
 export const DisplayTitre: FunctionalComponent<DisplayTitreProps> = props => {
   let annee: CaminoAnnee | null = null
-  if (isNotNullNorUndefined(props.titre.demarches?.[0]?.demarcheDateDebut)) {
-    annee = getAnnee(props.titre.demarches?.[0]?.demarcheDateDebut)
+  const firstDemarche = props.titre.demarches.find(({ordre}) => ordre === 1)
+  if (isNotNullNorUndefined(firstDemarche?.demarcheDateDebut)) {
+    annee = getAnnee(firstDemarche.demarcheDateDebut)
   }
 
   return (
diff --git a/packages/ui/src/components/titre/titre-api-client.ts b/packages/ui/src/components/titre/titre-api-client.ts
index 465118577..a6a1b4b9e 100644
--- a/packages/ui/src/components/titre/titre-api-client.ts
+++ b/packages/ui/src/components/titre/titre-api-client.ts
@@ -37,7 +37,7 @@ export type TitreForTitresRerchercherByNom = {
   id: TitreId
   nom: string
   typeId: TitreTypeId
-  demarches: { demarcheDateDebut: CaminoDate | null }[]
+  demarches: { demarcheDateDebut: CaminoDate | null; ordre: number }[]
 }
 export interface TitreApiClient {
   removeTitre: (titreId: TitreId) => Promise<void>
@@ -287,6 +287,10 @@ export const titreApiClient: TitreApiClient = {
               id
               nom
               typeId
+              demarches {
+                ordre
+                demarcheDateDebut
+              }
             }
           }
         }
@@ -304,6 +308,7 @@ export const titreApiClient: TitreApiClient = {
             nom
             typeId
             demarches {
+              ordre
               demarcheDateDebut
             }
           }
-- 
GitLab


From b9e90043666d3511c2dcf48d2281b75a25428c70 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bitard=20Micha=C3=ABl?= <bitard.michael@gmail.com>
Date: Wed, 2 Apr 2025 15:29:07 +0200
Subject: [PATCH 2/3] use dedicated rest route

---
 .../api/src/api/rest/quick-access.queries.ts  |  50 ++++++++
 .../api/rest/quick-access.queries.types.ts    |  42 +++++++
 .../api/rest/quick-access.test.integration.ts | 109 ++++++++++++++++++
 packages/api/src/api/rest/quick-access.ts     |  61 ++++++++++
 .../migrations/20250402125154_add-unaccent.ts |  19 +++
 packages/api/src/server/rest.ts               |   2 +
 packages/common/src/rest.ts                   |   6 +
 packages/common/src/titres.ts                 |   9 ++
 .../page/quick-access-titre.stories.tsx       |  12 +-
 .../components/page/quick-access-titre.tsx    |  48 ++++----
 .../src/components/titre/titre-api-client.ts  |   7 +-
 11 files changed, 335 insertions(+), 30 deletions(-)
 create mode 100644 packages/api/src/api/rest/quick-access.queries.ts
 create mode 100644 packages/api/src/api/rest/quick-access.queries.types.ts
 create mode 100644 packages/api/src/api/rest/quick-access.test.integration.ts
 create mode 100644 packages/api/src/api/rest/quick-access.ts
 create mode 100644 packages/api/src/knex/migrations/20250402125154_add-unaccent.ts

diff --git a/packages/api/src/api/rest/quick-access.queries.ts b/packages/api/src/api/rest/quick-access.queries.ts
new file mode 100644
index 000000000..8f9721776
--- /dev/null
+++ b/packages/api/src/api/rest/quick-access.queries.ts
@@ -0,0 +1,50 @@
+import { sql } from '@pgtyped/runtime'
+import { EffectDbQueryAndValidateErrors, Redefine, effectDbQueryAndValidate } from '../../pg-database.js'
+import { z } from 'zod'
+import { Pool } from 'pg'
+import { titreIdValidator } from 'camino-common/src/validators/titres.js'
+import { Effect } from 'effect'
+import { CaminoError } from 'camino-common/src/zod-tools.js'
+import { IGetTitresByNomDbQuery, IGetTitresByReferenceDbQuery } from './quick-access.queries.types.js'
+import { titreTypeIdValidator } from 'camino-common/src/static/titresTypes.js'
+import { caminoDateValidator } from 'camino-common/src/date.js'
+
+const titresByNomDbValidator = z.object({
+  id: titreIdValidator,
+  type_id: titreTypeIdValidator,
+  nom: z.string(),
+  public_lecture: z.boolean(),
+  demarche_date_debut: caminoDateValidator.nullable(),
+})
+type TitreByNomDb = z.infer<typeof titresByNomDbValidator>
+
+type GetTitresByNomErrors = EffectDbQueryAndValidateErrors
+export const getTitresByReference = (pool: Pool, reference: string): Effect.Effect<TitreByNomDb[], CaminoError<GetTitresByNomErrors>> =>
+  effectDbQueryAndValidate(getTitresByReferenceDb, { reference: `%${reference}%` }, pool, titresByNomDbValidator)
+
+const getTitresByReferenceDb = sql<Redefine<IGetTitresByReferenceDbQuery, { reference: string }, TitreByNomDb>>`
+SELECT
+  t.id,
+  t.type_id,
+  t.nom,
+  t.public_lecture,
+  td.demarche_date_debut
+FROM titres t
+LEFT JOIN titres_demarches td ON td.titre_id = t.id AND td.ordre = 1 AND td.archive IS FALSE
+WHERE t.archive IS FALSE AND EXISTS (SELECT 1 FROM jsonb_array_elements(t.references) titreRefs WHERE LOWER(titreRefs->>'nom') LIKE LOWER($reference!)) LIMIT 10
+`
+
+export const getTitresByNom = (pool: Pool, nom: string): Effect.Effect<TitreByNomDb[], CaminoError<GetTitresByNomErrors>> =>
+  effectDbQueryAndValidate(getTitresByNomDb, { nom: `%${nom}%` }, pool, titresByNomDbValidator)
+
+const getTitresByNomDb = sql<Redefine<IGetTitresByNomDbQuery, { nom: string }, TitreByNomDb>>`
+SELECT
+  t.id,
+  t.type_id,
+  t.nom,
+  t.public_lecture,
+  td.demarche_date_debut
+FROM titres t
+LEFT JOIN titres_demarches td ON td.titre_id = t.id and td.ordre = 1 AND td.archive IS FALSE
+WHERE t.archive IS FALSE AND LOWER(unaccent(t.nom)) LIKE LOWER(unaccent($nom)) LIMIT 10
+`
diff --git a/packages/api/src/api/rest/quick-access.queries.types.ts b/packages/api/src/api/rest/quick-access.queries.types.ts
new file mode 100644
index 000000000..763778101
--- /dev/null
+++ b/packages/api/src/api/rest/quick-access.queries.types.ts
@@ -0,0 +1,42 @@
+/** Types generated for queries found in "src/api/rest/quick-access.queries.ts" */
+
+/** 'GetTitresByReferenceDb' parameters type */
+export interface IGetTitresByReferenceDbParams {
+  reference: string;
+}
+
+/** 'GetTitresByReferenceDb' return type */
+export interface IGetTitresByReferenceDbResult {
+  demarche_date_debut: string | null;
+  id: string;
+  nom: string;
+  public_lecture: boolean;
+  type_id: string;
+}
+
+/** 'GetTitresByReferenceDb' query type */
+export interface IGetTitresByReferenceDbQuery {
+  params: IGetTitresByReferenceDbParams;
+  result: IGetTitresByReferenceDbResult;
+}
+
+/** 'GetTitresByNomDb' parameters type */
+export interface IGetTitresByNomDbParams {
+  nom?: string | null | void;
+}
+
+/** 'GetTitresByNomDb' return type */
+export interface IGetTitresByNomDbResult {
+  demarche_date_debut: string | null;
+  id: string;
+  nom: string;
+  public_lecture: boolean;
+  type_id: string;
+}
+
+/** 'GetTitresByNomDb' query type */
+export interface IGetTitresByNomDbQuery {
+  params: IGetTitresByNomDbParams;
+  result: IGetTitresByNomDbResult;
+}
+
diff --git a/packages/api/src/api/rest/quick-access.test.integration.ts b/packages/api/src/api/rest/quick-access.test.integration.ts
new file mode 100644
index 000000000..8adc12bee
--- /dev/null
+++ b/packages/api/src/api/rest/quick-access.test.integration.ts
@@ -0,0 +1,109 @@
+import { dbManager } from '../../../tests/db-manager'
+import { restNewCall } from '../../../tests/_utils/index'
+import { ADMINISTRATION_IDS } from 'camino-common/src/static/administrations'
+import { afterAll, beforeAll, describe, test, expect, vi } from 'vitest'
+import type { Pool } from 'pg'
+import { newTitreId } from '../../database/models/_format/id-create'
+import { insertTitreGraph } from '../../../tests/integration-test-helper'
+
+console.info = vi.fn()
+console.error = vi.fn()
+
+let dbPool: Pool
+beforeAll(async () => {
+  const { pool } = await dbManager.populateDb()
+  dbPool = pool
+})
+
+afterAll(async () => {
+  await dbManager.closeKnex()
+})
+
+describe('quickAccess', () => {
+  test('par référence', async () => {
+    const reference = 'refTest'
+    await insertTitreGraph({
+      id: newTitreId('id-par-reference'),
+      nom: 'Nom-par-référence',
+      typeId: 'arm',
+      titreStatutId: 'val',
+      propsTitreEtapesIds: {},
+      publicLecture: true,
+      references: [{ nom: reference, referenceTypeId: 'brg' }],
+    })
+    const tested = await restNewCall(
+      dbPool,
+      '/rest/quickAccess',
+      {},
+      {
+        role: 'admin',
+        administrationId: ADMINISTRATION_IDS['DGTM - GUYANE'],
+      },
+      { search: reference }
+    )
+
+    expect(tested.body).toMatchInlineSnapshot(`
+      [
+        {
+          "id": "id-par-reference",
+          "nom": "Nom-par-référence",
+          "titreDateDebut": null,
+          "typeId": "arm",
+        },
+      ]
+    `)
+  })
+
+  test('par nom', async () => {
+    await insertTitreGraph({
+      id: newTitreId('id-titre-avec-accents'),
+      nom: 'Titre avÉc des Accènts',
+      typeId: 'arm',
+      titreStatutId: 'val',
+      propsTitreEtapesIds: {},
+      publicLecture: true,
+    })
+    const tested = await restNewCall(
+      dbPool,
+      '/rest/quickAccess',
+      {},
+      {
+        role: 'admin',
+        administrationId: ADMINISTRATION_IDS['DGTM - GUYANE'],
+      },
+      { search: 'avec des acc' }
+    )
+
+    expect(tested.body).toMatchInlineSnapshot(`
+        [
+          {
+            "id": "id-titre-avec-accents",
+            "nom": "Titre avÉc des Accènts",
+            "titreDateDebut": null,
+            "typeId": "arm",
+          },
+        ]
+      `)
+  })
+  test('droits insuffisants', async () => {
+    await insertTitreGraph({
+      id: newTitreId('id-titre-filtré'),
+      nom: 'titre filtré',
+      typeId: 'arm',
+      titreStatutId: 'val',
+      propsTitreEtapesIds: {},
+      publicLecture: false,
+    })
+    const tested = await restNewCall(
+      dbPool,
+      '/rest/quickAccess',
+      {},
+      {
+        role: 'defaut',
+      },
+      { search: 'filtre' }
+    )
+
+    expect(tested.body).toMatchInlineSnapshot(`[]`)
+  })
+})
diff --git a/packages/api/src/api/rest/quick-access.ts b/packages/api/src/api/rest/quick-access.ts
new file mode 100644
index 000000000..476d789e1
--- /dev/null
+++ b/packages/api/src/api/rest/quick-access.ts
@@ -0,0 +1,61 @@
+import { canReadTitre } from 'camino-common/src/permissions/titres'
+import { QuickAccessResult } from 'camino-common/src/titres'
+import { HTTP_STATUS } from 'camino-common/src/http'
+import { Effect, Match } from 'effect'
+import { CaminoApiError } from '../../types'
+import { RestNewGetCall } from '../../server/rest'
+import { EffectDbQueryAndValidateErrors } from '../../pg-database'
+import { getTitresByNom, getTitresByReference } from './quick-access.queries'
+import { getAdministrationsLocalesByTitreId, getTitulairesAmodiatairesByTitreId } from './titres.queries'
+import { isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools'
+
+const errorCanReadTitre = "Erreur lors de l'accès aux permissions" as const
+type QuickAccessSearchErrors = EffectDbQueryAndValidateErrors | typeof errorCanReadTitre
+
+export const quickAccessSearch: RestNewGetCall<'/rest/quickAccess'> = (rootPipe): Effect.Effect<QuickAccessResult[], CaminoApiError<QuickAccessSearchErrors>> => {
+  return rootPipe.pipe(
+    Effect.bind('unfilteredTitres', ({ searchParams, pool }) =>
+      getTitresByNom(pool, searchParams.search).pipe(
+        Effect.flatMap(result => {
+          if (isNullOrUndefinedOrEmpty(result)) {
+            return getTitresByReference(pool, searchParams.search)
+          } else {
+            return Effect.succeed(result)
+          }
+        })
+      )
+    ),
+    Effect.bind('filteredTitres', ({ unfilteredTitres, user, pool }) =>
+      Effect.tryPromise({
+        try: async () => {
+          const titres = []
+          for (const titre of unfilteredTitres) {
+            if (
+              await canReadTitre(
+                user,
+                () => Promise.resolve(titre.type_id),
+                () => getAdministrationsLocalesByTitreId(pool, titre.id),
+                () => getTitulairesAmodiatairesByTitreId(pool, titre.id),
+                titre
+              )
+            ) {
+              titres.push(titre)
+            }
+          }
+          return titres
+        },
+        catch: e => ({ message: errorCanReadTitre, extra: e }),
+      })
+    ),
+    Effect.map(({ filteredTitres }) => filteredTitres.map<QuickAccessResult>(titre => ({ id: titre.id, nom: titre.nom, typeId: titre.type_id, titreDateDebut: titre.demarche_date_debut }))),
+    Effect.mapError(caminoError =>
+      Match.value(caminoError.message).pipe(
+        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', "Erreur lors de l'accès aux permissions", () => ({
+          ...caminoError,
+          status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
+        })),
+        Match.exhaustive
+      )
+    )
+  )
+}
diff --git a/packages/api/src/knex/migrations/20250402125154_add-unaccent.ts b/packages/api/src/knex/migrations/20250402125154_add-unaccent.ts
new file mode 100644
index 000000000..f8e521a8a
--- /dev/null
+++ b/packages/api/src/knex/migrations/20250402125154_add-unaccent.ts
@@ -0,0 +1,19 @@
+import { Knex } from 'knex'
+
+export const up = async (knex: Knex): Promise<void> => {
+  await knex.raw('CREATE EXTENSION unaccent')
+  await knex.raw(`CREATE OR REPLACE FUNCTION my_unaccent(some_time varchar)
+    RETURNS text
+  AS
+  $BODY$
+      select unaccent($1);
+  $BODY$
+  LANGUAGE sql
+  IMMUTABLE;`)
+  await knex.raw('CREATE INDEX ON titres (lower(my_unaccent(nom)));')
+}
+
+export const down = (): void => {}
+
+// export const config = { transaction: false };
+// export default config
diff --git a/packages/api/src/server/rest.ts b/packages/api/src/server/rest.ts
index 40264ccf3..6b8f2c1ed 100644
--- a/packages/api/src/server/rest.ts
+++ b/packages/api/src/server/rest.ts
@@ -58,6 +58,7 @@ import { addLog } from '../api/rest/logs.queries'
 import { HTTP_STATUS } from 'camino-common/src/http'
 import { zodParseEffectTyped } from '../tools/fp-tools'
 import { Cause, Effect, Exit, Option, pipe } from 'effect'
+import { quickAccessSearch } from '../api/rest/quick-access'
 
 interface IRestResolverResult {
   nom: string
@@ -191,6 +192,7 @@ const restRouteImplementations: Readonly<{ [key in CaminoRestRoute]: Transform<k
   '/config': { newGetCall: getConfig, ...CaminoRestRoutes['/config'] },
   '/rest/titres/:id/titreLiaisons': { newGetCall: getTitreLiaisons, newPostCall: postTitreLiaisons, ...CaminoRestRoutes['/rest/titres/:id/titreLiaisons'] },
   '/rest/etapesTypes/:demarcheId/:date': { newGetCall: getEtapesTypesEtapesStatusWithMainStep, ...CaminoRestRoutes['/rest/etapesTypes/:demarcheId/:date'] },
+  '/rest/quickAccess': { newGetCall: quickAccessSearch, ...CaminoRestRoutes['/rest/quickAccess'] },
   '/rest/titres': { newPostCall: titreDemandeCreer, ...CaminoRestRoutes['/rest/titres'] },
   '/rest/titres/:titreId': { deleteCall: removeTitre, postCall: updateTitre, getCall: getTitre, ...CaminoRestRoutes['/rest/titres/:titreId'] },
   '/rest/titres/:titreId/abonne': { postCall: utilisateurTitreAbonner, newGetCall: getUtilisateurTitreAbonner, ...CaminoRestRoutes['/rest/titres/:titreId/abonne'] },
diff --git a/packages/common/src/rest.ts b/packages/common/src/rest.ts
index 193ce0d15..96a20d901 100644
--- a/packages/common/src/rest.ts
+++ b/packages/common/src/rest.ts
@@ -22,6 +22,7 @@ import { qgisTokenRestValidator, utilisateurToEdit, utilisateursSearchParamsVali
 import {
   editableTitreValidator,
   getDemarcheByIdOrSlugValidator,
+  quickAccessResultValidator,
   superTitreValidator,
   titreAdministrationValidator,
   titreDemandeOutputValidator,
@@ -98,6 +99,7 @@ const IDS = [
   '/rest/titres/:titreId',
   '/rest/titres/:titreId/abonne',
   '/rest/titresAdministrations',
+  '/rest/quickAccess',
   '/rest/titresSuper',
   '/rest/titres/:id/titreLiaisons',
   '/rest/demarches',
@@ -155,6 +157,9 @@ const entrepriseIdParamsValidator = z.object({ entrepriseId: entrepriseIdValidat
 const etapeIdParamsValidator = z.object({ etapeId: etapeIdValidator })
 const administrationIdParamsValidator = z.object({ administrationId: administrationIdValidator })
 const geoSystemIdParamsValidator = z.object({ geoSystemeId: geoSystemeIdValidator })
+const quickAccessSearchParamsValidator = z.object({ search: z.string() })
+
+const quickAccessArrayResultValidator = z.array(quickAccessResultValidator)
 export const CaminoRestRoutes = {
   '/config': { params: noParamsValidator, newGet: { output: caminoConfigValidator } },
   '/moi': { params: noParamsValidator, newGet: { output: userValidator } },
@@ -169,6 +174,7 @@ export const CaminoRestRoutes = {
   '/rest/statistiques/granulatsMarins': { params: noParamsValidator, get: { output: statistiquesGranulatsMarinsValidator } },
   '/rest/statistiques/granulatsMarins/:annee': { params: z.object({ annee: caminoAnneeValidator }), get: { output: statistiquesGranulatsMarinsValidator } },
   '/rest/statistiques/datagouv': { params: noParamsValidator, get: { output: z.array(statistiquesDataGouvValidator) } },
+  '/rest/quickAccess': { params: noParamsValidator, newGet: { searchParams: quickAccessSearchParamsValidator, output: quickAccessArrayResultValidator } },
   '/rest/titres': { params: noParamsValidator, newPost: { input: titreDemandeValidator, output: titreDemandeOutputValidator } },
   '/rest/titres/:titreId': { params: z.object({ titreId: titreIdOrSlugValidator }), get: { output: titreGetValidator }, delete: true, post: { output: z.void(), input: editableTitreValidator } },
   '/rest/titres/:titreId/abonne': { params: z.object({ titreId: titreIdValidator }), post: { input: utilisateurTitreAbonneValidator, output: z.void() }, newGet: { output: z.boolean() } },
diff --git a/packages/common/src/titres.ts b/packages/common/src/titres.ts
index de3e3a45c..fa593498f 100644
--- a/packages/common/src/titres.ts
+++ b/packages/common/src/titres.ts
@@ -182,3 +182,12 @@ export const titreDemandeOutputValidator = z.object({ etapeId: etapeIdValidator.
 export type TitreDemandeOutput = z.infer<typeof titreDemandeOutputValidator>
 
 export const createAutomaticallyEtapeWhenCreatingTitre = (user: User): boolean => isEntrepriseOrBureauDEtude(user)
+
+export const quickAccessResultValidator = z.object({
+  id: titreIdValidator,
+  nom: z.string(),
+  typeId: titreTypeIdValidator,
+  titreDateDebut: caminoDateValidator.nullable(),
+})
+
+export type QuickAccessResult = z.infer<typeof quickAccessResultValidator>
diff --git a/packages/ui/src/components/page/quick-access-titre.stories.tsx b/packages/ui/src/components/page/quick-access-titre.stories.tsx
index 4b24445c3..a0b98a734 100644
--- a/packages/ui/src/components/page/quick-access-titre.stories.tsx
+++ b/packages/ui/src/components/page/quick-access-titre.stories.tsx
@@ -21,13 +21,13 @@ export const Simple: StoryFn = () => (
         id: titreIdValidator.parse('1'),
         nom: 'monTitre',
         typeId: 'arm',
-        demarches: [],
+        titreDateDebut: null,
       },
       {
         id: titreIdValidator.parse('1'),
         nom: 'monSecondTitre',
         typeId: 'arg',
-        demarches: [],
+        titreDateDebut: null,
       },
     ]}
     onSearch={onSearch}
@@ -42,7 +42,7 @@ export const Full: StoryFn = () => (
       id: titreIdValidator.parse(`${index}`),
       nom: `Nom du titre ${index}`,
       typeId: index % 3 === 0 ? 'arg' : index % 2 === 0 ? 'cxh' : 'axm',
-      demarches: [{ demarcheDateDebut: toCaminoDate(`2023-01-0${(index % 9) + 1}`) }],
+      titreDateDebut: toCaminoDate(`2023-01-0${(index % 9) + 1}`),
     }))}
     onSearch={onSearch}
     onSelectedTitre={onSelectedTitre}
@@ -56,7 +56,7 @@ export const FullAlwaysOpen: StoryFn = () => (
       id: titreIdValidator.parse(`${index}`),
       nom: `Nom du titre ${index}`,
       typeId: index % 3 === 0 ? 'arg' : index % 2 === 0 ? 'cxh' : 'axm',
-      demarches: [{ demarcheDateDebut: toCaminoDate(`2023-01-0${(index % 9) + 1}`) }],
+      titreDateDebut: toCaminoDate(`2023-01-0${(index % 9) + 1}`),
     }))}
     onSearch={onSearch}
     onSelectedTitre={onSelectedTitre}
@@ -71,7 +71,7 @@ export const DisplayTitreSeulSansDate: StoryFn = () => (
     titre={{
       nom: 'monTitre',
       typeId: 'arm',
-      demarches: [],
+      titreDateDebut: null,
     }}
   />
 )
@@ -81,7 +81,7 @@ export const DisplayTitreSeulAvecDate: StoryFn = () => (
     titre={{
       nom: 'monTitre',
       typeId: 'arm',
-      demarches: [{ demarcheDateDebut: toCaminoDate('2023-09-26') }],
+      titreDateDebut: toCaminoDate('2023-09-26'),
     }}
   />
 )
diff --git a/packages/ui/src/components/page/quick-access-titre.tsx b/packages/ui/src/components/page/quick-access-titre.tsx
index 99f112d57..fa05b35f9 100644
--- a/packages/ui/src/components/page/quick-access-titre.tsx
+++ b/packages/ui/src/components/page/quick-access-titre.tsx
@@ -4,32 +4,37 @@ import { getDomaineId, getTitreTypeType } from 'camino-common/src/static/titresT
 import { createDebounce } from '@/utils/debounce'
 import { useRouter } from 'vue-router'
 import { ref, FunctionalComponent, defineComponent } from 'vue'
-import { titreApiClient, TitreForTitresRerchercherByNom } from '../titre/titre-api-client'
+import { titreApiClient } from '../titre/titre-api-client'
 import { capitalize } from 'camino-common/src/strings'
 import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
 import { CaminoAnnee, getAnnee } from 'camino-common/src/date'
 import { TypeAheadSingle } from '../_ui/typeahead-single'
 import { useState } from '@/utils/vue-tsx-utils'
+import { QuickAccessResult } from 'camino-common/src/titres'
 
 export const QuickAccessTitre = defineComponent<{ id: string; onSelectTitre: () => void }>(props => {
   const router = useRouter()
-  const [titres, setTitres] = useState<TitreForTitresRerchercherByNom[]>([])
+  const [titres, setTitres] = useState<QuickAccessResult[]>([])
 
   const search = async (searchTerm: string): Promise<void> => {
-    const intervalle = 10
-
-    let searchTitres = await titreApiClient.titresRechercherByNom(searchTerm)
-
-    if (searchTitres.elements.length === 0) {
-      searchTitres = await titreApiClient.titresRechercherByReferences({
-        intervalle,
-        references: searchTerm,
-      })
+    const searchTitres = await titreApiClient.quickAccess(searchTerm)
+    if ('message' in searchTitres) {
+      console.error(searchTitres)
+      setTitres([])
+    } else {
+      setTitres(searchTitres)
     }
-    setTitres(searchTitres.elements)
+
+    // if (searchTitres.elements.length === 0) {
+    //   searchTitres = await titreApiClient.titresRechercherByReferences({
+    //     intervalle,
+    //     references: searchTerm,
+    //   })
+    // }
+    // setTitres(searchTitres.elements)
   }
 
-  const onSelectedTitre = (titre: TitreForTitresRerchercherByNom | undefined) => {
+  const onSelectedTitre = (titre: QuickAccessResult | undefined) => {
     if (titre) {
       router.push({ name: 'titre', params: { id: titre.id } })
       props.onSelectTitre()
@@ -44,19 +49,18 @@ QuickAccessTitre.props = ['id', 'onSelectTitre']
 
 interface Props {
   id: string
-  titres: TitreForTitresRerchercherByNom[]
-  onSelectedTitre: (titre: TitreForTitresRerchercherByNom | undefined) => void
+  titres: QuickAccessResult[]
+  onSelectedTitre: (titre: QuickAccessResult | undefined) => void
   alwaysOpen?: boolean
   onSearch: (searchTerm: string) => void
 }
 interface DisplayTitreProps {
-  titre: Pick<TitreForTitresRerchercherByNom, 'nom' | 'typeId' | 'demarches'>
+  titre: Pick<QuickAccessResult, 'nom' | 'typeId' | 'titreDateDebut'>
 }
 export const DisplayTitre: FunctionalComponent<DisplayTitreProps> = props => {
   let annee: CaminoAnnee | null = null
-  const firstDemarche = props.titre.demarches.find(({ordre}) => ordre === 1)
-  if (isNotNullNorUndefined(firstDemarche?.demarcheDateDebut)) {
-    annee = getAnnee(firstDemarche.demarcheDateDebut)
+  if (isNotNullNorUndefined(props.titre.titreDateDebut)) {
+    annee = getAnnee(props.titre.titreDateDebut)
   }
 
   return (
@@ -74,12 +78,12 @@ export const DisplayTitre: FunctionalComponent<DisplayTitreProps> = props => {
 }
 
 export const PureQuickAccessTitre = defineComponent<Props>(props => {
-  const display = (titre: TitreForTitresRerchercherByNom) => {
+  const display = (titre: QuickAccessResult) => {
     return <DisplayTitre titre={titre} />
   }
 
-  const overrideItem = ref<TitreForTitresRerchercherByNom | null>(null)
-  const selectItem = (item: TitreForTitresRerchercherByNom | undefined) => {
+  const overrideItem = ref<QuickAccessResult | null>(null)
+  const selectItem = (item: QuickAccessResult | undefined) => {
     overrideItem.value = null
     props.onSelectedTitre(item)
   }
diff --git a/packages/ui/src/components/titre/titre-api-client.ts b/packages/ui/src/components/titre/titre-api-client.ts
index a6a1b4b9e..1595e1910 100644
--- a/packages/ui/src/components/titre/titre-api-client.ts
+++ b/packages/ui/src/components/titre/titre-api-client.ts
@@ -1,4 +1,4 @@
-import { EditableTitre, TitreDemande, TitreDemandeOutput, TitreGet } from 'camino-common/src/titres'
+import { EditableTitre, QuickAccessResult, TitreDemande, TitreDemandeOutput, TitreGet } from 'camino-common/src/titres'
 import { TitreId, TitreIdOrSlug } from 'camino-common/src/validators/titres'
 import { deleteWithJson, getWithJson, newGetWithJson, newPostWithJson, postWithJson } from '../../api/client-rest'
 import { CaminoDate } from 'camino-common/src/date'
@@ -33,12 +33,13 @@ export type TitreForTable = {
   references?: { referenceTypeId: ReferenceTypeId; nom: string }[]
 }
 
-export type TitreForTitresRerchercherByNom = {
+type TitreForTitresRerchercherByNom = {
   id: TitreId
   nom: string
   typeId: TitreTypeId
   demarches: { demarcheDateDebut: CaminoDate | null; ordre: number }[]
 }
+
 export interface TitreApiClient {
   removeTitre: (titreId: TitreId) => Promise<void>
   titreUtilisateurAbonne: (titreId: TitreId, abonne: boolean) => Promise<void>
@@ -98,6 +99,7 @@ export interface TitreApiClient {
     facadesMaritimes: FacadesMaritimes[]
     perimetre?: [number, number, number, number]
   }) => Promise<{ elements: TitreWithPerimetre[]; total: number }>
+  quickAccess: (nom: string) => Promise<QuickAccessResult[] | CaminoError<string>>
   titresRechercherByNom: (nom: string) => Promise<{ elements: TitreForTitresRerchercherByNom[] }>
   titresRechercherByReferences: (params: { intervalle: number; references: string }) => Promise<{ elements: TitreForTitresRerchercherByNom[] }>
   getTitresByIds: (titreIds: TitreId[], cacheKey: string) => Promise<{ elements: Pick<TitreForTable, 'id' | 'nom'>[] }>
@@ -105,6 +107,7 @@ export interface TitreApiClient {
 }
 
 export const titreApiClient: TitreApiClient = {
+  quickAccess: nom => newGetWithJson('/rest/quickAccess', {}, { search: nom }),
   removeTitre: async (titreId: TitreId): Promise<void> => {
     return deleteWithJson('/rest/titres/:titreId', { titreId })
   },
-- 
GitLab


From 6fa775de7af1fa4f7953e4c3d0fcbf9cb1bd9855 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bitard=20Micha=C3=ABl?= <bitard.michael@gmail.com>
Date: Wed, 2 Apr 2025 15:41:19 +0200
Subject: [PATCH 3/3] pr review

---
 .../src/knex/migrations/20250402125154_add-unaccent.ts    | 3 ---
 packages/ui/src/components/page/quick-access-titre.tsx    | 8 --------
 2 files changed, 11 deletions(-)

diff --git a/packages/api/src/knex/migrations/20250402125154_add-unaccent.ts b/packages/api/src/knex/migrations/20250402125154_add-unaccent.ts
index f8e521a8a..eea299502 100644
--- a/packages/api/src/knex/migrations/20250402125154_add-unaccent.ts
+++ b/packages/api/src/knex/migrations/20250402125154_add-unaccent.ts
@@ -14,6 +14,3 @@ export const up = async (knex: Knex): Promise<void> => {
 }
 
 export const down = (): void => {}
-
-// export const config = { transaction: false };
-// export default config
diff --git a/packages/ui/src/components/page/quick-access-titre.tsx b/packages/ui/src/components/page/quick-access-titre.tsx
index fa05b35f9..1767b7b60 100644
--- a/packages/ui/src/components/page/quick-access-titre.tsx
+++ b/packages/ui/src/components/page/quick-access-titre.tsx
@@ -24,14 +24,6 @@ export const QuickAccessTitre = defineComponent<{ id: string; onSelectTitre: ()
     } else {
       setTitres(searchTitres)
     }
-
-    // if (searchTitres.elements.length === 0) {
-    //   searchTitres = await titreApiClient.titresRechercherByReferences({
-    //     intervalle,
-    //     references: searchTerm,
-    //   })
-    // }
-    // setTitres(searchTitres.elements)
   }
 
   const onSelectedTitre = (titre: QuickAccessResult | undefined) => {
-- 
GitLab