Compare commits

..

11 Commits

7 changed files with 70 additions and 252 deletions

View File

@@ -57,4 +57,3 @@ Ajouter dans le fichier .env du frontend
### Changed
### Fixed

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '0.0.61'
app.version: '0.0.57'

View File

@@ -29,7 +29,7 @@
</template>
<script setup lang="ts">
import type { AddressPayload } from "~/services/address"
import { AddressPayload } from "~/services/address"
const route = useRoute()

View File

@@ -14,7 +14,6 @@
:min="0"
:max="10"
wrapperClass="w-44 flex-col"
inputClass="font-medium"
/>
</div>
<UiNumberInput
@@ -22,7 +21,6 @@
v-model="localOtherQuantity"
:disabled="!isAdmin"
wrapperClass="w-44 flex-col"
inputClass="font-medium"
/>
</div>
</form>
@@ -48,38 +46,8 @@ 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)
@@ -110,33 +78,20 @@ function syncLocalFromProps() {
watch(
() => props.otherQuantity,
(value) => {
if (isSyncing.value) {
return
}
const next = value ?? 0
isSyncing.value = true
localOtherQuantity.value = next
isSyncing.value = false
localOtherQuantity.value = value ?? 0
}
)
watch(localOtherQuantity, (value) => {
if (isSyncing.value) {
return
}
const next = value ?? 0
emit('update:otherQuantity', next)
emit('update:otherQuantity', value ?? 0)
})
watch(
() => props.modelValue,
() => {
// Hydratation locale uniquement quand le parent change.
syncLocalFromProps()
},
{ immediate: true }
{ deep: true }
)
watch(
@@ -145,11 +100,7 @@ watch(
if (isSyncing.value) {
return
}
// 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)
}
emit('update:modelValue', buildEntriesFromLocal())
},
{ deep: true }
)
@@ -157,5 +108,6 @@ watch(
onMounted(async () => {
bovineTypes.value = await getBovineTypeList()
syncLocalFromProps()
emit('update:modelValue', buildEntriesFromLocal())
})
</script>

View File

@@ -109,8 +109,7 @@ const selectedMerchandiseTypeId = ref('')
const selectedBuildingIds = ref<string[]>([])
const selectedPelletBuildingIds = ref<Record<string, string[]>>({})
const merchandiseDetail = ref('')
// Verrou de synchro pour empêcher les aller-retours infinis entre parent et composant.
const isSyncing = ref(false)
const isHydrating = ref(false)
const isReady = ref(false)
const selectedMerchandiseType = computed(() =>
@@ -131,39 +130,6 @@ 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)
@@ -174,7 +140,7 @@ function ensurePelletKeys() {
}
function hydrateFromModelValue(value: MerchandiseEntryData) {
isSyncing.value = true
isHydrating.value = true
try {
selectedMerchandiseTypeId.value = value.merchandiseTypeId ?? ''
merchandiseDetail.value = value.merchandiseDetail ?? ''
@@ -184,71 +150,51 @@ function hydrateFromModelValue(value: MerchandiseEntryData) {
)
ensurePelletKeys()
} finally {
isSyncing.value = false
isHydrating.value = false
}
}
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)
function emitModelValue() {
emit('update:modelValue', {
merchandiseTypeId: selectedMerchandiseTypeId.value,
merchandiseDetail: merchandiseDetail.value,
selectedBuildingIds: [...selectedBuildingIds.value],
selectedPelletBuildingIds: clonePelletSelections(selectedPelletBuildingIds.value)
})
}
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)
},
{ immediate: true }
{ deep: true }
)
watch(
[selectedMerchandiseTypeId, selectedBuildingIds, selectedPelletBuildingIds, merchandiseDetail],
() => {
if (isSyncing.value || !isReady.value) {
if (isHydrating.value || !isReady.value) {
return
}
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 (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] = []
}
}
}
emitCurrentModel()
if (!isAutres.value && merchandiseDetail.value !== '') {
merchandiseDetail.value = ''
}
emitModelValue()
},
{ deep: true }
)
@@ -265,5 +211,6 @@ onMounted(async () => {
hydrateFromModelValue(props.modelValue)
isReady.value = true
emitModelValue()
})
</script>

View File

@@ -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,

View File

@@ -172,28 +172,28 @@
/>
<update-merchandise
v-if="activeTab === 'merchandise' && isMerchandise"
v-show="activeTab === 'merchandise' && isMerchandise"
v-model="merchandiseForm"
:isAdmin="auth.isAdmin"
/>
<update-bovin
v-if="activeTab === 'merchandise' && !isMerchandise"
v-show="activeTab === 'merchandise' && !isMerchandise"
v-model="bovineEntries"
v-model:otherQuantity="bovineOtherQuantity"
:isAdmin="auth.isAdmin"
/>
</div>
<div class="flex justify-center">
<UiButton
v-if="auth.isAdmin"
type="submit"
class="inline-flex mb-16 items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2 justify-self-end"
>
<Icon name="mdi:check" size="28" />
Valider
</UiButton>
</div>
<UiButton
v-if="auth.isAdmin"
type="submit"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] mb-16"
>
Enregistrer
</UiButton>
</div>
</div>
</form>
</template>
@@ -279,25 +279,9 @@ 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: '',
@@ -343,7 +327,7 @@ const filteredVehicles = computed<VehicleData[]>(() => {
watch(
() => idReception,
async (id) => {
if (!Number.isFinite(id) || id <= 0) {
if (id === null) {
return
}
isLoading.value = true
@@ -516,7 +500,7 @@ async function loadTypes() {
try {
receptionTypes.value = await getReceptionTypeList()
} finally {
isLoadingTypes.value = false
isLoadingSuppliers.value = false
}
}
@@ -597,16 +581,8 @@ 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
})
}
@@ -809,11 +785,8 @@ async function validate() {
})
}
// É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)
}
const refreshedReception = await getReception(idReception)
hydrateFromReception(refreshedReception)
return
}
@@ -836,20 +809,12 @@ onMounted(async () => {
watch(
() => [form.supplierId, form.addressId, suppliers.value],
() => {
if (isHydrating.value) {
return
}
if (!form.supplierId) {
if (form.addressId !== '') {
form.addressId = ''
}
form.addressId = ''
return
}
if (!form.addressId && supplierAddresses.value.length === 1) {
const nextAddressId = String(supplierAddresses.value[0].id)
if (form.addressId !== nextAddressId) {
form.addressId = nextAddressId
}
form.addressId = String(supplierAddresses.value[0].id)
return
}
if (!form.addressId) {
@@ -860,14 +825,9 @@ watch(
)
if (!matches) {
if (supplierAddresses.value.length === 1) {
const nextAddressId = String(supplierAddresses.value[0].id)
if (form.addressId !== nextAddressId) {
form.addressId = nextAddressId
}
form.addressId = String(supplierAddresses.value[0].id)
} else {
if (form.addressId !== '') {
form.addressId = ''
}
form.addressId = ''
}
}
},
@@ -877,36 +837,24 @@ watch(
watch(
() => form.carrierId,
() => {
if (isHydrating.value || vehicleSyncLock.value) {
if (isHydrating.value) {
return
}
if (!form.carrierId && idReception == null) {
runWithVehicleSyncLock(() => {
form.driverId = ''
form.vehicleId = ''
})
form.driverId = ''
form.vehicleId = ''
return
}
if (!isLiotCarrier.value && idReception == null) {
runWithVehicleSyncLock(() => {
form.driverId = ''
form.vehicleId = ''
})
form.driverId = ''
form.vehicleId = ''
return
}
if (filteredDrivers.value.length === 1) {
const nextDriverId = String(filteredDrivers.value[0].id)
if (form.driverId !== nextDriverId) {
form.driverId = nextDriverId
}
form.driverId = String(filteredDrivers.value[0].id)
}
if (filteredVehicles.value.length === 1) {
const nextVehicleId = String(filteredVehicles.value[0].id)
if (form.vehicleId !== nextVehicleId) {
runWithVehicleSyncLock(() => {
form.vehicleId = nextVehicleId
})
}
form.vehicleId = String(filteredVehicles.value[0].id)
}
},
{ immediate: true }
@@ -915,19 +863,11 @@ watch(
watch(
() => [form.truckId, form.carrierId, vehicles.value],
() => {
if (isHydrating.value || vehicleSyncLock.value) {
return
}
if (!isLiotCarrier.value) {
return
}
if (filteredVehicles.value.length === 1) {
const nextVehicleId = String(filteredVehicles.value[0].id)
if (form.vehicleId !== nextVehicleId) {
runWithVehicleSyncLock(() => {
form.vehicleId = nextVehicleId
})
}
form.vehicleId = String(filteredVehicles.value[0].id)
return
}
if (!form.vehicleId) {
@@ -937,11 +877,7 @@ watch(
(vehicle) => String(vehicle.id) === form.vehicleId
)
if (!matches) {
if (form.vehicleId !== '') {
runWithVehicleSyncLock(() => {
form.vehicleId = ''
})
}
form.vehicleId = ''
}
},
{ immediate: true }
@@ -950,9 +886,6 @@ watch(
watch(
() => [form.vehicleId, form.carrierId, vehicles.value],
() => {
if (vehicleSyncLock.value) {
return
}
if (!isLiotCarrier.value) {
return
}
@@ -962,11 +895,9 @@ watch(
const selected = filteredVehicles.value.find(
(vehicle) => String(vehicle.id) === form.vehicleId
)
if (selected && form.licensePlate !== selected.plate) {
runWithVehicleSyncLock(() => {
form.licensePlate = selected.plate
allowAnyLicensePlate.value = false
})
if (selected) {
form.licensePlate = selected.plate
allowAnyLicensePlate.value = false
}
}
)
@@ -974,12 +905,6 @@ watch(
watch(
() => [form.licensePlate, form.carrierId, vehicles.value],
() => {
if (vehicleSyncLock.value) {
return
}
if (isHydrating.value) {
return
}
if (!isLiotCarrier.value || form.vehicleId) {
return
}
@@ -987,12 +912,7 @@ watch(
(vehicle) => vehicle.plate === form.licensePlate
)
if (match) {
const nextVehicleId = String(match.id)
if (form.vehicleId !== nextVehicleId) {
runWithVehicleSyncLock(() => {
form.vehicleId = nextVehicleId
})
}
form.vehicleId = String(match.id)
}
}
)