300 lines
13 KiB
Vue
300 lines
13 KiB
Vue
<template>
|
|
<div class="relative grid grid-cols-4 gap-x-[44px] gap-y-4 bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
|
<!-- Suppression : modal de confirmation côté parent. -->
|
|
<MalioButtonIcon
|
|
v-if="removable && !readonly"
|
|
icon="mdi:delete-outline"
|
|
variant="ghost"
|
|
button-class="absolute top-3 right-3"
|
|
v-bind="{ ariaLabel: t('transport.carriers.form.price.remove') }"
|
|
@click="$emit('remove')"
|
|
/>
|
|
|
|
<!-- RG-4.09 : sens du prix (CLIENT / FOURNISSEUR), empilé en colonne 1 (pas de
|
|
label de groupe). Tout le reste masqué tant qu'aucun sens n'est choisi. -->
|
|
<div class="flex flex-col justify-center gap-2">
|
|
<MalioRadioButton
|
|
:model-value="model.direction"
|
|
name="price-direction"
|
|
value="CLIENT"
|
|
:label="t('transport.carriers.form.price.directionClient')"
|
|
:disabled="readonly"
|
|
group-class="mt-0"
|
|
@update:model-value="onDirectionChange"
|
|
/>
|
|
<MalioRadioButton
|
|
:model-value="model.direction"
|
|
name="price-direction"
|
|
value="FOURNISSEUR"
|
|
:label="t('transport.carriers.form.price.directionSupplier')"
|
|
:disabled="readonly"
|
|
group-class="mt-0"
|
|
@update:model-value="onDirectionChange"
|
|
/>
|
|
<p v-if="errors?.direction" class="mt-1 ml-[2px] text-xs text-m-danger">{{ errors.direction }}</p>
|
|
</div>
|
|
|
|
<!-- Branche CLIENT (RG-4.10). -->
|
|
<template v-if="model.direction === 'CLIENT'">
|
|
<MalioSelect
|
|
:model-value="model.clientIri"
|
|
:options="clientOptions"
|
|
:label="t('transport.carriers.form.price.client')"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:readonly="readonly"
|
|
:error="errors?.client"
|
|
@update:model-value="onClientChange"
|
|
/>
|
|
<MalioSelect
|
|
:model-value="model.clientDeliveryAddressIri"
|
|
:options="clientAddressOptions"
|
|
:label="t('transport.carriers.form.price.clientDeliveryAddress')"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:readonly="readonly"
|
|
:error="errors?.clientDeliveryAddress"
|
|
@update:model-value="(v: string | number | null) => update('clientDeliveryAddressIri', v === null ? null : String(v))"
|
|
/>
|
|
<MalioSelect
|
|
:model-value="model.departureSiteIri"
|
|
:options="siteOptions"
|
|
:label="t('transport.carriers.form.price.departureSite')"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:readonly="readonly"
|
|
:error="errors?.departureSite"
|
|
@update:model-value="(v: string | number | null) => update('departureSiteIri', v === null ? null : String(v))"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Branche FOURNISSEUR (RG-4.11). -->
|
|
<template v-else-if="model.direction === 'FOURNISSEUR'">
|
|
<MalioSelect
|
|
:model-value="model.supplierIri"
|
|
:options="supplierOptions"
|
|
:label="t('transport.carriers.form.price.supplier')"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:readonly="readonly"
|
|
:error="errors?.supplier"
|
|
@update:model-value="onSupplierChange"
|
|
/>
|
|
<MalioSelect
|
|
:model-value="model.supplierSupplyAddressIri"
|
|
:options="supplierAddressOptions"
|
|
:label="t('transport.carriers.form.price.supplierSupplyAddress')"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:readonly="readonly"
|
|
:error="errors?.supplierSupplyAddress"
|
|
@update:model-value="(v: string | number | null) => update('supplierSupplyAddressIri', v === null ? null : String(v))"
|
|
/>
|
|
<MalioSelect
|
|
:model-value="model.deliverySiteIri"
|
|
:options="siteOptions"
|
|
:label="t('transport.carriers.form.price.deliverySite')"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:readonly="readonly"
|
|
:error="errors?.deliverySite"
|
|
@update:model-value="(v: string | number | null) => update('deliverySiteIri', v === null ? null : String(v))"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Communs (visibles dès qu'un sens est choisi). -->
|
|
<template v-if="model.direction !== null">
|
|
<!-- Contenant : Benne / Fond mouvant (pas de label de groupe). -->
|
|
<div class="flex flex-col justify-center">
|
|
<div class="flex gap-4">
|
|
<MalioRadioButton
|
|
:model-value="model.containerType"
|
|
name="price-container"
|
|
value="BENNE"
|
|
:label="t('transport.carriers.containerType.BENNE')"
|
|
:disabled="readonly"
|
|
group-class="mt-0"
|
|
@update:model-value="(v: string | number | boolean | null) => update('containerType', v === null ? null : String(v))"
|
|
/>
|
|
<MalioRadioButton
|
|
:model-value="model.containerType"
|
|
name="price-container"
|
|
value="FOND_MOUVANT"
|
|
:label="t('transport.carriers.containerType.FOND_MOUVANT')"
|
|
:disabled="readonly"
|
|
group-class="mt-0"
|
|
@update:model-value="(v: string | number | boolean | null) => update('containerType', v === null ? null : String(v))"
|
|
/>
|
|
</div>
|
|
<p v-if="errors?.containerType" class="mt-1 ml-[2px] text-xs text-m-danger">{{ errors.containerType }}</p>
|
|
</div>
|
|
|
|
<!-- Tarification : Forfait / Tonne (pas de label de groupe). -->
|
|
<div class="flex flex-col justify-center">
|
|
<div class="flex gap-4">
|
|
<MalioRadioButton
|
|
:model-value="model.pricingUnit"
|
|
name="price-unit"
|
|
value="FORFAIT"
|
|
:label="t('transport.carriers.form.price.pricingForfait')"
|
|
:disabled="readonly"
|
|
group-class="mt-0"
|
|
@update:model-value="(v: string | number | boolean | null) => update('pricingUnit', v === null ? null : String(v))"
|
|
/>
|
|
<MalioRadioButton
|
|
:model-value="model.pricingUnit"
|
|
name="price-unit"
|
|
value="TONNE"
|
|
:label="t('transport.carriers.form.price.pricingTonne')"
|
|
:disabled="readonly"
|
|
group-class="mt-0"
|
|
@update:model-value="(v: string | number | boolean | null) => update('pricingUnit', v === null ? null : String(v))"
|
|
/>
|
|
</div>
|
|
<p v-if="errors?.pricingUnit" class="mt-1 ml-[2px] text-xs text-m-danger">{{ errors.pricingUnit }}</p>
|
|
</div>
|
|
|
|
<MalioInputAmount
|
|
:model-value="model.price"
|
|
:label="t('transport.carriers.form.price.price')"
|
|
:required="true"
|
|
:readonly="readonly"
|
|
:error="errors?.price"
|
|
@update:model-value="(v: string) => update('price', v)"
|
|
/>
|
|
|
|
<MalioSelect
|
|
:model-value="model.priceState"
|
|
:options="priceStateOptions"
|
|
:label="t('transport.carriers.form.price.priceState')"
|
|
empty-option-label=""
|
|
:required="true"
|
|
:readonly="readonly"
|
|
:error="errors?.priceState"
|
|
@update:model-value="(v: string | number | null) => update('priceState', v === null ? null : String(v))"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
import type { CarrierPriceFormDraft } from '~/modules/transport/types/carrierForm'
|
|
|
|
interface SelectOption {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
const props = defineProps<{
|
|
/** Brouillon du prix (v-model). */
|
|
modelValue: CarrierPriceFormDraft
|
|
/** Clients disponibles (IRI en value). */
|
|
clientOptions: SelectOption[]
|
|
/** Fournisseurs disponibles (IRI en value). */
|
|
supplierOptions: SelectOption[]
|
|
/** Sites Starseed (3 sites — IRI en value). */
|
|
siteOptions: SelectOption[]
|
|
removable?: boolean
|
|
readonly?: boolean
|
|
/** Erreurs serveur 422 de cette ligne, indexées par champ (ERP-101). */
|
|
errors?: Record<string, string>
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: CarrierPriceFormDraft]
|
|
'remove': []
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const api = useApi()
|
|
|
|
const model = computed(() => props.modelValue)
|
|
|
|
const priceStateOptions = computed<SelectOption[]>(() => [
|
|
{ value: 'EN_COURS', label: t('transport.carriers.form.price.stateEnCours') },
|
|
{ value: 'VALIDE', label: t('transport.carriers.form.price.stateValide') },
|
|
{ value: 'NON_VALIDE', label: t('transport.carriers.form.price.stateNonValide') },
|
|
])
|
|
|
|
// Adresses chargées à la volée pour le client / fournisseur sélectionné (par bloc).
|
|
const clientAddressOptions = ref<SelectOption[]>([])
|
|
const supplierAddressOptions = ref<SelectOption[]>([])
|
|
|
|
/** Émet un nouveau brouillon avec le champ modifié (immutabilité). */
|
|
function update<K extends keyof CarrierPriceFormDraft>(field: K, value: CarrierPriceFormDraft[K]): void {
|
|
emit('update:modelValue', { ...props.modelValue, [field]: value })
|
|
}
|
|
|
|
/** Changement de sens : réinitialise les DEUX branches (cohérence CHECK BDD). */
|
|
function onDirectionChange(value: string | number | boolean | null): void {
|
|
const direction = value === 'CLIENT' || value === 'FOURNISSEUR' ? value : null
|
|
emit('update:modelValue', {
|
|
...props.modelValue,
|
|
direction,
|
|
clientIri: null,
|
|
clientDeliveryAddressIri: null,
|
|
departureSiteIri: null,
|
|
supplierIri: null,
|
|
supplierSupplyAddressIri: null,
|
|
deliverySiteIri: null,
|
|
})
|
|
}
|
|
|
|
/** Sélection d'un client → maj IRI + reset de l'adresse de livraison (autre client). */
|
|
function onClientChange(value: string | number | null): void {
|
|
emit('update:modelValue', {
|
|
...props.modelValue,
|
|
clientIri: value === null ? null : String(value),
|
|
clientDeliveryAddressIri: null,
|
|
})
|
|
}
|
|
|
|
/** Sélection d'un fournisseur → maj IRI + reset de l'adresse d'appro. */
|
|
function onSupplierChange(value: string | number | null): void {
|
|
emit('update:modelValue', {
|
|
...props.modelValue,
|
|
supplierIri: value === null ? null : String(value),
|
|
supplierSupplyAddressIri: null,
|
|
})
|
|
}
|
|
|
|
/** Réponse détail (client / fournisseur) embarquant ses adresses. */
|
|
interface ParentWithAddresses {
|
|
addresses?: { '@id': string, street?: string | null, postalCode?: string | null, city?: string | null }[]
|
|
}
|
|
|
|
/** Mappe les adresses embarquées en options (IRI en value, voie · CP · ville en label). */
|
|
function toAddressOptions(parent: ParentWithAddresses): SelectOption[] {
|
|
return (parent.addresses ?? []).map(a => ({
|
|
value: a['@id'],
|
|
label: [a.street, a.postalCode, a.city].filter(Boolean).join(' · ') || a['@id'],
|
|
}))
|
|
}
|
|
|
|
/**
|
|
* Charge les adresses d'un parent (client/fournisseur) à la volée via son détail
|
|
* (GET de l'IRI, qui embarque `addresses`). Échec → liste vide (non bloquant).
|
|
*/
|
|
async function loadAddresses(iri: string | null, target: typeof clientAddressOptions): Promise<void> {
|
|
if (!iri) {
|
|
target.value = []
|
|
return
|
|
}
|
|
try {
|
|
// L'IRI est absolue (/api/clients/5) ; useApi préfixe déjà /api → on le retire.
|
|
const path = iri.replace(/^\/api/, '')
|
|
const data = await api.get<ParentWithAddresses>(path, {}, { headers: { Accept: 'application/ld+json' }, toast: false })
|
|
target.value = toAddressOptions(data)
|
|
}
|
|
catch {
|
|
target.value = []
|
|
}
|
|
}
|
|
|
|
// Recharge les adresses quand le client / fournisseur change (immediate pour le
|
|
// pré-remplissage en édition).
|
|
watch(() => props.modelValue.clientIri, iri => loadAddresses(iri, clientAddressOptions), { immediate: true })
|
|
watch(() => props.modelValue.supplierIri, iri => loadAddresses(iri, supplierAddressOptions), { immediate: true })
|
|
</script>
|