a340d8139a
Auto Tag Develop / tag (push) Successful in 8s
## Contexte ERP-148 — mise à jour @malio/layer-ui et amélioration des champs date (onglet Information, Client & Fournisseur). ## Changements - **MalioDate v1.7.10** : le composant expose désormais son état de validité (`@update:valid`) et la saisie brute invalide (`@update:rawValue`). - **Validation back-autoritaire du format** : `foundedAt` n'accepte plus que l'ISO strict `Y-m-d` (`#[Context]` DateTimeNormalizer) + `collectDenormalizationErrors` sur `Client` et `Supplier`. Toute saisie non-ISO renvoie un **422 porté sur le champ**. - Corrige un cas piège : `12/25/2026` (invalide en JJ/MM/AAAA côté front) était auparavant accepté par PHP en M/J/AAAA → 25 décembre. Désormais rejeté. - **Front** : la saisie invalide est transmise au back ; le message technique de type-error est surchargé par une clé i18n via le **code de violation** (`resolveViolationMessage` / `VIOLATION_MESSAGE_I18N`), affiché inline par `useFormErrors`. - Réorganisation des utils de formulaire sous `utils/forms/`. ## Tests - Back : `ClientFoundedAtFormatTest` / `SupplierFoundedAtFormatTest` (dont le cas piège `12/25/2026`). - Front : résolveur i18n (`api.test.ts`, `useFormErrors.test.ts`) + payloads (`clientEdit`/`supplierEdit` specs). - Suite Commercial verte ; vérifié bout-en-bout en navigateur (PATCH → 422, erreur inline, submit bloqué). ## Note Échecs JWT aléatoires connus du hook pre-commit (401/500 sur tests d'auth sans rapport) ; tous verts en isolation. Reviewed-on: #92 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
117 lines
4.0 KiB
TypeScript
117 lines
4.0 KiB
TypeScript
/**
|
|
* Composable d'erreurs de formulaire — convention de mapping erreur→champ pour
|
|
* tous les forms du projet (ERP-101).
|
|
*
|
|
* Le back renvoie TOUTES les violations d'une 422 d'un coup (un `propertyPath`
|
|
* + `message` par champ fautif). Ce composable centralise leur affichage
|
|
* inline : il tient un `Record<propertyPath, message>` reactif que le template
|
|
* branche directement sur la prop `:error` des composants `Malio*` (le nom du
|
|
* champ cote front = le `propertyPath` cote back, donc aucun mapping manuel).
|
|
*
|
|
* Chaque appel cree son propre etat (refs internes a la fonction) — un form =
|
|
* une instance, pas de singleton partage.
|
|
*
|
|
* Convention d'usage : les appels API qui veulent un retour inline doivent
|
|
* passer `{ toast: false }` a `useApi` (sinon le toast natif masque le mapping
|
|
* fin), puis router l'erreur via `handleApiError`. Pour les collections (1
|
|
* appel par ligne), utiliser directement `mapViolationsToRecord` par ligne.
|
|
*/
|
|
import { computed, reactive } from 'vue'
|
|
import { extractApiErrorMessage, extractApiViolations, resolveViolationMessage } from '~/shared/utils/api'
|
|
|
|
/**
|
|
* Erreur HTTP capturee par ofetch. On n'expose que les champs lus ici (status
|
|
* + payload) pour eviter de typer toute la lib.
|
|
*/
|
|
interface ApiFetchError {
|
|
response?: {
|
|
status?: number
|
|
_data?: unknown
|
|
}
|
|
}
|
|
|
|
/** Options de `handleApiError`. */
|
|
interface HandleApiErrorOptions {
|
|
/** Message de toast si l'erreur n'est pas une 422 exploitable. */
|
|
fallbackMessage?: string
|
|
}
|
|
|
|
export function useFormErrors() {
|
|
const toast = useToast()
|
|
const { t } = useI18n()
|
|
|
|
// Etat d'erreurs indexe par propertyPath. Reactif : muter une cle suffit a
|
|
// rafraichir la prop `:error` du champ correspondant.
|
|
const errors = reactive<Record<string, string>>({})
|
|
|
|
const hasErrors = computed(() => Object.keys(errors).length > 0)
|
|
|
|
/** Pose une erreur sur un champ. */
|
|
function setError(field: string, message: string): void {
|
|
errors[field] = message
|
|
}
|
|
|
|
/** Retire l'erreur d'un champ (no-op si absente). */
|
|
function clearError(field: string): void {
|
|
delete errors[field]
|
|
}
|
|
|
|
/** Vide toutes les erreurs (a appeler en debut de submit). */
|
|
function clearErrors(): void {
|
|
for (const key of Object.keys(errors)) {
|
|
delete errors[key]
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mappe les violations 422 d'un payload sur les champs. Retourne true des
|
|
* qu'au moins une violation a ete posee, false sinon (payload sans
|
|
* violation exploitable).
|
|
*/
|
|
function setServerErrors(data: unknown): boolean {
|
|
const violations = extractApiViolations(data)
|
|
let mapped = false
|
|
for (const v of violations) {
|
|
if (!v.propertyPath) continue
|
|
// Message back tel quel, sauf code surcharge par une cle i18n (ex.
|
|
// erreur de type sur une date non parsable -> « Date invalide »).
|
|
errors[v.propertyPath] = resolveViolationMessage(v, t)
|
|
mapped = true
|
|
}
|
|
return mapped
|
|
}
|
|
|
|
/**
|
|
* Route une erreur API : 422 avec violations exploitables → mapping inline
|
|
* (pas de toast, l'erreur s'affiche sous le champ) ; sinon → toast de
|
|
* fallback (message serveur extrait, ou `fallbackMessage`).
|
|
*
|
|
* Retourne true si l'erreur a ete mappee inline, false si fallback toast.
|
|
*/
|
|
function handleApiError(e: unknown, opts: HandleApiErrorOptions = {}): boolean {
|
|
const status = (e as ApiFetchError)?.response?.status
|
|
const data = (e as ApiFetchError)?.response?._data
|
|
|
|
if (status === 422 && setServerErrors(data)) {
|
|
return true
|
|
}
|
|
|
|
const message
|
|
= extractApiErrorMessage(data)
|
|
|| opts.fallbackMessage
|
|
|| t('errors.generic')
|
|
toast.error({ title: t('errors.title'), message })
|
|
return false
|
|
}
|
|
|
|
return {
|
|
errors,
|
|
hasErrors,
|
|
setError,
|
|
clearError,
|
|
clearErrors,
|
|
setServerErrors,
|
|
handleApiError,
|
|
}
|
|
}
|