aa7cda48b3
- Formulaire de création à plat (pas d'onglets, HP-M7-06), gate catalog.storages.manage - Champs Site, Type de stockage, Numéro, État (multi ≥1) en composants Malio, validation inline 422 par champ via useFormErrors - 409 doublon (site, type, numéro) RG-7.01 → erreur inline sous Numéro + toast explicite - Composable useStorageForm (POST /storages, payload relations en IRI), libellés i18n - Référentiel des types PLAT : pas de cascade Site→Type (RG-7.03 non portée côté back, StorageType sans relation Site — à reclarifier spec) - Tests Vitest de useStorageForm (référentiel plat, submit, 409/422)
142 lines
5.6 KiB
TypeScript
142 lines
5.6 KiB
TypeScript
/**
|
|
* Composable du formulaire de creation d'un stockage (M7 — ERP-217).
|
|
*
|
|
* Porte l'etat du formulaire principal (a plat, PAS d'onglets — HP-M7-06), les
|
|
* referentiels des selects et la soumission `POST /api/storages` avec mapping des
|
|
* erreurs 422/409 inline (useFormErrors, ERP-101). Reference : ecran « Ajouter un
|
|
* produit » (M6) / ecran Client.
|
|
*
|
|
* Referentiel des types de stockage : PLAT (RG-6.06 / decision back). Le concept
|
|
* type<->site a ete retire en M6 (jointure storage_type_site droppee, migration
|
|
* Version20260626100000) et `StorageType` n'a plus de relation Site ; le provider
|
|
* ignore tout filtre `?siteId[]`. La cascade Site->Type de RG-7.03 n'est donc PAS
|
|
* portee (decision produit du 30/06 : referentiel plat, fidele au back ; RG-7.03 a
|
|
* reclarifier cote spec). On charge donc TOUS les types une fois, Site et Type
|
|
* independants.
|
|
*
|
|
* Etat 100 % local a l'instance.
|
|
*/
|
|
import { reactive, ref } from 'vue'
|
|
import {
|
|
useSiteOptions,
|
|
useStorageTypeOptions,
|
|
} from '~/modules/catalog/composables/useProductOptions'
|
|
|
|
/** Etats d'un stockage (miroir de l'enum back Storage::STATE_*, RG-7.04). */
|
|
export const STORAGE_STATES = ['RECEPTION', 'PRODUCTION', 'TRIAGE'] as const
|
|
|
|
export function useStorageForm() {
|
|
const api = useApi()
|
|
const { t } = useI18n()
|
|
const toast = useToast()
|
|
const formErrors = useFormErrors()
|
|
|
|
const sites = useSiteOptions()
|
|
const storageTypes = useStorageTypeOptions()
|
|
|
|
// ── Etat du formulaire ───────────────────────────────────────────────────
|
|
// Les relations (site, storageType) sont stockees en IRI (envoyees telles
|
|
// quelles au POST) ; `states` porte les codes enum.
|
|
const form = reactive({
|
|
siteIri: null as string | null,
|
|
storageTypeIri: null as string | null,
|
|
numero: null as string | null,
|
|
states: [] as string[],
|
|
})
|
|
|
|
const submitting = ref(false)
|
|
|
|
/** Met a jour le site (select simple, RG-7.02). */
|
|
function setSite(iri: string | null): void {
|
|
form.siteIri = iri
|
|
}
|
|
|
|
/** Met a jour le type de stockage (select simple, referentiel plat). */
|
|
function setStorageType(iri: string | null): void {
|
|
form.storageTypeIri = iri
|
|
}
|
|
|
|
/** Met a jour les etats (multi-select, >= 1, RG-7.04). */
|
|
function setStates(states: string[]): void {
|
|
form.states = states
|
|
}
|
|
|
|
/**
|
|
* Charge les referentiels (sites + TOUS les types de stockage). Resilient :
|
|
* un referentiel en echec reste vide sans casser l'autre. Pas de cascade par
|
|
* site (referentiel plat, cf. docblock).
|
|
*/
|
|
async function loadReferentials(): Promise<void> {
|
|
await Promise.allSettled([sites.load(), storageTypes.load()])
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
async function submit(): Promise<boolean> {
|
|
if (submitting.value) {
|
|
return false
|
|
}
|
|
submitting.value = true
|
|
formErrors.clearErrors()
|
|
try {
|
|
const payload: Record<string, unknown> = {
|
|
// Chaine vide (jamais null) : le setter back setNumero attend un
|
|
// `string` non-nullable -> envoyer null leverait une erreur de type
|
|
// (denormalisation) qui court-circuiterait les autres violations.
|
|
// Avec '', la contrainte NotBlank renvoie un message propre par champ.
|
|
numero: form.numero ?? '',
|
|
states: form.states,
|
|
}
|
|
// `site` / `storageType` attendent un IRI (string) : envoyer null
|
|
// declencherait une erreur de denormalisation API Platform qui
|
|
// court-circuiterait TOUTES les autres violations. On omet la cle quand
|
|
// la relation n'est pas choisie -> la contrainte NotNull renvoie un
|
|
// message propre, et les autres champs sont valides dans la meme 422.
|
|
if (form.siteIri) {
|
|
payload.site = form.siteIri
|
|
}
|
|
if (form.storageTypeIri) {
|
|
payload.storageType = form.storageTypeIri
|
|
}
|
|
|
|
const options = { headers: { Accept: 'application/ld+json' }, toast: false }
|
|
await api.post('/storages', payload, options)
|
|
toast.success({ title: t('admin.storages.toast.createSuccess') })
|
|
return true
|
|
}
|
|
catch (error) {
|
|
const status = (error as { response?: { status?: number } })?.response?.status
|
|
if (status === 409) {
|
|
// Doublon (site, type, numero) RG-7.01 : inline sur `numero` + toast.
|
|
const message = t('admin.storages.form.duplicateNumero')
|
|
formErrors.setError('numero', message)
|
|
toast.error({ title: t('admin.storages.toast.error'), message })
|
|
}
|
|
else {
|
|
formErrors.handleApiError(error, { fallbackMessage: t('admin.storages.toast.error') })
|
|
}
|
|
return false
|
|
}
|
|
finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
return {
|
|
form,
|
|
errors: formErrors.errors,
|
|
submitting,
|
|
siteOptions: sites.options,
|
|
storageTypeOptions: storageTypes.options,
|
|
setSite,
|
|
setStorageType,
|
|
setStates,
|
|
loadReferentials,
|
|
submit,
|
|
}
|
|
}
|