feat : Reception.validatedAt + statut entrées + mode consultation
- Backend : champ Reception.validatedAt (timestamp) avec PreUpdate + helpers
isFullyConfirmed/tryValidate ; sync EDNOTIF déclenche tryValidate sur
les receptions impactées ; expose Supplier.name dans le groupe bovine:read.
- Migration : ajout colonne validated_at sans backfill (les receptions
remontent en attente jusqu'au prochain sync).
- Front /entry-exit : remplace Historique par 'Entrées validées' (filtre
exists[validatedAt]=true), ajoute filtres et colonne Statut sur les
deux tableaux, retire Fournisseur, layout 2x2 (entrées + sorties).
- Front /entry-exit/entry/{id} : mode consultation quand entryCompleted=true
(formulaire + actions masqués, colonne EDNOTIF par bovin) ; ajoute
colonnes Vendeur et Cause dans le récap.
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">
|
||||
<div class="flex items-center justify-start gap-6 relative" :class="{ 'mb-8': isConsultationMode }">
|
||||
<Icon
|
||||
@click="router.push('/entry-exit')"
|
||||
name="gg:arrow-left-o"
|
||||
@@ -13,10 +13,11 @@
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-slate-600 mt-1 mb-8">
|
||||
<p v-if="!isConsultationMode" class="text-sm text-slate-600 mt-1 mb-8">
|
||||
{{ reception?.supplier?.name ?? '—' }} · Bovins déclarés : {{ declaredCount }} · Bovins saisis : {{ savedBovinesTotal }}
|
||||
</p>
|
||||
|
||||
<template v-if="!isConsultationMode">
|
||||
<form
|
||||
class="grid grid-cols-4 gap-x-16 gap-y-8 mb-12 items-end"
|
||||
:class="{ submitted }"
|
||||
@@ -36,37 +37,13 @@
|
||||
<UiDateMaskedInput
|
||||
v-model="form.arrivalDate"
|
||||
label="Date d'entrée"
|
||||
required
|
||||
/>
|
||||
<UiSelect
|
||||
v-model="form.supplierId"
|
||||
label="Vendeur"
|
||||
:options="supplierOptions"
|
||||
/>
|
||||
<UiSelect
|
||||
v-model="form.buildingId"
|
||||
label="Bâtiment"
|
||||
:options="buildingOptions"
|
||||
/>
|
||||
<UiSelect
|
||||
v-model="form.caseId"
|
||||
label="Case"
|
||||
:options="caseOptions"
|
||||
:disabled="!form.buildingId"
|
||||
/>
|
||||
<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"
|
||||
required
|
||||
/>
|
||||
</form>
|
||||
|
||||
@@ -81,6 +58,7 @@
|
||||
Ajouter
|
||||
</UiButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UiDataTable
|
||||
v-model:page="recapPage"
|
||||
@@ -89,29 +67,67 @@
|
||||
:items="savedBovines"
|
||||
:total-items="savedBovinesTotal"
|
||||
:loading="savedBovinesLoading"
|
||||
:show-actions="true"
|
||||
:show-actions="!isConsultationMode"
|
||||
>
|
||||
<template #header-nationalNumber>
|
||||
<UiTextInput v-model="recapFilters.nationalNumber" placeholder="N° National" size="compact" />
|
||||
</template>
|
||||
<template #header-workNumber>
|
||||
<UiTextInput v-model="recapFilters.workNumber" placeholder="N° Travail" size="compact" />
|
||||
</template>
|
||||
<template #header-bovineType.label>
|
||||
<UiTextInput v-model="recapFilters['bovineType.label']" placeholder="Race" size="compact" />
|
||||
</template>
|
||||
<template #header-sex>
|
||||
<UiTextInput v-model="recapFilters.sex" placeholder="Sexe" size="compact" />
|
||||
</template>
|
||||
<template #header-birthDate>
|
||||
<UiDateMaskedInput v-model="birthDateFilter" placeholder="Né le" size="compact" />
|
||||
</template>
|
||||
<template #header-arrivalDate>
|
||||
<UiDateMaskedInput v-model="arrivalDateFilter" placeholder="Entrée le" size="compact" />
|
||||
</template>
|
||||
<template #header-supplier.name>
|
||||
<UiTextInput :model-value="''" placeholder="Vendeur" size="compact" disabled />
|
||||
</template>
|
||||
<template #header-entryCause>
|
||||
<UiTextInput :model-value="''" placeholder="Cause" size="compact" disabled />
|
||||
</template>
|
||||
<template #header-ednotifConfirmedAt>
|
||||
<UiTextInput :model-value="''" placeholder="EDNOTIF" size="compact" disabled />
|
||||
</template>
|
||||
<template #header-actions>
|
||||
<UiTextInput :model-value="''" placeholder="Action" size="compact" disabled />
|
||||
</template>
|
||||
<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 #cell-supplier.name="{ item }">
|
||||
{{ supplierName(item.supplier) }}
|
||||
</template>
|
||||
<template #cell-entryCause="{ item }">
|
||||
{{ entryCauseLabel(item.entryCause) }}
|
||||
</template>
|
||||
<template #cell-ednotifConfirmedAt="{ item }">
|
||||
<span
|
||||
v-if="item.ednotifConfirmedAt"
|
||||
class="inline-block rounded px-2 py-0.5 text-xs font-semibold bg-green-100 text-green-700"
|
||||
>
|
||||
Validé
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="inline-block rounded px-2 py-0.5 text-xs font-semibold bg-orange-100 text-orange-700"
|
||||
>
|
||||
En attente
|
||||
</span>
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<Icon
|
||||
name="mdi:delete-outline"
|
||||
@@ -122,7 +138,7 @@
|
||||
</template>
|
||||
</UiDataTable>
|
||||
|
||||
<div class="flex justify-center mt-8">
|
||||
<div v-if="!isConsultationMode" class="flex justify-center mt-8">
|
||||
<UiButton
|
||||
type="button"
|
||||
class="text-md font-bold uppercase bg-primary-500 text-white h-[50px] px-8"
|
||||
@@ -140,9 +156,7 @@
|
||||
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'
|
||||
import { useDataTableServerState } from '~/composables/useDataTableServerState'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -153,38 +167,95 @@ const receptionId = computed(() => Number(route.params.id))
|
||||
|
||||
const reception = ref<ReceptionData | null>(null)
|
||||
const suppliers = ref<SupplierData[]>([])
|
||||
const buildings = ref<BuildingData[]>([])
|
||||
|
||||
const {
|
||||
items: savedBovines,
|
||||
totalItems: savedBovinesTotal,
|
||||
page: recapPage,
|
||||
perPage: recapPerPage,
|
||||
filters: recapFilters,
|
||||
loading: savedBovinesLoading,
|
||||
reload: reloadSavedBovines
|
||||
} = useDataTableServerState<BovineData>(
|
||||
'bovines',
|
||||
{ reception: receptionId.value },
|
||||
{ initialPerPage: 50 }
|
||||
{
|
||||
reception: receptionId.value,
|
||||
nationalNumber: '',
|
||||
workNumber: '',
|
||||
'bovineType.label': '',
|
||||
sex: '',
|
||||
'birthDate[after]': '',
|
||||
'birthDate[strictly_before]': '',
|
||||
'arrivalDate[after]': '',
|
||||
'arrivalDate[strictly_before]': ''
|
||||
},
|
||||
{ initialPerPage: 10 }
|
||||
)
|
||||
|
||||
const addOneDay = (dateString: string): string => {
|
||||
const [year, month, day] = dateString.split('-').map(Number)
|
||||
const next = new Date(Date.UTC(year, month - 1, day + 1))
|
||||
return next.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
const birthDateFilter = computed<string>({
|
||||
get: () => (recapFilters.value['birthDate[after]'] as string) ?? '',
|
||||
set: (value: string) => {
|
||||
if (!value) {
|
||||
recapFilters.value['birthDate[after]'] = ''
|
||||
recapFilters.value['birthDate[strictly_before]'] = ''
|
||||
return
|
||||
}
|
||||
recapFilters.value['birthDate[after]'] = value
|
||||
recapFilters.value['birthDate[strictly_before]'] = addOneDay(value)
|
||||
}
|
||||
})
|
||||
|
||||
const arrivalDateFilter = computed<string>({
|
||||
get: () => (recapFilters.value['arrivalDate[after]'] as string) ?? '',
|
||||
set: (value: string) => {
|
||||
if (!value) {
|
||||
recapFilters.value['arrivalDate[after]'] = ''
|
||||
recapFilters.value['arrivalDate[strictly_before]'] = ''
|
||||
return
|
||||
}
|
||||
recapFilters.value['arrivalDate[after]'] = value
|
||||
recapFilters.value['arrivalDate[strictly_before]'] = addOneDay(value)
|
||||
}
|
||||
})
|
||||
|
||||
const isAdding = ref(false)
|
||||
const isValidating = ref(false)
|
||||
const submitted = ref(false)
|
||||
|
||||
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 isConsultationMode = computed(() => reception.value?.entryCompleted === true)
|
||||
|
||||
const recapColumns = computed(() => {
|
||||
const cols: Array<{ key: string; label: string; width: string }> = [
|
||||
{ key: 'nationalNumber', label: 'N° National', width: '100px' },
|
||||
{ key: 'workNumber', label: 'N° Travail', width: '110px' },
|
||||
{ key: 'bovineType.label', label: 'Race', width: '1fr' },
|
||||
{ key: 'sex', label: 'Sexe', width: '70px' },
|
||||
{ key: 'birthDate', label: 'Né le', width: '75px' },
|
||||
{ key: 'arrivalDate', label: 'Entrée le', width: '75px' },
|
||||
{ key: 'supplier.name', label: 'Vendeur', width: '150px' },
|
||||
{ key: 'entryCause', label: 'Cause', width: '100px' }
|
||||
]
|
||||
if (isConsultationMode.value) {
|
||||
cols.push({ key: 'ednotifConfirmedAt', label: 'EDNOTIF', width: '110px' })
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
const entryCauseLabel = (code: string | null | undefined) => {
|
||||
if (!code) return '—'
|
||||
return entryCauseOptions.find(o => o.value === code)?.label ?? code
|
||||
}
|
||||
|
||||
const supplierName = (supplier: BovineData['supplier']) => {
|
||||
if (supplier && typeof supplier === 'object') return supplier.name
|
||||
return '—'
|
||||
}
|
||||
|
||||
const formatDate = (date: string | null | undefined) => {
|
||||
if (!date) return '—'
|
||||
@@ -193,11 +264,6 @@ const formatDate = (date: string | null | undefined) => {
|
||||
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
|
||||
@@ -231,10 +297,6 @@ interface FormState {
|
||||
entryCause: EntryCause
|
||||
arrivalDate: string
|
||||
supplierId: string | number | null
|
||||
buildingId: string | number | null
|
||||
caseId: string | number | null
|
||||
receivedWeight: number | null
|
||||
pricePerKg: number | null
|
||||
}
|
||||
|
||||
const entryCauseOptions = [
|
||||
@@ -247,11 +309,7 @@ const initialForm = (): FormState => ({
|
||||
nationalNumber: '',
|
||||
entryCause: 'A',
|
||||
arrivalDate: reception.value?.receptionDate?.slice(0, 10) ?? '',
|
||||
supplierId: reception.value?.supplier?.id ?? null,
|
||||
buildingId: reception.value?.buildings?.[0]?.id ?? null,
|
||||
caseId: null,
|
||||
receivedWeight: null,
|
||||
pricePerKg: null
|
||||
supplierId: reception.value?.supplier?.id ?? null
|
||||
})
|
||||
|
||||
const form = reactive<FormState>(initialForm())
|
||||
@@ -260,30 +318,13 @@ 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(() => reception.value?.declaredBovineCount ?? 0)
|
||||
|
||||
const isFormValid = computed(() =>
|
||||
form.nationalNumber.trim() !== ''
|
||||
&& !!form.entryCause
|
||||
&& !!form.arrivalDate
|
||||
&& form.supplierId !== null
|
||||
)
|
||||
|
||||
const resetForm = () => {
|
||||
@@ -306,16 +347,13 @@ const addBovine = async () => {
|
||||
|
||||
isAdding.value = true
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
const payload = {
|
||||
nationalNumber: form.nationalNumber.trim(),
|
||||
entryCause: form.entryCause,
|
||||
arrivalDate: form.arrivalDate,
|
||||
supplier: `/api/suppliers/${form.supplierId}`,
|
||||
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' }
|
||||
@@ -332,10 +370,7 @@ const addBovine = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
[suppliers.value, buildings.value] = await Promise.all([
|
||||
getSupplierList(),
|
||||
getBuildingList()
|
||||
])
|
||||
suppliers.value = await getSupplierList()
|
||||
await loadReception()
|
||||
reloadSavedBovines()
|
||||
})
|
||||
|
||||
@@ -23,6 +23,25 @@
|
||||
row-clickable
|
||||
@row-click="goToEntry"
|
||||
>
|
||||
<template #header-identificationNumber>
|
||||
<UiTextInput
|
||||
v-model="entryFilters.identificationNumber"
|
||||
placeholder="Numéro"
|
||||
size="compact"
|
||||
/>
|
||||
</template>
|
||||
<template #header-receptionDate>
|
||||
<UiDateMaskedInput v-model="entryDateFilter" placeholder="Date" size="compact" />
|
||||
</template>
|
||||
<template #header-declaredCount>
|
||||
<UiTextInput :model-value="''" placeholder="Déclarés" size="compact" disabled />
|
||||
</template>
|
||||
<template #header-registeredBovineCount>
|
||||
<UiTextInput :model-value="''" placeholder="Saisis" size="compact" disabled />
|
||||
</template>
|
||||
<template #header-status>
|
||||
<UiTextInput :model-value="''" placeholder="Statut" size="compact" disabled />
|
||||
</template>
|
||||
<template #cell-identificationNumber="{ item }">
|
||||
{{ item.identificationNumber }}
|
||||
</template>
|
||||
@@ -35,49 +54,48 @@
|
||||
<template #cell-registeredBovineCount="{ item }">
|
||||
{{ item.registeredBovineCount ?? 0 }}
|
||||
</template>
|
||||
<template #cell-status="{ item }">
|
||||
<span
|
||||
v-if="!item.entryCompleted"
|
||||
class="inline-block rounded px-2 py-0.5 text-xs font-semibold bg-yellow-100 text-yellow-700"
|
||||
>
|
||||
Attente saisie
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="inline-block rounded px-2 py-0.5 text-xs font-semibold bg-orange-100 text-orange-700"
|
||||
>
|
||||
Attente EDNOTIF
|
||||
</span>
|
||||
</template>
|
||||
</UiDataTable>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-xl font-bold uppercase text-primary-500 mb-4">Sorties en attente</h2>
|
||||
<div class="rounded border border-dashed border-slate-300 p-8 text-center text-slate-500">
|
||||
À venir
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="mt-12 mb-16">
|
||||
<h2 class="text-xl font-bold uppercase text-primary-500 mb-4">Historique</h2>
|
||||
<h2 class="text-xl font-bold uppercase text-primary-500 mb-4">Entrées validées</h2>
|
||||
<UiDataTable
|
||||
v-model:page="historyPage"
|
||||
v-model:per-page="historyPerPage"
|
||||
:columns="historyColumns"
|
||||
:items="history"
|
||||
:total-items="totalHistory"
|
||||
:loading="historyLoading"
|
||||
v-model:page="validatedPage"
|
||||
v-model:per-page="validatedPerPage"
|
||||
:columns="validatedColumns"
|
||||
:items="validated"
|
||||
:total-items="totalValidated"
|
||||
:loading="validatedLoading"
|
||||
>
|
||||
<template #header-identificationNumber>
|
||||
<UiTextInput
|
||||
v-model="historyFilters.identificationNumber"
|
||||
v-model="validatedFilters.identificationNumber"
|
||||
placeholder="Numéro"
|
||||
size="compact"
|
||||
/>
|
||||
</template>
|
||||
<template #header-receptionDate>
|
||||
<UiDateMaskedInput v-model="historyDateFilter" placeholder="Date" size="compact" />
|
||||
</template>
|
||||
<template #header-supplier.name>
|
||||
<UiTextInput
|
||||
v-model="historyFilters['supplier.name']"
|
||||
placeholder="Fournisseur"
|
||||
size="compact"
|
||||
/>
|
||||
<UiDateMaskedInput v-model="validatedDateFilter" placeholder="Date" size="compact" />
|
||||
</template>
|
||||
<template #header-registeredBovineCount>
|
||||
<UiTextInput :model-value="''" placeholder="Saisis" size="compact" disabled />
|
||||
</template>
|
||||
<template #header-confirmedBovineCount>
|
||||
<UiTextInput :model-value="''" placeholder="Confirmés" size="compact" disabled />
|
||||
<template #header-validatedAt>
|
||||
<UiTextInput :model-value="''" placeholder="Validée le" size="compact" disabled />
|
||||
</template>
|
||||
<template #header-status>
|
||||
<UiTextInput :model-value="''" placeholder="Statut" size="compact" disabled />
|
||||
@@ -91,26 +109,36 @@
|
||||
<template #cell-registeredBovineCount="{ item }">
|
||||
{{ item.registeredBovineCount ?? 0 }}
|
||||
</template>
|
||||
<template #cell-confirmedBovineCount="{ item }">
|
||||
{{ item.confirmedBovineCount ?? 0 }} / {{ item.registeredBovineCount ?? 0 }}
|
||||
<template #cell-validatedAt="{ item }">
|
||||
{{ formatDate(item.validatedAt) }}
|
||||
</template>
|
||||
<template #cell-status="{ item }">
|
||||
<template #cell-status>
|
||||
<span
|
||||
v-if="(item.confirmedBovineCount ?? 0) >= (item.registeredBovineCount ?? 0) && (item.registeredBovineCount ?? 0) > 0"
|
||||
class="inline-block rounded px-2 py-0.5 text-xs font-semibold bg-green-100 text-green-700"
|
||||
>
|
||||
Confirmée
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="inline-block rounded px-2 py-0.5 text-xs font-semibold bg-yellow-100 text-yellow-700"
|
||||
>
|
||||
EDNOTIF en attente
|
||||
Validée
|
||||
</span>
|
||||
</template>
|
||||
</UiDataTable>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 mb-16 grid grid-cols-2 gap-8">
|
||||
<section>
|
||||
<h2 class="text-xl font-bold uppercase text-primary-500 mb-4">Sorties en attente</h2>
|
||||
<div class="rounded border border-dashed border-slate-300 p-8 text-center text-slate-500">
|
||||
À venir
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-xl font-bold uppercase text-primary-500 mb-4">Sorties validées</h2>
|
||||
<div class="rounded border border-dashed border-slate-300 p-8 text-center text-slate-500">
|
||||
À venir
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -124,38 +152,41 @@ const {
|
||||
totalItems: totalEntries,
|
||||
page: entryPage,
|
||||
perPage: entryPerPage,
|
||||
filters: entryFilters,
|
||||
loading: entriesLoading,
|
||||
reload
|
||||
} = useDataTableServerState<ReceptionData>(
|
||||
'receptions',
|
||||
{
|
||||
'isValid': 'true',
|
||||
'entryCompleted': 'false',
|
||||
'receptionType.code': 'BOVINS'
|
||||
'exists[validatedAt]': 'false',
|
||||
'receptionType.code': 'BOVINS',
|
||||
'identificationNumber': '',
|
||||
'receptionDate[after]': '',
|
||||
'receptionDate[strictly_before]': ''
|
||||
},
|
||||
{ initialPerPage: 5 }
|
||||
)
|
||||
|
||||
const {
|
||||
items: history,
|
||||
totalItems: totalHistory,
|
||||
page: historyPage,
|
||||
perPage: historyPerPage,
|
||||
filters: historyFilters,
|
||||
loading: historyLoading,
|
||||
reload: reloadHistory
|
||||
items: validated,
|
||||
totalItems: totalValidated,
|
||||
page: validatedPage,
|
||||
perPage: validatedPerPage,
|
||||
filters: validatedFilters,
|
||||
loading: validatedLoading,
|
||||
reload: reloadValidated
|
||||
} = useDataTableServerState<ReceptionData>(
|
||||
'receptions',
|
||||
{
|
||||
'isValid': 'true',
|
||||
'entryCompleted': 'true',
|
||||
'exists[validatedAt]': 'true',
|
||||
'receptionType.code': 'BOVINS',
|
||||
'identificationNumber': '',
|
||||
'supplier.name': '',
|
||||
'receptionDate[after]': '',
|
||||
'receptionDate[strictly_before]': ''
|
||||
},
|
||||
{ initialPerPage: 10 }
|
||||
{ initialPerPage: 5 }
|
||||
)
|
||||
|
||||
const addOneDay = (dateString: string): string => {
|
||||
@@ -164,37 +195,49 @@ const addOneDay = (dateString: string): string => {
|
||||
return next.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
const historyDateFilter = computed<string>({
|
||||
get: () => (historyFilters.value['receptionDate[after]'] as string) ?? '',
|
||||
const entryDateFilter = computed<string>({
|
||||
get: () => (entryFilters.value['receptionDate[after]'] as string) ?? '',
|
||||
set: (value: string) => {
|
||||
if (!value) {
|
||||
historyFilters.value['receptionDate[after]'] = ''
|
||||
historyFilters.value['receptionDate[strictly_before]'] = ''
|
||||
entryFilters.value['receptionDate[after]'] = ''
|
||||
entryFilters.value['receptionDate[strictly_before]'] = ''
|
||||
return
|
||||
}
|
||||
historyFilters.value['receptionDate[after]'] = value
|
||||
historyFilters.value['receptionDate[strictly_before]'] = addOneDay(value)
|
||||
entryFilters.value['receptionDate[after]'] = value
|
||||
entryFilters.value['receptionDate[strictly_before]'] = addOneDay(value)
|
||||
}
|
||||
})
|
||||
|
||||
const validatedDateFilter = computed<string>({
|
||||
get: () => (validatedFilters.value['receptionDate[after]'] as string) ?? '',
|
||||
set: (value: string) => {
|
||||
if (!value) {
|
||||
validatedFilters.value['receptionDate[after]'] = ''
|
||||
validatedFilters.value['receptionDate[strictly_before]'] = ''
|
||||
return
|
||||
}
|
||||
validatedFilters.value['receptionDate[after]'] = value
|
||||
validatedFilters.value['receptionDate[strictly_before]'] = addOneDay(value)
|
||||
}
|
||||
})
|
||||
|
||||
const entryColumns = [
|
||||
{ key: 'identificationNumber', label: 'Numéro', width: '80px' },
|
||||
{ key: 'identificationNumber', label: 'Numéro', width: '75px' },
|
||||
{ key: 'receptionDate', label: 'Date', width: '75px' },
|
||||
{ key: 'supplier.name', label: 'Fournisseur', width: '1fr' },
|
||||
{ key: 'declaredCount', label: 'Déclarés', width: '85px' },
|
||||
{ key: 'registeredBovineCount', label: 'Saisis', width: '50px' }
|
||||
{ key: 'declaredCount', label: 'Déclarés', width: '75px' },
|
||||
{ key: 'registeredBovineCount', label: 'Saisis', width: '70px' },
|
||||
{ key: 'status', label: 'Statut', width: '1fr' }
|
||||
]
|
||||
|
||||
const historyColumns = [
|
||||
{ key: 'identificationNumber', label: 'Numéro', width: '110px' },
|
||||
{ key: 'receptionDate', label: 'Date', width: '110px' },
|
||||
{ key: 'supplier.name', label: 'Fournisseur', width: '1fr' },
|
||||
{ key: 'registeredBovineCount', label: 'Saisis', width: '80px' },
|
||||
{ key: 'confirmedBovineCount', label: 'Confirmés', width: '110px' },
|
||||
{ key: 'status', label: 'Statut', width: '170px' }
|
||||
const validatedColumns = [
|
||||
{ key: 'identificationNumber', label: 'Numéro', width: '75px' },
|
||||
{ key: 'receptionDate', label: 'Date', width: '75px' },
|
||||
{ key: 'registeredBovineCount', label: 'Saisis', width: '50px' },
|
||||
{ key: 'validatedAt', label: 'Validée le', width: '75px' },
|
||||
{ key: 'status', label: 'Statut', width: '1fr' }
|
||||
]
|
||||
|
||||
const formatDate = (date: string | null) => {
|
||||
const formatDate = (date: string | null | undefined) => {
|
||||
if (!date) return '—'
|
||||
const d = new Date(date.replace(' ', 'T'))
|
||||
if (isNaN(d.getTime())) return date
|
||||
@@ -211,6 +254,6 @@ const goToEntry = (reception: ReceptionData) => {
|
||||
|
||||
onMounted(() => {
|
||||
reload()
|
||||
reloadHistory()
|
||||
reloadValidated()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface BovineData {
|
||||
buildingCase: BovineBuildingCaseRef | null
|
||||
building: BovineBuildingRef | null
|
||||
effectiveBuilding: BovineBuildingRef | null
|
||||
supplier: string | null
|
||||
supplier: { id: number; name: string } | string | null
|
||||
workNumber: string | null
|
||||
birthDate: string | null
|
||||
bovineType: { id: number; label: string; code: string } | null
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface ReceptionData {
|
||||
currentStep: number
|
||||
isValid: boolean
|
||||
entryCompleted?: boolean
|
||||
validatedAt?: string | null
|
||||
registeredBovineCount?: number
|
||||
confirmedBovineCount?: number
|
||||
declaredBovineCount?: number
|
||||
|
||||
26
migrations/Version20260430090000.php
Normal file
26
migrations/Version20260430090000.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260430090000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Ajout de reception.validated_at (timestamp de validation complète : entrée terminée + tous bovins confirmés EDNOTIF).';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE reception ADD validated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE reception DROP validated_at');
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,7 @@ class Bovine
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[Groups(['bovine:read', 'bovine:write', 'building_case:read'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?Supplier $supplier = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\ExistsFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
@@ -32,6 +33,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ORM\Table(name: 'reception')]
|
||||
#[ApiFilter(BooleanFilter::class, properties: ['isValid', 'entryCompleted'])]
|
||||
#[ApiFilter(ExistsFilter::class, properties: ['validatedAt'])]
|
||||
#[ApiFilter(SearchFilter::class, properties: [
|
||||
'identificationNumber' => 'ipartial',
|
||||
'supplier.name' => 'ipartial',
|
||||
@@ -115,6 +117,10 @@ class Reception
|
||||
#[Groups(['reception:read', 'reception:write', 'reception-bovine:read'])]
|
||||
private bool $entryCompleted = false;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
#[Groups(['reception:read', 'reception-bovine:read'])]
|
||||
private ?DateTimeImmutable $validatedAt = null;
|
||||
|
||||
#[ORM\Column(name: 'date_reception', type: 'datetime_immutable')]
|
||||
#[Groups(['reception:read', 'reception:write', 'reception-bovine:read'])]
|
||||
#[Context(
|
||||
@@ -295,6 +301,45 @@ class Reception
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValidatedAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->validatedAt;
|
||||
}
|
||||
|
||||
public function setValidatedAt(?DateTimeImmutable $validatedAt): self
|
||||
{
|
||||
$this->validatedAt = $validatedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isFullyConfirmed(): bool
|
||||
{
|
||||
if ($this->bovines->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
foreach ($this->bovines as $bovine) {
|
||||
if (null === $bovine->getEdnotifConfirmedAt()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function tryValidate(): void
|
||||
{
|
||||
if ($this->entryCompleted && null === $this->validatedAt && $this->isFullyConfirmed()) {
|
||||
$this->validatedAt = new DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function onPreUpdate(): void
|
||||
{
|
||||
$this->tryValidate();
|
||||
}
|
||||
|
||||
#[Groups(['reception:read'])]
|
||||
public function getRegisteredBovineCount(): int
|
||||
{
|
||||
|
||||
@@ -54,11 +54,11 @@ class Supplier
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['supplier:read', 'reception:read'])]
|
||||
#[Groups(['supplier:read', 'reception:read', 'bovine:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 180)]
|
||||
#[Groups(['supplier:read', 'reception:read', 'supplier:write'])]
|
||||
#[Groups(['supplier:read', 'reception:read', 'supplier:write', 'bovine:read'])]
|
||||
private string $name = '';
|
||||
|
||||
#[ORM\Column(length: 180, nullable: true)]
|
||||
|
||||
@@ -53,6 +53,7 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
|
||||
}
|
||||
|
||||
$seen = [];
|
||||
$impactedReceptions = [];
|
||||
foreach ($inventory->animals as $animal) {
|
||||
$nationalNumber = $animal->identification?->bovin?->nationalNumber;
|
||||
if (null === $nationalNumber || '' === $nationalNumber) {
|
||||
@@ -77,8 +78,16 @@ final class BovineSyncInventoryProcessor implements ProcessorInterface
|
||||
// voit ce bovin remonter dans l'inventaire.
|
||||
if (null === $bovine->getEdnotifConfirmedAt()) {
|
||||
$bovine->setEdnotifConfirmedAt(new DateTimeImmutable());
|
||||
$reception = $bovine->getReception();
|
||||
if (null !== $reception) {
|
||||
$impactedReceptions[$reception->getId()] = $reception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($impactedReceptions as $reception) {
|
||||
$reception->tryValidate();
|
||||
}
|
||||
|
||||
$now = new DateTimeImmutable();
|
||||
foreach ($existingByNationalNumber as $nationalNumber => $bovine) {
|
||||
|
||||
Reference in New Issue
Block a user