From e3e1f9738c3255e4f9828506d7e2d124a26e9f5d Mon Sep 17 00:00:00 2001 From: Tristan Autin Date: Tue, 30 Jun 2026 11:58:02 +0200 Subject: [PATCH] =?UTF-8?q?feat(catalog)=20:=20M7=20=E2=80=94=20=C3=A9cran?= =?UTF-8?q?=20Modification=20d'un=20stockage=20/admin/storages/{id}/edit?= =?UTF-8?q?=20(ERP-218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Route /admin/storages/{id}/edit, gate catalog.storages.manage, détail via useStorage (GET /api/storages/{id}) - Formulaire factorisé create/edit dans useStorageForm : prefill + bouton « Enregistrer » → PATCH /api/storages/{id} (RG-7.08) - Mêmes champs/validations que l'ajout (RG-7.01→7.06), erreurs 422 inline par champ - 409 doublon (site, type, numéro, exclut le courant côté back) → inline sous Numéro + toast - Pas d'onglets (HP-M7-06) ; libellés i18n edit.* + toast.updateSuccess - Tests Vitest useStorageForm mode édition (prefill + PATCH + 409) --- frontend/i18n/locales/fr.json | 10 +- .../__tests__/useStorageForm.spec.ts | 62 +++++++- .../modules/catalog/composables/useStorage.ts | 41 +++++ .../catalog/composables/useStorageForm.ts | 43 ++++- .../pages/admin/storages/[id]/edit.vue | 150 ++++++++++++++++++ 5 files changed, 298 insertions(+), 8 deletions(-) create mode 100644 frontend/modules/catalog/composables/useStorage.ts create mode 100644 frontend/modules/catalog/pages/admin/storages/[id]/edit.vue diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 8c77293..5bc6110 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -1127,10 +1127,18 @@ "states": "État du type de stockage", "duplicateNumero": "Un stockage avec ce site, ce type et ce numéro existe déjà." }, + "edit": { + "title": "Modifier le stockage", + "back": "Retour à la liste", + "save": "Enregistrer", + "loading": "Chargement du stockage…", + "notFound": "Stockage introuvable." + }, "toast": { "error": "Une erreur est survenue. Réessayez.", "exportError": "L'export du répertoire stockage a échoué. Réessayez.", - "createSuccess": "Stockage créé avec succès" + "createSuccess": "Stockage créé avec succès", + "updateSuccess": "Stockage mis à jour avec succès" } } } diff --git a/frontend/modules/catalog/composables/__tests__/useStorageForm.spec.ts b/frontend/modules/catalog/composables/__tests__/useStorageForm.spec.ts index d3ca789..b9bd58a 100644 --- a/frontend/modules/catalog/composables/__tests__/useStorageForm.spec.ts +++ b/frontend/modules/catalog/composables/__tests__/useStorageForm.spec.ts @@ -5,6 +5,7 @@ import { useStorageForm } from '../useStorageForm' // Stubs des auto-imports Nuxt consommes par le composable + ses dependances. const mockGet = vi.hoisted(() => vi.fn()) const mockPost = vi.hoisted(() => vi.fn()) +const mockPatch = vi.hoisted(() => vi.fn()) const mockToastSuccess = vi.hoisted(() => vi.fn()) const mockToastError = vi.hoisted(() => vi.fn()) @@ -12,7 +13,7 @@ vi.stubGlobal('useApi', () => ({ get: mockGet, post: mockPost, put: vi.fn(), - patch: vi.fn(), + patch: mockPatch, delete: vi.fn(), })) vi.stubGlobal('useToast', () => ({ @@ -39,6 +40,7 @@ describe('useStorageForm', () => { beforeEach(() => { mockGet.mockReset() mockPost.mockReset() + mockPatch.mockReset() mockToastSuccess.mockReset() mockToastError.mockReset() @@ -186,4 +188,62 @@ describe('useStorageForm', () => { expect(errors.states).toBe('Sélectionnez au moins un état.') }) }) + + describe('RG-7.08 — mode edition (prefill + PATCH)', () => { + // Stockage charge (memes cles que la reponse reelle § 4.0.bis : @id sur les relations). + const STORAGE = { + id: 42, + numero: '12', + states: ['RECEPTION', 'PRODUCTION'], + displayName: 'Cellule 12', + site: { '@id': '/api/sites/1', id: 1, name: 'Chatellerault', code: '86' }, + storageType: { '@id': '/api/storage_types/9', id: 9, code: 'CELLULE', label: 'Cellule' }, + createdAt: '', updatedAt: '', + } + + it('pre-remplit le formulaire depuis le stockage (relations en IRI)', () => { + const { form, storageId, prefill } = useStorageForm() + prefill(STORAGE) + + expect(storageId.value).toBe(42) + expect(form.siteIri).toBe('/api/sites/1') + expect(form.storageTypeIri).toBe('/api/storage_types/9') + expect(form.numero).toBe('12') + expect(form.states).toEqual(['RECEPTION', 'PRODUCTION']) + }) + + it('soumet un PATCH /storages/{id} apres prefill (RG-7.08)', async () => { + mockPatch.mockResolvedValueOnce({ ...STORAGE }) + const { prefill, submit } = useStorageForm() + prefill(STORAGE) + + const ok = await submit() + + expect(ok).toBe(true) + expect(mockPost).not.toHaveBeenCalled() + expect(mockPatch).toHaveBeenCalledWith( + '/storages/42', + { + numero: '12', + states: ['RECEPTION', 'PRODUCTION'], + site: '/api/sites/1', + storageType: '/api/storage_types/9', + }, + expect.objectContaining({ toast: false }), + ) + expect(mockToastSuccess).toHaveBeenCalled() + }) + + it('mappe un 409 doublon sur errors.numero aussi en edition (exclut le courant cote back)', async () => { + mockPatch.mockRejectedValueOnce({ response: { status: 409, _data: {} } }) + const { errors, prefill, submit } = useStorageForm() + prefill(STORAGE) + + const ok = await submit() + + expect(ok).toBe(false) + expect(errors.numero).toBe('admin.storages.form.duplicateNumero') + expect(mockToastError).toHaveBeenCalled() + }) + }) }) diff --git a/frontend/modules/catalog/composables/useStorage.ts b/frontend/modules/catalog/composables/useStorage.ts new file mode 100644 index 0000000..23b8efa --- /dev/null +++ b/frontend/modules/catalog/composables/useStorage.ts @@ -0,0 +1,41 @@ +import { ref } from 'vue' +import type { Storage } from '~/modules/catalog/types/storage' + +/** + * Chargement d'un stockage unique (ecran « Modification stockage », M7 — ERP-218). + * Lit le detail via `GET /api/storages/{id}` — meme structure que la ligne de liste + * (site / storageType embarques + displayName, § 4.0.bis). + * + * L'en-tete `Accept: application/ld+json` est impose pour obtenir le payload Hydra + * complet (IRI `@id` des relations, necessaires au pre-remplissage des selects). + * Etat 100 % local a l'instance (refs) — aucune persistance URL. + */ +export function useStorage(id: number | string) { + const api = useApi() + + const storage = ref(null) + const loading = ref(false) + const error = ref(false) + + /** Charge le detail du stockage. En cas d'echec : `error = true`, `storage = null`. */ + async function load(): Promise { + loading.value = true + error.value = false + try { + storage.value = await api.get( + `/storages/${id}`, + {}, + { headers: { Accept: 'application/ld+json' }, toast: false }, + ) + } + catch { + error.value = true + storage.value = null + } + finally { + loading.value = false + } + } + + return { storage, loading, error, load } +} diff --git a/frontend/modules/catalog/composables/useStorageForm.ts b/frontend/modules/catalog/composables/useStorageForm.ts index fb42d00..5713b1e 100644 --- a/frontend/modules/catalog/composables/useStorageForm.ts +++ b/frontend/modules/catalog/composables/useStorageForm.ts @@ -21,6 +21,7 @@ import { useSiteOptions, useStorageTypeOptions, } from '~/modules/catalog/composables/useProductOptions' +import type { Storage } from '~/modules/catalog/types/storage' /** Etats d'un stockage (miroir de l'enum back Storage::STATE_*, RG-7.04). */ export const STORAGE_STATES = ['RECEPTION', 'PRODUCTION', 'TRIAGE'] as const @@ -46,6 +47,11 @@ export function useStorageForm() { const submitting = ref(false) + // Id du stockage edite (null = creation). Pilote l'URL/methode du submit + // (RG-7.08 : « Modification » = meme formulaire/regles que « Ajouter », + // bouton « Enregistrer » → PATCH). + const storageId = ref(null) + /** Met a jour le site (select simple, RG-7.02). */ function setSite(iri: string | null): void { form.siteIri = iri @@ -71,10 +77,26 @@ export function useStorageForm() { } /** - * Soumet la creation. Retourne true au succes (la page redirige), false sinon. - * 422 → mapping inline par champ (useFormErrors, `{ toast: false }`) ; 409 - * doublon du triplet (site, type, numero, RG-7.01) → erreur inline sur `numero` - * (propertyPath exploitable cote back) + toast explicite. + * Pre-remplit le formulaire depuis un stockage charge (mode edition, RG-7.08). + * Les relations sont reprises via leur IRI `@id` (= valeur d'option des selects). + * Le referentiel de types est plat (charge par loadReferentials) : prefill se + * contente de mapper la selection courante (pas de cascade par site). + */ + function prefill(storage: Storage): void { + storageId.value = storage.id + form.siteIri = storage.site?.['@id'] ?? null + form.storageTypeIri = storage.storageType?.['@id'] ?? null + form.numero = storage.numero + form.states = [...storage.states] + } + + /** + * Soumet le formulaire. Retourne true au succes (la page redirige), false sinon. + * Creation → `POST /storages` ; edition (storageId non nul, RG-7.08) → + * `PATCH /storages/{id}` (mode merge-patch gere par useApi). 422 → mapping inline + * par champ (useFormErrors, `{ toast: false }`) ; 409 doublon du triplet (site, + * type, numero, RG-7.01 — le back exclut le stockage courant en PATCH) → erreur + * inline sur `numero` (propertyPath exploitable cote back) + toast explicite. */ async function submit(): Promise { if (submitting.value) { @@ -82,6 +104,7 @@ export function useStorageForm() { } submitting.value = true formErrors.clearErrors() + const editing = storageId.value !== null try { const payload: Record = { // Chaine vide (jamais null) : le setter back setNumero attend un @@ -104,8 +127,14 @@ export function useStorageForm() { } const options = { headers: { Accept: 'application/ld+json' }, toast: false } - await api.post('/storages', payload, options) - toast.success({ title: t('admin.storages.toast.createSuccess') }) + if (editing) { + await api.patch(`/storages/${storageId.value}`, payload, options) + toast.success({ title: t('admin.storages.toast.updateSuccess') }) + } + else { + await api.post('/storages', payload, options) + toast.success({ title: t('admin.storages.toast.createSuccess') }) + } return true } catch (error) { @@ -128,6 +157,7 @@ export function useStorageForm() { return { form, + storageId, errors: formErrors.errors, submitting, siteOptions: sites.options, @@ -136,6 +166,7 @@ export function useStorageForm() { setStorageType, setStates, loadReferentials, + prefill, submit, } } diff --git a/frontend/modules/catalog/pages/admin/storages/[id]/edit.vue b/frontend/modules/catalog/pages/admin/storages/[id]/edit.vue new file mode 100644 index 0000000..1f4f3b2 --- /dev/null +++ b/frontend/modules/catalog/pages/admin/storages/[id]/edit.vue @@ -0,0 +1,150 @@ + + +