feat : cause d'entrée bovin + confirmation EDNOTIF asynchrone + historique entrées
- Champ entryCause sur Bovine (enum App\Enum\CauseEntree : Achat/Naissance/PretOuPension) - Sélecteur "Cause d'entrée" sur le formulaire de saisie (default Achat, required) - ednotif_confirmed_at sur Bovine : timestamp set par le sync EDNOTIF la première fois qu'un bovin remonte dans getInventory. Backfill des bovins existants au jour de la migration. - Inventaire (page + export + stats) filtre les bovins encore "en attente EDNOTIF" : ils n'apparaissent qu'une fois confirmés par le sync. - getter getConfirmedBovineCount sur Reception, exposé en reception:read. - Tableau Historique full-width sur /entry-exit listant les entrées validées, avec filtres de colonnes (numéro, date, fournisseur), compteur Confirmés/Saisis, et badge de statut "Confirmée" / "EDNOTIF en attente". - Tableau récap de l'écran de saisie passé en useDataTableServerState pour bénéficier du loading et de la pagination serveur. - Validation entrée : confirm window obligatoire, message renforcé en cas d'écart entre saisis et déclarés. - Pattern projet "submitted" sur le formulaire d'ajout pour le visuel required. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="px-[86px]">
|
||||
<div class="flex items-center justify-start gap-6 relative mb-8">
|
||||
<div class="flex items-center justify-start gap-6 relative">
|
||||
<Icon
|
||||
@click="router.push('/entry-exit')"
|
||||
name="gg:arrow-left-o"
|
||||
@@ -9,16 +9,17 @@
|
||||
/>
|
||||
<div>
|
||||
<h1 class="font-bold text-3xl uppercase text-primary-500">
|
||||
Entrée bovins {{ reception?.identificationNumber ?? `#${receptionId}` }}
|
||||
Entrée bovins {{ reception?.identificationNumber ?? '' }}
|
||||
</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>
|
||||
<p class="text-sm text-slate-600 mt-1 mb-8">
|
||||
{{ reception?.supplier?.name ?? '—' }} · Bovins déclarés : {{ declaredCount }} · Bovins saisis : {{ savedBovinesTotal }}
|
||||
</p>
|
||||
|
||||
<form
|
||||
class="grid grid-cols-4 gap-4 mb-6 items-end"
|
||||
class="grid grid-cols-4 gap-x-16 gap-y-8 mb-12 items-end"
|
||||
:class="{ submitted }"
|
||||
@submit.prevent="addBovine"
|
||||
>
|
||||
<UiTextInput
|
||||
@@ -26,59 +27,68 @@
|
||||
label="Numéro national"
|
||||
required
|
||||
/>
|
||||
<UiNumberInput
|
||||
v-model="form.receivedWeight"
|
||||
label="Poids à l'arrivée (kg)"
|
||||
:min="1"
|
||||
<UiSelect
|
||||
v-model="form.entryCause"
|
||||
label="Cause d'entrée"
|
||||
:options="entryCauseOptions"
|
||||
required
|
||||
/>
|
||||
<UiDateMaskedInput
|
||||
v-model="form.arrivalDate"
|
||||
label="Date d'arrivée"
|
||||
required
|
||||
label="Date d'entrée"
|
||||
/>
|
||||
<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
|
||||
/>
|
||||
<UiNumberInput
|
||||
v-model="form.receivedWeight"
|
||||
label="Poids (kg)"
|
||||
wrapperClass="flex-col"
|
||||
labelClass="font-bold uppercase text-xl text-primary-700"
|
||||
:min="1"
|
||||
/>
|
||||
<UiNumberInput
|
||||
v-model="form.pricePerKg"
|
||||
label="Prix au kilo (€)"
|
||||
wrapperClass="flex-col"
|
||||
labelClass="font-bold uppercase text-xl text-primary-700"
|
||||
:min="0"
|
||||
:step="0.01"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<div class="flex justify-center mb-12">
|
||||
<UiButton
|
||||
type="submit"
|
||||
class="text-md font-bold uppercase bg-primary-500 text-white h-[50px]"
|
||||
:disabled="!isFormValid || isAdding"
|
||||
type="button"
|
||||
class="text-md font-bold uppercase bg-primary-500 text-white h-[50px] px-8"
|
||||
:disabled="isAdding"
|
||||
:loading="isAdding"
|
||||
@click="addBovine"
|
||||
>
|
||||
Ajouter
|
||||
</UiButton>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<UiDataTable
|
||||
v-model:page="recapPage"
|
||||
v-model:per-page="recapPerPage"
|
||||
:columns="recapColumns"
|
||||
:items="savedBovines"
|
||||
:total-items="savedBovines.length"
|
||||
:total-items="savedBovinesTotal"
|
||||
:loading="savedBovinesLoading"
|
||||
:show-actions="true"
|
||||
>
|
||||
<template #cell-birthDate="{ item }">
|
||||
@@ -112,11 +122,11 @@
|
||||
</template>
|
||||
</UiDataTable>
|
||||
|
||||
<div class="flex justify-end mt-8 mb-16">
|
||||
<div class="flex justify-center mt-8">
|
||||
<UiButton
|
||||
type="button"
|
||||
class="text-md font-bold uppercase bg-primary-500 text-white h-[50px] px-8"
|
||||
:disabled="savedBovines.length === 0 || isValidating"
|
||||
:disabled="savedBovinesTotal === 0 || isValidating"
|
||||
:loading="isValidating"
|
||||
@click="validateEntry"
|
||||
>
|
||||
@@ -133,6 +143,7 @@ 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'
|
||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -143,12 +154,23 @@ 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 {
|
||||
items: savedBovines,
|
||||
totalItems: savedBovinesTotal,
|
||||
page: recapPage,
|
||||
perPage: recapPerPage,
|
||||
loading: savedBovinesLoading,
|
||||
reload: reloadSavedBovines
|
||||
} = useDataTableServerState<BovineData>(
|
||||
'bovines',
|
||||
{ reception: receptionId.value },
|
||||
{ initialPerPage: 50 }
|
||||
)
|
||||
|
||||
const isAdding = ref(false)
|
||||
const isValidating = ref(false)
|
||||
const recapPage = ref(1)
|
||||
const recapPerPage = ref(50)
|
||||
const submitted = ref(false)
|
||||
|
||||
const recapColumns = [
|
||||
{ key: 'nationalNumber', label: 'N° National', width: '110px' },
|
||||
@@ -181,18 +203,17 @@ const confirmDeleteBovine = async (bovine: BovineData) => {
|
||||
if (!confirmed) return
|
||||
|
||||
await api.delete(`bovines/${bovine.id}`)
|
||||
await loadSavedBovines()
|
||||
reloadSavedBovines()
|
||||
}
|
||||
|
||||
const validateEntry = async () => {
|
||||
if (savedBovines.value.length === 0 || isValidating.value) return
|
||||
if (savedBovinesTotal.value === 0 || isValidating.value) return
|
||||
|
||||
if (savedBovines.value.length < declaredCount.value) {
|
||||
const confirmed = window.confirm(
|
||||
`Vous n'avez saisi que ${savedBovines.value.length}/${declaredCount.value} bovins. Confirmer la fermeture de l'entrée ?`
|
||||
)
|
||||
if (!confirmed) return
|
||||
}
|
||||
const message = savedBovinesTotal.value !== declaredCount.value
|
||||
? `Attention : ${savedBovinesTotal.value} bovins saisis sur ${declaredCount.value} déclarés. Êtes-vous sûr de vouloir valider l'entrée ?`
|
||||
: `Êtes-vous sûr de vouloir valider l'entrée ?`
|
||||
|
||||
if (!window.confirm(message)) return
|
||||
|
||||
isValidating.value = true
|
||||
try {
|
||||
@@ -203,24 +224,34 @@ const validateEntry = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
type EntryCause = 'A' | 'N' | 'P'
|
||||
|
||||
interface FormState {
|
||||
nationalNumber: string
|
||||
receivedWeight: number | null
|
||||
entryCause: EntryCause
|
||||
arrivalDate: string
|
||||
supplierId: string | number | null
|
||||
pricePerKg: number | null
|
||||
buildingId: string | number | null
|
||||
caseId: string | number | null
|
||||
receivedWeight: number | null
|
||||
pricePerKg: number | null
|
||||
}
|
||||
|
||||
const entryCauseOptions = [
|
||||
{ value: 'A', label: 'Achat' },
|
||||
{ value: 'N', label: 'Naissance' },
|
||||
{ value: 'P', label: 'Prêt ou pension' }
|
||||
]
|
||||
|
||||
const initialForm = (): FormState => ({
|
||||
nationalNumber: '',
|
||||
receivedWeight: null,
|
||||
entryCause: 'A',
|
||||
arrivalDate: reception.value?.receptionDate?.slice(0, 10) ?? '',
|
||||
supplierId: reception.value?.supplier?.id ?? null,
|
||||
pricePerKg: null,
|
||||
buildingId: reception.value?.buildings?.[0]?.id ?? null,
|
||||
caseId: null
|
||||
caseId: null,
|
||||
receivedWeight: null,
|
||||
pricePerKg: null
|
||||
})
|
||||
|
||||
const form = reactive<FormState>(initialForm())
|
||||
@@ -248,23 +279,11 @@ 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 declaredCount = computed(() => reception.value?.declaredBovineCount ?? 0)
|
||||
|
||||
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
|
||||
&& !!form.entryCause
|
||||
)
|
||||
|
||||
const resetForm = () => {
|
||||
@@ -276,43 +295,35 @@ const loadReception = async () => {
|
||||
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 () => {
|
||||
submitted.value = true
|
||||
if (!isFormValid.value || isAdding.value) return
|
||||
|
||||
isAdding.value = true
|
||||
try {
|
||||
const payload = {
|
||||
const payload: Record<string, unknown> = {
|
||||
nationalNumber: form.nationalNumber.trim(),
|
||||
receivedWeight: form.receivedWeight,
|
||||
pricePerKg: form.pricePerKg,
|
||||
arrivalDate: form.arrivalDate,
|
||||
supplier: `/api/suppliers/${form.supplierId}`,
|
||||
buildingCase: `/api/building_cases/${form.caseId}`,
|
||||
entryCause: form.entryCause,
|
||||
reception: `/api/receptions/${receptionId.value}`
|
||||
}
|
||||
if (form.receivedWeight !== null) payload.receivedWeight = form.receivedWeight
|
||||
if (form.pricePerKg !== null) payload.pricePerKg = form.pricePerKg
|
||||
if (form.arrivalDate) payload.arrivalDate = form.arrivalDate
|
||||
if (form.supplierId !== null) payload.supplier = `/api/suppliers/${form.supplierId}`
|
||||
if (form.caseId !== null) payload.buildingCase = `/api/building_cases/${form.caseId}`
|
||||
|
||||
await api.post<BovineData>('bovines', payload, {
|
||||
headers: { 'Content-Type': 'application/ld+json' }
|
||||
})
|
||||
|
||||
await loadSavedBovines()
|
||||
reloadSavedBovines()
|
||||
resetForm()
|
||||
submitted.value = false
|
||||
await nextTick()
|
||||
focusFirstField()
|
||||
} finally {
|
||||
@@ -326,6 +337,6 @@ onMounted(async () => {
|
||||
getBuildingList()
|
||||
])
|
||||
await loadReception()
|
||||
await loadSavedBovines()
|
||||
reloadSavedBovines()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user