310 lines
10 KiB
Vue
310 lines
10 KiB
Vue
<template>
|
|
<div class="px-[86px]">
|
|
<div class="flex items-center justify-start gap-6 relative mb-8">
|
|
<Icon
|
|
@click="router.push('/entry-exit')"
|
|
name="gg:arrow-left-o"
|
|
size="44"
|
|
class="cursor-pointer text-primary-500 absolute -left-[60px]"
|
|
/>
|
|
<div>
|
|
<h1 class="font-bold text-3xl uppercase text-primary-500">
|
|
Entrée bovins {{ reception?.identificationNumber ?? `#${receptionId}` }}
|
|
</h1>
|
|
<p class="text-sm text-slate-600 mt-1">
|
|
{{ reception?.supplier?.name ?? '—' }} · Bovins déclarés : {{ declaredCount }} · Bovins saisis : {{ savedBovines.length }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<form
|
|
class="grid grid-cols-4 gap-4 mb-6 items-end"
|
|
@submit.prevent="addBovine"
|
|
>
|
|
<UiTextInput
|
|
v-model="form.nationalNumber"
|
|
label="Numéro national"
|
|
required
|
|
/>
|
|
<UiNumberInput
|
|
v-model="form.receivedWeight"
|
|
label="Poids à l'arrivée (kg)"
|
|
:min="1"
|
|
required
|
|
/>
|
|
<UiDateMaskedInput
|
|
v-model="form.arrivalDate"
|
|
label="Date d'arrivée"
|
|
required
|
|
/>
|
|
<UiSelect
|
|
v-model="form.supplierId"
|
|
label="Vendeur"
|
|
:options="supplierOptions"
|
|
required
|
|
/>
|
|
<UiNumberInput
|
|
v-model="form.pricePerKg"
|
|
label="Prix au kilo (€)"
|
|
:min="0"
|
|
:step="0.01"
|
|
required
|
|
/>
|
|
<UiSelect
|
|
v-model="form.buildingId"
|
|
label="Bâtiment"
|
|
:options="buildingOptions"
|
|
required
|
|
/>
|
|
<UiSelect
|
|
v-model="form.caseId"
|
|
label="Case"
|
|
:options="caseOptions"
|
|
:disabled="!form.buildingId"
|
|
required
|
|
/>
|
|
<UiButton
|
|
type="submit"
|
|
class="text-md font-bold uppercase bg-primary-500 text-white h-[50px]"
|
|
:disabled="!isFormValid || isAdding"
|
|
:loading="isAdding"
|
|
>
|
|
Ajouter
|
|
</UiButton>
|
|
</form>
|
|
|
|
<UiDataTable
|
|
v-model:page="recapPage"
|
|
v-model:per-page="recapPerPage"
|
|
:columns="recapColumns"
|
|
:items="savedBovines"
|
|
:total-items="savedBovines.length"
|
|
:show-actions="true"
|
|
>
|
|
<template #cell-birthDate="{ item }">
|
|
{{ formatDate(item.birthDate) }}
|
|
</template>
|
|
<template #cell-arrivalDate="{ item }">
|
|
{{ formatDate(item.arrivalDate) }}
|
|
</template>
|
|
<template #cell-finalPrice="{ item }">
|
|
{{ formatPrice(item.finalPrice) }}
|
|
</template>
|
|
<template #cell-pricePerKg="{ item }">
|
|
{{ formatPrice(item.pricePerKg) }}
|
|
</template>
|
|
<template #cell-buildingCase.building.label="{ item }">
|
|
{{ item.effectiveBuilding?.label ?? '—' }}
|
|
</template>
|
|
<template #cell-buildingCase.caseNumber="{ item }">
|
|
{{ item.buildingCase?.caseNumber ?? '—' }}
|
|
</template>
|
|
<template #cell-bovineType.label="{ item }">
|
|
{{ item.bovineType?.label ?? '—' }}
|
|
</template>
|
|
<template #actions="{ item }">
|
|
<Icon
|
|
name="mdi:delete-outline"
|
|
size="24"
|
|
class="cursor-pointer text-red-500 hover:text-red-700"
|
|
@click="confirmDeleteBovine(item)"
|
|
/>
|
|
</template>
|
|
</UiDataTable>
|
|
|
|
<div class="flex justify-end mt-8 mb-16">
|
|
<UiButton
|
|
type="button"
|
|
class="text-md font-bold uppercase bg-primary-500 text-white h-[50px] px-8"
|
|
disabled
|
|
>
|
|
Valider l'entrée
|
|
</UiButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { ReceptionData } from '~/services/dto/reception-data'
|
|
import type { BovineData } from '~/services/dto/bovine-data'
|
|
import type { SupplierData } from '~/services/dto/supplier-data'
|
|
import type { BuildingData } from '~/services/dto/building-data'
|
|
import { getSupplierList } from '~/services/supplier'
|
|
import { getBuildingList } from '~/services/building'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const api = useApi()
|
|
|
|
const receptionId = computed(() => Number(route.params.id))
|
|
|
|
const reception = ref<ReceptionData | null>(null)
|
|
const suppliers = ref<SupplierData[]>([])
|
|
const buildings = ref<BuildingData[]>([])
|
|
const savedBovines = ref<BovineData[]>([])
|
|
|
|
const isAdding = ref(false)
|
|
const recapPage = ref(1)
|
|
const recapPerPage = ref(50)
|
|
|
|
const recapColumns = [
|
|
{ key: 'nationalNumber', label: 'N° National', width: '110px' },
|
|
{ key: 'workNumber', label: 'N° Travail', width: '90px' },
|
|
{ key: 'bovineType.label', label: 'Race', width: '110px' },
|
|
{ key: 'sex', label: 'Sexe', width: '60px' },
|
|
{ key: 'birthDate', label: 'Né le', width: '90px' },
|
|
{ key: 'receivedWeight', label: 'Poids', width: '70px' },
|
|
{ key: 'arrivalDate', label: 'Entrée le', width: '90px' },
|
|
{ key: 'pricePerKg', label: 'Prix/kg', width: '80px' },
|
|
{ key: 'finalPrice', label: 'Prix total', width: '90px' },
|
|
{ key: 'buildingCase.building.label', label: 'Bâtiment', width: '1fr' },
|
|
{ key: 'buildingCase.caseNumber', label: 'Case', width: '60px' }
|
|
]
|
|
|
|
const formatDate = (date: string | null | undefined) => {
|
|
if (!date) return '—'
|
|
const d = new Date(date.replace(' ', 'T'))
|
|
if (isNaN(d.getTime())) return date
|
|
return d.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
|
}
|
|
|
|
const formatPrice = (price: number | null | undefined) => {
|
|
if (price === null || price === undefined) return '—'
|
|
return `${price.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`
|
|
}
|
|
|
|
const confirmDeleteBovine = async (bovine: BovineData) => {
|
|
const confirmed = window.confirm(`Supprimer le bovin ${bovine.nationalNumber} ?`)
|
|
if (!confirmed) return
|
|
|
|
await api.delete(`bovines/${bovine.id}`)
|
|
await loadSavedBovines()
|
|
}
|
|
|
|
interface FormState {
|
|
nationalNumber: string
|
|
receivedWeight: number | null
|
|
arrivalDate: string
|
|
supplierId: string | number | null
|
|
pricePerKg: number | null
|
|
buildingId: string | number | null
|
|
caseId: string | number | null
|
|
}
|
|
|
|
const initialForm = (): FormState => ({
|
|
nationalNumber: '',
|
|
receivedWeight: null,
|
|
arrivalDate: reception.value?.receptionDate?.slice(0, 10) ?? '',
|
|
supplierId: reception.value?.supplier?.id ?? null,
|
|
pricePerKg: null,
|
|
buildingId: reception.value?.buildings?.[0]?.id ?? null,
|
|
caseId: null
|
|
})
|
|
|
|
const form = reactive<FormState>(initialForm())
|
|
|
|
const supplierOptions = computed(() =>
|
|
suppliers.value.map(s => ({ value: s.id, label: s.name }))
|
|
)
|
|
|
|
const buildingOptions = computed(() =>
|
|
buildings.value.map(b => ({ value: b.id, label: b.label }))
|
|
)
|
|
|
|
const caseOptions = computed(() => {
|
|
const building = buildings.value.find(b => b.id === Number(form.buildingId))
|
|
if (!building?.buildingCases) return []
|
|
return [...building.buildingCases]
|
|
.sort((a, b) => (a.caseNumber ?? 0) - (b.caseNumber ?? 0))
|
|
.map(c => ({
|
|
value: c.id,
|
|
label: `Case ${c.caseNumber ?? c.code ?? c.id}`
|
|
}))
|
|
})
|
|
|
|
watch(() => form.buildingId, (newVal, oldVal) => {
|
|
if (newVal !== oldVal) form.caseId = null
|
|
})
|
|
|
|
const declaredCount = computed(() => {
|
|
if (!reception.value) return 0
|
|
const fromTypes = (reception.value.bovinesTypes ?? []).reduce((sum: number, bt: any) => {
|
|
return sum + (typeof bt.quantity === 'number' ? bt.quantity : 0)
|
|
}, 0)
|
|
const fromOther = parseInt(reception.value.bovineDetail ?? '0', 10) || 0
|
|
return fromTypes + fromOther
|
|
})
|
|
|
|
const isFormValid = computed(() =>
|
|
form.nationalNumber.trim() !== ''
|
|
&& (form.receivedWeight ?? 0) > 0
|
|
&& (form.pricePerKg ?? 0) > 0
|
|
&& form.arrivalDate !== ''
|
|
&& form.supplierId !== null
|
|
&& form.buildingId !== null
|
|
&& form.caseId !== null
|
|
)
|
|
|
|
const resetForm = () => {
|
|
Object.assign(form, initialForm())
|
|
}
|
|
|
|
const loadReception = async () => {
|
|
reception.value = await api.get<ReceptionData>(`receptions/${receptionId.value}`)
|
|
resetForm()
|
|
}
|
|
|
|
const loadSavedBovines = async () => {
|
|
const response = await api.get<{ 'hydra:member'?: BovineData[] } | BovineData[]>(
|
|
`bovines?reception=${receptionId.value}`,
|
|
{},
|
|
{ toast: false }
|
|
)
|
|
savedBovines.value = Array.isArray(response)
|
|
? response
|
|
: (response['hydra:member'] ?? [])
|
|
}
|
|
|
|
const focusFirstField = () => {
|
|
const el = document.querySelector<HTMLInputElement>('form input[type="text"]')
|
|
el?.focus()
|
|
}
|
|
|
|
const addBovine = async () => {
|
|
if (!isFormValid.value || isAdding.value) return
|
|
|
|
isAdding.value = true
|
|
try {
|
|
const payload = {
|
|
nationalNumber: form.nationalNumber.trim(),
|
|
receivedWeight: form.receivedWeight,
|
|
pricePerKg: form.pricePerKg,
|
|
arrivalDate: form.arrivalDate,
|
|
supplier: `/api/suppliers/${form.supplierId}`,
|
|
buildingCase: `/api/building_cases/${form.caseId}`,
|
|
reception: `/api/receptions/${receptionId.value}`
|
|
}
|
|
|
|
await api.post<BovineData>('bovines', payload, {
|
|
headers: { 'Content-Type': 'application/ld+json' }
|
|
})
|
|
|
|
await loadSavedBovines()
|
|
resetForm()
|
|
await nextTick()
|
|
focusFirstField()
|
|
} finally {
|
|
isAdding.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
[suppliers.value, buildings.value] = await Promise.all([
|
|
getSupplierList(),
|
|
getBuildingList()
|
|
])
|
|
await loadReception()
|
|
await loadSavedBovines()
|
|
})
|
|
</script>
|