342 lines
13 KiB
Vue
342 lines
13 KiB
Vue
<template>
|
|
<form>
|
|
<div
|
|
class="flex flex-col">
|
|
<div class="w-full col-start-1 row-start-1">
|
|
<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"
|
|
item-class="text-primary-700/50 [&:has(input:checked)]:text-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"
|
|
/>
|
|
</div>
|
|
|
|
<div class="w-full grid grid-cols-[3fr_1fr] gap-12 col-start-2 row-start-1">
|
|
<div
|
|
v-if="merchandiseTypeId && !isGranule"
|
|
class="flex gap-[218px]"
|
|
>
|
|
<div
|
|
v-for="building in buildings"
|
|
:key="building.id"
|
|
>
|
|
<UiCheckbox
|
|
v-model="selectedBuildingIds"
|
|
:value="String(building.id)"
|
|
:label="building.label"
|
|
:disabled="!auth.isAdmin"
|
|
input-class="accent-primary-700 focus:ring-primary-700"
|
|
label-class="text-xl uppercase"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="merchandiseTypeId && isAutres"
|
|
class="flex flex-col justify-self-end max-w-[182px]"
|
|
>
|
|
<UiTextInput
|
|
id="merchandise-detail"
|
|
:disabled="!auth.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 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>
|
|
</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";
|
|
|
|
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
|
|
|
|
// 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)
|
|
)
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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}`)
|
|
}
|
|
}
|
|
return pairs.sort()
|
|
}
|
|
|
|
// Charge les référentiels et hydrate le formulaire depuis la réception
|
|
onMounted(async () => {
|
|
const [merchandiseTypeList, buildingList, pelletTypeList] = await Promise.all([
|
|
getMerchandiseTypeList(),
|
|
getBuildingList(),
|
|
getPelletTypeList()
|
|
])
|
|
merchandiseTypes.value = merchandiseTypeList
|
|
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))
|
|
})
|
|
|
|
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>
|