fix : page de modification reception qui crash en prod
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
This commit is contained in:
@@ -48,8 +48,38 @@ const emit = defineEmits<{
|
||||
const bovineTypes = ref<BovineTypeData[]>([])
|
||||
const localQuantities = reactive<Record<string, number | null>>({})
|
||||
const localOtherQuantity = ref<number | null>(props.otherQuantity ?? 0)
|
||||
// Verrou pour éviter les boucles props -> local -> emit -> props.
|
||||
const isSyncing = ref(false)
|
||||
|
||||
function entriesEqualByTypeAndQuantity(
|
||||
left: ReceptionBovineTypeData[],
|
||||
right: ReceptionBovineTypeData[]
|
||||
): boolean {
|
||||
const toMap = (entries: ReceptionBovineTypeData[]) => {
|
||||
const map = new Map<number, number>()
|
||||
for (const entry of entries) {
|
||||
const typeId = entry.bovineType?.id ?? 0
|
||||
map.set(typeId, entry.quantity ?? 0)
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
const a = toMap(left)
|
||||
const b = toMap(right)
|
||||
if (a.size !== b.size) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const [typeId, quantity] of a.entries()) {
|
||||
if ((b.get(typeId) ?? 0) !== quantity) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function buildEntriesFromLocal(): ReceptionBovineTypeData[] {
|
||||
return bovineTypes.value.map((type) => {
|
||||
const existing = props.modelValue.find((entry) => entry.bovineType.id === type.id)
|
||||
@@ -80,20 +110,33 @@ function syncLocalFromProps() {
|
||||
watch(
|
||||
() => props.otherQuantity,
|
||||
(value) => {
|
||||
localOtherQuantity.value = value ?? 0
|
||||
if (isSyncing.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = value ?? 0
|
||||
isSyncing.value = true
|
||||
localOtherQuantity.value = next
|
||||
isSyncing.value = false
|
||||
}
|
||||
)
|
||||
|
||||
watch(localOtherQuantity, (value) => {
|
||||
emit('update:otherQuantity', value ?? 0)
|
||||
if (isSyncing.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = value ?? 0
|
||||
emit('update:otherQuantity', next)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
// Hydratation locale uniquement quand le parent change.
|
||||
syncLocalFromProps()
|
||||
},
|
||||
{ deep: true }
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
@@ -102,7 +145,11 @@ watch(
|
||||
if (isSyncing.value) {
|
||||
return
|
||||
}
|
||||
emit('update:modelValue', buildEntriesFromLocal())
|
||||
// N'émet que si les quantités diffèrent réellement du parent.
|
||||
const nextEntries = buildEntriesFromLocal()
|
||||
if (!entriesEqualByTypeAndQuantity(nextEntries, props.modelValue)) {
|
||||
emit('update:modelValue', nextEntries)
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -110,6 +157,5 @@ watch(
|
||||
onMounted(async () => {
|
||||
bovineTypes.value = await getBovineTypeList()
|
||||
syncLocalFromProps()
|
||||
emit('update:modelValue', buildEntriesFromLocal())
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -109,7 +109,8 @@ const selectedMerchandiseTypeId = ref('')
|
||||
const selectedBuildingIds = ref<string[]>([])
|
||||
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
|
||||
const merchandiseDetail = ref('')
|
||||
const isHydrating = ref(false)
|
||||
// Verrou de synchro pour empêcher les aller-retours infinis entre parent et composant.
|
||||
const isSyncing = ref(false)
|
||||
const isReady = ref(false)
|
||||
|
||||
const selectedMerchandiseType = computed(() =>
|
||||
@@ -130,6 +131,39 @@ function clonePelletSelections(value: Record<string, string[]>) {
|
||||
return clone
|
||||
}
|
||||
|
||||
function sorted(values: string[]): string[] {
|
||||
return [...values].sort()
|
||||
}
|
||||
|
||||
function normalizeModel(value: MerchandiseEntryData): MerchandiseEntryData {
|
||||
// Normalisation stable pour comparer deux modèles sans faux positifs (ordre des tableaux).
|
||||
const pellet: Record<string, string[]> = {}
|
||||
const pelletKeys = Object.keys(value.selectedPelletBuildingIds ?? {}).sort()
|
||||
for (const key of pelletKeys) {
|
||||
pellet[key] = sorted(value.selectedPelletBuildingIds[key] ?? [])
|
||||
}
|
||||
|
||||
return {
|
||||
merchandiseTypeId: value.merchandiseTypeId ?? '',
|
||||
merchandiseDetail: value.merchandiseDetail ?? '',
|
||||
selectedBuildingIds: sorted(value.selectedBuildingIds ?? []),
|
||||
selectedPelletBuildingIds: pellet
|
||||
}
|
||||
}
|
||||
|
||||
function buildCurrentModel(): MerchandiseEntryData {
|
||||
return {
|
||||
merchandiseTypeId: selectedMerchandiseTypeId.value,
|
||||
merchandiseDetail: merchandiseDetail.value,
|
||||
selectedBuildingIds: [...selectedBuildingIds.value],
|
||||
selectedPelletBuildingIds: clonePelletSelections(selectedPelletBuildingIds.value)
|
||||
}
|
||||
}
|
||||
|
||||
function isSameModel(left: MerchandiseEntryData, right: MerchandiseEntryData): boolean {
|
||||
return JSON.stringify(normalizeModel(left)) === JSON.stringify(normalizeModel(right))
|
||||
}
|
||||
|
||||
function ensurePelletKeys() {
|
||||
for (const pelletType of pelletTypes.value) {
|
||||
const key = String(pelletType.id)
|
||||
@@ -140,7 +174,7 @@ function ensurePelletKeys() {
|
||||
}
|
||||
|
||||
function hydrateFromModelValue(value: MerchandiseEntryData) {
|
||||
isHydrating.value = true
|
||||
isSyncing.value = true
|
||||
try {
|
||||
selectedMerchandiseTypeId.value = value.merchandiseTypeId ?? ''
|
||||
merchandiseDetail.value = value.merchandiseDetail ?? ''
|
||||
@@ -150,51 +184,71 @@ function hydrateFromModelValue(value: MerchandiseEntryData) {
|
||||
)
|
||||
ensurePelletKeys()
|
||||
} finally {
|
||||
isHydrating.value = false
|
||||
isSyncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function emitModelValue() {
|
||||
emit('update:modelValue', {
|
||||
merchandiseTypeId: selectedMerchandiseTypeId.value,
|
||||
merchandiseDetail: merchandiseDetail.value,
|
||||
selectedBuildingIds: [...selectedBuildingIds.value],
|
||||
selectedPelletBuildingIds: clonePelletSelections(selectedPelletBuildingIds.value)
|
||||
})
|
||||
function sanitizeLocalState() {
|
||||
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 = ''
|
||||
}
|
||||
}
|
||||
|
||||
function emitCurrentModel() {
|
||||
const currentModel = buildCurrentModel()
|
||||
// Ne pas réémettre si rien n'a changé côté métier.
|
||||
if (isSameModel(currentModel, props.modelValue)) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('update:modelValue', currentModel)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
const currentModel = buildCurrentModel()
|
||||
// Si local == parent, on ignore pour éviter la boucle de réhydratation.
|
||||
if (isSameModel(currentModel, value)) {
|
||||
return
|
||||
}
|
||||
hydrateFromModelValue(value)
|
||||
},
|
||||
{ deep: true }
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
[selectedMerchandiseTypeId, selectedBuildingIds, selectedPelletBuildingIds, merchandiseDetail],
|
||||
() => {
|
||||
if (isHydrating.value || !isReady.value) {
|
||||
if (isSyncing.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] = []
|
||||
}
|
||||
}
|
||||
const beforeSanitize = buildCurrentModel()
|
||||
isSyncing.value = true
|
||||
// Applique les règles métier (granulé / autres) avant émission.
|
||||
sanitizeLocalState()
|
||||
isSyncing.value = false
|
||||
|
||||
const afterSanitize = buildCurrentModel()
|
||||
// Si la sanitation a modifié l'état, on laisse le watcher repasser proprement.
|
||||
if (!isSameModel(beforeSanitize, afterSanitize)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isAutres.value && merchandiseDetail.value !== '') {
|
||||
merchandiseDetail.value = ''
|
||||
}
|
||||
|
||||
emitModelValue()
|
||||
emitCurrentModel()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -211,6 +265,5 @@ onMounted(async () => {
|
||||
|
||||
hydrateFromModelValue(props.modelValue)
|
||||
isReady.value = true
|
||||
emitModelValue()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -62,7 +62,7 @@ export const useWeighing = ({
|
||||
})
|
||||
} else {
|
||||
await createWeight({
|
||||
reception: `api/receptions/${reception.value.id}`,
|
||||
reception: `/api/receptions/${reception.value.id}`,
|
||||
type: mode,
|
||||
dsd: baseDsd,
|
||||
weight: baseWeight,
|
||||
@@ -146,7 +146,7 @@ export const useWeighingShipment = ({
|
||||
})
|
||||
} else {
|
||||
await createWeight({
|
||||
shipment: `api/shipments/${shipment.value.id}`,
|
||||
shipment: `/api/shipments/${shipment.value.id}`,
|
||||
type: modeShipment,
|
||||
dsd: baseDsd,
|
||||
weight: baseWeight,
|
||||
|
||||
@@ -172,13 +172,13 @@
|
||||
/>
|
||||
|
||||
<update-merchandise
|
||||
v-show="activeTab === 'merchandise' && isMerchandise"
|
||||
v-if="activeTab === 'merchandise' && isMerchandise"
|
||||
v-model="merchandiseForm"
|
||||
:isAdmin="auth.isAdmin"
|
||||
/>
|
||||
|
||||
<update-bovin
|
||||
v-show="activeTab === 'merchandise' && !isMerchandise"
|
||||
v-if="activeTab === 'merchandise' && !isMerchandise"
|
||||
v-model="bovineEntries"
|
||||
v-model:otherQuantity="bovineOtherQuantity"
|
||||
:isAdmin="auth.isAdmin"
|
||||
@@ -279,9 +279,25 @@ const isLoadingVehicles = ref(false)
|
||||
const formIsLoading = ref(false)
|
||||
const isMerchandise = ref(false)
|
||||
const isHydrating = ref(false)
|
||||
const vehicleSyncLock = ref(false)
|
||||
|
||||
const idReception = Number(route.params.id)
|
||||
|
||||
function runWithVehicleSyncLock(mutator: () => void) {
|
||||
if (vehicleSyncLock.value) {
|
||||
return
|
||||
}
|
||||
|
||||
vehicleSyncLock.value = true
|
||||
try {
|
||||
mutator()
|
||||
} finally {
|
||||
queueMicrotask(() => {
|
||||
vehicleSyncLock.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const form = reactive<ReceptionFormData>({
|
||||
identificationNumber: null,
|
||||
licensePlate: '',
|
||||
@@ -581,8 +597,16 @@ async function saveWeightEntry(entry: WeightEntryData) {
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback: if id is missing in local state, reuse existing weight by type.
|
||||
const reception = await getReception(idReception)
|
||||
const existingEntry = reception?.weights?.find((weight) => weight.type === entry.type) ?? null
|
||||
if (existingEntry?.id) {
|
||||
await updateWeight(existingEntry.id, payload)
|
||||
return
|
||||
}
|
||||
|
||||
await createWeight({
|
||||
reception: `api/receptions/${idReception}`,
|
||||
reception: `/api/receptions/${idReception}`,
|
||||
...payload
|
||||
})
|
||||
}
|
||||
@@ -785,8 +809,11 @@ async function validate() {
|
||||
})
|
||||
}
|
||||
|
||||
const refreshedReception = await getReception(idReception)
|
||||
hydrateFromReception(refreshedReception)
|
||||
// Évite une réhydratation complète après save (source de cascades de watchers).
|
||||
// On recharge uniquement les bovins quand on est en mode bovins.
|
||||
if (!isMerchandise.value) {
|
||||
await loadBovineEntries(idReception)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -809,12 +836,20 @@ onMounted(async () => {
|
||||
watch(
|
||||
() => [form.supplierId, form.addressId, suppliers.value],
|
||||
() => {
|
||||
if (isHydrating.value) {
|
||||
return
|
||||
}
|
||||
if (!form.supplierId) {
|
||||
form.addressId = ''
|
||||
if (form.addressId !== '') {
|
||||
form.addressId = ''
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!form.addressId && supplierAddresses.value.length === 1) {
|
||||
form.addressId = String(supplierAddresses.value[0].id)
|
||||
const nextAddressId = String(supplierAddresses.value[0].id)
|
||||
if (form.addressId !== nextAddressId) {
|
||||
form.addressId = nextAddressId
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!form.addressId) {
|
||||
@@ -825,9 +860,14 @@ watch(
|
||||
)
|
||||
if (!matches) {
|
||||
if (supplierAddresses.value.length === 1) {
|
||||
form.addressId = String(supplierAddresses.value[0].id)
|
||||
const nextAddressId = String(supplierAddresses.value[0].id)
|
||||
if (form.addressId !== nextAddressId) {
|
||||
form.addressId = nextAddressId
|
||||
}
|
||||
} else {
|
||||
form.addressId = ''
|
||||
if (form.addressId !== '') {
|
||||
form.addressId = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -837,24 +877,36 @@ watch(
|
||||
watch(
|
||||
() => form.carrierId,
|
||||
() => {
|
||||
if (isHydrating.value) {
|
||||
if (isHydrating.value || vehicleSyncLock.value) {
|
||||
return
|
||||
}
|
||||
if (!form.carrierId && idReception == null) {
|
||||
form.driverId = ''
|
||||
form.vehicleId = ''
|
||||
runWithVehicleSyncLock(() => {
|
||||
form.driverId = ''
|
||||
form.vehicleId = ''
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!isLiotCarrier.value && idReception == null) {
|
||||
form.driverId = ''
|
||||
form.vehicleId = ''
|
||||
runWithVehicleSyncLock(() => {
|
||||
form.driverId = ''
|
||||
form.vehicleId = ''
|
||||
})
|
||||
return
|
||||
}
|
||||
if (filteredDrivers.value.length === 1) {
|
||||
form.driverId = String(filteredDrivers.value[0].id)
|
||||
const nextDriverId = String(filteredDrivers.value[0].id)
|
||||
if (form.driverId !== nextDriverId) {
|
||||
form.driverId = nextDriverId
|
||||
}
|
||||
}
|
||||
if (filteredVehicles.value.length === 1) {
|
||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||
const nextVehicleId = String(filteredVehicles.value[0].id)
|
||||
if (form.vehicleId !== nextVehicleId) {
|
||||
runWithVehicleSyncLock(() => {
|
||||
form.vehicleId = nextVehicleId
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -863,11 +915,19 @@ watch(
|
||||
watch(
|
||||
() => [form.truckId, form.carrierId, vehicles.value],
|
||||
() => {
|
||||
if (isHydrating.value || vehicleSyncLock.value) {
|
||||
return
|
||||
}
|
||||
if (!isLiotCarrier.value) {
|
||||
return
|
||||
}
|
||||
if (filteredVehicles.value.length === 1) {
|
||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||
const nextVehicleId = String(filteredVehicles.value[0].id)
|
||||
if (form.vehicleId !== nextVehicleId) {
|
||||
runWithVehicleSyncLock(() => {
|
||||
form.vehicleId = nextVehicleId
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!form.vehicleId) {
|
||||
@@ -877,7 +937,11 @@ watch(
|
||||
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||
)
|
||||
if (!matches) {
|
||||
form.vehicleId = ''
|
||||
if (form.vehicleId !== '') {
|
||||
runWithVehicleSyncLock(() => {
|
||||
form.vehicleId = ''
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -886,6 +950,9 @@ watch(
|
||||
watch(
|
||||
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||
() => {
|
||||
if (vehicleSyncLock.value) {
|
||||
return
|
||||
}
|
||||
if (!isLiotCarrier.value) {
|
||||
return
|
||||
}
|
||||
@@ -895,9 +962,11 @@ watch(
|
||||
const selected = filteredVehicles.value.find(
|
||||
(vehicle) => String(vehicle.id) === form.vehicleId
|
||||
)
|
||||
if (selected) {
|
||||
form.licensePlate = selected.plate
|
||||
allowAnyLicensePlate.value = false
|
||||
if (selected && form.licensePlate !== selected.plate) {
|
||||
runWithVehicleSyncLock(() => {
|
||||
form.licensePlate = selected.plate
|
||||
allowAnyLicensePlate.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -905,6 +974,12 @@ watch(
|
||||
watch(
|
||||
() => [form.licensePlate, form.carrierId, vehicles.value],
|
||||
() => {
|
||||
if (vehicleSyncLock.value) {
|
||||
return
|
||||
}
|
||||
if (isHydrating.value) {
|
||||
return
|
||||
}
|
||||
if (!isLiotCarrier.value || form.vehicleId) {
|
||||
return
|
||||
}
|
||||
@@ -912,7 +987,12 @@ watch(
|
||||
(vehicle) => vehicle.plate === form.licensePlate
|
||||
)
|
||||
if (match) {
|
||||
form.vehicleId = String(match.id)
|
||||
const nextVehicleId = String(match.id)
|
||||
if (form.vehicleId !== nextVehicleId) {
|
||||
runWithVehicleSyncLock(() => {
|
||||
form.vehicleId = nextVehicleId
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user