diff --git a/frontend/modules/transport/components/CarrierPriceBlock.vue b/frontend/modules/transport/components/CarrierPriceBlock.vue index c46ee46..da5bfae 100644 --- a/frontend/modules/transport/components/CarrierPriceBlock.vue +++ b/frontend/modules/transport/components/CarrierPriceBlock.vue @@ -10,9 +10,10 @@ @click="$emit('remove')" /> - -
+ +
{ const base = { ...emptyCarrierPrice(), containerType: 'BENNE', pricingUnit: 'FORFAIT', price: '10', priceState: 'EN_COURS' } // Direction non choisie → invalide. + expect(isCarrierPriceValid({ ...base, direction: null })).toBe(false) + // Sens CLIENT par défaut mais branche incomplète → invalide. expect(isCarrierPriceValid(base)).toBe(false) // CLIENT sans adresse/site → invalide. expect(isCarrierPriceValid({ ...base, direction: 'CLIENT', clientIri: CLIENT })).toBe(false) @@ -786,17 +788,28 @@ describe('useCarrierForm — onglet Prix (ERP-169)', () => { return form } - it('démarre vide ; « + Nouveau prix » autorisé sur liste vide, bloqué si dernier bloc incomplet', () => { + it('démarre avec un bloc CLIENT par défaut ; « + Nouveau prix » bloqué tant qu\'il est incomplet', () => { const form = createdForm() - expect(form.prices.value).toHaveLength(0) - expect(form.canAddPrice.value).toBe(true) - - form.addPrice() + // Un bloc présent d'office, sens CLIENT pré-sélectionné. expect(form.prices.value).toHaveLength(1) - // Bloc vide → on ne peut pas en ajouter un autre. + expect(form.prices.value[0]?.direction).toBe('CLIENT') + // Bloc incomplet → on ne peut pas en ajouter un autre. expect(form.canAddPrice.value).toBe(false) form.addPrice() expect(form.prices.value).toHaveLength(1) + + // Une fois le bloc complété, l'ajout est autorisé. + const p = form.prices.value[0] + if (p) { + p.clientIri = '/api/clients/3' + p.clientDeliveryAddressIri = '/api/client_addresses/8' + p.departureSiteIri = '/api/sites/1' + p.price = '120' + p.priceState = 'EN_COURS' + } + expect(form.canAddPrice.value).toBe(true) + form.addPrice() + expect(form.prices.value).toHaveLength(2) }) it('submitPrices : POST des nouveaux prix (branche CLIENT), capture l\'id, finalise', async () => { diff --git a/frontend/modules/transport/composables/useCarrierForm.ts b/frontend/modules/transport/composables/useCarrierForm.ts index 787cff9..4657646 100644 --- a/frontend/modules/transport/composables/useCarrierForm.ts +++ b/frontend/modules/transport/composables/useCarrierForm.ts @@ -469,8 +469,9 @@ export function useCarrierForm() { } // ── Onglet Prix (ERP-169) ───────────────────────────────────────────────── - // Démarre VIDE : aucun bloc auto, l'utilisateur ajoute via « + Nouveau prix ». - const prices = ref([]) + // Un bloc présent par défaut (sens CLIENT pré-sélectionné). L'utilisateur ajoute + // les suivants via « + Nouveau prix ». + const prices = ref([emptyCarrierPrice()]) // Erreurs 422 par ligne (alignées sur l'index du v-for), peuplées par submitRows. const priceErrors = ref[]>([]) diff --git a/frontend/modules/transport/types/carrierForm.ts b/frontend/modules/transport/types/carrierForm.ts index 9d8eb4e..b31db2d 100644 --- a/frontend/modules/transport/types/carrierForm.ts +++ b/frontend/modules/transport/types/carrierForm.ts @@ -157,7 +157,9 @@ export interface CarrierPriceFormDraft { export function emptyCarrierPrice(): CarrierPriceFormDraft { return { id: null, - direction: null, + // Défaut métier : sens CLIENT pré-sélectionné (un bloc prix CLIENT est présent + // d'office à l'ouverture de l'onglet). + direction: 'CLIENT', clientIri: null, clientDeliveryAddressIri: null, departureSiteIri: null, diff --git a/frontend/modules/transport/utils/forms/carrierPrice.ts b/frontend/modules/transport/utils/forms/carrierPrice.ts index ee6f20c..3293234 100644 --- a/frontend/modules/transport/utils/forms/carrierPrice.ts +++ b/frontend/modules/transport/utils/forms/carrierPrice.ts @@ -15,46 +15,46 @@ function isFilled(value: string | null | undefined): boolean { /** * Payload de la sous-ressource prix (groupe `carrier:write:prices`). Envoie les * communs + UNIQUEMENT la branche active (l'autre branche à null, exigée par les - * CHECK BDD). Les relations partent en IRI (string|null). Le back re-valide - * l'obligation conditionnelle + l'appartenance de l'adresse (422 inline). + * CHECK BDD). Les relations partent en IRI (string|null). + * + * IMPORTANT : les scalaires obligatoires (direction / containerType / pricingUnit / + * price / priceState) sont OMIS s'ils sont vides — on n'envoie JAMAIS `null` sur un + * champ string. Sinon API Platform lève un 400 « The type of the "price" attribute + * must be "string", "NULL" given. » AVANT la validation (non mappable inline). Omis, + * le champ reste null côté entité → l'Assert\NotBlank renvoie un 422 propre rattaché + * au champ, affiché sous l'input comme les autres blocs (ERP-101). Le back re-valide + * aussi l'obligation conditionnelle de branche + l'appartenance de l'adresse. */ export function buildCarrierPricePayload(price: CarrierPriceFormDraft): Record { - const common: Record = { - direction: price.direction, - containerType: price.containerType || null, - pricingUnit: price.pricingUnit || null, - price: price.price || null, - priceState: price.priceState || null, - } + const payload: Record = {} + // Scalaires : présents seulement si remplis (jamais `null` → évite le 400 de type). + if (isFilled(price.direction)) payload.direction = price.direction + if (isFilled(price.containerType)) payload.containerType = price.containerType + if (isFilled(price.pricingUnit)) payload.pricingUnit = price.pricingUnit + if (isFilled(price.price)) payload.price = price.price + if (isFilled(price.priceState)) payload.priceState = price.priceState + + // Branche active en IRI (null toléré sur une relation, ne déclenche pas le 400 de + // type) ; branche inactive forcée à null (CHECK BDD chk_carrier_price_*_branch). if (price.direction === 'CLIENT') { - return { - ...common, - client: price.clientIri || null, - clientDeliveryAddress: price.clientDeliveryAddressIri || null, - departureSite: price.departureSiteIri || null, - // Branche FOURNISSEUR forcée à null (CHECK chk_carrier_price_client_branch). - supplier: null, - supplierSupplyAddress: null, - deliverySite: null, - } + payload.client = price.clientIri || null + payload.clientDeliveryAddress = price.clientDeliveryAddressIri || null + payload.departureSite = price.departureSiteIri || null + payload.supplier = null + payload.supplierSupplyAddress = null + payload.deliverySite = null + } + else if (price.direction === 'FOURNISSEUR') { + payload.supplier = price.supplierIri || null + payload.supplierSupplyAddress = price.supplierSupplyAddressIri || null + payload.deliverySite = price.deliverySiteIri || null + payload.client = null + payload.clientDeliveryAddress = null + payload.departureSite = null } - if (price.direction === 'FOURNISSEUR') { - return { - ...common, - supplier: price.supplierIri || null, - supplierSupplyAddress: price.supplierSupplyAddressIri || null, - deliverySite: price.deliverySiteIri || null, - // Branche CLIENT forcée à null (CHECK chk_carrier_price_supplier_branch). - client: null, - clientDeliveryAddress: null, - departureSite: null, - } - } - - // Direction non choisie : on envoie les communs ; le back 422 sur `direction`. - return common + return payload } /**