diff --git a/config/sidebar.php b/config/sidebar.php index e38e1e0..6192941 100644 --- a/config/sidebar.php +++ b/config/sidebar.php @@ -78,23 +78,6 @@ return [ ], ], ], - // Section "Transport" (M4, ERP-153) : pole logistique, porte le repertoire - // transporteurs. L'item est gate par `transport.carriers.view` ; la section - // disparait automatiquement (SidebarProvider) si le module `transport` est - // desactive ou si l'user n'a pas la permission (Compta / Usine). - [ - 'label' => 'sidebar.transport.section', - 'icon' => 'mdi:truck-outline', - 'items' => [ - [ - 'label' => 'sidebar.transport.carriers', - 'to' => '/carriers', - 'icon' => 'mdi:truck-outline', - 'module' => 'transport', - 'permission' => 'transport.carriers.view', - ], - ], - ], // Section "Administration" : regroupe toutes les pages de configuration // applicative (RBAC, users, sites, audit log). // @@ -117,8 +100,20 @@ return [ // individuelles"), ajouter : 'permission' => 'core.admin.access'. [ 'label' => 'sidebar.administration.section', - 'icon' => 'mdi:cog-outline', + 'icon' => 'mdi:file-settings-cog-outline', 'items' => [ + // Transport — Repertoire transporteurs (M4, ERP-164). Rattache a + // l'Administration (premier item) plutot qu'a une section dediee : + // referentiel global de configuration applicative, sans cloisonnement + // par site. Reste gate par sa propre permission `transport.carriers.view` + // (Admin / Bureau / Commerciale) et son module owner `transport`. + [ + 'label' => 'sidebar.transport.carriers', + 'to' => '/carriers', + 'icon' => 'mdi:truck-outline', + 'module' => 'transport', + 'permission' => 'transport.carriers.view', + ], [ 'label' => 'sidebar.core.roles', 'to' => '/admin/roles', diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index c469f5a..686b1a7 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -495,6 +495,40 @@ } } }, + "transport": { + "carriers": { + "title": "Répertoire transporteurs", + "add": "Ajouter", + "export": "Exporter", + "empty": "Aucun transporteur pour l'instant.", + "column": { + "name": "Nom", + "certification": "Certification", + "validityDate": "Date de validité", + "lastActivity": "Dernière activité" + }, + "certification": { + "QUALIMAT": "QUALIMAT", + "GMP_PLUS": "GMP+", + "OVOCOM": "OVOCOM", + "COMPTE_PROPRE": "Compte-propre", + "AUTRE": "Autre" + }, + "filters": { + "title": "Filtres", + "search": "Recherche", + "certification": "Certification", + "status": "Statut", + "archivedOnly": "Voir les archivés", + "apply": "Voir les résultats", + "reset": "Réinitialiser" + }, + "toast": { + "error": "Une erreur est survenue. Réessayez.", + "exportError": "L'export du répertoire transporteurs a échoué. Réessayez." + } + } + }, "auth": { "login": "Connexion", "logout": "Deconnexion", diff --git a/frontend/modules/commercial/composables/useSuppliersRepository.ts b/frontend/modules/commercial/composables/useSuppliersRepository.ts index 5c05b10..37031ea 100644 --- a/frontend/modules/commercial/composables/useSuppliersRepository.ts +++ b/frontend/modules/commercial/composables/useSuppliersRepository.ts @@ -41,9 +41,10 @@ export interface Supplier { * sur la ressource `/suppliers` (RG-13 : pagination serveur obligatoire ; jamais * de chargement integral en memoire). Miroir de `useClientsRepository` (M1). * - * Les filtres (recherche, categories, sites, inclusion des archives) sont pilotes - * par la page via `setFilters` du composable partage — la remise en page 1 est - * garantie. + * Les filtres (recherche, categories, sites, archives) sont pilotes par la page + * via `setFilters` du composable partage — la remise en page 1 est garantie. + * Cocher « Voir les archivés » envoie `archivedOnly=true` → seules les archives + * sont listees (aligne sur Client). * * Volontairement PAR INSTANCE (pas de singleton module-level) : l'etat tableau * est propre a l'ecran Repertoire et meurt avec lui, comme tout consommateur de diff --git a/frontend/modules/transport/composables/__tests__/useCarriersRepository.test.ts b/frontend/modules/transport/composables/__tests__/useCarriersRepository.test.ts new file mode 100644 index 0000000..3da48a3 --- /dev/null +++ b/frontend/modules/transport/composables/__tests__/useCarriersRepository.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { useCarriersRepository, type Carrier } from '../useCarriersRepository' + +const mockApiGet = vi.hoisted(() => vi.fn()) +vi.stubGlobal('useApi', () => ({ get: mockApiGet })) + +/** + * Tests du repertoire transporteurs (ERP-164). + * + * `useCarriersRepository` est une fine enveloppe de `usePaginatedList` + * sur `/carriers`. Les invariants generiques de pagination sont deja couverts par + * `usePaginatedList.test.ts` ; on verifie ici le CONTRAT propre au repertoire : + * - la ressource ciblee est bien `/carriers` ; + * - l'enveloppe Hydra (member / totalItems) est consommee ; + * - le header `Accept: application/ld+json` est envoye (sinon API Platform 4 + * renvoie un tableau plat sans pagination) ; + * - EXCLUSION DES ARCHIVES PAR DEFAUT : aucun `archivedOnly` n'est envoye + * tant que l'utilisateur ne coche pas le filtre (le back masque alors les + * archives) ; le filtre « Voir les archivés » est bien transmis une fois + * applique (aligne sur Client / Fournisseur / Prestataire). + */ +describe('useCarriersRepository', () => { + beforeEach(() => { + mockApiGet.mockReset() + }) + + /** Une page de transporteurs Hydra, avec qualimatCarrier embarque (RG-4.04). */ + const PAGE: Carrier[] = [ + { + id: 1, + name: 'TRANSPORTS ACME', + certificationType: 'QUALIMAT', + qualimatCarrier: { + id: '42', + name: 'TRANSPORTS ACME', + validityDate: '2027-01-15', + status: 'VALIDE', + }, + updatedAt: '2026-06-15T08:12:01+02:00', + isArchived: false, + }, + ] + + it('cible /carriers, consomme l\'enveloppe Hydra et envoie l\'Accept ld+json', async () => { + mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 }) + const repo = useCarriersRepository() + + await repo.fetch() + + expect(mockApiGet).toHaveBeenCalledTimes(1) + const [url, query, opts] = mockApiGet.mock.calls[0] + expect(url).toBe('/carriers') + expect(query).toMatchObject({ page: 1, itemsPerPage: 10 }) + expect(opts).toMatchObject({ + toast: false, + headers: { Accept: 'application/ld+json' }, + }) + expect(repo.items.value).toEqual(PAGE) + expect(repo.totalItems.value).toBe(1) + }) + + it('exclut les archives par defaut : aucun archivedOnly au premier fetch', async () => { + mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 }) + const repo = useCarriersRepository() + + await repo.fetch() + + const query = mockApiGet.mock.calls[0][1] as Record + expect(query.archivedOnly).toBeUndefined() + }) + + it('transmet archivedOnly une fois le filtre applique (retour page 1)', async () => { + mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 }) + const repo = useCarriersRepository() + await repo.fetch() + + mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 }) + await repo.setFilters({ archivedOnly: true }) + + expect(repo.currentPage.value).toBe(1) + const query = mockApiGet.mock.calls.at(-1)?.[1] as Record + expect(query.archivedOnly).toBe(true) + }) + + it('transmet les certifications multiples + la recherche', async () => { + mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 }) + const repo = useCarriersRepository() + await repo.fetch() + + mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 }) + await repo.setFilters({ search: 'acme', 'certificationType[]': ['QUALIMAT', 'AUTRE'] }) + + const query = mockApiGet.mock.calls.at(-1)?.[1] as Record + expect(query.search).toBe('acme') + expect(query['certificationType[]']).toEqual(['QUALIMAT', 'AUTRE']) + }) +}) diff --git a/frontend/modules/transport/composables/useCarriersRepository.ts b/frontend/modules/transport/composables/useCarriersRepository.ts new file mode 100644 index 0000000..941eee7 --- /dev/null +++ b/frontend/modules/transport/composables/useCarriersRepository.ts @@ -0,0 +1,70 @@ +import { usePaginatedList } from '~/shared/composables/usePaginatedList' + +/** + * Vue MINIMALE du referentiel QUALIMAT embarque (groupe `qualimat:read`) dans la + * LISTE des transporteurs. Seuls les champs consommes par le Repertoire sont + * types : `validityDate` alimente la colonne « Date de validité » (fond rouge si + * perimee — RG-4.04). L'id QUALIMAT est une chaine (colonne BIGINT cote back). + */ +export interface CarrierQualimat { + id: string + name: string | null + /** Date ISO de validite de l'agrement QUALIMAT (date_immutable) — RG-4.04. */ + validityDate: string | null + status: string | null +} + +/** + * Vue MINIMALE d'un transporteur pour le Repertoire (datatable). Volontairement + * partielle : seuls les champs des colonnes + l'id (navigation) sont types ici. + * Le detail complet (onglets Adresses / Contacts / Prix) est hors perimetre de + * cet ecran (ERP-164, ticket #9). + * + * `certificationType` : QUALIMAT | GMP_PLUS | OVOCOM | COMPTE_PROPRE | AUTRE, ou + * `null` dans le cas LIOT (compte-propre interne sans certification — RG-4.01). + * Le libelle affiche est resolu cote front (cle i18n `transport.carriers.certification.*`). + */ +export interface Carrier { + id: number + name: string | null + certificationType: string | null + /** Lien editable vers le referentiel QUALIMAT (null si transporteur non QUALIMAT). */ + qualimatCarrier: CarrierQualimat | null + /** Date ISO de derniere modification (default:read) — colonne « Dernière activité ». */ + updatedAt: string | null + isArchived: boolean +} + +/** + * Filtres du Repertoire transporteurs, branches sur les query params de + * `GET /api/carriers` (spec-back § 4.1). Pilotes par la page via `setFilters` : + * - `search` : recherche fuzzy sur le nom ; + * - `certificationType[]` : multi-valeurs (OR cote back) ; + * - `archivedOnly` : n'affiche QUE les archives (toggle « Voir les archivés », + * aligne sur les autres repertoires M1/M2/M3). + */ +export interface CarrierFilters { + search?: string + 'certificationType[]'?: string[] + archivedOnly?: boolean +} + +/** + * Repertoire transporteurs (M4, ERP-164) — simple enveloppe de + * `usePaginatedList` sur la ressource `/carriers` (regle ABSOLUE n°13 : + * pagination serveur obligatoire ; jamais de chargement integral en memoire). + * Miroir de `useSuppliersRepository` (M2) / `useProvidersRepository` (M3). + * + * Les filtres (recherche, certifications, archives) sont pilotes par la page via + * `setFilters` du composable partage — la remise en page 1 est garantie. Par + * defaut AUCUN `archivedOnly` n'est envoye : le back masque alors les archives + * (§ 2.4). Cocher « Voir les archivés » envoie `archivedOnly=true` (seules les + * archives sont listees, aligne sur Client / Fournisseur / Prestataire). + * + * Volontairement PAR INSTANCE (pas de singleton module-level) : l'etat tableau + * est propre a l'ecran Repertoire et meurt avec lui, comme tout consommateur de + * `usePaginatedList`. Aucun reset au logout a gerer. + */ +export function useCarriersRepository() { + return usePaginatedList({ url: '/carriers' }) +} diff --git a/frontend/modules/transport/pages/__tests__/carriersIndex.spec.ts b/frontend/modules/transport/pages/__tests__/carriersIndex.spec.ts new file mode 100644 index 0000000..f3d0d38 --- /dev/null +++ b/frontend/modules/transport/pages/__tests__/carriersIndex.spec.ts @@ -0,0 +1,211 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import { defineComponent, h, ref } from 'vue' + +// ── Auto-imports Nuxt stubbes globalement ─────────────────────────────────── +// La page ne les importe pas (auto-import) : on les expose en globals pour le +// runtime de test (happy-dom). Meme philosophie que les specs M1/M2/M3. +const mockPush = vi.hoisted(() => vi.fn()) +const mockApiGet = vi.hoisted(() => vi.fn()) +const mockCan = vi.hoisted(() => vi.fn()) +const mockSetFilters = vi.hoisted(() => vi.fn()) +const mockFetch = vi.hoisted(() => vi.fn()) +const mockToastError = vi.hoisted(() => vi.fn()) + +vi.stubGlobal('useI18n', () => ({ t: (key: string) => key })) +vi.stubGlobal('useHead', () => undefined) +vi.stubGlobal('useApi', () => ({ get: mockApiGet })) +vi.stubGlobal('useRouter', () => ({ push: mockPush })) +vi.stubGlobal('useToast', () => ({ error: mockToastError, success: vi.fn() })) +vi.stubGlobal('usePermissions', () => ({ can: mockCan })) + +// Le repository est lui aussi un auto-import : on controle items + setFilters. +vi.stubGlobal('useCarriersRepository', () => ({ + items: ref([ + { + id: 7, + name: 'TRANSPORTS ACME', + certificationType: 'QUALIMAT', + qualimatCarrier: { id: '42', name: 'TRANSPORTS ACME', validityDate: '2027-01-15', status: 'VALIDE' }, + updatedAt: '2026-01-15T10:00:00+00:00', + isArchived: false, + }, + ]), + totalItems: ref(1), + currentPage: ref(1), + itemsPerPage: ref(10), + itemsPerPageOptions: ref([10, 25, 50]), + fetch: mockFetch, + goToPage: vi.fn(), + setItemsPerPage: vi.fn(), + setFilters: mockSetFilters, +})) + +// happy-dom n'implemente pas createObjectURL : on ajoute les methodes statiques +// sur la classe URL existante (sans la remplacer — sinon `new URL()` casse). +globalThis.URL.createObjectURL = vi.fn(() => 'blob:fake') +globalThis.URL.revokeObjectURL = vi.fn() + +// Import APRES les stubs (la page resout les auto-imports au top-level du module). +const CarriersIndex = (await import('../carriers/index.vue')).default + +// ── Stubs de composants ────────────────────────────────────────────────────── +const ButtonStub = defineComponent({ + props: { label: { type: String, default: '' }, disabled: { type: Boolean, default: false } }, + emits: ['click'], + setup(props, { emit }) { + return () => h('button', { 'data-label': props.label, onClick: () => emit('click') }, props.label) + }, +}) + +const DataTableStub = defineComponent({ + props: { items: { type: Array, default: () => [] } }, + emits: ['row-click', 'update:page', 'update:per-page'], + setup(props, { emit }) { + return () => h('div', { 'data-testid': 'datatable' }, + (props.items as Array<{ id: number }>).map(it => + h('tr', { 'data-row-id': it.id, onClick: () => emit('row-click', it) }), + ), + ) + }, +}) + +const DrawerStub = defineComponent({ + props: { modelValue: { type: Boolean, default: false } }, + setup(_, { slots }) { + return () => h('div', {}, [slots.header?.(), slots.default?.(), slots.footer?.()]) + }, +}) + +const SlotStub = defineComponent({ setup(_, { slots }) { return () => h('div', {}, slots.default?.()) } }) + +const PageHeaderStub = defineComponent({ + setup(_, { slots }) { return () => h('div', {}, [slots.default?.(), slots.actions?.()]) }, +}) + +const CheckboxStub = defineComponent({ + props: { id: { type: String, default: '' }, modelValue: { type: Boolean, default: false } }, + emits: ['update:model-value'], + setup(props, { emit }) { + return () => h('input', { + 'type': 'checkbox', + 'data-id': props.id, + 'onChange': (e: Event) => emit('update:model-value', (e.target as HTMLInputElement).checked), + }) + }, +}) + +const InputTextStub = defineComponent({ setup() { return () => h('input') } }) + +function mountPage() { + return mount(CarriersIndex, { + global: { + stubs: { + PageHeader: PageHeaderStub, + MalioButton: ButtonStub, + MalioDataTable: DataTableStub, + MalioDrawer: DrawerStub, + MalioAccordion: SlotStub, + MalioAccordionItem: SlotStub, + MalioInputText: InputTextStub, + MalioCheckbox: CheckboxStub, + }, + }, + }) +} + +describe('Répertoire transporteurs (page /carriers)', () => { + beforeEach(() => { + mockPush.mockReset() + mockApiGet.mockReset().mockResolvedValue({ member: [] }) + mockCan.mockReset().mockReturnValue(true) + mockSetFilters.mockReset() + mockFetch.mockReset() + mockToastError.mockReset() + }) + + it('charge la liste au montage', async () => { + mountPage() + await flushPromises() + expect(mockFetch).toHaveBeenCalled() + }) + + it('affiche « + Ajouter » uniquement avec la permission manage', async () => { + mockCan.mockImplementation((perm: string) => perm === 'transport.carriers.manage') + const wrapper = mountPage() + await flushPromises() + expect(wrapper.find('[data-label="transport.carriers.add"]').exists()).toBe(true) + }) + + it('masque « + Ajouter » sans la permission manage (view seul)', async () => { + mockCan.mockImplementation((perm: string) => perm === 'transport.carriers.view') + const wrapper = mountPage() + await flushPromises() + expect(wrapper.find('[data-label="transport.carriers.add"]').exists()).toBe(false) + }) + + it('navigue vers la consultation au clic sur une ligne', async () => { + const wrapper = mountPage() + await flushPromises() + await wrapper.find('tr[data-row-id="7"]').trigger('click') + expect(mockPush).toHaveBeenCalledWith('/carriers/7') + }) + + it('appelle l\'export XLSX sur /carriers/export.xlsx en blob', async () => { + const wrapper = mountPage() + await flushPromises() + await wrapper.find('[data-label="transport.carriers.export"]').trigger('click') + await flushPromises() + expect(mockApiGet).toHaveBeenCalledWith( + '/carriers/export.xlsx', + expect.any(Object), + expect.objectContaining({ responseType: 'blob', toast: false }), + ) + }) + + it('repercute le filtre « Voir les archivés » dans setFilters sans toucher l\'URL', async () => { + const wrapper = mountPage() + await flushPromises() + + // Coche « Voir les archivés » puis applique les filtres. + await wrapper.find('input[data-id="filter-archived-only"]').setValue(true) + await wrapper.find('[data-label="transport.carriers.filters.apply"]').trigger('click') + + expect(mockSetFilters).toHaveBeenLastCalledWith( + { archivedOnly: true }, + { replace: true }, + ) + // Etat 100 % local (regle n°6) : aucune navigation/query string declenchee. + expect(mockPush).not.toHaveBeenCalled() + }) + + it('repercute les certifications cochees dans setFilters (filtre multi)', async () => { + const wrapper = mountPage() + await flushPromises() + + // Coche deux certifications via les cases a cocher (pattern repertoire clients). + await wrapper.find('input[data-id="filter-certification-QUALIMAT"]').setValue(true) + await wrapper.find('input[data-id="filter-certification-AUTRE"]').setValue(true) + await wrapper.find('[data-label="transport.carriers.filters.apply"]').trigger('click') + + expect(mockSetFilters).toHaveBeenLastCalledWith( + { 'certificationType[]': ['QUALIMAT', 'AUTRE'] }, + { replace: true }, + ) + }) + + it('badge filtres actifs + Réinitialiser vide l\'etat applique', async () => { + const wrapper = mountPage() + await flushPromises() + + await wrapper.find('input[data-id="filter-archived-only"]').setValue(true) + await wrapper.find('[data-label="transport.carriers.filters.apply"]').trigger('click') + + // Le libelle du bouton Filtrer porte le compteur (1 filtre actif). + expect(wrapper.find('[data-label="transport.carriers.filters.title (1)"]').exists()).toBe(true) + + // Réinitialiser → query propre (setFilters avec objet vide). + await wrapper.find('[data-label="transport.carriers.filters.reset"]').trigger('click') + expect(mockSetFilters).toHaveBeenLastCalledWith({}, { replace: true }) + }) +}) diff --git a/frontend/modules/transport/pages/carriers/index.vue b/frontend/modules/transport/pages/carriers/index.vue new file mode 100644 index 0000000..1e5bdea --- /dev/null +++ b/frontend/modules/transport/pages/carriers/index.vue @@ -0,0 +1,389 @@ + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2344110..299887c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,7 +7,7 @@ "name": "starseed-frontend", "hasInstallScript": true, "dependencies": { - "@malio/layer-ui": "^1.7.10", + "@malio/layer-ui": "^1.7.12", "@nuxt/icon": "^2.2.1", "@nuxtjs/i18n": "^10.2.3", "@nuxtjs/tailwindcss": "^6.14.0", @@ -583,9 +583,9 @@ "license": "MIT" }, "node_modules/@emnapi/core": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", - "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "license": "MIT", "optional": true, "dependencies": { @@ -594,9 +594,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", - "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", "optional": true, "dependencies": { @@ -1866,9 +1866,9 @@ "license": "MIT" }, "node_modules/@malio/layer-ui": { - "version": "1.7.10", - "resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.7.10/layer-ui-1.7.10.tgz", - "integrity": "sha512-ZWYaKvl+VpGAqeTE+4xdyKOmuRd4zwjlUYVppeIBZwGeNAK16kZnrztR+4eQmnzUqPZVybBhEBdKP9weqWHSUg==", + "version": "1.7.12", + "resolved": "https://gitea.malio.fr/api/packages/MALIO-DEV/npm/%40malio%2Flayer-ui/-/1.7.12/layer-ui-1.7.12.tgz", + "integrity": "sha512-ezQLqi19K2ogI3XwSMsUyluU9x5C4W0tu1muxFbL3foKjibRYRg/FdvySivEhEsalAAt1E88V6Sv/06xPqyYTw==", "dependencies": { "@nuxt/icon": "^2.2.1", "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/frontend/package.json b/frontend/package.json index b4cf9b0..776fe52 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,7 @@ "test:e2e:ui": "playwright test --ui" }, "dependencies": { - "@malio/layer-ui": "^1.7.10", + "@malio/layer-ui": "^1.7.12", "@nuxt/icon": "^2.2.1", "@nuxtjs/i18n": "^10.2.3", "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/frontend/tests/e2e/_fixtures/personas.ts b/frontend/tests/e2e/_fixtures/personas.ts index b690cc2..5bd15c0 100644 --- a/frontend/tests/e2e/_fixtures/personas.ts +++ b/frontend/tests/e2e/_fixtures/personas.ts @@ -95,10 +95,11 @@ export const personas: Record = { 'technique.providers.accounting.view', 'technique.providers.accounting.manage', 'technique.providers.archive', - // Transport — Repertoire transporteurs (M4, ERP-153). Meme logique : + // Transport — Repertoire transporteurs (M4, ERP-164). Meme logique : // mappe sur le persona "tout", pas de nouveau persona (regle ABSOLUE - // n°7). transport.carriers.view n'ajoute pas de lien dans la section - // Administration, donc expectedAdminLinks reste inchange. + // n°7). L'item transporteurs vit desormais dans la section Administration + // (1er item, ERP-164) mais sur la route `/carriers` (hors `/admin/`), + // donc il n'entre pas dans ALL_ADMIN_LINKS : expectedAdminLinks reste inchange. 'transport.carriers.view', 'transport.carriers.manage', 'transport.carriers.archive',