feat(transport) : onglet adresses transporteur (ERP-167)
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { computed, reactive, ref, type Ref } from 'vue'
|
||||
import { useFormErrors } from '~/shared/composables/useFormErrors'
|
||||
import { extractApiErrorMessage, mapViolationsToRecord } from '~/shared/utils/api'
|
||||
import { removeCollectionRow } from '~/shared/utils/collectionRow'
|
||||
import {
|
||||
emptyCarrierAddress,
|
||||
emptyCarrierAddressCopy,
|
||||
emptyCarrierMain,
|
||||
type CarrierAddressCopy,
|
||||
type CarrierAddressFormDraft,
|
||||
type CarrierMainDraft,
|
||||
type CarrierMainResponse,
|
||||
} from '~/modules/transport/types/carrierForm'
|
||||
import { buildCarrierAddressPayload, isCarrierAddressValid } from '~/modules/transport/utils/forms/carrierAddress'
|
||||
import type { QualimatCarrierRow } from '~/modules/transport/composables/useQualimatSearch'
|
||||
|
||||
/** Nom du cas spécial « compte-propre » LIOT (comparaison insensible à la casse, RG-4.01). */
|
||||
@@ -52,6 +57,7 @@ export function useCarrierForm() {
|
||||
const carrierId = ref<number | null>(null)
|
||||
const mainLocked = ref(false)
|
||||
const mainSubmitting = ref(false)
|
||||
const tabSubmitting = ref(false)
|
||||
|
||||
// ── Formulaire principal ──────────────────────────────────────────────────
|
||||
const main = reactive<CarrierMainDraft>(emptyCarrierMain())
|
||||
@@ -255,6 +261,127 @@ export function useCarrierForm() {
|
||||
await api.patch(`/carriers/${carrierId.value}`, payload, { toast: false })
|
||||
}
|
||||
|
||||
/** Remontée d'erreur de suppression immédiate d'une sous-ressource (toast dédié). */
|
||||
function notifyRemovalError(error: unknown): void {
|
||||
toast.error({
|
||||
title: t('transport.carriers.toast.error'),
|
||||
message: extractApiErrorMessage((error as { data?: unknown })?.data) || t('transport.carriers.toast.error'),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Soumet TOUS les blocs d'une collection en collectant les erreurs PAR INDEX :
|
||||
* on n'arrête pas au premier bloc en échec (décision ERP-101). Réinitialise la
|
||||
* cible, tente chaque ligne via `saveRow`, mappe les 422 inline ou délègue le
|
||||
* fallback à `onUnmappedError`. `shouldSkip` ignore les amorces vides. Retourne
|
||||
* true si au moins un bloc a échoué. Miroir de `useProviderForm.submitRows`.
|
||||
*/
|
||||
async function submitRows<T>(
|
||||
rows: T[],
|
||||
target: Ref<Record<string, string>[]>,
|
||||
saveRow: (row: T, index: number) => Promise<void>,
|
||||
onUnmappedError: (error: unknown, index: number) => void,
|
||||
shouldSkip?: (row: T, index: number) => boolean,
|
||||
): Promise<boolean> {
|
||||
target.value = []
|
||||
let hasError = false
|
||||
for (let index = 0; index < rows.length; index++) {
|
||||
const row = rows[index] as T
|
||||
if (shouldSkip?.(row, index)) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
await saveRow(row, index)
|
||||
}
|
||||
catch (error) {
|
||||
const response = (error as { response?: { status?: number, _data?: unknown } })?.response
|
||||
const mapped = response?.status === 422 ? mapViolationsToRecord(response._data) : {}
|
||||
if (Object.keys(mapped).length > 0) {
|
||||
target.value[index] = mapped
|
||||
}
|
||||
else {
|
||||
onUnmappedError(error, index)
|
||||
}
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
return hasError
|
||||
}
|
||||
|
||||
// ── Onglet Adresses (ERP-167) ─────────────────────────────────────────────
|
||||
const addresses = ref<CarrierAddressFormDraft[]>([emptyCarrierAddress()])
|
||||
// Erreurs 422 par ligne (alignées sur l'index du v-for), peuplées par submitRows.
|
||||
const addressErrors = ref<Record<string, string>[]>([])
|
||||
|
||||
// « + Nouvelle adresse » désactivé tant que la dernière adresse n'est pas
|
||||
// complète (CP + ville + rue — RG-4.05, gate d'ajout).
|
||||
const canAddAddress = computed(() => {
|
||||
const last = addresses.value[addresses.value.length - 1]
|
||||
return last !== undefined && isCarrierAddressValid(last)
|
||||
})
|
||||
|
||||
function addAddress(): void {
|
||||
if (canAddAddress.value) {
|
||||
addresses.value.push(emptyCarrierAddress())
|
||||
}
|
||||
}
|
||||
|
||||
/** Suppression immédiate d'une adresse existante (DELETE /carrier_addresses/{id}). */
|
||||
async function removeAddress(index: number): Promise<void> {
|
||||
await removeCollectionRow({
|
||||
rows: addresses.value,
|
||||
errors: addressErrors.value,
|
||||
index,
|
||||
endpoint: '/carrier_addresses',
|
||||
deleteRow: url => api.delete(url, {}, { toast: false }),
|
||||
makeEmpty: emptyCarrierAddress,
|
||||
onError: notifyRemovalError,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide l'onglet Adresses : POST des nouvelles adresses sur
|
||||
* /carriers/{id}/addresses, PATCH des existantes sur /carrier_addresses/{id}
|
||||
* (groupe carrier:write:addresses). Erreurs 422 collectées par ligne (RG-4.05
|
||||
* « obligatoire si affrété » re-validée back). Retourne true si l'onglet a été
|
||||
* validé (avancé/terminé).
|
||||
*/
|
||||
async function submitAddresses(onError: (error: unknown) => void): Promise<boolean> {
|
||||
if (carrierId.value === null || tabSubmitting.value) {
|
||||
return false
|
||||
}
|
||||
tabSubmitting.value = true
|
||||
try {
|
||||
const hasError = await submitRows(
|
||||
addresses.value,
|
||||
addressErrors,
|
||||
async (address) => {
|
||||
const body = buildCarrierAddressPayload(address)
|
||||
if (address.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/carriers/${carrierId.value}/addresses`,
|
||||
body,
|
||||
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||
)
|
||||
address.id = created.id
|
||||
}
|
||||
else {
|
||||
await api.patch(`/carrier_addresses/${address.id}`, body, { toast: false })
|
||||
}
|
||||
},
|
||||
onError,
|
||||
)
|
||||
if (hasError) {
|
||||
return false
|
||||
}
|
||||
completeTab('addresses')
|
||||
return true
|
||||
}
|
||||
finally {
|
||||
tabSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intègre une ligne QUALIMAT sélectionnée dans l'onglet Qualimat (RG-4.01 /
|
||||
* § 2.5) : copie le nom, force la certification à « QUALIMAT » (lecture seule),
|
||||
@@ -273,6 +400,16 @@ export function useCarrierForm() {
|
||||
city: row.city ?? '',
|
||||
street: row.address ?? '',
|
||||
}
|
||||
// RG-4.05 : pré-remplit le 1er bloc de l'onglet Adresses par copie (la FK
|
||||
// QUALIMAT survit, les champs restent éditables — § 2.5).
|
||||
addresses.value = [{
|
||||
id: null,
|
||||
country: 'France',
|
||||
postalCode: row.postalCode || null,
|
||||
city: row.city || null,
|
||||
street: row.address || null,
|
||||
streetComplement: null,
|
||||
}]
|
||||
|
||||
if (carrierId.value === null) {
|
||||
return true
|
||||
@@ -320,6 +457,7 @@ export function useCarrierForm() {
|
||||
carrierId,
|
||||
mainLocked,
|
||||
mainSubmitting,
|
||||
tabSubmitting,
|
||||
mainErrors,
|
||||
// affichage conditionnel
|
||||
isLiot,
|
||||
@@ -335,6 +473,13 @@ export function useCarrierForm() {
|
||||
validated,
|
||||
editMode,
|
||||
isValidated,
|
||||
// adresses
|
||||
addresses,
|
||||
addressErrors,
|
||||
canAddAddress,
|
||||
addAddress,
|
||||
removeAddress,
|
||||
submitAddresses,
|
||||
// actions
|
||||
validateMainFront,
|
||||
buildMainPayload,
|
||||
@@ -342,5 +487,6 @@ export function useCarrierForm() {
|
||||
patchCarrier,
|
||||
applyQualimatSelection,
|
||||
completeTab,
|
||||
submitRows,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user