From eb94204c550e13607ec361ad92e00537d911d2c5 Mon Sep 17 00:00:00 2001 From: tristan Date: Sat, 27 Jun 2026 17:18:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(catalog)=20:=20M6=20=E2=80=94=20=C3=A9cran?= =?UTF-8?q?=20consultation=20produit=20+=20onglets=20conditionn=C3=A9s=20+?= =?UTF-8?q?=20=C3=A9dition=20sans=20redirection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nouvel écran de consultation lecture seule /admin/products/{id} (calque client/fournisseur) : clic sur une ligne ouvre la consultation (plus l'édition directe), bouton « Modifier » → édition. - Règle ERP-193 en consultation : champs vides / checkbox non cochées masqués (isFilled) ; onglets vides masqués → les coquilles Fournisseurs/Clients (placeholder, module Contrat inexistant) ne sont pas rendues en consultation. - Onglets Fournisseurs/Clients : non affichés à l'ajout (avant validation du formulaire principal) ; visibilité conditionnée par l'état (spec C3, « Aucun » = OTHER) : Fournisseurs si Achat/Aucun, Clients si Vendu/Aucun. - Édition : après « Enregistrer » on reste sur l'écran (l'utilisateur garde la main, calque client/fournisseur) ; réaffichage des valeurs normalisées serveur (RG-6.07) via re-prefill, plus de redirection. - i18n consultation + tests (consultation, onglets, no-redirect) ; spec écran 8.bis. --- docs/specs/M6-produit/spec-back.md | 3 +- frontend/i18n/locales/fr.json | 11 +- .../components/ProductPlaceholderTabs.vue | 43 ++++- .../__tests__/ProductPlaceholderTabs.spec.ts | 64 ++++++++ .../__tests__/useProductForm.spec.ts | 19 ++- .../catalog/composables/useProductForm.ts | 6 +- .../__tests__/productConsultation.spec.ts | 139 ++++++++++++++++ .../pages/__tests__/productEdit.spec.ts | 9 +- .../pages/__tests__/productNew.spec.ts | 5 + .../pages/__tests__/productsIndex.spec.ts | 4 +- .../pages/admin/products/[id]/edit.vue | 33 ++-- .../pages/admin/products/[id]/index.vue | 155 ++++++++++++++++++ .../catalog/pages/admin/products/index.vue | 4 +- .../catalog/pages/admin/products/new.vue | 6 +- 14 files changed, 464 insertions(+), 37 deletions(-) create mode 100644 frontend/modules/catalog/components/__tests__/ProductPlaceholderTabs.spec.ts create mode 100644 frontend/modules/catalog/pages/__tests__/productConsultation.spec.ts create mode 100644 frontend/modules/catalog/pages/admin/products/[id]/index.vue diff --git a/docs/specs/M6-produit/spec-back.md b/docs/specs/M6-produit/spec-back.md index 188ced2..f9945ae 100644 --- a/docs/specs/M6-produit/spec-back.md +++ b/docs/specs/M6-produit/spec-back.md @@ -637,7 +637,7 @@ Toute permission `catalog.products.*` doit être posée **simultanément** dans | **RG-6.07** | back | Normalisation serveur : `code` trim+UPPER, `name` trim (§ 6). | | **RG-6.08** | back | « Modification » = même formulaire/mêmes règles que « Ajouter » ; bouton « Valider » → « Enregistrer » (docx p.7). Code & contraintes inchangés. | | **RG-6.09** | back | Liste : exclut par défaut les produits soft-deleted ; pas de Delete exposé (§ 2.7). | -| **RG-6.10** | back | Onglets « Fournisseurs » / « Clients » = **hors périmètre V0** (placeholder), dépendent du module Contrat inexistant (HP-M6-01). | +| **RG-6.10** | back+front | Onglets « Fournisseurs » / « Clients » = **hors périmètre V0** (placeholder), dépendent du module Contrat inexistant (HP-M6-01). **Front** : (a) NON affichés à l'ajout — n'apparaissent qu'après validation du formulaire principal (écran de modification) ; (b) visibilité conditionnée par l'état (cf. C3, « Aucun » = `OTHER`) : « Fournisseurs » si `PURCHASE` ou `OTHER`, « Clients » si `SALE` ou `OTHER`. | Cohérence inter-champs (RG-6.03 / 6.05) implémentée via `#[Assert\Callback]` portant des messages FR + CHECK Postgres (non-vacuité `states`). RG-6.06 : simple `Assert\Count(min:1)` (référentiel plat, plus de validation de disponibilité par site). @@ -675,6 +675,7 @@ Cohérence inter-champs (RG-6.03 / 6.05) implémentée via `#[Assert\Callback]` | 6 | Tests PHPUnit RG-6.01→6.10 + capture contrat JSON | Backend | | 7 | Page liste `/admin/products` (usePaginatedList) + drawer filtre + export | Frontend | | 8 | Écran Ajouter (champs conditionnels SALE, selects filtrés catégorie/stockage) | Frontend | +| 8.bis | Écran **Consultation** (lecture seule) `/admin/products/{id}` : clic sur une ligne → consultation (pas l'édition directe), bouton « Modifier » → édition. **Règle ERP-193 (calque client/fournisseur)** : champs vides + checkbox non cochées masqués, et **onglets vides masqués** → les coquilles Fournisseurs/Clients (placeholder, module Contrat inexistant) ne sont **pas affichées en consultation** (elles restent visibles à l'édition). | Frontend | | 9 | Écran Modification (« Enregistrer ») + onglets **placeholder « en cours de dev »** (Fournisseurs / Clients) | Frontend | | 10 | i18n + libellé audit (`catalog_product`) | Frontend | diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 837c850..00d3da2 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -1063,11 +1063,20 @@ }, "edit": { "title": "Modifier le produit", - "back": "Retour au catalogue", + "back": "Retour", "save": "Enregistrer", "loading": "Chargement du produit…", "notFound": "Produit introuvable." }, + "consultation": { + "title": "Fiche produit", + "back": "Retour au catalogue", + "loading": "Chargement du produit…", + "notFound": "Produit introuvable." + }, + "action": { + "edit": "Modifier" + }, "tab": { "suppliers": "Fournisseurs", "clients": "Clients", diff --git a/frontend/modules/catalog/components/ProductPlaceholderTabs.vue b/frontend/modules/catalog/components/ProductPlaceholderTabs.vue index 29a9461..75614eb 100644 --- a/frontend/modules/catalog/components/ProductPlaceholderTabs.vue +++ b/frontend/modules/catalog/components/ProductPlaceholderTabs.vue @@ -4,24 +4,51 @@ V0 (HP-M6-01, RG-6.10) : ils dependent d'un module Contrat inexistant. Rendu en placeholder « en cours de développement » (meme composant que les onglets non-dev des fiches M1→M4). AUCUN appel API, AUCUN champ saisissable. - Affiches sans condition d'etat (a raffiner avec le module Contrat). + + Visibilite conditionnee par l'etat du produit (cf. spec C3, « Aucun » = OTHER) : + - « Fournisseurs » : visible si l'etat contient Achat (PURCHASE) ou Aucun (OTHER) ; + - « Clients » : visible si l'etat contient Vendu (SALE) ou Aucun (OTHER). + Si aucun onglet n'est applicable (etat vide), rien n'est rendu. --> - + diff --git a/frontend/modules/catalog/components/__tests__/ProductPlaceholderTabs.spec.ts b/frontend/modules/catalog/components/__tests__/ProductPlaceholderTabs.spec.ts new file mode 100644 index 0000000..9f11ab6 --- /dev/null +++ b/frontend/modules/catalog/components/__tests__/ProductPlaceholderTabs.spec.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import { defineComponent, h, nextTick } from 'vue' +import ProductPlaceholderTabs from '../ProductPlaceholderTabs.vue' + +// i18n auto-import : retourne la cle telle quelle. +vi.stubGlobal('useI18n', () => ({ t: (key: string) => key })) + +// Stub de MalioTabList : expose les `key` des onglets recus (data-tab) pour +// verifier la visibilite conditionnee par l'etat, sans dependre de la lib UI. +const TabListStub = defineComponent({ + props: { tabs: { type: Array, default: () => [] }, modelValue: { type: String, default: '' } }, + setup(props) { + return () => h( + 'div', + { 'data-testid': 'tablist' }, + (props.tabs as { key: string }[]).map(tab => h('span', { 'data-tab': tab.key })), + ) + }, +}) +const PlaceholderStub = defineComponent({ setup() { return () => h('div') } }) + +function mountTabs(states: string[]) { + return mount(ProductPlaceholderTabs, { + props: { states }, + global: { stubs: { MalioTabList: TabListStub, ComingSoonPlaceholder: PlaceholderStub } }, + }) +} + +const tabKeys = (wrapper: ReturnType): string[] => + wrapper.findAll('[data-tab]').map(node => node.attributes('data-tab') ?? '') + +describe('ProductPlaceholderTabs — visibilite conditionnee par l\'etat', () => { + it('Achat (PURCHASE) : affiche uniquement « Fournisseurs »', () => { + expect(tabKeys(mountTabs(['PURCHASE']))).toEqual(['suppliers']) + }) + + it('Vendu (SALE) : affiche uniquement « Clients »', () => { + expect(tabKeys(mountTabs(['SALE']))).toEqual(['clients']) + }) + + it('Aucun (OTHER) : affiche les deux onglets', () => { + expect(tabKeys(mountTabs(['OTHER']))).toEqual(['suppliers', 'clients']) + }) + + it('Achat + Vendu : affiche les deux onglets', () => { + expect(tabKeys(mountTabs(['PURCHASE', 'SALE']))).toEqual(['suppliers', 'clients']) + }) + + it('etat vide : ne rend aucun onglet (MalioTabList absent)', () => { + const wrapper = mountTabs([]) + expect(wrapper.find('[data-testid="tablist"]').exists()).toBe(false) + }) + + it('retombe sur le premier onglet visible si l\'actif disparait', async () => { + // OTHER -> suppliers actif par defaut ; passage a SALE retire « Fournisseurs ». + const wrapper = mountTabs(['OTHER']) + await wrapper.setProps({ states: ['SALE'] }) + await nextTick() + // Seul « Clients » subsiste : pas d'onglet actif fantome (verifie via le modelValue). + const tablist = wrapper.findComponent(TabListStub) + expect(tablist.props('modelValue')).toBe('clients') + }) +}) diff --git a/frontend/modules/catalog/composables/__tests__/useProductForm.spec.ts b/frontend/modules/catalog/composables/__tests__/useProductForm.spec.ts index 9294f47..d0261f3 100644 --- a/frontend/modules/catalog/composables/__tests__/useProductForm.spec.ts +++ b/frontend/modules/catalog/composables/__tests__/useProductForm.spec.ts @@ -245,7 +245,9 @@ describe('useProductForm', () => { }) it('soumet un PATCH /products/{id} apres prefill (RG-6.08)', async () => { - mockPatch.mockResolvedValueOnce({ id: 34 }) + // Le PATCH renvoie le produit normalise : submit re-prefill le form a partir + // de la reponse (l'utilisateur reste sur l'ecran, pas de redirection). + mockPatch.mockResolvedValueOnce({ ...PRODUCT, code: 'BLE-01', name: 'Blé tendre' }) const { prefill, submit } = useProductForm() await prefill(PRODUCT) @@ -261,6 +263,21 @@ describe('useProductForm', () => { expect(mockToastSuccess).toHaveBeenCalled() }) + it('re-affiche les valeurs normalisees du serveur apres un PATCH (RG-6.07, pas de redirection)', async () => { + // Le back normalise code (trim+UPPER) et name (trim) : le form doit refleter + // la reponse serveur, pas la saisie locale. + mockPatch.mockResolvedValueOnce({ ...PRODUCT, code: 'BLE-01', name: 'Blé tendre' }) + const { form, prefill, submit } = useProductForm() + await prefill(PRODUCT) + form.code = 'ble-01 ' + form.name = ' Blé tendre ' + + await submit() + + expect(form.code).toBe('BLE-01') + expect(form.name).toBe('Blé tendre') + }) + it('mappe un 409 doublon de code aussi en edition', async () => { mockPatch.mockRejectedValueOnce({ response: { status: 409, _data: {} } }) const { errors, prefill, submit } = useProductForm() diff --git a/frontend/modules/catalog/composables/useProductForm.ts b/frontend/modules/catalog/composables/useProductForm.ts index 1d1a57a..d6ff1ce 100644 --- a/frontend/modules/catalog/composables/useProductForm.ts +++ b/frontend/modules/catalog/composables/useProductForm.ts @@ -151,8 +151,12 @@ export function useProductForm() { } const options = { headers: { Accept: 'application/ld+json' }, toast: false } if (editing) { - await api.patch(`/products/${productId.value}`, payload, options) + const updated = await api.patch(`/products/${productId.value}`, payload, options) toast.success({ title: t('admin.products.toast.updateSuccess') }) + // L'utilisateur garde la main (pas de redirection, calque client/ + // fournisseur) : on reaffiche les valeurs normalisees renvoyees par le + // serveur (code trim+UPPER, name trim — RG-6.07) directement dans le form. + await prefill(updated) } else { await api.post('/products', payload, options) diff --git a/frontend/modules/catalog/pages/__tests__/productConsultation.spec.ts b/frontend/modules/catalog/pages/__tests__/productConsultation.spec.ts new file mode 100644 index 0000000..7fbf1a7 --- /dev/null +++ b/frontend/modules/catalog/pages/__tests__/productConsultation.spec.ts @@ -0,0 +1,139 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import { defineComponent, h, Suspense } from 'vue' + +// Produit charge simule (cles de la reponse reelle § 4.0.bis). +const PRODUCT = { + id: 34, + code: 'BLE-01', + name: 'Blé tendre', + states: ['PURCHASE', 'SALE'], + manufactured: true, + containsMolasses: false, + category: { '@id': '/api/categories/12', id: 12, name: 'Céréales', code: 'CEREALES' }, + sites: [{ '@id': '/api/sites/1', id: 1, name: 'Chatellerault' }], + storageTypes: [{ '@id': '/api/storage_types/9', id: 9, code: 'TAS', label: 'Tas' }], + createdAt: '', updatedAt: '', +} + +const fx = vi.hoisted(() => ({ load: vi.fn() })) + +vi.mock('~/modules/catalog/composables/useProduct', async () => { + const { ref } = await import('vue') + return { + useProduct: () => ({ + product: ref(PRODUCT), + loading: ref(false), + error: ref(false), + load: fx.load, + }), + } +}) + +// ── Auto-imports Nuxt stubbes globalement ─────────────────────────────────── +const mockPush = vi.hoisted(() => vi.fn()) +const mockNavigateTo = vi.hoisted(() => vi.fn()) +const mockCan = vi.hoisted(() => vi.fn()) + +vi.stubGlobal('useI18n', () => ({ t: (key: string) => key })) +vi.stubGlobal('useHead', () => undefined) +vi.stubGlobal('useRoute', () => ({ params: { id: '34' } })) +vi.stubGlobal('useRouter', () => ({ push: mockPush })) +vi.stubGlobal('usePermissions', () => ({ can: mockCan })) +vi.stubGlobal('navigateTo', mockNavigateTo) + +const ViewPage = (await import('../admin/products/[id]/index.vue')).default + +const ButtonStub = defineComponent({ + props: { label: { type: String, default: '' } }, + emits: ['click'], + setup(props, { emit }) { + return () => h('button', { 'data-label': props.label, onClick: () => emit('click') }, props.label) + }, +}) +// Input lecture seule : expose le label + la valeur affichee (model-value). +const InputStub = defineComponent({ + props: { label: { type: String, default: '' }, modelValue: { default: null } }, + setup(props) { return () => h('input', { 'data-label': props.label, 'data-value': String(props.modelValue ?? '') }) }, +}) +const CheckboxStub = defineComponent({ + props: { label: { type: String, default: '' }, modelValue: { type: Boolean, default: false } }, + setup(props) { return () => h('input', { 'type': 'checkbox', 'data-label': props.label }) }, +}) +const TabsStub = defineComponent({ setup() { return () => h('div', { 'data-testid': 'placeholder-tabs' }) } }) + +const stubs = { + MalioButtonIcon: ButtonStub, + MalioButton: ButtonStub, + MalioInputText: InputStub, + MalioCheckbox: CheckboxStub, + ProductPlaceholderTabs: TabsStub, +} + +async function mountPage() { + const wrapper = mount(defineComponent({ + components: { ViewPage }, + setup: () => () => h(Suspense, null, { default: () => h(ViewPage) }), + }), { global: { stubs } }) + await flushPromises() + return wrapper +} + +describe('Écran Consultation produit (page /admin/products/{id})', () => { + beforeEach(() => { + fx.load.mockReset().mockResolvedValue(undefined) + mockPush.mockReset() + mockNavigateTo.mockReset() + mockCan.mockReset().mockReturnValue(true) + }) + + it('charge le produit au montage', async () => { + await mountPage() + expect(fx.load).toHaveBeenCalled() + }) + + it('redirige vers la liste sans la permission view', async () => { + mockCan.mockReturnValue(false) + await mountPage() + expect(mockNavigateTo).toHaveBeenCalledWith('/admin/products') + }) + + it('affiche les champs en lecture seule (libelles mappes)', async () => { + const wrapper = await mountPage() + const valueOf = (label: string) => + wrapper.find(`[data-label="${label}"]`).attributes('data-value') + expect(valueOf('admin.products.form.name')).toBe('Blé tendre') + expect(valueOf('admin.products.form.code')).toBe('BLE-01') + expect(valueOf('admin.products.form.category')).toBe('Céréales') + expect(valueOf('admin.products.form.sites')).toBe('Chatellerault') + expect(valueOf('admin.products.form.storageTypes')).toBe('Tas') + // Etats : libelles i18n joints. + expect(valueOf('admin.products.form.states')).toBe('admin.products.state.PURCHASE, admin.products.state.SALE') + }) + + it('bouton « Modifier » (manage) → ecran d\'edition', async () => { + const wrapper = await mountPage() + await wrapper.find('[data-label="admin.products.action.edit"]').trigger('click') + expect(mockPush).toHaveBeenCalledWith('/admin/products/34/edit') + }) + + it('masque « Modifier » sans la permission manage', async () => { + // view OK mais manage refuse. + mockCan.mockImplementation((perm: string) => perm === 'catalog.products.view') + const wrapper = await mountPage() + expect(wrapper.find('[data-label="admin.products.action.edit"]').exists()).toBe(false) + }) + + it('n\'affiche AUCUN onglet en consultation (coquilles vides masquees, ERP-193)', async () => { + const wrapper = await mountPage() + expect(wrapper.find('[data-testid="placeholder-tabs"]').exists()).toBe(false) + }) + + it('masque un champ vide / une checkbox non cochee (ERP-193, isFilled)', async () => { + const wrapper = await mountPage() + // containsMolasses = false dans le fixture => case masquee. + expect(wrapper.find('[data-label="admin.products.form.containsMolasses"]').exists()).toBe(false) + // manufactured = true => case affichee. + expect(wrapper.find('[data-label="admin.products.form.manufactured"]').exists()).toBe(true) + }) +}) diff --git a/frontend/modules/catalog/pages/__tests__/productEdit.spec.ts b/frontend/modules/catalog/pages/__tests__/productEdit.spec.ts index 2f10e4b..5f02f9d 100644 --- a/frontend/modules/catalog/pages/__tests__/productEdit.spec.ts +++ b/frontend/modules/catalog/pages/__tests__/productEdit.spec.ts @@ -133,18 +133,19 @@ describe('Écran Modifier un produit (page /admin/products/{id}/edit)', () => { expect(fx.prefill).toHaveBeenCalledWith(PRODUCT) }) - it('redirige vers la liste sans la permission manage', async () => { + it('redirige vers la consultation sans la permission manage', async () => { mockCan.mockReturnValue(false) await mountPage() - expect(mockNavigateTo).toHaveBeenCalledWith('/admin/products') + expect(mockNavigateTo).toHaveBeenCalledWith('/admin/products/34') }) - it('bouton « Enregistrer » : submit (PATCH) puis retour a la liste', async () => { + it('bouton « Enregistrer » : submit (PATCH) SANS redirection (l\'utilisateur garde la main)', async () => { const wrapper = await mountPage() await wrapper.find('[data-label="admin.products.edit.save"]').trigger('click') await flushPromises() expect(fx.submit).toHaveBeenCalled() - expect(mockPush).toHaveBeenCalledWith('/admin/products') + // On reste sur l'ecran d'edition : aucune navigation au succes (calque client/fournisseur). + expect(mockPush).not.toHaveBeenCalled() }) it('affiche les onglets placeholder (rendu sans appel API)', async () => { diff --git a/frontend/modules/catalog/pages/__tests__/productNew.spec.ts b/frontend/modules/catalog/pages/__tests__/productNew.spec.ts index 60ffa63..5b2dc52 100644 --- a/frontend/modules/catalog/pages/__tests__/productNew.spec.ts +++ b/frontend/modules/catalog/pages/__tests__/productNew.spec.ts @@ -139,4 +139,9 @@ describe('Écran Ajouter un produit (page /admin/products/new)', () => { await flushPromises() expect(mockPush).not.toHaveBeenCalled() }) + + it('n\'affiche PAS les onglets Fournisseurs/Clients a l\'ajout (avant validation)', async () => { + const wrapper = await mountPage() + expect(wrapper.find('[data-testid="placeholder-tabs"]').exists()).toBe(false) + }) }) diff --git a/frontend/modules/catalog/pages/__tests__/productsIndex.spec.ts b/frontend/modules/catalog/pages/__tests__/productsIndex.spec.ts index 3b14ef0..dd105f9 100644 --- a/frontend/modules/catalog/pages/__tests__/productsIndex.spec.ts +++ b/frontend/modules/catalog/pages/__tests__/productsIndex.spec.ts @@ -188,11 +188,11 @@ describe('Catalogue produit (page /admin/products)', () => { expect(wrapper.find('[data-label="admin.products.add"]').exists()).toBe(false) }) - it('navigue vers l\'édition au clic sur une ligne', async () => { + it('navigue vers la consultation au clic sur une ligne', async () => { const wrapper = mountPage() await flushPromises() await wrapper.find('tr[data-row-id="34"]').trigger('click') - expect(mockPush).toHaveBeenCalledWith('/admin/products/34/edit') + expect(mockPush).toHaveBeenCalledWith('/admin/products/34') }) it('navigue vers la création au clic sur « + Ajouter »', async () => { diff --git a/frontend/modules/catalog/pages/admin/products/[id]/edit.vue b/frontend/modules/catalog/pages/admin/products/[id]/edit.vue index d2c7c03..6fbb767 100644 --- a/frontend/modules/catalog/pages/admin/products/[id]/edit.vue +++ b/frontend/modules/catalog/pages/admin/products/[id]/edit.vue @@ -102,8 +102,10 @@ /> - - + + @@ -119,13 +121,14 @@ const route = useRoute() const router = useRouter() const { can } = usePermissions() -// Gating de la route : la modification est reservee a `manage` (catalogue admin-only). -if (!can('catalog.products.manage')) { - await navigateTo('/admin/products') -} - const productId = route.params.id as string +// Gating de la route : la modification est reservee a `manage` ; sinon retour +// consultation (la lecture seule reste accessible avec `view`). +if (!can('catalog.products.manage')) { + await navigateTo(`/admin/products/${productId}`) +} + const { product, loading, error, load } = useProduct(productId) const { @@ -154,17 +157,19 @@ const stateOptions = computed(() => PRODUCT_STATES.map(code => ({ value: code, label: t(`admin.products.state.${code}`) })), ) -/** Retour vers le catalogue produit (fleche d'en-tete). */ +/** Retour vers la consultation du produit (fleche d'en-tete). */ function goBack(): void { - router.push('/admin/products') + router.push(`/admin/products/${productId}`) } -/** Soumet la modification (PATCH) ; au succes, retour a la liste. */ +/** + * Soumet la modification (PATCH). Au succes : on RESTE sur l'ecran d'edition + * (l'utilisateur garde la main, calque client/fournisseur) — le toast de succes et + * la reaffichage des valeurs normalisees sont geres par `submit()`. La navigation + * reste manuelle (fleche retour -> consultation). + */ async function onSubmit(): Promise { - const ok = await submit() - if (ok) { - router.push('/admin/products') - } + await submit() } onMounted(async () => { diff --git a/frontend/modules/catalog/pages/admin/products/[id]/index.vue b/frontend/modules/catalog/pages/admin/products/[id]/index.vue new file mode 100644 index 0000000..0efb627 --- /dev/null +++ b/frontend/modules/catalog/pages/admin/products/[id]/index.vue @@ -0,0 +1,155 @@ + + + diff --git a/frontend/modules/catalog/pages/admin/products/index.vue b/frontend/modules/catalog/pages/admin/products/index.vue index 7485077..7c3d7c0 100644 --- a/frontend/modules/catalog/pages/admin/products/index.vue +++ b/frontend/modules/catalog/pages/admin/products/index.vue @@ -186,9 +186,9 @@ const columns = [ { key: 'categoryName', label: t('admin.products.column.category') }, ] -/** Clic sur une ligne → ecran d'edition (route imbriquee /admin/products/{id}/edit). */ +/** Clic sur une ligne → ecran de consultation (lecture seule) /admin/products/{id}. */ function onRowClick(item: Record): void { - router.push(`/admin/products/${item.id}/edit`) + router.push(`/admin/products/${item.id}`) } function goToCreate(): void { diff --git a/frontend/modules/catalog/pages/admin/products/new.vue b/frontend/modules/catalog/pages/admin/products/new.vue index de9cff8..1e6bc81 100644 --- a/frontend/modules/catalog/pages/admin/products/new.vue +++ b/frontend/modules/catalog/pages/admin/products/new.vue @@ -97,9 +97,9 @@ /> - - +