feat(catalog) : M7 — écran Ajouter un stockage /admin/storages/new (ERP-217)
- 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)
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- En-tete : retour vers la liste + titre. -->
|
||||
<div class="flex items-center gap-3 pt-11">
|
||||
<MalioButtonIcon
|
||||
icon="mdi:arrow-left-bold"
|
||||
icon-size="24"
|
||||
variant="ghost"
|
||||
:title="t('admin.storages.form.back')"
|
||||
v-bind="{ ariaLabel: t('admin.storages.form.back') }"
|
||||
@click="goBack"
|
||||
/>
|
||||
<h1 class="text-[30px] font-semibold text-m-primary">{{ t('admin.storages.form.title') }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- ── Formulaire principal de creation (a plat, PAS d'onglets — HP-M7-06)
|
||||
Bouton « Valider » TOUJOURS actif (ERP-101) : la validation
|
||||
autoritaire est serveur, les erreurs 422 reviennent inline. -->
|
||||
<div class="mt-[48px] grid grid-cols-3 xl:grid-cols-4 gap-x-[44px] gap-y-4">
|
||||
<!-- Site : select simple obligatoire (RG-7.02). -->
|
||||
<MalioSelect
|
||||
:model-value="form.siteIri"
|
||||
:options="siteOptions"
|
||||
:label="t('admin.storages.form.site')"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="errors.site"
|
||||
@update:model-value="(v: string | number | null) => setSite(v === null || v === '' ? null : String(v))"
|
||||
/>
|
||||
<!-- Type de stockage : select simple obligatoire. Referentiel plat :
|
||||
tous les types (pas de cascade par site, RG-7.03 non portee back). -->
|
||||
<MalioSelect
|
||||
:model-value="form.storageTypeIri"
|
||||
:options="storageTypeOptions"
|
||||
:label="t('admin.storages.form.storageType')"
|
||||
empty-option-label=""
|
||||
:required="true"
|
||||
:error="errors.storageType"
|
||||
@update:model-value="(v: string | number | null) => setStorageType(v === null || v === '' ? null : String(v))"
|
||||
/>
|
||||
<!-- Numero : texte libre obligatoire (RG-7.01, normalise trim cote serveur). -->
|
||||
<MalioInputText
|
||||
v-model="form.numero"
|
||||
:mask="FREE_TEXT_MASK"
|
||||
:label="t('admin.storages.form.numero')"
|
||||
:required="true"
|
||||
:error="errors.numero"
|
||||
/>
|
||||
<!-- Etat du type de stockage : multi-select obligatoire (>= 1, RG-7.04). -->
|
||||
<MalioSelectCheckbox
|
||||
:model-value="form.states"
|
||||
:options="stateOptions"
|
||||
:max-tags="3"
|
||||
:label="t('admin.storages.form.states')"
|
||||
:display-tag="true"
|
||||
:required="true"
|
||||
:error="errors.states"
|
||||
@update:model-value="(v: (string | number)[]) => setStates(v.map(String))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<MalioButton
|
||||
variant="primary"
|
||||
:label="t('admin.storages.form.submit')"
|
||||
:disabled="submitting"
|
||||
@click="onSubmit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Onglets de la maquette (Clients / Règles / Etiquette / Comptabilité) :
|
||||
HORS perimetre HP-M7-06 — aucune barre d'onglets a l'ajout. -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useStorageForm, STORAGE_STATES } from '~/modules/catalog/composables/useStorageForm'
|
||||
import { FREE_TEXT_MASK } from '~/shared/utils/textSanitize'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const { can } = usePermissions()
|
||||
|
||||
useHead({ title: t('admin.storages.form.title') })
|
||||
|
||||
// Gating de la route : la creation est reservee a `manage` (repertoire admin-only).
|
||||
if (!can('catalog.storages.manage')) {
|
||||
await navigateTo('/admin/storages')
|
||||
}
|
||||
|
||||
const {
|
||||
form,
|
||||
errors,
|
||||
submitting,
|
||||
siteOptions,
|
||||
storageTypeOptions,
|
||||
setSite,
|
||||
setStorageType,
|
||||
setStates,
|
||||
loadReferentials,
|
||||
submit,
|
||||
} = useStorageForm()
|
||||
|
||||
// Options de l'etat : libelles i18n (la valeur d'option = code enum).
|
||||
const stateOptions = computed(() =>
|
||||
STORAGE_STATES.map(code => ({ value: code, label: t(`admin.storages.state.${code}`) })),
|
||||
)
|
||||
|
||||
/** Retour vers la liste des stockages (fleche d'en-tete). */
|
||||
function goBack(): void {
|
||||
router.push('/admin/storages')
|
||||
}
|
||||
|
||||
/** Soumet la creation ; au succes, retour a la liste. */
|
||||
async function onSubmit(): Promise<void> {
|
||||
const ok = await submit()
|
||||
if (ok) {
|
||||
router.push('/admin/storages')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Echec du chargement des referentiels non bloquant : les selects restent vides.
|
||||
loadReferentials().catch(() => {})
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user