Finalisation réception marchandise, ajout de composant UI et ajout de fixtures #7

Merged
tristan merged 10 commits from feat/finalisation-reception-marchandise into develop 2026-01-30 14:10:41 +00:00
8 changed files with 456 additions and 282 deletions
Showing only changes of commit bb8fbe4907 - Show all commits

27
.idea/workspace.xml generated
View File

@@ -6,15 +6,10 @@
<component name="ChangeListManager">
<list default="true" id="7c107abe-5995-4428-8429-b146aaca8386" name="Changes" comment="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/AGENTS.md" beforeDir="false" afterPath="$PROJECT_DIR$/AGENTS.md" 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-unloading.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-product-received.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/composables/usePdfPrinter.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/composables/usePdfPrinter.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/i18n/locales/fr.json" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/i18n/locales/fr.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/services/dto/reception-data.ts" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/services/dto/reception-data.ts" 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$/templates/reception_voucher.html.twig" beforeDir="false" afterPath="$PROJECT_DIR$/templates/reception_voucher.html.twig" 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-product-received.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/reception/reception-product-received.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/components/ui/license-plate-input.vue" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/components/ui/license-plate-input.vue" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -279,7 +274,7 @@
<workItem from="1769413136483" duration="58000" />
<workItem from="1769413279223" duration="40490000" />
<workItem from="1769612160652" duration="23952000" />
<workItem from="1769696465294" duration="4048000" />
<workItem from="1769696465294" duration="7587000" />
</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)">
<option name="closed" value="true" />
@@ -585,7 +580,15 @@
<option name="project" value="LOCAL" />
<updated>1769676223697</updated>
</task>
<option name="localTasksCounter" value="39" />
<task id="LOCAL-00039" summary="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception">
<option name="closed" value="true" />
<created>1769700808988</created>
<option name="number" value="00039" />
<option name="presentableId" value="LOCAL-00039" />
<option name="project" value="LOCAL" />
<updated>1769700808988</updated>
</task>
<option name="localTasksCounter" value="40" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -635,7 +638,6 @@
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="fix : correction de l'accès au swagger en mode dev qui n'était plus accessible" />
<MESSAGE value="feat : ajout de la conf pour le déploiement en recette" />
<MESSAGE value="fix : fix de la conf pour le déploiement en recette" />
<MESSAGE value="fix : migration apache vers nginx pour un déploiement plus simple" />
@@ -660,7 +662,8 @@
<MESSAGE value="feat : update du CHANGELOG.md" />
<MESSAGE value="feat : finalisation de l'étape 1 &quot;Réception&quot; (formulaire)" />
<MESSAGE value="feat : ajout du numéro identification des receptions et ajustement du bon de reception" />
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout du numéro identification des receptions et ajustement du bon de reception" />
<MESSAGE value="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception" />
<option name="LAST_COMMIT_MESSAGE" value="feat : ajout de la partie reception des marchandises (étape 3) et modification du bon de réception" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

View File

@@ -3,181 +3,96 @@
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
<h1 class="font-bold text-5xl uppercase col-start-1 row-start-1">Réception</h1>
<!-- Nom de l'utilisateur -->
<div class="flex flex-col col-start-1 row-start-2">
<label for="reception-user" class="font-bold uppercase text-xl mb-2">Nom de l'utilisateur</label>
<select
<UiSelect
id="reception-user"
v-model="form.userId"
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent"
:class="[
form.userId ? 'text-black' : 'text-neutral-400',
isLoadingUsers ? 'cursor-not-allowed' : 'cursor-pointer'
]"
:disabled="isLoadingUsers"
>
<option value="" disabled class="text-neutral-400">Sélectionner</option>
<option
v-for="user in users"
:key="user.id"
:value="String(user.id)"
class="text-black"
>
{{ user.username }}
</option>
</select>
</div>
label="Nom de l'utilisateur"
:options="users.map((user) => ({
value: String(user.id),
label: user.username
}))"
:loading="isLoadingUsers"
wrapper-class="col-start-1 row-start-2"
/>
<!-- Date de réception -->
<div class="flex flex-col col-start-1 row-start-3">
<label for="reception-date" class="font-bold uppercase text-xl mb-2">Date de réception</label>
<input
<UiDateInput
id="reception-date"
v-model="form.receptionDate"
type="date"
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase cursor-pointer bg-transparent appearance-none h-[34px]"
label="Date de réception"
wrapper-class="col-start-1 row-start-3"
/>
</div>
<!-- Type de réception -->
<div class="flex flex-col col-start-1 row-start-4">
<label for="reception-type" class="font-bold uppercase text-xl mb-2">Type de réception</label>
<select
<UiSelect
id="reception-type"
v-model="form.receptionTypeId"
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent cursor-pointer"
:class="form.receptionTypeId ? 'text-black' : 'text-neutral-400'"
>
<option value="" disabled class="text-neutral-400">Sélectionner</option>
<option
v-for="type in receptionTypes"
:key="type.id"
:value="String(type.id)"
class="text-black"
>
{{ type.label }}
</option>
</select>
</div>
label="Type de réception"
:options="receptionTypes.map((type) => ({
value: String(type.id),
label: type.label
}))"
wrapper-class="col-start-1 row-start-4"
/>
<!-- Fournisseur -->
<div class="flex flex-col col-start-1 row-start-5">
<label for="reception-supplier" class="font-bold uppercase text-xl mb-2">Fournisseur</label>
<select
<UiSelect
id="reception-supplier"
v-model="form.supplierId"
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent"
:class="[
form.supplierId ? 'text-black' : 'text-neutral-400',
isLoadingSuppliers ? 'cursor-not-allowed' : 'cursor-pointer'
]"
:disabled="isLoadingSuppliers"
>
<option value="" disabled class="text-neutral-400">Sélectionner</option>
<option
v-for="supplier in suppliers"
:key="supplier.id"
:value="String(supplier.id)"
class="text-black"
>
{{ supplier.name }}
</option>
</select>
</div>
label="Fournisseur"
:options="suppliers.map((supplier) => ({
value: String(supplier.id),
label: supplier.name
}))"
:loading="isLoadingSuppliers"
wrapper-class="col-start-1 row-start-5"
/>
<!-- Adresse fournisseur -->
<div class="flex flex-col col-start-2 row-start-1">
<label for="reception-address" class="font-bold uppercase text-xl mb-2">Adresse</label>
<select
<UiSelect
id="reception-address"
v-model="form.addressId"
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent"
:class="[
form.addressId ? 'text-black' : 'text-neutral-400',
isLoadingSuppliers || supplierAddresses.length === 0
? 'cursor-not-allowed'
: 'cursor-pointer'
]"
label="Adresse"
:options="supplierAddresses.map((address) => ({
value: String(address.id),
label: address.fullAddress
}))"
:disabled="isLoadingSuppliers || supplierAddresses.length === 0"
>
<option value="" disabled class="text-neutral-400">Sélectionner</option>
<option
v-for="address in supplierAddresses"
:key="address.id"
:value="String(address.id)"
class="text-black"
>
{{ address.fullAddress }}
</option>
</select>
</div>
wrapper-class="col-start-2 row-start-1"
/>
<!-- Camion -->
<div class="flex flex-col col-start-2 row-start-2">
<label for="reception-truck" class="font-bold uppercase text-xl mb-2">Camion</label>
<select
<UiSelect
id="reception-truck"
v-model="form.truckId"
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent"
:class="[
form.truckId ? 'text-black' : 'text-neutral-400',
isLoadingTrucks ? 'cursor-not-allowed' : 'cursor-pointer'
]"
:disabled="isLoadingTrucks"
>
<option value="" disabled class="text-neutral-400">Sélectionner</option>
<option
v-for="truck in trucks"
:key="truck.id"
:value="String(truck.id)"
class="text-black"
>
{{ truck.name }}
</option>
</select>
</div>
label="Camion"
:options="trucks.map((truck) => ({
value: String(truck.id),
label: truck.name
}))"
:loading="isLoadingTrucks"
wrapper-class="col-start-2 row-start-2"
/>
<!-- Transporteur -->
<div class="flex flex-col col-start-2 row-start-3">
<label for="reception-carrier" class="font-bold uppercase text-xl mb-2">Transporteur</label>
<select
<UiSelect
id="reception-carrier"
v-model="form.carrierId"
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent h-[34px]"
:class="[
form.carrierId ? 'text-black' : 'text-neutral-400',
isLoadingCarriers ? 'cursor-not-allowed' : 'cursor-pointer'
]"
:disabled="isLoadingCarriers"
>
<option value="" disabled class="text-neutral-400">Sélectionner</option>
<option
v-for="carrier in carriers"
:key="carrier.id"
:value="String(carrier.id)"
class="text-black"
>
{{ carrier.name }}
</option>
</select>
</div>
label="Transporteur"
:options="carriers.map((carrier) => ({
value: String(carrier.id),
label: carrier.name
}))"
:loading="isLoadingCarriers"
select-class="h-[34px]"
wrapper-class="col-start-2 row-start-3"
/>
<!-- Chauffeur (LIOT) -->
<div class="flex flex-col col-start-2 row-start-4">
<label for="reception-driver" class="font-bold uppercase text-xl mb-2">Nom du chauffeur si LIOT</label>
<select
<UiSelect
id="reception-driver"
v-model="form.driverId"
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent"
:class="[
form.driverId ? 'text-black' : 'text-neutral-400',
isLoadingDrivers ? 'cursor-not-allowed' : 'cursor-pointer'
]"
:disabled="isLoadingDrivers"
>
<option value="" disabled class="text-neutral-400">Sélectionner</option>
<option
v-for="driver in filteredDrivers"
:key="driver.id"
:value="String(driver.id)"
class="text-black"
>
{{ driver.name }}
</option>
</select>
</div>
label="Nom du chauffeur si LIOT"
:options="filteredDrivers.map((driver) => ({
value: String(driver.id),
label: driver.name
}))"
:loading="isLoadingDrivers"
wrapper-class="col-start-2 row-start-4"
/>
<!-- Plaque d'immatriculation -->
<div v-if="!isLiotCarrier" class="col-start-2 row-start-5">
<UiLicensePlateInput
@@ -186,29 +101,19 @@
/>
</div>
<!-- Immatriculation (LIOT) -->
<div v-if="isLiotCarrier" class="flex flex-col col-start-2 row-start-5">
<label for="reception-vehicle" class="font-bold uppercase text-xl mb-2">Immatriculation</label>
<select
<UiSelect
v-if="isLiotCarrier"
id="reception-vehicle"
v-model="form.vehicleId"
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent"
:class="[
form.vehicleId ? 'text-black' : 'text-neutral-400',
isLoadingVehicles ? 'cursor-not-allowed' : 'cursor-pointer'
]"
label="Immatriculation"
:options="filteredVehicles.map((vehicle) => ({
value: String(vehicle.id),
label: vehicle.plate
}))"
:loading="isLoadingVehicles"
:disabled="isLoadingVehicles || filteredVehicles.length === 0"
>
<option value="" disabled class="text-neutral-400">Sélectionner</option>
<option
v-for="vehicle in filteredVehicles"
:key="vehicle.id"
:value="String(vehicle.id)"
class="text-black"
>
{{ vehicle.plate }}
</option>
</select>
</div>
wrapper-class="col-start-2 row-start-5"
/>
</div>
<div class="flex justify-center">
<button

View File

@@ -5,39 +5,25 @@
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
class="flex flex-col gap-8 items-center w-full">
<h1 class="text-4xl uppercase font-bold">Sélectionner des marchandises réceptionnnées</h1>
<div class="flex flex-col w-[550px]">
<label for="merchandise-type" class="font-bold uppercase text-xl mb-2">Type de marchandises</label>
<select
<UiSelect
id="merchandise-type"
v-model="selectedMerchandiseTypeId"
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent cursor-pointer"
:class="selectedMerchandiseTypeId ? 'text-black' : 'text-neutral-400'"
>
<option value="" disabled class="text-neutral-400">Sélectionner</option>
<option
v-for="type in merchandiseTypes"
:key="type.id"
:value="String(type.id)"
class="text-black"
>
{{ type.label }}
</option>
</select>
</div>
label="Type de marchandises"
:options="merchandiseTypes.map((type) => ({ value: String(type.id), label: type.label }))"
wrapper-class="w-[550px]"
/>
<div
v-if="selectedMerchandiseTypeId && isAutres"
class="flex flex-col w-full max-w-[550px]"
>
<label for="merchandise-detail" class="font-bold uppercase text-xl mb-2">Préciser</label>
<input
<UiTextInput
id="merchandise-detail"
v-model="merchandiseDetail"
label="Préciser"
placeholder="Précisions complémentaires"
type="text"
maxlength="255"
class="border-b border-black text-xl pb-[6px] bg-transparent"
>
:maxlength="255"
/>
</div>
<div
@@ -48,16 +34,12 @@
v-for="building in buildings"
:key="building.id"
>
<label
class="flex items-center gap-2 text-xl"
>
<input
<UiCheckbox
v-model="selectedBuildingIds"
type="checkbox"
:value="String(building.id)"
>
<span>{{ building.label }}</span>
</label>
:label="building.label"
label-class="text-xl"
/>
</div>
</div>
@@ -68,18 +50,18 @@
<div class="grid grid-cols-1 gap-10 md:grid-cols-4">
<div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
<p class="font-bold uppercase">{{ type.label }}</p>
<label
<div
v-for="building in buildings"
:key="building.id"
class="flex items-center gap-2 text-lg"
>
<input
<UiCheckbox
v-model="selectedPelletBuildingIds[String(type.id)]"
type="checkbox"
:value="String(building.id)"
>
<span>{{ building.label }}</span>
</label>
:label="building.label"
label-class="text-lg"
/>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,76 @@
<template>
<div :class="wrapperClass">
<label
class="flex items-center gap-2"
:class="labelClass"
>
<input
type="checkbox"
:checked="checked"
:disabled="disabled"
:class="inputClass"
@change="onChange"
>
<span v-if="label">{{ label }}</span>
</label>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
type CheckboxValue = string | number
const props = withDefaults(
defineProps<{
modelValue: boolean | CheckboxValue[]
value?: CheckboxValue
label?: string
disabled?: boolean
wrapperClass?: string
labelClass?: string
inputClass?: string
}>(),
{
value: undefined,
label: '',
disabled: false,
wrapperClass: '',
labelClass: '',
inputClass: ''
}
)
const emit = defineEmits<{
(event: 'update:modelValue', value: boolean | CheckboxValue[]): void
}>()
const checked = computed(() => {
if (Array.isArray(props.modelValue)) {
if (props.value === undefined) {
return false
}
return props.modelValue.includes(props.value)
}
return Boolean(props.modelValue)
})
const onChange = (event: Event) => {
const target = event.target as HTMLInputElement
if (Array.isArray(props.modelValue)) {
if (props.value === undefined) {
emit('update:modelValue', props.modelValue)
return
}
const next = new Set(props.modelValue)
if (target.checked) {
next.add(props.value)
} else {
next.delete(props.value)
}
emit('update:modelValue', Array.from(next))
return
}
emit('update:modelValue', target.checked)
}
</script>

View File

@@ -0,0 +1,62 @@
<template>
<div :class="['flex flex-col', wrapperClass]">
<label
v-if="label"
:for="id"
class="font-bold uppercase text-xl mb-2"
:class="labelClass"
>
{{ label }}
</label>
<input
:id="id"
type="date"
:value="modelValue ?? ''"
:disabled="disabled"
v-bind="attrs"
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase bg-transparent appearance-none h-[34px]"
:class="[
isEmpty ? 'text-neutral-400' : 'text-black',
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
inputClass
]"
@input="onInput"
/>
</div>
</template>
<script setup lang="ts">
import { computed, useAttrs } from 'vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
id?: string
label?: string
modelValue: string | null | undefined
disabled?: boolean
wrapperClass?: string
labelClass?: string
inputClass?: string
}>(),
{
disabled: false,
wrapperClass: '',
labelClass: '',
inputClass: ''
}
)
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void
}>()
const attrs = useAttrs()
const isEmpty = computed(() => !props.modelValue)
const onInput = (event: Event) => {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
</script>

View File

@@ -0,0 +1,85 @@
<template>
<div :class="['flex flex-col', wrapperClass]">
<label
v-if="label"
:for="id"
class="font-bold uppercase text-xl mb-2"
:class="labelClass"
>
{{ label }}
</label>
<select
:id="id"
:value="modelValue ?? ''"
:disabled="disabled || loading"
v-bind="attrs"
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent"
:class="[
isEmpty ? 'text-neutral-400' : 'text-black',
disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer',
selectClass
]"
@change="onChange"
>
<option value="" disabled class="text-neutral-400">
{{ placeholderText }}
</option>
<option
v-for="option in options"
:key="option.value"
:value="option.value"
class="text-black"
>
{{ option.label }}
</option>
</select>
</div>
</template>
<script setup lang="ts">
import { computed, useAttrs } from 'vue'
type SelectOption = {
value: string | number
label: string
}
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
id?: string
label?: string
placeholder?: string
modelValue: string | number | null | undefined
options: SelectOption[]
disabled?: boolean
loading?: boolean
wrapperClass?: string
labelClass?: string
selectClass?: string
}>(),
{
placeholder: 'Sélectionner',
disabled: false,
loading: false,
wrapperClass: '',
labelClass: '',
selectClass: ''
}
)
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void
}>()
const attrs = useAttrs()
const isEmpty = computed(() => props.modelValue === '' || props.modelValue === null || props.modelValue === undefined)
const placeholderText = computed(() => props.placeholder || 'Sélectionner')
const onChange = (event: Event) => {
const target = event.target as HTMLSelectElement
emit('update:modelValue', target.value)
}
</script>

View File

@@ -0,0 +1,68 @@
<template>
<div :class="['flex flex-col', wrapperClass]">
<label
v-if="label"
:for="id"
class="font-bold uppercase text-xl mb-2"
:class="labelClass"
>
{{ label }}
</label>
<input
:id="id"
type="text"
:value="modelValue ?? ''"
:placeholder="placeholder"
:maxlength="maxlength"
:disabled="disabled"
v-bind="attrs"
class="border-b border-black text-xl pb-[6px] bg-transparent"
:class="[
isEmpty ? 'text-neutral-400' : 'text-black',
disabled ? 'cursor-not-allowed' : 'cursor-text',
inputClass
]"
@input="onInput"
>
</div>
</template>
<script setup lang="ts">
import { computed, useAttrs } from 'vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
id?: string
label?: string
modelValue: string | null | undefined
placeholder?: string
maxlength?: number | string
disabled?: boolean
wrapperClass?: string
labelClass?: string
inputClass?: string
}>(),
{
placeholder: '',
maxlength: undefined,
disabled: false,
wrapperClass: '',
labelClass: '',
inputClass: ''
}
)
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void
}>()
const attrs = useAttrs()
const isEmpty = computed(() => !props.modelValue)
const onInput = (event: Event) => {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
</script>

View File

@@ -12,16 +12,15 @@
class="border-b border-black flex-1 min-w-0 text-xl uppercase h-[30px]"
@input="handleInput"
/>
<label :for="checkboxId" class="ml-auto flex items-center gap-3 whitespace-nowrap text-sm">
<input
<UiCheckbox
:id="checkboxId"
:checked="allowAny"
type="checkbox"
class="h-4 w-4 accent-primary-500"
@change="toggleAllowAny"
:model-value="allowAny"
label="Autoriser un format libre"
wrapper-class="ml-auto"
label-class="gap-3 whitespace-nowrap text-sm"
input-class="h-4 w-4 accent-primary-500"
@update:modelValue="handleAllowAnyChange"
/>
Autoriser un format libre
</label>
</div>
</div>
</template>
@@ -80,13 +79,7 @@ const handleInput = (event: Event) => {
emit('update:modelValue', target.value)
}
const toggleAllowAny = (event: Event) => {
const target = event.target as HTMLInputElement | null
if (!target) {
return
}
const nextValue = target.checked
const handleAllowAnyChange = (nextValue: boolean) => {
emit('update:allowAny', nextValue)
if (!nextValue) {
emit('update:modelValue', props.modelValue)