feat : Ajout d'un composable pour la pesée qui sera réutilisable pour l'expédition, ajout de contrainte sur les entity de reception et weight pour plus de robustesse et correction de la class active des liens dans la nav

This commit is contained in:
2026-01-13 16:05:49 +01:00
parent 6dab1d789a
commit 46af62483f
6 changed files with 157 additions and 80 deletions

29
.idea/workspace.xml generated
View File

@@ -4,23 +4,13 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)"> <list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception">
<change afterPath="$PROJECT_DIR$/frontend/components/reception/reception-unloading.vue" afterDir="false" />
<change afterPath="$PROJECT_DIR$/frontend/components/ui/loading-dots.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" /> <change beforePath="$PROJECT_DIR$/config/reference.php" beforeDir="false" afterPath="$PROJECT_DIR$/config/reference.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-form.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-weight.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package-lock.json" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/layouts/default.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/layouts/default.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" afterDir="false" /> <change beforePath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/pages/reception/[[id]].vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/services/dto/weight-data.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/dto/weight-data.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/services/reception.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/reception.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Dto/PontBasculeReading.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Dto/PontBasculeReading.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Entity/Reception.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Reception.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/Entity/Weight.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Weight.php" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/Entity/Weight.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/Entity/Weight.php" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/State/ReceptionWeighingProvider.php" beforeDir="false" afterPath="$PROJECT_DIR$/src/State/ReceptionWeighingProvider.php" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -255,7 +245,7 @@
<updated>1767956826164</updated> <updated>1767956826164</updated>
<workItem from="1767956827666" duration="7866000" /> <workItem from="1767956827666" duration="7866000" />
<workItem from="1768201706520" duration="13383000" /> <workItem from="1768201706520" duration="13383000" />
<workItem from="1768287908317" duration="20308000" /> <workItem from="1768287908317" duration="21021000" />
</task> </task>
<task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)"> <task id="LOCAL-00001" summary="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -265,7 +255,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1768237763998</updated> <updated>1768237763998</updated>
</task> </task>
<option name="localTasksCounter" value="2" /> <task id="LOCAL-00002" summary="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception">
<option name="closed" value="true" />
<created>1768316052474</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1768316052474</updated>
</task>
<option name="localTasksCounter" value="3" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -286,7 +284,8 @@
<MESSAGE value="Feat : (WIP) Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions" /> <MESSAGE value="Feat : (WIP) Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions" />
<MESSAGE value="Feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)" /> <MESSAGE value="Feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)" />
<MESSAGE value="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)" /> <MESSAGE value="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)" />
<option name="LAST_COMMIT_MESSAGE" value="feat : Ajout de pinia, création de la table weight et reception mise en place du système de step pour les receptions (WIP)" /> <MESSAGE value="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception" />
<option name="LAST_COMMIT_MESSAGE" value="feat : Ajout de zod, création d'un composant de chargement loading-dots.vue et finalisation du flow d'une reception" />
</component> </component>
<component name="XDebuggerManager"> <component name="XDebuggerManager">
<breakpoint-manager> <breakpoint-manager>

View File

@@ -25,12 +25,12 @@
<div class="flex justify-center mt-[54px]"> <div class="flex justify-center mt-[54px]">
<button <button
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]" class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
@click="getReceptionWeight" @click="fetchWeight"
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button> >{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
<button <button
v-if="displayWeight !== null" v-if="displayWeight !== null"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4" class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
@click="validateWeight" @click="saveWeight"
>Valider la pesée</button> >Valider la pesée</button>
</div> </div>
@@ -38,73 +38,30 @@
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { getWeight } from '~/services/reception' import { useWeighing } from '~/composables/useWeighing'
import type { WeightData } from '~/services/dto/weight-data'
import { createWeight, updateWeight } from '~/services/weight'
import { useReceptionStore } from '~/stores/reception' import { useReceptionStore } from '~/stores/reception'
const props = defineProps<{ const props = defineProps<{
mode: 'gross' | 'tare' mode: 'gross' | 'tare'
}>() }>()
const weightData = ref<WeightData | null>(null)
const localErrorMessage = ref<string | null>(null)
const receptionStore = useReceptionStore() const receptionStore = useReceptionStore()
const { current: storeReception, errorMessage: storeErrorMessage } = storeToRefs(receptionStore) const { current: storeReception, errorMessage: storeErrorMessage } = storeToRefs(receptionStore)
const errorMessage = computed(() => localErrorMessage.value ?? storeErrorMessage.value) const {
const currentWeightEntry = computed( displayWeight,
() => storeReception.value?.weights?.find((entry) => entry.type === props.mode) ?? null displayDsd,
) title,
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null) errorMessage,
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-') showLoadingBox,
const title = computed(() => (props.mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide')) fetchWeight,
const showLoadingBox = computed(() => displayWeight.value === null && !errorMessage.value) saveWeight
} = useWeighing({
async function getReceptionWeight() { mode: props.mode,
localErrorMessage.value = null reception: storeReception,
try { updateReception: receptionStore.updateReception,
weightData.value = await getWeight() loadReception: receptionStore.loadReception,
} catch (error) { storeError: storeErrorMessage
localErrorMessage.value = error?.data?.error ?? error?.message ?? 'Erreur inconnue.'
}
}
async function validateWeight() {
localErrorMessage.value = null
const existingEntry = currentWeightEntry.value
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
try {
if (existingEntry?.id) {
await updateWeight(existingEntry.id, {
type: props.mode,
dsd: baseDsd,
weight: baseWeight,
weighedAt: baseWeighedAt
}) })
} else {
await createWeight({
reception: `/receptions/${storeReception.value.id}`,
type: props.mode,
dsd: baseDsd,
weight: baseWeight,
weighedAt: baseWeighedAt
})
}
} catch (error) {
localErrorMessage.value = error?.data?.error ?? error?.message ?? 'Erreur inconnue.'
return
}
// @TODO Voir comment mettre en place la genération du bon, la validation de la reception et le dernier step // @TODO Voir comment mettre en place la genération du bon, la validation de la reception et le dernier step
const nextStep = storeReception.value.currentStep + 1
await receptionStore.updateReception(storeReception.value.id, {
currentStep: nextStep,
isValid: props.mode === 'tare' ? true : storeReception.value.isValid
})
await receptionStore.loadReception(storeReception.value.id)
}
</script> </script>

View File

@@ -0,0 +1,109 @@
import { computed, ref } from 'vue'
import type { Ref } from 'vue'
import type { ReceptionData, WeightEntryData } from '~/services/dto/reception-data'
import type { WeightData } from '~/services/dto/weight-data'
import { getWeight } from '~/services/reception'
import { createWeight, updateWeight } from '~/services/weight'
export type WeighingMode = 'gross' | 'tare'
type UseWeighingOptions = {
mode: WeighingMode
reception: Ref<ReceptionData | null>
updateReception: (id: number, payload: Partial<ReceptionData>) => Promise<ReceptionData | null>
loadReception?: (id: number) => Promise<ReceptionData | null>
storeError?: Ref<string | null>
}
export const useWeighing = ({
mode,
reception,
updateReception,
loadReception,
storeError
}: UseWeighingOptions) => {
const weightData = ref<WeightData | null>(null)
const localErrorMessage = ref<string | null>(null)
const currentWeightEntry = computed<WeightEntryData | null>(() => {
const weights = reception.value?.weights ?? []
return weights.find((entry) => entry.type === mode) ?? null
})
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
const title = computed(() => (mode === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
const errorMessage = computed(() => localErrorMessage.value ?? storeError?.value ?? null)
const showLoadingBox = computed(() => displayWeight.value === null && !errorMessage.value)
const fetchWeight = async () => {
localErrorMessage.value = null
try {
weightData.value = await getWeight()
} catch (error) {
localErrorMessage.value = error?.data?.error ?? error?.message ?? 'Erreur inconnue.'
}
}
const saveWeight = async () => {
localErrorMessage.value = null
if (!reception.value) {
localErrorMessage.value = 'Réception introuvable.'
return
}
const existingEntry = currentWeightEntry.value
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
if (baseWeight === null) {
localErrorMessage.value = 'Veuillez dabord peser.'
return
}
try {
if (existingEntry?.id) {
await updateWeight(existingEntry.id, {
type: mode,
dsd: baseDsd,
weight: baseWeight,
weighedAt: baseWeighedAt
})
} else {
await createWeight({
reception: `/receptions/${reception.value.id}`,
type: mode,
dsd: baseDsd,
weight: baseWeight,
weighedAt: baseWeighedAt
})
}
} catch (error) {
localErrorMessage.value = error?.data?.error ?? error?.message ?? 'Erreur inconnue.'
return
}
const nextStep = reception.value.currentStep + 1
await updateReception(reception.value.id, {
currentStep: nextStep,
isValid: mode === 'tare' ? true : reception.value.isValid
})
if (loadReception) {
await loadReception(reception.value.id)
}
}
return {
weightData,
currentWeightEntry,
displayWeight,
displayDsd,
title,
errorMessage,
showLoadingBox,
fetchWeight,
saveWeight
}
}

View File

@@ -23,7 +23,7 @@
<a <a
:href="href" :href="href"
@click="navigate" @click="navigate"
:class="isActive ? 'opacity-100' : 'opacity-50'" :class="isReceptionActive ? 'opacity-100' : 'opacity-50'"
> >
Reception Reception
</a> </a>
@@ -36,3 +36,8 @@
</main> </main>
</div> </div>
</template> </template>
<script setup lang="ts">
const route = useRoute()
const isReceptionActive = computed(() => route.path.startsWith('/reception'))
</script>

View File

@@ -3,7 +3,7 @@
<div v-else> <div v-else>
<div class="flex justify-between h-[52px] mb-[90px]"> <div class="flex justify-between h-[52px] mb-[90px]">
<p class="self-center">Indicateur détapes</p> <p class="self-center">Indicateur détapes</p>
<NuxtLink to="/" class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center">Mettre en pause</NuxtLink> <NuxtLink to="/" class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center">Mettre en attente</NuxtLink>
</div> </div>
<ReceptionForm v-if="storeReception?.currentStep === 0"/> <ReceptionForm v-if="storeReception?.currentStep === 0"/>
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/> <ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
@@ -20,7 +20,7 @@ const route = useRoute()
const router = useRouter() const router = useRouter()
const receptionStore = useReceptionStore() const receptionStore = useReceptionStore()
const { current: storeReception, isLoading, errorMessage } = storeToRefs(receptionStore) const { current: storeReception, errorMessage } = storeToRefs(receptionStore)
onMounted(async () => { onMounted(async () => {
const raw = route.params.id const raw = route.params.id

View File

@@ -11,9 +11,11 @@ use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Post;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Attribute\Context; use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity] #[ORM\Entity]
#[ORM\Table(name: 'weight')] #[ORM\Table(name: 'weight')]
@@ -31,6 +33,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
), ),
], ],
)] )]
#[UniqueEntity(fields: ['reception', 'type'], message: 'A weighing already exists for this type.')]
class Weight class Weight
{ {
#[ORM\Id] #[ORM\Id]
@@ -46,10 +49,12 @@ class Weight
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
#[Groups(['reception:read', 'weight:read', 'weight:write'])] #[Groups(['reception:read', 'weight:read', 'weight:write'])]
#[Assert\PositiveOrZero]
private ?int $dsd = null; private ?int $dsd = null;
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
#[Groups(['reception:read', 'weight:read', 'weight:write'])] #[Groups(['reception:read', 'weight:read', 'weight:write'])]
#[Assert\PositiveOrZero]
private ?int $weight = null; private ?int $weight = null;
#[ORM\Column(type: 'datetime_immutable', nullable: true)] #[ORM\Column(type: 'datetime_immutable', nullable: true)]
@@ -59,6 +64,8 @@ class Weight
#[ORM\Column(length: 10)] #[ORM\Column(length: 10)]
#[Groups(['reception:read', 'weight:read', 'weight:write'])] #[Groups(['reception:read', 'weight:read', 'weight:write'])]
#[Assert\NotBlank]
#[Assert\Choice(choices: ['gross', 'tare'])]
private string $type = 'gross'; private string $type = 'gross';
public function getId(): ?int public function getId(): ?int