fix : correction des retours de la V0

This commit is contained in:
tristan
2026-03-18 14:47:03 +01:00
parent 995e7de2cc
commit a905c6a1de
64 changed files with 1979 additions and 1640 deletions
+46 -52
View File
@@ -2,7 +2,7 @@
<div class="flex justify-between h-[52px] mb-[80px]">
<div class="flex flex-1 mr-16">
<UiStepper
:labels="RECEPTION_STEP_LABELS"
:labels="stepLabels"
:current-step="storeReception?.currentStep ?? 0"
@select="handleStepSelect"
/>
@@ -15,76 +15,70 @@
</UiButton>
</div>
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
<WorkflowWeight
v-if="storeReception?.currentStep === 1"
mode="gross"
entity-name="reception"
api-resource="receptions"
:title-label="receptionConfig.weighingLabels.gross"
:is-final="false"
:entity="storeReception"
:get-weight-from-scale="getWeight"
:update-entity="receptionStore.updateReception"
:load-entity="receptionStore.loadReception"
:clear-entity="receptionStore.clearCurrent"
:build-receipt-filename="receptionConfig.buildReceiptFilename"
/>
<ReceptionProductReceived
v-if="storeReception?.currentStep === 2 &&
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"/>
<ReceptionBovineReceived
v-if="storeReception?.currentStep === 2 &&
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
<WorkflowWeight
v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3"
mode="tare"
entity-name="reception"
api-resource="receptions"
:title-label="receptionConfig.weighingLabels.tare"
:is-final="true"
:entity="storeReception"
:get-weight-from-scale="getWeight"
:update-entity="receptionStore.updateReception"
:load-entity="receptionStore.loadReception"
:clear-entity="receptionStore.clearCurrent"
:build-receipt-filename="receptionConfig.buildReceiptFilename"
/>
</template>
<script setup lang="ts">
import {useReceptionStore} from '~/stores/reception'
import {storeToRefs} from 'pinia'
import {RECEPTION_STEP_LABELS} from '~/constants/steps'
import {RECEPTION_TYPE_CODES} from "~/utils/constants";
const route = useRoute()
const router = useRouter()
import { useReceptionStore } from '~/stores/reception'
import { storeToRefs } from 'pinia'
import { useWorkflowSteps } from '~/composables/useWorkflowSteps'
import { receptionConfig } from '~/config/reception.config'
import { getWeight } from '~/services/reception'
import { RECEPTION_TYPE_CODES } from '~/utils/constants'
const receptionStore = useReceptionStore()
const {current: storeReception} = storeToRefs(receptionStore)
const { current: storeReception } = storeToRefs(receptionStore)
const resolveReceptionId = (param: unknown) => {
const idStr = Array.isArray(param) ? param[0] : param
if (!idStr) {
return null
}
const id = Number(idStr)
return Number.isFinite(id) ? id : null
}
const { stepLabels, saveAndHold, handleStepSelect } = useWorkflowSteps(receptionConfig, receptionStore)
// Init route watcher
const route = useRoute()
watch(
() => route.params.id,
async (param) => {
const id = resolveReceptionId(param)
if (id === null) {
const idStr = Array.isArray(param) ? param[0] : param
if (!idStr) {
receptionStore.clearCurrent()
return
}
await receptionStore.loadReception(id)
const id = Number(idStr)
if (Number.isFinite(id)) {
await receptionStore.loadReception(id)
}
},
{immediate: true}
{ immediate: true }
)
const saveAndHold = async () => {
if (!receptionStore.current) {
await router.push('/')
return
}
await receptionStore.updateReception(receptionStore.current.id, {
currentStep: receptionStore.current.currentStep,
licensePlate: receptionStore.current.licensePlate,
receptionDate: receptionStore.current.receptionDate
})
await router.push('/')
}
const handleStepSelect = async (step: number) => {
if (!receptionStore.current) {
return
}
if (step === receptionStore.current.currentStep) {
return
}
await receptionStore.updateReception(receptionStore.current.id, {
currentStep: step
})
await receptionStore.loadReception(receptionStore.current.id)
}
</script>
+15 -2
View File
@@ -8,7 +8,7 @@
<div class="mt-6 border border-slate-200 mb-16 ">
<div class="grid grid-cols-6 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Numéro</div>
<div>Date</div>
<div>Date et heure</div>
<div>Fournisseur</div>
<div>Adresse</div>
<div>Type réception</div>
@@ -23,7 +23,7 @@
@click="goToReception(reception.id)"
>
<div>{{ reception.identificationNumber}}</div>
<div>{{ reception.receptionDate}}</div>
<div>{{ formatDate(reception.receptionDate) }}</div>
<div>{{ reception.supplier?.name }}</div>
<div>{{ reception.address?.fullAddress }}</div>
<div>{{ reception.receptionType?.label }}</div>
@@ -41,6 +41,19 @@ import type {ShipmentData} from "~/services/dto/shipment-data";
const receptionList = ref<ReceptionData[]>()
const router = useRouter()
const formatDate = (date: string | null) => {
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',
hour: '2-digit',
minute: '2-digit'
})
}
const formatWeighing = (reception: ReceptionData) => {
const gross = reception.weights?.find((weight) => weight.type === 'gross')?.weight
const tare = reception.weights?.find((weight) => weight.type === 'tare')?.weight
+100 -10
View File
@@ -1,13 +1,15 @@
<template>
<form @submit.prevent="validate">
<form :class="{ submitted }" @submit.prevent="validate">
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-[60px]">
<div class="flex items-center justify-between gap-10 relative">
<div class="flex flex-row absolute -left-[60px] justify-between">
<Icon @click="router.push('/reception/finish-reception')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
</div>
<h1 class="font-bold text-4xl col-start-1 row-start-1 text-primary-500 uppercase">Réception {{ form.identificationNumber }}</h1>
<Icon @click="printReceipt" name="mdi:printer-outline" size="44" class="cursor-pointer text-primary-500"/>
<div class="bg-primary-500 p-1 rounded-md flex items-center" title="Imprimer" @click="printReceipt">
<Icon name="mdi:printer-outline" size="32" class="cursor-pointer text-white"/>
</div>
</div>
<!-- Nom de l'utilisateur -->
<UiSelect
@@ -21,6 +23,7 @@
}))"
:loading="isLoadingUsers"
wrapper-class="col-start-1 row-start-2"
required
/>
<!-- Date de réception -->
<UiDateInput
@@ -29,6 +32,7 @@
v-model="form.receptionDate"
label="Date de réception"
wrapper-class="col-start-1 row-start-3"
required
/>
<!-- type de reception -->
<UiSelect
@@ -42,6 +46,7 @@
}))"
:loading="isLoadingSuppliers"
wrapper-class="col-start-1 row-start-4"
required
/>
<!-- Fournisseur -->
<UiSelect
@@ -55,6 +60,7 @@
}))"
:loading="isLoadingSuppliers"
wrapper-class="col-start-1 row-start-5"
required
/>
<!-- Adresse fournisseur -->
<UiSelect
@@ -67,6 +73,7 @@
}))"
:disabled="(isLoadingSuppliers || supplierAddresses.length === 0) && !auth.isAdmin"
wrapper-class="col-start-2 row-start-1"
required
/>
<!-- Camion -->
<UiSelect
@@ -80,6 +87,7 @@
}))"
:loading="isLoadingTrucks"
wrapper-class="col-start-2 row-start-2"
required
/>
<!-- Transporteur -->
<UiSelect
@@ -94,6 +102,7 @@
:loading="isLoadingCarriers"
select-class="h-[34px]"
wrapper-class="col-start-2 row-start-3"
required
/>
<!-- Chauffeur (LIOT) -->
<UiSelect
@@ -108,6 +117,7 @@
:loading="isLoadingDrivers"
wrapper-class="col-start-2 row-start-5"
v-if="isLiotCarrier"
required
/>
<!-- Plaque d'immatriculation -->
<div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
@@ -115,6 +125,7 @@
:disabled="!auth.isAdmin"
v-model="form.licensePlate"
v-model:allowAny="allowAnyLicensePlate"
required
/>
</div>
<!-- Immatriculation (LIOT) -->
@@ -130,27 +141,37 @@
:loading="isLoadingVehicles"
:disabled="(isLoadingVehicles || filteredVehicles.length === 0) && !auth.isAdmin"
wrapper-class="col-start-2 row-start-4"
required
/>
</div>
<div v-if="formIsLoading">
<div class="flex justify-evenly gap-y-8 gap-x-41 mb-10 border-b border-primary-500/60">
<h1
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer "
:class="activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
class="font-bold text-3xl uppercase px-12 col-start-1 row-start-1 cursor-pointer"
:class="[
activeTab === 'weights' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
hasGrossWeightError ? '!text-red-500 !border-red-500' : ''
]"
@click="activeTab = 'weights'"
>
pesée à plein
</h1>
<h1
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer "
:class="activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500 ' : 'text-primary-500/50'"
class="font-bold text-3xl uppercase col-start-1 row-start-1 px-12 cursor-pointer"
:class="[
activeTab === 'weightsEmpty' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
hasTareWeightError ? '!text-red-500 !border-red-500' : ''
]"
@click="activeTab = 'weightsEmpty'"
>
pesée à vide
</h1>
<h1
class="font-bold text-3xl uppercase px-12 col-start-2 row-start-1 cursor-pointer "
:class="activeTab === 'merchandise' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50'"
class="font-bold text-3xl uppercase px-12 col-start-2 row-start-1 cursor-pointer"
:class="[
activeTab === 'merchandise' ? 'border-b-[6px] border-primary-500 text-primary-500' : 'text-primary-500/50',
hasMerchandiseTabError ? '!text-red-500 !border-red-500' : ''
]"
@click="activeTab = 'merchandise'"
>
{{ isMerchandise ? "Marchandise" : "Bovins" }}
@@ -176,6 +197,9 @@
v-model="merchandiseForm"
:isAdmin="auth.isAdmin"
/>
<p v-if="activeTab === 'merchandise' && isMerchandise" class="text-red-500 text-sm mt-2" :class="showMerchandiseError ? '' : 'invisible'">
{{ merchandiseErrorMessage || '&nbsp;' }}
</p>
<update-bovin
v-if="activeTab === 'merchandise' && !isMerchandise"
@@ -183,12 +207,16 @@
v-model:otherQuantity="bovineOtherQuantity"
:isAdmin="auth.isAdmin"
/>
<p v-if="activeTab === 'merchandise' && !isMerchandise" class="text-red-500 text-sm mt-2" :class="showMerchandiseError ? '' : 'invisible'">
{{ merchandiseErrorMessage || '&nbsp;' }}
</p>
</div>
<div class="flex justify-center">
<UiButton
v-if="auth.isAdmin"
type="submit"
class="inline-flex mb-16 items-center justify-center text-xl text-white uppercase bg-primary-500 h-[50px] px-8 rounded hover:opacity-80 gap-2 justify-self-end"
@click="submitted = true"
>
Valider
</UiButton>
@@ -239,7 +267,9 @@ import { getVehicleList } from '~/services/vehicle'
import { createWeight, updateWeight } from '~/services/weight'
import { useAuthStore } from '~/stores/auth'
import { useReceptionStore } from '~/stores/reception'
import { RECEPTION_TYPE_CODES, SUPPLIER_CODE } from '~/utils/constants'
import { MERCHANDISE_TYPE_CODES, RECEPTION_TYPE_CODES, SUPPLIER_CODE } from '~/utils/constants'
import { getMerchandiseTypeList } from '~/services/merchandise-type'
import type { MerchandiseTypeData } from '~/services/dto/merchandise-type-data'
const router = useRouter()
const route = useRoute()
@@ -260,6 +290,17 @@ const merchandiseForm = ref<MerchandiseEntryData>({
selectedPelletBuildingIds: {}
})
const allowAnyLicensePlate = ref(false)
const submitted = ref(false)
const showMerchandiseError = ref(false)
const merchandiseErrorMessage = ref('')
const hasGrossWeightError = computed(() =>
submitted.value && (grossWeight.value.weight === null || grossWeight.value.weighedAt === null || grossWeight.value.dsd === null)
)
const hasTareWeightError = computed(() =>
submitted.value && (tareWeight.value.weight === null || tareWeight.value.weighedAt === null || tareWeight.value.dsd === null)
)
const hasMerchandiseTabError = computed(() => submitted.value && showMerchandiseError.value)
const isLoading = ref(false)
const users = ref<UserData[]>([])
const isLoadingUsers = ref(false)
@@ -277,6 +318,7 @@ const vehicles = ref<VehicleData[]>([])
const isLoadingVehicles = ref(false)
const formIsLoading = ref(false)
const isMerchandise = ref(false)
const merchandiseTypesList = ref<MerchandiseTypeData[]>([])
const isHydrating = ref(false)
const vehicleSyncLock = ref(false)
@@ -436,7 +478,7 @@ function hydrateFromReception(reception: ReceptionData | null) {
isHydrating.value = true
form.identificationNumber = reception?.identificationNumber ?? ''
form.licensePlate = reception?.licensePlate ?? ''
form.receptionDate = reception?.receptionDate ?? new Date().toISOString().slice(0, 10)
form.receptionDate = reception?.receptionDate?.slice(0, 10) ?? new Date().toISOString().slice(0, 10)
form.userId = reception?.user?.id
? String(reception.user.id)
: form.userId
@@ -776,6 +818,53 @@ async function validate() {
}
if (idReception) {
const hasInvalidWeights =
grossWeight.value.weight === null || grossWeight.value.weighedAt === null || grossWeight.value.dsd === null ||
tareWeight.value.weight === null || tareWeight.value.weighedAt === null || tareWeight.value.dsd === null
if (hasInvalidWeights) {
return
}
showMerchandiseError.value = false
merchandiseErrorMessage.value = ''
if (!isMerchandise.value && getTotalBovines() === 0) {
showMerchandiseError.value = true
merchandiseErrorMessage.value = 'Veuillez saisir au moins une race bovine.'
return
}
if (isMerchandise.value) {
const selectedType = merchandiseTypesList.value.find(
(t) => String(t.id) === merchandiseForm.value.merchandiseTypeId
)
const isAutresType = selectedType?.code === MERCHANDISE_TYPE_CODES.AUTRES
const isGranuleType = selectedType?.code === MERCHANDISE_TYPE_CODES.GRANULE
if (isAutresType && !merchandiseForm.value.merchandiseDetail.trim()) {
showMerchandiseError.value = true
merchandiseErrorMessage.value = 'Veuillez préciser le type de marchandise.'
return
}
if (!isAutresType && !isGranuleType && merchandiseForm.value.selectedBuildingIds.length === 0) {
showMerchandiseError.value = true
merchandiseErrorMessage.value = 'Veuillez sélectionner au moins un bâtiment.'
return
}
if (isGranuleType) {
const hasAny = Object.values(merchandiseForm.value.selectedPelletBuildingIds)
.some((ids) => ids.length > 0)
if (!hasAny) {
showMerchandiseError.value = true
merchandiseErrorMessage.value = 'Veuillez sélectionner au moins un bâtiment.'
return
}
}
}
await receptionStore.updateReception(idReception, {
...payload
})
@@ -820,6 +909,7 @@ async function validate() {
onMounted(async () => {
await loadTypes()
merchandiseTypesList.value = await getMerchandiseTypeList()
syncMerchandiseFlag()
formIsLoading.value = true
await loadUsers()
+51 -38
View File
@@ -1,48 +1,61 @@
<template>
<div class="flex items-center justify-between">
<div class="flex items-center gap-10">
<Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
<h1 class="text-3xl font-bold uppercase text-primary-500">listes des réceptions en attente</h1>
</div>
</div>
<div class="px-[86px]">
<div class="mt-6 border border-slate-200 mb-16">
<div class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Fournisseur</div>
<div>Adresse</div>
<div>Type réception</div>
<div>Transporteur</div>
<div>Immatriculation</div>
</div>
<div
v-for="reception in receptionList"
:key="reception.id"
class="grid grid-cols-5 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
role="button"
tabindex="0"
@click="goToReception(reception.id)"
@keydown.enter="goToReception(reception.id)"
>
<div>{{ reception.supplier?.name }}</div>
<div>{{ reception.address?.fullAddress }}</div>
<div>{{ reception.receptionType?.label }}</div>
<div>{{ reception.carrier?.name }}</div>
<div>{{ reception.licensePlate }}</div>
</div>
</div>
</div>
<WorkflowWaitingList
title="listes des réceptions en attente"
:columns="columns"
:items="receptionList ?? []"
route-prefix="/reception"
show-actions
>
<template #cell-receptionDate="{ item }">
{{ formatDate(item.receptionDate) }}
</template>
<template #actions="{ item }">
<Icon
name="mdi:delete-outline"
size="24"
class="cursor-pointer text-red-500 hover:text-red-700"
@click="confirmDelete(item)"
/>
</template>
</WorkflowWaitingList>
</template>
<script setup lang="ts">
import type {ReceptionData} from "~/services/dto/reception-data";
import {getReceptionList} from "~/services/reception";
import type { ReceptionData } from '~/services/dto/reception-data'
import { getReceptionList, deleteReception } from '~/services/reception'
const columns = [
{ key: 'receptionDate', label: 'Date et heure' },
{ key: 'supplier.name', label: 'Fournisseur' },
{ key: 'address.fullAddress', label: 'Adresse' },
{ key: 'receptionType.label', label: 'Type réception' },
{ key: 'carrier.name', label: 'Transporteur' },
{ key: 'licensePlate', label: 'Immatriculation' }
]
const receptionList = ref<ReceptionData[]>()
const router = useRouter()
const goToReception = (id: number) => {
router.push(`/reception/${id}`)
const formatDate = (date: string | null) => {
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',
hour: '2-digit',
minute: '2-digit'
})
}
const confirmDelete = async (reception: ReceptionData) => {
const confirmed = window.confirm(
`Êtes-vous sûr de vouloir supprimer la réception ${reception.identificationNumber ?? `#${reception.id}`} ? Toutes les données liées seront supprimées.`
)
if (!confirmed) return
await deleteReception(reception.id)
receptionList.value = receptionList.value?.filter(r => r.id !== reception.id)
}
onMounted(async () => {