feat(catalog) : M7 — écran Modification d'un stockage /admin/storages/{id}/edit (ERP-218)
- 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)
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<Storage | null>(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<void> {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
try {
|
||||
storage.value = await api.get<Storage>(
|
||||
`/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 }
|
||||
}
|
||||
@@ -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<number | null>(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<boolean> {
|
||||
if (submitting.value) {
|
||||
@@ -82,6 +104,7 @@ export function useStorageForm() {
|
||||
}
|
||||
submitting.value = true
|
||||
formErrors.clearErrors()
|
||||
const editing = storageId.value !== null
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user