fix : trie du code et reorganisation composant parent enfant

This commit is contained in:
2026-02-25 15:42:59 +01:00
parent 141f174d0a
commit 0bb8497871
5 changed files with 653 additions and 693 deletions

5
.idea/workspace.xml generated
View File

@@ -7,7 +7,8 @@
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="fix : correction du retour sur la liste des réceptions + suppression de la redirection après enregistrement">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/reception/update-weight.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/update-weight.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/reception/update-bovin.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/update-bovin.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/reception/update-merchandise.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/update-merchandise.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/pages/reception/update/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/update/[[id]].vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" afterDir="false" />
</list>
@@ -232,7 +233,7 @@
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"git-widget-placeholder": "fit/332-refonte-reception-terminee",
"git-widget-placeholder": "feat/332-refonte-reception-terminee",
"last_opened_file_path": "//wsl.localhost/Ubuntu-24.04/home/kevin/Stage/Ferme/frontend/pages/shipment",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",

View File

@@ -1,15 +1,15 @@
<template>
<form>
<div class="flex flex-row justify-between gap-x-12 font-bold uppercase mb-8">
<div class="flex flex-row justify-between gap-x-12 font-bold uppercase mb-8">
<div
v-for="type in bovineType"
v-for="type in bovineTypes"
:key="type.id"
>
<UiNumberInput
:label="type.label"
:code="type.code"
v-model="bovineQuantities[String(type.id)]"
:disabled="!auth.isAdmin"
v-model="localQuantities[String(type.id)]"
:disabled="!isAdmin"
:placeholder="0"
:min="0"
:max="10"
@@ -18,187 +18,96 @@
</div>
<UiNumberInput
label="Autres"
v-model="otherQuantity"
:disabled="!auth.isAdmin"
v-model="localOtherQuantity"
:disabled="!isAdmin"
wrapperClass="w-44 flex-col"
/>
</div>
</form>
</template>
<script setup lang="ts">
import type {BovineTypeData} from "~/services/dto/bovine-type-data";
import {getBovineTypeList} from "~/services/bovine-type";
import {createReceptionBovine, deleteReceptionBovine, getReceptionBovineList, updateReceptionBovine} from "~/services/reception-bovine";
import {computed, onMounted, reactive, ref, watch} from "vue";
import {getReception, updateReception} from "~/services/reception";
const toast = useToast()
const isLoadingBovineType = ref(false)
const bovineType = ref<BovineTypeData[]>([])
const bovineQuantities = reactive<Record<string, number | null>>({})
const otherQuantity = ref<number | null>(0)
const initialBovineQuantities = ref<Record<string, number | null>>({})
const initialOtherQuantity = ref<number | null>(0)
const auth = useAuthStore()
<script setup lang="ts">
import { onMounted, reactive, ref, watch } from 'vue'
import { getBovineTypeList } from '~/services/bovine-type'
import type { BovineTypeData } from '~/services/dto/bovine-type-data'
import type { ReceptionBovineTypeData } from '~/services/dto/reception-bovine-data'
const props = defineProps<{
idReception: number
isValidate: boolean
modelValue: ReceptionBovineTypeData[]
otherQuantity: number | null
isAdmin: boolean
}>()
const receptionId = props.idReception
const reception = await getReception(receptionId)
const emit = defineEmits<{
(event: 'update:modelValue', value: ReceptionBovineTypeData[]): void
(event: 'update:otherQuantity', value: number | null): void
}>()
const receptionIri = computed(() =>
receptionId ? `/api/receptions/${receptionId}` : null
const bovineTypes = ref<BovineTypeData[]>([])
const localQuantities = reactive<Record<string, number | null>>({})
const localOtherQuantity = ref<number | null>(props.otherQuantity ?? 0)
const isSyncing = ref(false)
function buildEntriesFromLocal(): ReceptionBovineTypeData[] {
return bovineTypes.value.map((type) => {
const existing = props.modelValue.find((entry) => entry.bovineType.id === type.id)
return {
id: existing?.id ?? 0,
bovineType: type,
quantity: localQuantities[String(type.id)] ?? 0
}
})
}
function syncLocalFromProps() {
isSyncing.value = true
try {
for (const key of Object.keys(localQuantities)) {
delete localQuantities[key]
}
for (const type of bovineTypes.value) {
const existing = props.modelValue.find((entry) => entry.bovineType.id === type.id)
localQuantities[String(type.id)] = existing?.quantity ?? 0
}
} finally {
isSyncing.value = false
}
}
watch(
() => props.otherQuantity,
(value) => {
localOtherQuantity.value = value ?? 0
}
)
const totalBovines = computed(() => {
const base = Object.values(bovineQuantities).reduce((sum, value) => {
return sum + (value ?? 0)
}, 0)
return base + (otherQuantity.value ?? 0)
})
const hasBovineChanged = () => {
if ((initialOtherQuantity.value ?? 0) !== (otherQuantity.value ?? 0)) {
return true
}
for (const [key, value] of Object.entries(bovineQuantities)) {
if ((initialBovineQuantities.value[key] ?? 0) !== (value ?? 0)) {
return true
}
}
return false
}
const loadBovineType = async () => {
isLoadingBovineType.value = true
try {
bovineType.value = await getBovineTypeList()
} finally {
isLoadingBovineType.value = false
}
}
onMounted(async () => {
await loadBovineType()
watch(localOtherQuantity, (value) => {
emit('update:otherQuantity', value ?? 0)
})
watch(
[() => receptionId, () => bovineType.value],
async ([id, types]) => {
if (!id || !receptionIri.value || types.length === 0) {
() => props.modelValue,
() => {
syncLocalFromProps()
},
{ deep: true }
)
watch(
localQuantities,
() => {
if (isSyncing.value) {
return
}
const selectionMap: Record<string, number | null> = {}
for (const type of types) {
selectionMap[String(type.id)] = 0
}
const existing = await getReceptionBovineList(receptionIri.value)
for (const selection of existing) {
const bovineTypeId = String(selection.bovineType.id)
selectionMap[bovineTypeId] = selection.quantity ?? 0
}
for (const key of Object.keys(bovineQuantities)) {
delete bovineQuantities[key]
}
Object.assign(bovineQuantities, selectionMap)
const existingOther = reception.bovineDetail
const parsedOther =
typeof existingOther === 'string' && existingOther.trim() !== ''
? Number(existingOther)
: 0
otherQuantity.value = Number.isFinite(parsedOther) ? parsedOther : 0
initialBovineQuantities.value = {...selectionMap}
initialOtherQuantity.value = otherQuantity.value ?? 0
emit('update:modelValue', buildEntriesFromLocal())
},
{immediate: true}
{ deep: true }
)
async function syncBovineSelections(receptionIri: string) {
const existing = await getReceptionBovineList(receptionIri)
const existingMap = new Map<string, { id: number; quantity: number | null }>()
for (const selection of existing) {
const bovineTypeId = String(selection.bovineType.id)
existingMap.set(bovineTypeId, {
id: selection.id,
quantity: selection.quantity ?? 0
})
}
// Supprime les entrées supprimées ou modifiées
for (const [bovineTypeId, entry] of existingMap.entries()) {
const selectedQuantity = bovineQuantities[bovineTypeId] ?? 0
if (!selectedQuantity) {
await deleteReceptionBovine(entry.id)
existingMap.delete(bovineTypeId)
continue
}
if (selectedQuantity !== entry.quantity) {
await updateReceptionBovine(entry.id, {quantity: selectedQuantity})
existingMap.set(bovineTypeId, {
id: entry.id,
quantity: selectedQuantity
})
}
}
// Crée les entrées manquantes
for (const [bovineTypeId, quantity] of Object.entries(bovineQuantities)) {
if (!quantity) {
continue
}
if (existingMap.has(bovineTypeId)) {
continue
}
await createReceptionBovine({
reception: receptionIri,
bovineType: `/api/bovine_types/${bovineTypeId}`,
quantity
})
}
}
watch(
() => props.isValidate,
async (val) => {
if (!val) return
await runValidate()
}
)
const runValidate = async () => {
if (!hasBovineChanged()) {
return
}
const receptionIri = `/api/receptions/${reception.id}`
// @TODO Ajouter un composable pour le toaster qui gère les key i18n
if (totalBovines.value > 52) {
toast.error({
title: 'Erreur',
message: ('Le total des bovins ne peut pas dépasser 52.')
})
return
}
await syncBovineSelections(receptionIri)
await updateReception(receptionId, {
merchandiseType: null,
merchandiseDetail: null,
bovineDetail: otherQuantity.value ? String(otherQuantity.value) : null,
})
initialBovineQuantities.value = {...bovineQuantities}
initialOtherQuantity.value = otherQuantity.value ?? 0
}
onMounted(async () => {
bovineTypes.value = await getBovineTypeList()
syncLocalFromProps()
emit('update:modelValue', buildEntriesFromLocal())
})
</script>

View File

@@ -1,8 +1,7 @@
<template>
<form>
<div
class="flex flex-col">
<div class="w-full col-start-1 row-start-1">
<div class="flex flex-col">
<div class="w-full col-start-1 row-start-1">
<UiRadioGroup
id="merchandise-type"
v-model="selectedMerchandiseTypeId"
@@ -14,14 +13,14 @@
input-class="accent-primary-700 focus:ring-primary-700"
option-label-class="uppercase"
wrapper-class="w-full uppercase"
group-class="grid grid-cols-[336px_336px_355px_200px] w-[160px_160px_200px_180px] mt-9 mb-7"
:disabled="!auth.isAdmin"
group-class="grid grid-cols-[336px_336px_355px_200px] w-[160px_160px_200px_180px] mt-9 mb-7"
:disabled="!isAdmin"
/>
</div>
</div>
<div class="w-full grid grid-cols-[3fr_1fr] gap-12 col-start-2 row-start-1">
<div class="w-full grid grid-cols-[3fr_1fr] gap-12 col-start-2 row-start-1">
<div
v-if="merchandiseTypeId && !isGranule"
v-if="selectedMerchandiseTypeId && !isGranule"
class="flex gap-[218px]"
>
<div
@@ -32,172 +31,174 @@
v-model="selectedBuildingIds"
:value="String(building.id)"
:label="building.label"
:disabled="!auth.isAdmin"
:disabled="!isAdmin"
input-class="accent-primary-700 focus:ring-primary-700"
label-class="text-xl uppercase"
/>
</div>
</div>
<div
v-if="merchandiseTypeId && isAutres"
v-if="selectedMerchandiseTypeId && isAutres"
class="flex flex-col justify-self-end max-w-[182px]"
>
<UiTextInput
id="merchandise-detail"
:disabled="!auth.isAdmin"
:disabled="!isAdmin"
v-model="merchandiseDetail"
placeholder="Préciser"
:maxlength="255"
class="h-6"
/>
</div>
</div>
<div
v-if="merchandiseTypeId && isGranule"
class="flex flex-col gap-10 w-full col-start-2 row-start-1 "
>
<div class="grid grid-cols-1 md:grid-cols-[max-content_max-content_max-content_max-content] justify-between">
</div>
<div
v-if="selectedMerchandiseTypeId && isGranule"
class="flex flex-col gap-10 w-full col-start-2 row-start-1"
>
<div class="grid grid-cols-1 md:grid-cols-[max-content_max-content_max-content_max-content] justify-between">
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
<p class="mb-1 font-medium uppercase">{{ type.label }}</p>
<div
v-for="building in buildings"
:key="building.id"
class="flex text-lg"
>
<UiCheckbox
v-model="selectedPelletBuildingIds[String(type.id)]"
:value="String(building.id)"
:label="building.label"
:disabled="!auth.isAdmin"
input-class="accent-primary-700 focus:ring-primary-700"
label-class="text-lg"
/>
</div>
<p class="mb-1 font-medium uppercase">{{ type.label }}</p>
<div
v-for="building in buildings"
:key="building.id"
class="flex text-lg"
>
<UiCheckbox
v-model="selectedPelletBuildingIds[String(type.id)]"
:value="String(building.id)"
:label="building.label"
:disabled="!isAdmin"
input-class="accent-primary-700 focus:ring-primary-700"
label-class="text-lg"
/>
</div>
</div>
</div>
</div>
</div>
</form>
</template>
<script setup lang="ts">
import {computed, onMounted, ref, watch} from 'vue'
import {getBuildingList} from '~/services/building'
import {getMerchandiseTypeList} from '~/services/merchandise-type'
import type {MerchandiseTypeData} from '~/services/dto/merchandise-type-data'
import type {BuildingData} from '~/services/dto/building-data'
import type {PelletTypeData} from '~/services/dto/pellet-type-data'
import {getPelletTypeList} from '~/services/pellet-type'
import {
createReceptionPelletBuilding,
deleteReceptionPelletBuilding,
getReceptionPelletBuildingList
} from '~/services/reception-pellet-building'
import {MERCHANDISE_TYPE_CODES} from '~/utils/constants'
import {getReception, updateReception} from "~/services/reception";
import { computed, onMounted, ref, watch } from 'vue'
import type { BuildingData } from '~/services/dto/building-data'
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
import type { PelletTypeData } from '~/services/dto/pellet-type-data'
import type { MerchandiseEntryData } from '~/services/dto/reception-data'
import { getBuildingList } from '~/services/building'
import { getMerchandiseTypeList } from '~/services/merchandise-type'
import { getPelletTypeList } from '~/services/pellet-type'
import { MERCHANDISE_TYPE_CODES } from '~/utils/constants'
const props = defineProps<{
modelValue: MerchandiseEntryData
isAdmin: boolean
}>()
const emit = defineEmits<{
(event: 'update:modelValue', value: MerchandiseEntryData): void
}>()
const merchandiseTypes = ref<MerchandiseTypeData[]>([])
const buildings = ref<BuildingData[]>([])
const pelletTypes = ref<PelletTypeData[]>([])
const selectedMerchandiseTypeId = ref('')
const selectedBuildingIds = ref<string[]>([])
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
const merchandiseDetail = ref('')
const auth = useAuthStore()
const initialMerchandiseTypeId = ref<string | null>(null)
const initialMerchandiseDetail = ref<string | null>(null)
const initialBuildingIds = ref<string[]>([])
const initialPelletSelections = ref<Record<string, string[]>>({})
const props = defineProps<{
idReception: number
isValidate: boolean
}>()
const receptionId = props.idReception
const reception = await getReception(receptionId)
const merchandiseTypeId = reception.receptionType?.id
const isHydrating = ref(false)
const isReady = ref(false)
// Extrait l'ID d'une relation depuis un IRI ou un objet complet.
const getRelationId = (value: unknown): string | null => {
if (!value) {
return null
}
if (typeof value === 'string') {
const match = value.match(/\/(\d+)$/)
return match ? match[1] : null
}
if (typeof value === 'object' && 'id' in value) {
const record = value as { id?: number | string }
if (typeof record.id === 'number') {
return String(record.id)
}
if (typeof record.id === 'string') {
return record.id
}
}
return null
}
// Type de marchandise sélectionné dans le select
const selectedMerchandiseType = computed(() =>
merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value)
merchandiseTypes.value.find((type) => String(type.id) === selectedMerchandiseTypeId.value) ?? null
)
const isGranule = computed(
() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE
)
const isAutres = computed(
() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES
)
// Indique si le type est "Granulé"
const isGranule = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.GRANULE)
// Indique si le type est "Autres"
const isAutres = computed(() => selectedMerchandiseType.value?.code === MERCHANDISE_TYPE_CODES.AUTRES)
const hasMerchandiseChanged = () => {
const currentTypeId = selectedMerchandiseTypeId.value || null
if (initialMerchandiseTypeId.value !== currentTypeId) {
return true
function clonePelletSelections(value: Record<string, string[]>) {
const clone: Record<string, string[]> = {}
for (const [key, buildingIds] of Object.entries(value)) {
clone[key] = [...buildingIds]
}
const currentDetail = isAutres.value ? merchandiseDetail.value.trim() : ''
if ((initialMerchandiseDetail.value ?? '') !== currentDetail) {
return true
}
const currentBuildings = isGranule.value ? [] : [...selectedBuildingIds.value].sort()
const initialBuildings = [...initialBuildingIds.value].sort()
if (currentBuildings.length !== initialBuildings.length) {
return true
}
for (let i = 0; i < currentBuildings.length; i += 1) {
if (currentBuildings[i] !== initialBuildings[i]) {
return true
}
}
const currentPellets = normalizePelletSelections(selectedPelletBuildingIds.value)
const initialPellets = normalizePelletSelections(initialPelletSelections.value)
if (currentPellets.length !== initialPellets.length) {
return true
}
for (let i = 0; i < currentPellets.length; i += 1) {
if (currentPellets[i] !== initialPellets[i]) {
return true
}
}
return false
return clone
}
const normalizePelletSelections = (selections: Record<string, string[]>) => {
const pairs: string[] = []
for (const [pelletTypeId, buildingIds] of Object.entries(selections)) {
for (const buildingId of buildingIds) {
pairs.push(`${pelletTypeId}:${buildingId}`)
function ensurePelletKeys() {
for (const pelletType of pelletTypes.value) {
const key = String(pelletType.id)
if (!selectedPelletBuildingIds.value[key]) {
selectedPelletBuildingIds.value[key] = []
}
}
return pairs.sort()
}
// Charge les référentiels et hydrate le formulaire depuis la réception
function hydrateFromModelValue(value: MerchandiseEntryData) {
isHydrating.value = true
try {
selectedMerchandiseTypeId.value = value.merchandiseTypeId ?? ''
merchandiseDetail.value = value.merchandiseDetail ?? ''
selectedBuildingIds.value = [...(value.selectedBuildingIds ?? [])]
selectedPelletBuildingIds.value = clonePelletSelections(
value.selectedPelletBuildingIds ?? {}
)
ensurePelletKeys()
} finally {
isHydrating.value = false
}
}
function emitModelValue() {
emit('update:modelValue', {
merchandiseTypeId: selectedMerchandiseTypeId.value,
merchandiseDetail: merchandiseDetail.value,
selectedBuildingIds: [...selectedBuildingIds.value],
selectedPelletBuildingIds: clonePelletSelections(selectedPelletBuildingIds.value)
})
}
watch(
() => props.modelValue,
(value) => {
hydrateFromModelValue(value)
},
{ deep: true }
)
watch(
[selectedMerchandiseTypeId, selectedBuildingIds, selectedPelletBuildingIds, merchandiseDetail],
() => {
if (isHydrating.value || !isReady.value) {
return
}
if (isGranule.value) {
if (selectedBuildingIds.value.length > 0) {
selectedBuildingIds.value = []
}
} else {
for (const key of Object.keys(selectedPelletBuildingIds.value)) {
if (selectedPelletBuildingIds.value[key].length > 0) {
selectedPelletBuildingIds.value[key] = []
}
}
}
if (!isAutres.value && merchandiseDetail.value !== '') {
merchandiseDetail.value = ''
}
emitModelValue()
},
{ deep: true }
)
onMounted(async () => {
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
getMerchandiseTypeList(),
@@ -208,133 +209,8 @@ onMounted(async () => {
buildings.value = buildingList
pelletTypes.value = pelletTypeList
const currentId = reception.merchandiseType?.id
if (currentId) {
selectedMerchandiseTypeId.value = String(currentId)
}
merchandiseDetail.value = reception.merchandiseDetail ?? ''
selectedBuildingIds.value =
reception.buildings?.map((building) => String(building.id)) ?? []
const existingPelletSelections = reception.pelletBuildings ?? []
const selectionMap: Record<string, string[]> = {}
for (const selection of existingPelletSelections) {
// L'API peut renvoyer les relations comme IRI ou comme objets selon le contexte.
const pelletTypeId = getRelationId(selection.pelletType)
const buildingId = getRelationId(selection.building)
if (!pelletTypeId || !buildingId) {
continue
}
if (!selectionMap[pelletTypeId]) {
selectionMap[pelletTypeId] = []
}
selectionMap[pelletTypeId].push(buildingId)
}
for (const pelletType of pelletTypes.value) {
const key = String(pelletType.id)
if (!selectionMap[key]) {
selectionMap[key] = []
}
}
selectedPelletBuildingIds.value = selectionMap
initialMerchandiseTypeId.value = selectedMerchandiseTypeId.value || null
initialMerchandiseDetail.value = isAutres.value ? merchandiseDetail.value.trim() : ''
initialBuildingIds.value = [...selectedBuildingIds.value]
initialPelletSelections.value = JSON.parse(JSON.stringify(selectedPelletBuildingIds.value))
hydrateFromModelValue(props.modelValue)
isReady.value = true
emitModelValue()
})
watch(
() => props.isValidate,
async (val) => {
if (!val) return
await runValidate()
}
)
const runValidate = async () => {
if (!hasMerchandiseChanged()) {
return
}
const receptionIri = `/api/receptions/${reception.id}`
// 1) supprimer toutes les anciennes associations
await clearPelletSelections(receptionIri)
// 2) update reception (type + détails + buildings non-granulé)
await updateReception(reception.id, {
merchandiseType: selectedMerchandiseTypeId.value
? `/api/merchandise_types/${selectedMerchandiseTypeId.value}`
: null,
merchandiseDetail: isAutres.value ? merchandiseDetail.value.trim() :
null,
buildings: isGranule.value
? []
: selectedBuildingIds.value.map((id) => `/api/buildings/${id}`),
bovineDetail: null,
bovinesTypes: null,
})
// 3) si granulé ALORS créer les nouvelles associations
if (isGranule.value) {
await syncPelletSelections(receptionIri)
}
initialMerchandiseTypeId.value = selectedMerchandiseTypeId.value || null
initialMerchandiseDetail.value = isAutres.value ? merchandiseDetail.value.trim() : ''
initialBuildingIds.value = [...selectedBuildingIds.value]
initialPelletSelections.value = JSON.parse(JSON.stringify(selectedPelletBuildingIds.value))
}
// Supprime toutes les associations granulés/bâtiments existantes
async function clearPelletSelections(receptionIri: string) {
const existing = await getReceptionPelletBuildingList(receptionIri)
for (const selection of existing) {
await deleteReceptionPelletBuilding(selection.id)
}
}
// Synchronise les associations granulés/bâtiments avec l'état du formulaire
async function syncPelletSelections(receptionIri: string) {
const existing = await getReceptionPelletBuildingList(receptionIri)
const existingMap = new Map<string, number>()
for (const selection of existing) {
// Construit la table de correspondance avec des IDs normalisés pour éviter les doublons.
const pelletTypeId = getRelationId(selection.pelletType)
const buildingId = getRelationId(selection.building)
if (!pelletTypeId || !buildingId) {
continue
}
const key = `${pelletTypeId}:${buildingId}`
existingMap.set(key, selection.id)
}
const desiredEntries: Array<{ pelletTypeId: string; buildingId: string }> = []
for (const [pelletTypeId, buildingIds] of Object.entries(selectedPelletBuildingIds.value)) {
for (const buildingId of buildingIds) {
desiredEntries.push({pelletTypeId, buildingId})
}
}
const desiredKeys = new Set(desiredEntries.map(
(entry) => `${entry.pelletTypeId}:${entry.buildingId}`
))
for (const [key, id] of existingMap.entries()) {
if (!desiredKeys.has(key)) {
await deleteReceptionPelletBuilding(id)
}
}
for (const entry of desiredEntries) {
const key = `${entry.pelletTypeId}:${entry.buildingId}`
if (!existingMap.has(key)) {
await createReceptionPelletBuilding({
reception: receptionIri,
pelletType: `/api/pellet_types/${entry.pelletTypeId}`,
building: `/api/buildings/${entry.buildingId}`
})
}
}
}
</script>

View File

@@ -173,17 +173,15 @@
<update-merchandise
v-show="activeTab === 'merchandise' && isMerchandise"
:idReception="idReception"
:disabled="!auth.isAdmin"
:isValidate="isValidMerchandise"
v-model="merchandiseForm"
:isAdmin="auth.isAdmin"
/>
<update-bovin
v-show="activeTab === 'merchandise' && !isMerchandise"
:idReception="idReception"
:disabled="!auth.isAdmin"
:isValidate="isValidBovin"
v-model="bovineEntries"
v-model:otherQuantity="bovineOtherQuantity"
:isAdmin="auth.isAdmin"
/>
</div>
<div class="flex justify-center">
@@ -201,62 +199,67 @@
</template>
<script setup lang="ts">
import {usePdfPrinter} from "#imports"
import {useReceptionStore} from '~/stores/reception'
import type {UserData} from '~/services/dto/user-data'
import {getUsers} from '~/services/auth'
import {useAuthStore} from '~/stores/auth'
import type {SupplierData} from '~/services/dto/supplier-data'
import {getSupplierList} from '~/services/supplier'
import type {TruckData} from '~/services/dto/truck-data'
import {getTruckList} from '~/services/truck'
import type {CarrierData} from '~/services/dto/carrier-data'
import {getCarrierList} from '~/services/carrier'
import type {DriverData} from '~/services/dto/driver-data'
import {getDriverList} from '~/services/driver'
import type {VehicleData} from '~/services/dto/vehicle-data'
import {getVehicleList} from '~/services/vehicle'
import {RECEPTION_TYPE_CODES, SUPPLIER_CODE} from "~/utils/constants";
import {deleteReceptionBovine, getReceptionBovineList,} from "~/services/reception-bovine";
import type {ReceptionData, ReceptionFormData, WeightEntryData} from "~/services/dto/reception-data";
import {getReception, updateReception} from "~/services/reception";
import UpdateWeight from "~/components/reception/update-weight.vue";
import UpdateMerchandise from "~/components/reception/update-merchandise.vue";
import UpdateBovin from "~/components/reception/update-bovin.vue";
import {getReceptionTypeList} from "~/services/reception-type";
import type {ReceptionTypeData} from "~/services/dto/reception-type-data";
import {computed} from "vue";
import {createWeight, updateWeight} from "~/services/weight";
import {deleteReceptionPelletBuilding, getReceptionPelletBuildingList} from "~/services/reception-pellet-building";
import { usePdfPrinter } from '#imports'
import { computed } from 'vue'
import UpdateBovin from '~/components/reception/update-bovin.vue'
import UpdateMerchandise from '~/components/reception/update-merchandise.vue'
import UpdateWeight from '~/components/reception/update-weight.vue'
import { getUsers } from '~/services/auth'
import { getCarrierList } from '~/services/carrier'
import type { CarrierData } from '~/services/dto/carrier-data'
import type { DriverData } from '~/services/dto/driver-data'
import type { ReceptionBovineTypeData } from '~/services/dto/reception-bovine-data'
import type {
MerchandiseEntryData,
ReceptionData,
ReceptionFormData,
WeightEntryData
} from '~/services/dto/reception-data'
import type { ReceptionTypeData } from '~/services/dto/reception-type-data'
import type { SupplierData } from '~/services/dto/supplier-data'
import type { TruckData } from '~/services/dto/truck-data'
import type { UserData } from '~/services/dto/user-data'
import type { VehicleData } from '~/services/dto/vehicle-data'
import { getDriverList } from '~/services/driver'
import {
createReceptionBovine,
deleteReceptionBovine,
getReceptionBovineList,
updateReceptionBovine
} from '~/services/reception-bovine'
import {
createReceptionPelletBuilding,
deleteReceptionPelletBuilding,
getReceptionPelletBuildingList
} from '~/services/reception-pellet-building'
import { getReception, updateReception } from '~/services/reception'
import { getReceptionTypeList } from '~/services/reception-type'
import { getSupplierList } from '~/services/supplier'
import { getTruckList } from '~/services/truck'
import { getVehicleList } from '~/services/vehicle'
import { createWeight, updateWeight } from '~/services/weight'
import { useAuthStore } from '~/stores/auth'
import { useReceptionStore } from '~/stores/reception'
import { RECEPTION_TYPE_CODES, SUPPLIER_CODE } from '~/utils/constants'
const form = reactive<ReceptionFormData>({
identificationNumber: null,
licensePlate: '',
receptionDate: new Date().toISOString().slice(0, 10),
receptionTypeId: '',
userId: '',
supplierId: '',
addressId: '',
truckId: '',
carrierId: '',
driverId: '',
vehicleId: ''
})
const createEmptyWeightEntry = (type: 'gross' | 'tare'): WeightEntryData => ({
type,
dsd: null,
weight: null,
weighedAt: null
})
const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
const authStore = useAuthStore()
const receptionStore = useReceptionStore()
const { printPdf } = usePdfPrinter()
const activeTab = ref<'weightsEmpty' | 'weights' | 'merchandise'>('weights')
const grossWeight = ref<WeightEntryData>(createEmptyWeightEntry('gross'))
const tareWeight = ref<WeightEntryData>(createEmptyWeightEntry('tare'))
const { printPdf } = usePdfPrinter()
const activeTab = ref<'weightsEmpty' | 'weights' | 'merchandise'>('weights')
const router = useRouter()
const receptionStore = useReceptionStore()
const bovineEntries = ref<ReceptionBovineTypeData[]>([])
const bovineOtherQuantity = ref<number | null>(0)
const merchandiseForm = ref<MerchandiseEntryData>({
merchandiseTypeId: '',
merchandiseDetail: '',
selectedBuildingIds: [],
selectedPelletBuildingIds: {}
})
const allowAnyLicensePlate = ref(false)
const isLoading = ref(false)
const users = ref<UserData[]>([])
@@ -273,28 +276,30 @@ const drivers = ref<DriverData[]>([])
const isLoadingDrivers = ref(false)
const vehicles = ref<VehicleData[]>([])
const isLoadingVehicles = ref(false)
const authStore = useAuthStore()
const isValid = ref(false)
const isValidBovin = ref(false)
const isValidMerchandise = ref(false)
const formIsLoading = ref(false)
const route = useRoute()
const idReception = Number(route.params.id)
const auth = useAuthStore()
const isMerchandise = ref(false)
// Empêche les watchers de reset des champs pendant le remplissage initial
const isHydrating = ref(false)
// Transporteur sélectionné dans le formulaire
const idReception = Number(route.params.id)
const form = reactive<ReceptionFormData>({
identificationNumber: null,
licensePlate: '',
receptionDate: new Date().toISOString().slice(0, 10),
receptionTypeId: '',
userId: '',
supplierId: '',
addressId: '',
truckId: '',
carrierId: '',
driverId: '',
vehicleId: ''
})
const selectedCarrier = computed(() =>
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
)
// Indique si le transporteur est LIOT
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
// Adresses disponibles pour le fournisseur sélectionné
const supplierAddresses = computed(() => {
const supplierId = Number(form.supplierId)
if (!Number.isFinite(supplierId)) {
@@ -302,16 +307,12 @@ const supplierAddresses = computed(() => {
}
return suppliers.value.find((supplier) => supplier.id === supplierId)?.addresses ?? []
})
// Chauffeurs filtrés par transporteur (LIOT)
const filteredDrivers = computed<DriverData[]>(() => {
if (!form.carrierId) {
return []
}
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
})
// Véhicules filtrés par transporteur + type de camion
const filteredVehicles = computed<VehicleData[]>(() => {
if (!form.carrierId) {
return []
@@ -323,8 +324,42 @@ const filteredVehicles = computed<VehicleData[]>(() => {
)
})
// Supprime les données bovines si on change de type de réception
const clearReceptionBovines = async (receptionId: number) => {
watch(
() => idReception,
async (id) => {
if (id === null) {
return
}
isLoading.value = true
try {
const reception = await getReception(id)
hydrateFromReception(reception)
await loadBovineEntries(id)
} finally {
isLoading.value = false
}
},
{immediate: true}
)
watch(
() => form.receptionTypeId,
() => {
const receptionType = receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
isMerchandise.value = receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES
}
)
function createEmptyWeightEntry(type: 'gross' | 'tare'): WeightEntryData {
return {
type,
dsd: null,
weight: null,
weighedAt: null
}
}
async function clearReceptionBovines(receptionId: number) {
const receptionIri = `/api/receptions/${receptionId}`
const existing = await getReceptionBovineList(receptionIri)
for (const selection of existing) {
@@ -332,22 +367,46 @@ const clearReceptionBovines = async (receptionId: number) => {
}
}
const syncMerchandiseFlag = () => {
async function loadBovineEntries(receptionId: number) {
const receptionIri = `/api/receptions/${receptionId}`
bovineEntries.value = await getReceptionBovineList(receptionIri)
}
function syncMerchandiseFlag() {
const receptionType =
receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
isMerchandise.value = receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES
}
const clearReceptionMerchandise = async (receptionId: number) => {
const receptionIri = `/api/receptions/${receptionId}`
function getRelationId(value: unknown): string | null {
if (!value) {
return null
}
// supprime toutes les associations granulés/bâtiments
if (typeof value === 'string') {
const match = value.match(/\/(\d+)$/)
return match ? match[1] : null
}
if (typeof value === 'object' && 'id' in value) {
const relation = value as { id?: number | string }
if (typeof relation.id === 'number') {
return String(relation.id)
}
if (typeof relation.id === 'string') {
return relation.id
}
}
return null
}
async function clearReceptionMerchandise(receptionId: number) {
const receptionIri = `/api/receptions/${receptionId}`
const existing = await getReceptionPelletBuildingList(receptionIri)
for (const selection of existing) {
await deleteReceptionPelletBuilding(selection.id)
}
// reset des champs marchandise
await updateReception(receptionId, {
merchandiseType: null,
merchandiseDetail: null,
@@ -355,7 +414,7 @@ const clearReceptionMerchandise = async (receptionId: number) => {
})
}
const hydrateFromReception = (reception: ReceptionData | null) => {
function hydrateFromReception(reception: ReceptionData | null) {
if (!reception) {
return
}
@@ -384,41 +443,41 @@ const hydrateFromReception = (reception: ReceptionData | null) => {
form.receptionTypeId = reception?.receptionType?.id
? String(reception.receptionType.id)
: ''
const selectionMap: Record<string, string[]> = {}
for (const selection of reception?.pelletBuildings ?? []) {
const pelletTypeId = getRelationId(selection.pelletType)
const buildingId = getRelationId(selection.building)
if (!pelletTypeId || !buildingId) {
continue
}
if (!selectionMap[pelletTypeId]) {
selectionMap[pelletTypeId] = []
}
selectionMap[pelletTypeId].push(buildingId)
}
merchandiseForm.value = {
merchandiseTypeId: reception?.merchandiseType?.id ? String(reception.merchandiseType.id) : '',
merchandiseDetail: reception?.merchandiseDetail ?? '',
selectedBuildingIds: reception?.buildings?.map((building) => String(building.id)) ?? [],
selectedPelletBuildingIds: selectionMap
}
const parsedOther =
typeof reception?.bovineDetail === 'string' && reception.bovineDetail.trim() !== ''
? Number(reception.bovineDetail)
: 0
bovineOtherQuantity.value = Number.isFinite(parsedOther) ? parsedOther : 0
const gross = reception.weights?.find((weight) => weight.type === 'gross') ?? null
const tare = reception.weights?.find((weight) => weight.type === 'tare') ?? null
grossWeight.value = gross ? {...gross} : createEmptyWeightEntry('gross')
tareWeight.value = tare ? {...tare} : createEmptyWeightEntry('tare')
grossWeight.value = gross ? { ...gross } : createEmptyWeightEntry('gross')
tareWeight.value = tare ? { ...tare } : createEmptyWeightEntry('tare')
isHydrating.value = false
syncMerchandiseFlag()
}
watch(
() => idReception,
async (id) => {
if (id === null) {
return
}
isLoading.value = true
try {
const reception = await getReception(id)
hydrateFromReception(reception)
} finally {
isLoading.value = false
}
},
{immediate: true}
)
watch(
() => form.receptionTypeId,
() => {
const receptionType = receptionTypes.value.find((type) => String(type.id) === form.receptionTypeId) ?? null
isMerchandise.value = receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES
}
)
// Charge la liste des users pour le select
const loadUsers = async () => {
async function loadUsers() {
isLoadingUsers.value = true
try {
users.value = await getUsers()
@@ -427,8 +486,7 @@ const loadUsers = async () => {
}
}
// Charge la liste des fournisseurs pour le select
const loadSuppliers = async () => {
async function loadSuppliers() {
isLoadingSuppliers.value = true
try {
suppliers.value = await getSupplierList()
@@ -437,8 +495,7 @@ const loadSuppliers = async () => {
}
}
//charge la liste des types pour le select
const loadTypes = async () => {
async function loadTypes() {
isLoadingTypes.value = true
try {
receptionTypes.value = await getReceptionTypeList()
@@ -447,8 +504,7 @@ const loadTypes = async () => {
}
}
// Charge la liste des camions pour le select
const loadTrucks = async () => {
async function loadTrucks() {
isLoadingTrucks.value = true
try {
trucks.value = await getTruckList()
@@ -457,8 +513,7 @@ const loadTrucks = async () => {
}
}
// Charge la liste des transporteurs pour le select
const loadCarriers = async () => {
async function loadCarriers() {
isLoadingCarriers.value = true
try {
carriers.value = await getCarrierList()
@@ -467,8 +522,7 @@ const loadCarriers = async () => {
}
}
// Charge la liste des chauffeurs pour le select
const loadDrivers = async () => {
async function loadDrivers() {
isLoadingDrivers.value = true
try {
drivers.value = await getDriverList()
@@ -477,8 +531,7 @@ const loadDrivers = async () => {
}
}
// Charge la liste des véhicules pour le select
const loadVehicles = async () => {
async function loadVehicles() {
isLoadingVehicles.value = true
try {
vehicles.value = await getVehicleList()
@@ -487,8 +540,7 @@ const loadVehicles = async () => {
}
}
// On met l'user connecté par défaut dans le select
const setDefaultUser = () => {
function setDefaultUser() {
if (form.userId) {
return
}
@@ -497,136 +549,7 @@ const setDefaultUser = () => {
}
}
// On récupère toutes les données des selects au chargement du composant
onMounted(async () => {
await loadTypes()
syncMerchandiseFlag()
formIsLoading.value =true
await loadUsers()
await loadSuppliers()
await loadTrucks()
await loadCarriers()
await loadDrivers()
await loadVehicles()
await authStore.ensureSession()
setDefaultUser()
})
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
watch(
() => [form.supplierId, form.addressId, suppliers.value],
() => {
if (!form.supplierId) {
form.addressId = ''
return
}
if (!form.addressId && supplierAddresses.value.length === 1) {
form.addressId = String(supplierAddresses.value[0].id)
return
}
if (!form.addressId) {
return
}
const matches = supplierAddresses.value.some(
(address) => String(address.id) === form.addressId
)
if (!matches) {
if (supplierAddresses.value.length === 1) {
form.addressId = String(supplierAddresses.value[0].id)
} else {
form.addressId = ''
}
}
},
{immediate: true}
)
// Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT)
watch(
() => form.carrierId,
() => {
if (isHydrating.value) {
return
}
if (!form.carrierId && idReception == null) {
form.driverId = ''
form.vehicleId = ''
return
}
if (!isLiotCarrier.value && idReception == null) {
form.driverId = ''
form.vehicleId = ''
return
}
if (filteredDrivers.value.length === 1) {
form.driverId = String(filteredDrivers.value[0].id)
}
if (filteredVehicles.value.length === 1) {
form.vehicleId = String(filteredVehicles.value[0].id)
}
},
{immediate: true}
)
// Récupère la plaque depuis le véhicule choisi (LIOT)
watch(
() => [form.truckId, form.carrierId, vehicles.value],
() => {
if (!isLiotCarrier.value) {
return
}
if (filteredVehicles.value.length === 1) {
form.vehicleId = String(filteredVehicles.value[0].id)
return
}
if (!form.vehicleId) {
return
}
const matches = filteredVehicles.value.some(
(vehicle) => String(vehicle.id) === form.vehicleId
)
if (!matches) {
form.vehicleId = ''
}
},
{immediate: true}
)
// Auto-renseigne le véhicule si la plaque correspond (LIOT)
watch(
() => [form.vehicleId, form.carrierId, vehicles.value],
() => {
if (!isLiotCarrier.value) {
return
}
if (isHydrating.value) {
return
}
const selected = filteredVehicles.value.find(
(vehicle) => String(vehicle.id) === form.vehicleId
)
if (selected) {
form.licensePlate = selected.plate
allowAnyLicensePlate.value = false
}
}
)
watch(
() => [form.licensePlate, form.carrierId, vehicles.value],
() => {
if (!isLiotCarrier.value || form.vehicleId) {
return
}
const match = filteredVehicles.value.find(
(vehicle) => vehicle.plate === form.licensePlate
)
if (match) {
form.vehicleId = String(match.id)
}
}
)
const printReceipt = async () => {
async function printReceipt() {
if (!import.meta.client || !Number.isFinite(idReception) || idReception <= 0) {
return
}
@@ -641,7 +564,7 @@ const printReceipt = async () => {
await new Promise((resolve) => setTimeout(resolve, 600))
}
const saveWeightEntry = async (entry: WeightEntryData) => {
async function saveWeightEntry(entry: WeightEntryData) {
if (!idReception || entry.weight === null) {
return
}
@@ -663,6 +586,114 @@ const saveWeightEntry = async (entry: WeightEntryData) => {
...payload
})
}
async function saveBovineEntry(entry: ReceptionBovineTypeData) {
if (!idReception || !entry.bovineType || entry.quantity === null || entry.quantity <= 0) {
return
}
const payload = {
quantity: entry.quantity
}
if (entry.id) {
await updateReceptionBovine(entry.id, payload)
return
}
await createReceptionBovine({
reception: `/api/receptions/${idReception}`,
bovineType: `/api/bovine_types/${entry.bovineType.id}`,
...payload
})
}
async function syncBovineEntries() {
if (!idReception) {
return
}
const receptionIri = `/api/receptions/${idReception}`
const existing = await getReceptionBovineList(receptionIri)
const currentPositive = bovineEntries.value.filter(
(entry) => (entry.quantity ?? 0) > 0
)
for (const existingEntry of existing) {
const stillPresent = currentPositive.some(
(entry) => entry.bovineType.id === existingEntry.bovineType.id
)
if (!stillPresent) {
await deleteReceptionBovine(existingEntry.id)
}
}
for (const entry of currentPositive) {
await saveBovineEntry(entry)
}
}
async function saveMerchandiseEntry(
receptionIri: string,
pelletTypeId: string,
buildingId: string,
existingKeys: Set<string>
) {
const key = `${pelletTypeId}:${buildingId}`
if (existingKeys.has(key)) {
return
}
await createReceptionPelletBuilding({
reception: receptionIri,
pelletType: `/api/pellet_types/${pelletTypeId}`,
building: `/api/buildings/${buildingId}`
})
}
async function syncMerchandiseEntries() {
if (!idReception) {
return
}
const receptionIri = `/api/receptions/${idReception}`
const existing = await getReceptionPelletBuildingList(receptionIri)
const existingMap = new Map<string, number>()
for (const selection of existing) {
const pelletTypeId = getRelationId(selection.pelletType)
const buildingId = getRelationId(selection.building)
if (!pelletTypeId || !buildingId) {
continue
}
existingMap.set(`${pelletTypeId}:${buildingId}`, selection.id)
}
const desiredEntries: Array<{ pelletTypeId: string; buildingId: string }> = []
for (const [pelletTypeId, buildingIds] of Object.entries(
merchandiseForm.value.selectedPelletBuildingIds
)) {
for (const buildingId of buildingIds) {
desiredEntries.push({ pelletTypeId, buildingId })
}
}
const desiredKeys = new Set(
desiredEntries.map((entry) => `${entry.pelletTypeId}:${entry.buildingId}`)
)
for (const [key, selectionId] of existingMap.entries()) {
if (!desiredKeys.has(key)) {
await deleteReceptionPelletBuilding(selectionId)
}
}
const existingKeys = new Set(existingMap.keys())
for (const entry of desiredEntries) {
await saveMerchandiseEntry(
receptionIri,
entry.pelletTypeId,
entry.buildingId,
existingKeys
)
}
}
async function validate() {
const normalizedLicensePlate = form.licensePlate.trim()
@@ -709,32 +740,168 @@ async function validate() {
const payload = {
...basePayload,
...(isLiotCarrier.value && driverIri ? {driver: driverIri} : {}),
...(isLiotCarrier.value && driverIri ? { driver: driverIri } : {}),
}
if (idReception) {
isValid.value = true
isMerchandise.value ? (isValidMerchandise.value = true) : (isValidBovin.value = true)
await receptionStore.updateReception(idReception, {
...payload
})
await saveWeightEntry(grossWeight.value)
await saveWeightEntry(tareWeight.value)
const refreshedReception = await getReception(idReception)
hydrateFromReception(refreshedReception)
if (isMerchandise.value) {
await clearReceptionBovines(idReception)
await updateReception(idReception, {
merchandiseType: merchandiseForm.value.merchandiseTypeId
? `/api/merchandise_types/${merchandiseForm.value.merchandiseTypeId}`
: null,
merchandiseDetail: merchandiseForm.value.merchandiseDetail.trim() || null,
buildings: merchandiseForm.value.selectedBuildingIds.map(
(buildingId) => `/api/buildings/${buildingId}`
),
bovineDetail: null,
bovinesTypes: null
})
await syncMerchandiseEntries()
} else {
await clearReceptionMerchandise(idReception)
await syncBovineEntries()
await updateReception(idReception, {
bovineDetail: bovineOtherQuantity.value ? String(bovineOtherQuantity.value) : null
})
}
isValid.value = false
isValidBovin.value = false
isValidMerchandise.value = false
const refreshedReception = await getReception(idReception)
hydrateFromReception(refreshedReception)
return
}
}
onMounted(async () => {
await loadTypes()
syncMerchandiseFlag()
formIsLoading.value = true
await loadUsers()
await loadSuppliers()
await loadTrucks()
await loadCarriers()
await loadDrivers()
await loadVehicles()
await authStore.ensureSession()
setDefaultUser()
})
watch(
() => [form.supplierId, form.addressId, suppliers.value],
() => {
if (!form.supplierId) {
form.addressId = ''
return
}
if (!form.addressId && supplierAddresses.value.length === 1) {
form.addressId = String(supplierAddresses.value[0].id)
return
}
if (!form.addressId) {
return
}
const matches = supplierAddresses.value.some(
(address) => String(address.id) === form.addressId
)
if (!matches) {
if (supplierAddresses.value.length === 1) {
form.addressId = String(supplierAddresses.value[0].id)
} else {
form.addressId = ''
}
}
},
{ immediate: true }
)
watch(
() => form.carrierId,
() => {
if (isHydrating.value) {
return
}
if (!form.carrierId && idReception == null) {
form.driverId = ''
form.vehicleId = ''
return
}
if (!isLiotCarrier.value && idReception == null) {
form.driverId = ''
form.vehicleId = ''
return
}
if (filteredDrivers.value.length === 1) {
form.driverId = String(filteredDrivers.value[0].id)
}
if (filteredVehicles.value.length === 1) {
form.vehicleId = String(filteredVehicles.value[0].id)
}
},
{ immediate: true }
)
watch(
() => [form.truckId, form.carrierId, vehicles.value],
() => {
if (!isLiotCarrier.value) {
return
}
if (filteredVehicles.value.length === 1) {
form.vehicleId = String(filteredVehicles.value[0].id)
return
}
if (!form.vehicleId) {
return
}
const matches = filteredVehicles.value.some(
(vehicle) => String(vehicle.id) === form.vehicleId
)
if (!matches) {
form.vehicleId = ''
}
},
{ immediate: true }
)
watch(
() => [form.vehicleId, form.carrierId, vehicles.value],
() => {
if (!isLiotCarrier.value) {
return
}
if (isHydrating.value) {
return
}
const selected = filteredVehicles.value.find(
(vehicle) => String(vehicle.id) === form.vehicleId
)
if (selected) {
form.licensePlate = selected.plate
allowAnyLicensePlate.value = false
}
}
)
watch(
() => [form.licensePlate, form.carrierId, vehicles.value],
() => {
if (!isLiotCarrier.value || form.vehicleId) {
return
}
const match = filteredVehicles.value.find(
(vehicle) => vehicle.plate === form.licensePlate
)
if (match) {
form.vehicleId = String(match.id)
}
}
)
</script>

View File

@@ -41,6 +41,13 @@ export interface WeightEntryData {
weighedAt: string | null
}
export interface MerchandiseEntryData {
merchandiseTypeId: string
merchandiseDetail: string
selectedBuildingIds: string[]
selectedPelletBuildingIds: Record<string, string[]>
}
export interface WeightFormData {
id: number
weight: number