266 lines
9.2 KiB
Vue
266 lines
9.2 KiB
Vue
<template>
|
|
<form>
|
|
<div class="flex flex-col">
|
|
<div class="w-full relative grid grid-cols-[1fr_200px]">
|
|
<UiRadioGroup
|
|
id="merchandise-type"
|
|
v-model="selectedMerchandiseTypeId"
|
|
label="Type de marchandises"
|
|
:options="merchandiseTypes.map((type) => ({
|
|
value: String(type.id),
|
|
label: type.label
|
|
}))"
|
|
input-class="accent-primary-700 focus:ring-primary-700"
|
|
option-label-class="uppercase"
|
|
wrapper-class="w-full uppercase"
|
|
group-class="grid grid-cols-4 mt-9 mb-7"
|
|
:disabled="!isAdmin"
|
|
/>
|
|
<UiTextInput
|
|
v-if="isAutres"
|
|
id="merchandise-detail"
|
|
:disabled="!isAdmin"
|
|
v-model="merchandiseDetail"
|
|
placeholder="Préciser"
|
|
:maxlength="255"
|
|
wrapper-class="w-[200px] mt-12 mb-7"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="selectedMerchandiseTypeId && !isGranule"
|
|
class="w-full grid grid-cols-[1fr_200px]"
|
|
>
|
|
<div class="grid grid-cols-4 gap-6"
|
|
>
|
|
<div
|
|
v-for="building in buildings"
|
|
:key="building.id"
|
|
>
|
|
<UiCheckbox
|
|
v-model="selectedBuildingIds"
|
|
:value="String(building.id)"
|
|
:label="building.label"
|
|
:disabled="!isAdmin"
|
|
input-class="accent-primary-700 focus:ring-primary-700"
|
|
label-class="uppercase"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="selectedMerchandiseTypeId && isGranule"
|
|
class="grid grid-cols-[1fr_200px] w-full col-start-2 row-start-1"
|
|
>
|
|
<div class="grid grid-cols-4 gap-6 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="!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 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('')
|
|
// 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(() =>
|
|
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
|
|
)
|
|
|
|
function clonePelletSelections(value: Record<string, string[]>) {
|
|
const clone: Record<string, string[]> = {}
|
|
for (const [key, buildingIds] of Object.entries(value)) {
|
|
clone[key] = [...buildingIds]
|
|
}
|
|
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)
|
|
if (!selectedPelletBuildingIds.value[key]) {
|
|
selectedPelletBuildingIds.value[key] = []
|
|
}
|
|
}
|
|
}
|
|
|
|
function hydrateFromModelValue(value: MerchandiseEntryData) {
|
|
isSyncing.value = true
|
|
try {
|
|
selectedMerchandiseTypeId.value = value.merchandiseTypeId ?? ''
|
|
merchandiseDetail.value = value.merchandiseDetail ?? ''
|
|
selectedBuildingIds.value = [...(value.selectedBuildingIds ?? [])]
|
|
selectedPelletBuildingIds.value = clonePelletSelections(
|
|
value.selectedPelletBuildingIds ?? {}
|
|
)
|
|
ensurePelletKeys()
|
|
} finally {
|
|
isSyncing.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)
|
|
}
|
|
|
|
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 }
|
|
)
|
|
|
|
watch(
|
|
[selectedMerchandiseTypeId, selectedBuildingIds, selectedPelletBuildingIds, merchandiseDetail],
|
|
() => {
|
|
if (isSyncing.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
|
|
}
|
|
|
|
emitCurrentModel()
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
onMounted(async () => {
|
|
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
|
|
getMerchandiseTypeList(),
|
|
getBuildingList(),
|
|
getPelletTypeList()
|
|
])
|
|
merchandiseTypes.value = merchandiseTypeList
|
|
buildings.value = buildingList
|
|
pelletTypes.value = pelletTypeList
|
|
|
|
hydrateFromModelValue(props.modelValue)
|
|
isReady.value = true
|
|
})
|
|
</script>
|