Compare commits

..

8 Commits

Author SHA1 Message Date
gitea-actions
6eee0745a7 chore: bump version to v0.0.50
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m22s
2026-02-17 07:20:06 +00:00
845f94db8c fix : correction du role pour la récupération de la liste des supplier
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
2026-02-17 08:19:56 +01:00
gitea-actions
86c0e74074 chore: bump version to v0.0.49
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m15s
2026-02-16 15:26:13 +00:00
be29daf4d1 fix : corrections de tous les retours + modification de la seed et fixtures
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
2026-02-16 16:26:00 +01:00
gitea-actions
08e7c1508c chore: bump version to v0.0.48
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m12s
2026-02-16 14:32:35 +00:00
358da6a8ad Navbar (!28)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
|  | Layout-Admin |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

Reviewed-on: #28
Reviewed-by: Autin <tristan@yuno.malio.fr>
Co-authored-by: Matteo <matteo@yuno.malio.fr>
Co-committed-by: Matteo <matteo@yuno.malio.fr>
2026-02-16 14:32:31 +00:00
gitea-actions
67428186f6 chore: bump version to v0.0.47
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m12s
2026-02-13 16:07:27 +00:00
09d108a1d5 fix : corrections de tous les retours
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
2026-02-13 17:07:15 +01:00
57 changed files with 725 additions and 497 deletions

View File

@@ -46,7 +46,8 @@ Ajouter dans le fichier .env du frontend
* [#324] Creation page admin listing clients * [#324] Creation page admin listing clients
* [#326] Admin modification creation client * [#326] Admin modification creation client
* [#325] Correction diverses * [#325] Correction diverses
* [#319] Réflexion sur des loaders de type skeleton * fix layout admin
### Changed ### Changed
### Fixed ### Fixed

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.0.46' app.version: '0.0.50'

View File

@@ -2,9 +2,9 @@
<template> <template>
<NuxtLink :to="link"> <NuxtLink :to="link">
<div class="w-[324px] h-[228px] border border-black rounded-md p-6 flex flex-col justify-between"> <div class="w-[300px] h-[216px] border border-black rounded-lg p-6 flex flex-col justify-between gap-4">
<div class="flex justify-between"> <div class="flex justify-between">
<div class="rounded-full w-[80px] h-[80px] bg-neutral-400 flex justify-center items-center"> <div class="rounded-full w-[80px] h-[80px] bg-[#D9D9D9] flex justify-center items-center">
<Icon :name="iconName" style="color: black" size="44" /> <Icon :name="iconName" style="color: black" size="44" />
</div> </div>
<div> <div>
@@ -12,12 +12,12 @@
</div> </div>
</div> </div>
<div class="uppercase font-bold"> <div class="uppercase font-bold">
<p class="text-3xl"> {{ label }} </p> <p class="text-3xl text-primary-500">
<slot name="label">{{ label }}</slot>
</p>
</div> </div>
</div> </div>
</NuxtLink> </NuxtLink>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -27,4 +27,3 @@ const props = defineProps<{
label: string label: string
}>() }>()
</script> </script>

View File

@@ -2,7 +2,7 @@
<div <div
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS" v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"
class="flex flex-col items-center gap-16"> class="flex flex-col items-center gap-16">
<h1 class="text-4xl uppercase font-bold">Sélection des marchandises réceptionnnées</h1> <h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des races réceptionnées</h1>
<div <div
class="flex flex-row gap-8 items-center"> class="flex flex-row gap-8 items-center">
<div <div
@@ -10,6 +10,7 @@
:key="type.id" :key="type.id"
class="mt-8 flex flex-row mb-2 gap-6"> class="mt-8 flex flex-row mb-2 gap-6">
<UiNumberInput <UiNumberInput
:id="type.id"
:label="type.label" :label="type.label"
:code="type.code" :code="type.code"
v-model="bovineQuantities[String(type.id)]" v-model="bovineQuantities[String(type.id)]"
@@ -77,14 +78,14 @@ onMounted(async () => {
}) })
watch( watch(
() => receptionId.value, [() => receptionId.value, () => bovineType.value],
async (id) => { async ([id, types]) => {
if (!id || !receptionIri.value) { if (!id || !receptionIri.value || types.length === 0) {
return return
} }
const selectionMap: Record<string, number | null> = {} const selectionMap: Record<string, number | null> = {}
for (const type of bovineType.value) { for (const type of types) {
selectionMap[String(type.id)] = 0 selectionMap[String(type.id)] = 0
} }

View File

@@ -1,8 +1,7 @@
<template> <template>
<skeletonForm v-if="isPageLoading"/> <form @submit.prevent="validate">
<form v-else @submit.prevent="validate">
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16"> <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> <h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Réception</h1>
<!-- Nom de l'utilisateur --> <!-- Nom de l'utilisateur -->
<UiSelect <UiSelect
id="reception-user" id="reception-user"
@@ -82,20 +81,8 @@
select-class="h-[34px]" select-class="h-[34px]"
wrapper-class="col-start-2 row-start-3" wrapper-class="col-start-2 row-start-3"
/> />
<!-- Chauffeur (LIOT) -->
<UiSelect
id="reception-driver"
v-model="form.driverId"
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 --> <!-- Plaque d'immatriculation -->
<div v-if="!isLiotCarrier" class="col-start-2 row-start-5"> <div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
<UiLicensePlateInput <UiLicensePlateInput
v-model="form.licensePlate" v-model="form.licensePlate"
v-model:allowAny="allowAnyLicensePlate" v-model:allowAny="allowAnyLicensePlate"
@@ -113,15 +100,28 @@
}))" }))"
:loading="isLoadingVehicles" :loading="isLoadingVehicles"
:disabled="isLoadingVehicles || filteredVehicles.length === 0" :disabled="isLoadingVehicles || filteredVehicles.length === 0"
wrapper-class="col-start-2 row-start-4 h-[64px]"
/>
<!-- Chauffeur (LIOT) -->
<UiSelect
id="reception-driver"
v-model="form.driverId"
label="Nom du chauffeur si LIOT"
:options="filteredDrivers.map((driver) => ({
value: String(driver.id),
label: driver.name
}))"
:loading="isLoadingDrivers"
v-if="isLiotCarrier"
wrapper-class="col-start-2 row-start-5" wrapper-class="col-start-2 row-start-5"
/> />
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<button <UiButton
type="submit" type="submit"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end" class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
>Valider >Valider
</button> </UiButton>
</div> </div>
</form> </form>
@@ -148,7 +148,6 @@ import {RECEPTION_TYPE_CODES, SUPPLIER_CODE} from "~/utils/constants";
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine"; import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
import type {ReceptionFormData} from "~/services/dto/reception-data"; import type {ReceptionFormData} from "~/services/dto/reception-data";
const isPageLoading = ref(true)
const router = useRouter() const router = useRouter()
const receptionStore = useReceptionStore() const receptionStore = useReceptionStore()
const form = reactive<ReceptionFormData>({ const form = reactive<ReceptionFormData>({
@@ -331,17 +330,15 @@ const setDefaultUser = () => {
// On récupère toutes les données des selects au chargement du composant // On récupère toutes les données des selects au chargement du composant
onMounted(async () => { onMounted(async () => {
receptionTypes.value = await getReceptionTypeList() receptionTypes.value = await getReceptionTypeList()
await loadUsers() await loadUsers()
await loadSuppliers() await loadSuppliers()
await loadTrucks() await loadTrucks()
await loadCarriers() await loadCarriers()
await loadDrivers() await loadDrivers()
await loadVehicles() await loadVehicles()
await authStore.ensureSession() await authStore.ensureSession()
setDefaultUser() setDefaultUser()
//isPageLoading.value = false
}) })
// Ajuste driver/vehicle quand le transporteur change (logique LIOT) // Ajuste driver/vehicle quand le transporteur change (logique LIOT)

View File

@@ -3,7 +3,7 @@
<div <div
v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES" v-if="receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"
class="flex flex-col gap-16 items-center w-full"> class="flex flex-col gap-16 items-center w-full">
<h1 class="text-4xl uppercase font-bold">Sélection des marchandises réceptionnnées</h1> <h1 class="text-4xl uppercase font-bold text-primary-500">Sélection des marchandises réceptionnnées</h1>
<UiSelect <UiSelect
id="merchandise-type" id="merchandise-type"
v-model="selectedMerchandiseTypeId" v-model="selectedMerchandiseTypeId"
@@ -26,7 +26,7 @@
<div <div
v-if="selectedMerchandiseTypeId && !isGranule" v-if="selectedMerchandiseTypeId && !isGranule"
class="flex gap-4 w-[550px] justify-evenly" class="flex gap-4 w-[550px] justify-between"
> >
<div <div
v-for="building in buildings" v-for="building in buildings"
@@ -47,17 +47,17 @@
> >
<div class="grid grid-cols-1 gap-10 md:grid-cols-4"> <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"> <div v-for="type in pelletTypes" :key="type.id" class="flex flex-col gap-4">
<p class="font-bold uppercase">{{ type.label }}</p> <p class="font-bold uppercase text-primary-500">{{ type.label }}</p>
<div <div
v-for="building in buildings" v-for="building in buildings"
:key="building.id" :key="building.id"
class="flex items-center gap-2 text-lg" class="flex items-center gap-2 text-lg pl-[2px]"
> >
<UiCheckbox <UiCheckbox
v-model="selectedPelletBuildingIds[String(type.id)]" v-model="selectedPelletBuildingIds[String(type.id)]"
:value="String(building.id)" :value="String(building.id)"
:label="building.label" :label="building.label"
label-class="text-lg" label-class="text-xl"
/> />
</div> </div>
</div> </div>

View File

@@ -1,9 +1,9 @@
<template> <template>
<div class="flex justify-center"> <div class="flex justify-center">
<div class="flex flex-col items-center w-[660px]"> <div class="flex flex-col items-center w-[660px]">
<h1 class="font-bold text-5xl uppercase">{{ title }}</h1> <h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette--> <!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p> <p class="text-primary-500 uppercase text-2xl text-primary-500 mt-2">Pont-bascule connecté</p>
<div <div
v-if="showLoadingBox" v-if="showLoadingBox"
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]"> class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
@@ -11,27 +11,27 @@
</div> </div>
<div v-else-if="displayWeight !== null" class="w-full"> <div v-else-if="displayWeight !== null" class="w-full">
<div <div
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl"> class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl text-primary-500">
{{ displayWeight }} kg {{ displayWeight }} kg
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-center mt-[54px]"> <div class="flex justify-center mt-[54px]">
<button <UiButton
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="fetchWeight" @click="fetchWeight"
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button> >{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
<button <UiButton
v-if="displayWeight !== null && !showGenerateReceipt" v-if="displayWeight !== null && !showGenerateReceipt"
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="saveWeight" @click="saveWeight"
>Valider</button> >Valider la pesée</UiButton>
<button <UiButton
v-if="showGenerateReceipt" v-if="showGenerateReceipt"
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="printReceipt" @click="printReceipt"
>Générer le bon</button> >Générer le bon</UiButton>
</div> </div>
</template> </template>

View File

@@ -27,12 +27,12 @@
/> />
</div> </div>
</div> </div>
<button <UiButton
type="submit" type="submit"
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]"
:disabled="!auth.isAdmin" :disabled="!auth.isAdmin"
>Valider >Valider
</button> </UiButton>
</div> </div>
</form> </form>
</template> </template>
@@ -83,14 +83,14 @@ onMounted(async () => {
}) })
watch( watch(
() => receptionId, [() => receptionId, () => bovineType.value],
async (id) => { async ([id, types]) => {
if (!id || !receptionIri.value) { if (!id || !receptionIri.value || types.length === 0) {
return return
} }
const selectionMap: Record<string, number | null> = {} const selectionMap: Record<string, number | null> = {}
for (const type of bovineType.value) { for (const type of types) {
selectionMap[String(type.id)] = 0 selectionMap[String(type.id)] = 0
} }
@@ -105,7 +105,7 @@ watch(
} }
Object.assign(bovineQuantities, selectionMap) Object.assign(bovineQuantities, selectionMap)
const existingOther = await reception.bovineDetail const existingOther = reception.bovineDetail
const parsedOther = const parsedOther =
typeof existingOther === 'string' && existingOther.trim() !== '' typeof existingOther === 'string' && existingOther.trim() !== ''
? Number(existingOther) ? Number(existingOther)

View File

@@ -67,13 +67,13 @@
</div> </div>
</div> </div>
</div> </div>
<button <UiButton
v-if="auth.isAdmin" v-if="auth.isAdmin"
type="submit" type="submit"
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]"
:disabled="!auth.isAdmin" :disabled="!auth.isAdmin"
>Valider >Valider
</button> </UiButton>
</div> </div>
</form> </form>
</template> </template>

View File

@@ -1,9 +1,10 @@
<template> <template>
<form @submit.prevent="validate"> <form @submit.prevent="validate">
<div class="grid grid-cols-2 gap-x-40 gap-y-8 mb-8"> <div class="grid grid-cols-2 gap-x-40 gap-y-8 mb-8">
<UiTextInput <UiNumberInput
label="Dsd" label="Dsd"
class="col-start-2" class="col-start-2"
labelClass="font-bold uppercase"
v-model="sharedWeightMeta.dsd" v-model="sharedWeightMeta.dsd"
:disabled="!auth.isAdmin" :disabled="!auth.isAdmin"
/> />
@@ -19,21 +20,24 @@
:key="weight.type" :key="weight.type"
:label="getWeightLabel(weight.type)" :label="getWeightLabel(weight.type)"
labelClass="font-bold uppercase text-xl" labelClass="font-bold uppercase text-xl"
inputClass="w-24"
v-model="weight.weight" v-model="weight.weight"
:wrapper-class="weight.type === 'tare' ? 'col-start-1 row-start-1' : 'col-start-2 row-start-1'" :wrapper-class="weight.type === 'tare' ? 'col-start-1 row-start-1' : 'col-start-2 row-start-1'"
:disabled="!auth.isAdmin" :disabled="!auth.isAdmin"
:min="0"
:max="48000"
/> />
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<button <UiButton
v-if="auth.isAdmin" v-if="auth.isAdmin"
type="submit" type="submit"
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]"
> >
Valider Valider
</button> </UiButton>
</div> </div>
</form> </form>

View File

@@ -1,8 +1,7 @@
<template> <template>
<skeletonForm v-if="isPageLoading"/> <form @submit.prevent="validate">
<form v-else @submit.prevent="validate">
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16"> <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">Expédition</h1> <h1 class="font-bold text-5xl uppercase col-start-1 row-start-1 text-primary-500">Expédition</h1>
<!-- Nom de l'utilisateur --> <!-- Nom de l'utilisateur -->
<UiSelect <UiSelect
id="shipment-user" id="shipment-user"
@@ -23,24 +22,27 @@
wrapper-class="col-start-1 row-start-3" wrapper-class="col-start-1 row-start-3"
/> />
<!-- Type d'expédition --> <!-- Type d'expédition -->
<div class="col-start-1 row-start-4"> <div class="col-start-1 row-start-4 h-[64px]">
<label class="font-bold uppercase text-xl mb-2"> <div class="flex items-end gap-8 justify-between">
Type d'expédition <UiRadioGroup
</label> id="shipment-type"
<div class="grid grid-cols-2 gap-x-8"> name="shipment-type"
<div label="Type d'expédition bovine"
v-for="type in bovineShipment" v-model="selectedShipmentTypeId"
:key="type.id" :options="bovineShipment.map((type) => ({
class="mt-2 flex flex-row gap-6" value: String(type.id),
> label: type.label
<UiNumberInput }))"
:label="type.label" />
v-model="bovineQuantities[String(type.id)]" <UiNumberInput
:placeholder="0" id="shipment-type-quantity"
:min="0" label="Quantité"
:max="10" v-model="shipmentQuantity"
/> :placeholder="0"
</div> :min="0"
:max="1200"
:disabled="!selectedShipmentTypeId"
/>
</div> </div>
</div> </div>
<!-- Client --> <!-- Client -->
@@ -87,20 +89,8 @@
}))" }))"
wrapper-class="col-start-2 row-start-3" wrapper-class="col-start-2 row-start-3"
/> />
<!-- Chauffeur (LIOT) -->
<UiSelect
id="shipment-driver"
v-model="form.driverId"
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 (hors LIOT) --> <!-- Plaque d'immatriculation (hors LIOT) -->
<div v-if="!isLiotCarrier" class="col-start-2 row-start-5"> <div v-if="!isLiotCarrier" class="col-start-2 row-start-4">
<UiLicensePlateInput <UiLicensePlateInput
v-model="form.licencePlate" v-model="form.licencePlate"
v-model:allowAny="allowAnyLicensePlate" v-model:allowAny="allowAnyLicensePlate"
@@ -118,15 +108,28 @@
}))" }))"
:loading="isLoadingVehicles" :loading="isLoadingVehicles"
:disabled="isLoadingVehicles || filteredVehicles.length === 0" :disabled="isLoadingVehicles || filteredVehicles.length === 0"
wrapper-class="col-start-2 row-start-4"
/>
<!-- Chauffeur (LIOT) -->
<UiSelect
id="shipment-driver"
v-model="form.driverId"
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-5" wrapper-class="col-start-2 row-start-5"
v-if="isLiotCarrier"
/> />
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<button <UiButton
type="submit" type="submit"
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] justify-self-end"
>Valider >Valider
</button> </UiButton>
</div> </div>
</form> </form>
</template> </template>
@@ -149,12 +152,13 @@ import type {ShipmentFormData} from '~/services/dto/shipment-data'
import {SUPPLIER_CODE} from "~/utils/constants" import {SUPPLIER_CODE} from "~/utils/constants"
import {useAuthStore} from '~/stores/auth' import {useAuthStore} from '~/stores/auth'
import {useShipmentStore} from '~/stores/shipment' import {useShipmentStore} from '~/stores/shipment'
import {computed, reactive, ref, watch, onMounted} from 'vue' import { computed, reactive, ref, watch, onMounted } from 'vue'
import type {ShipmentTypeData} from "~/services/dto/shipment-type-data"; import type {ShipmentTypeData} from "~/services/dto/shipment-type-data";
import {getShipmentTypeList} from "~/services/shipment-type"; import {getShipmentTypeList} from "~/services/shipment-type";
import { import {
createShipmentBovine, createShipmentBovine,
deleteShipmentBovine, deleteShipmentBovine,
getBovinShipmentList,
updateShipmentBovine updateShipmentBovine
} from "~/services/bovin-shipment"; } from "~/services/bovin-shipment";
@@ -164,7 +168,7 @@ const trucks = ref<TruckData[]>([])
const carriers = ref<CarrierData[]>([]) const carriers = ref<CarrierData[]>([])
const drivers = ref<DriverData[]>([]) const drivers = ref<DriverData[]>([])
const vehicles = ref<VehicleData[]>([]) const vehicles = ref<VehicleData[]>([])
const isPageLoading = ref(true)
const isLoadingUsers = ref(false) const isLoadingUsers = ref(false)
const isLoadingShipmentTypes = ref(false) const isLoadingShipmentTypes = ref(false)
const isLoadingCustomers = ref(false) const isLoadingCustomers = ref(false)
@@ -177,8 +181,9 @@ const isLoadingDrivers = ref(false)
const authStore = useAuthStore() const authStore = useAuthStore()
const shipmentStore = useShipmentStore() const shipmentStore = useShipmentStore()
const router = useRouter() const router = useRouter()
const bovineQuantities = ref<Record<string, number | null>>({})
const bovineShipment = ref<ShipmentTypeData[]>([]) const bovineShipment = ref<ShipmentTypeData[]>([])
const selectedShipmentTypeId = ref('')
const shipmentQuantity = ref<number | null>(0)
// Transporteur sélectionné dans le formulaire // Transporteur sélectionné dans le formulaire
const selectedCarrier = computed(() => const selectedCarrier = computed(() =>
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
@@ -310,7 +315,6 @@ onMounted(async () => {
await loadDrivers() await loadDrivers()
await authStore.ensureSession() await authStore.ensureSession()
setDefaultUser() setDefaultUser()
isPageLoading.value = false
}) })
// Hydrate le formulaire depuis l'expédition en cours // Hydrate le formulaire depuis l'expédition en cours
watch( watch(
@@ -329,15 +333,21 @@ watch(
form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : '' form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : ''
form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : '' form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : ''
if (!shipment || !shipment.bovinShipments) { if (!shipment || !shipment.bovinShipments) {
bovineQuantities.value = {} selectedShipmentTypeId.value = ''
shipmentQuantity.value = 0
} else { } else {
const next: Record<string, number | null> = {} const selectedEntry = shipment.bovinShipments.find((entry) => {
for (const entry of shipment.bovinShipments) {
const typeId = entry.shipmentType?.id const typeId = entry.shipmentType?.id
if (!typeId) continue return Boolean(typeId) && Number(entry.nbBovinSend ?? 0) > 0
next[String(typeId)] = entry.nbBovinSend ?? null }) ?? shipment.bovinShipments.find((entry) => Boolean(entry.shipmentType?.id))
if (!selectedEntry?.shipmentType?.id) {
selectedShipmentTypeId.value = ''
shipmentQuantity.value = 0
} else {
selectedShipmentTypeId.value = String(selectedEntry.shipmentType.id)
shipmentQuantity.value = selectedEntry.nbBovinSend ?? 0
} }
bovineQuantities.value = next
} }
isHydrating.value = false isHydrating.value = false
}, },
@@ -465,16 +475,22 @@ watch(
} }
) )
const buildDesiredBovinShipments = () => { const buildDesiredBovinShipments = () => {
return bovineShipment.value const typeId = Number(selectedShipmentTypeId.value)
.map((type) => { if (!Number.isFinite(typeId)) {
const raw = bovineQuantities.value[String(type.id)] return []
const quantity = raw === null || raw === undefined ? 0 : Number(raw) }
return { const type = bovineShipment.value.find((entry) => entry.id === typeId)
type, if (!type) {
quantity: Number.isFinite(quantity) ? Math.max(0, Math.trunc(quantity)) : 0 return []
} }
}) const raw = shipmentQuantity.value
.filter((entry) => entry.quantity > 0) const quantity = raw === null || raw === undefined ? 0 : Number(raw)
const normalizedQuantity = Number.isFinite(quantity) ? Math.max(0, Math.trunc(quantity)) : 0
if (normalizedQuantity <= 0) {
return []
}
return [{type, quantity: normalizedQuantity}]
} }
const syncBovinShipments = async ( const syncBovinShipments = async (
shipmentId: number, shipmentId: number,

View File

@@ -0,0 +1,26 @@
<template>
<div class="flex flex-col items-center gap-[118px]">
<h1 class="font-bold text-5xl uppercase text-primary-500">Charment des bovins</h1>
<div
class="w-full flex flex-col items-center justify-center">
<UiLoadingDots />
</div>
<UiButton
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
@click="goNext"
>Peser</UiButton>
</div>
</template>
<script setup lang="ts">
import {useShipmentStore} from "~/stores/shipment";
const shipmentStore = useShipmentStore()
const goNext = async () => {
const nextStep = shipmentStore.current.currentStep + 1
await shipmentStore.updateShipment(shipmentStore.current.id, {
currentStep: nextStep
})
}
</script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="flex justify-center"> <div class="flex justify-center">
<div class="flex flex-col items-center w-[660px]"> <div class="flex flex-col items-center w-[660px]">
<h1 class="font-bold text-5xl uppercase">{{ title }}</h1> <h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
<!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette--> <!--@TODO Voir comment faire pour savoir si le pont-bascule et bien connecté + ajouter un icon comme sur la maquette-->
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p> <p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
<div <div
@@ -11,27 +11,27 @@
</div> </div>
<div v-else-if="displayWeight !== null" class="w-full"> <div v-else-if="displayWeight !== null" class="w-full">
<div <div
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl"> class="w-full flex flex-col items-center justify-center border border-primary-500 h-[90px] mt-12 mb-[25px] text-4xl text-primary-500">
{{ displayWeight }} kg {{ displayWeight }} kg
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-center mt-[54px]"> <div class="flex justify-center mt-[54px]">
<button <UiButton
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="fetchWeight" @click="fetchWeight"
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button> >{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
<button <UiButton
v-if="displayWeight !== null && !showGenerateReceipt" v-if="displayWeight !== null && !showGenerateReceipt"
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="saveWeight" @click="saveWeight"
>Valider la pesée</button> >Valider la pesée</UiButton>
<button <UiButton
v-if="showGenerateReceipt" v-if="showGenerateReceipt"
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="printReceipt" @click="printReceipt"
>Générer le bon</button> >Générer le bon</UiButton>
</div> </div>
</template> </template>

View File

@@ -1,17 +0,0 @@
<template>
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16">
<UiSkeletonBlock height="48px"/>
<div class="flex flex-col gap-2" v-for="i in 9">
<UiSkeletonBlock width="96px"/>
<UiSkeletonBlock width="100%" height="42px"/>
</div>
</div>
<div class="flex justify-center">
<UiSkeletonBlock
width="272px"
height="50px"
/>
</div>
</template>
<script setup lang="ts">
</script>

View File

@@ -1,13 +0,0 @@
<template>
<div class="ps-20">
<UiSkeletonBlock height="48px" class="my-4"/>
<div class="grid grid-cols-3 justify-evenly bg-slate-100 py-4">
<UiSkeletonBlock v-for="i in 3"/>
</div>
<div
class="grid grid-cols-3 gap-4 px-4 py-4 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
v-for="i in 3">
<UiSkeletonBlock v-for="i in 3"/>
</div>
</div>
</template>

View File

@@ -0,0 +1,39 @@
<template>
<component
:is="'button'"
:type="type"
:disabled="isDisabled"
class="inline-flex items-center justify-center rounded-md"
:class="[
isDisabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
buttonClass
]"
v-bind="attrs"
>
<slot v-if="!loading" />
<UiLoadingDots v-else />
</component>
</template>
<script setup lang="ts">
import {computed, useAttrs} from 'vue'
defineOptions({inheritAttrs: false})
const props = withDefaults(
defineProps<{
type?: 'button' | 'submit' | 'reset'
disabled?: boolean
loading?: boolean
buttonClass?: string
}>(),
{
disabled: false,
loading: false,
buttonClass: ''
}
)
const attrs = useAttrs()
const isDisabled = computed(() => props.disabled || props.loading)
</script>

View File

@@ -1,14 +1,14 @@
<template> <template>
<div :class="wrapperClass"> <div :class="wrapperClass">
<label <label
class="flex items-center gap-2" class="flex items-center gap-2 cursor-pointer text-primary-500"
:class="labelClass" :class="labelClass"
> >
<input <input
type="checkbox" type="checkbox"
:checked="checked" :checked="checked"
:disabled="disabled" :disabled="disabled"
:class="inputClass" :class="['cursor-pointer text-primary-500', inputClass]"
@change="onChange" @change="onChange"
> >
<span v-if="label">{{ label }}</span> <span v-if="label">{{ label }}</span>

View File

@@ -3,7 +3,7 @@
<label <label
v-if="label" v-if="label"
:for="id" :for="id"
class="font-bold uppercase text-xl mb-2" class="font-bold uppercase text-xl text-primary-500"
:class="labelClass" :class="labelClass"
> >
{{ label }} {{ label }}
@@ -14,7 +14,7 @@
:value="modelValue ?? ''" :value="modelValue ?? ''"
:disabled="disabled" :disabled="disabled"
v-bind="attrs" v-bind="attrs"
class="border-b border-black justify-self-start text-xl pb-[6px] uppercase bg-transparent appearance-none h-[34px]" class="border-b border-black justify-self-start text-xl text-primary-500 py-[6px] uppercase bg-transparent appearance-none h-[34px]"
:class="[ :class="[
isEmpty ? 'text-neutral-400' : 'text-black', isEmpty ? 'text-neutral-400' : 'text-black',
disabled ? 'cursor-not-allowed' : 'cursor-pointer', disabled ? 'cursor-not-allowed' : 'cursor-pointer',

View File

@@ -3,7 +3,7 @@
<label <label
v-if="label" v-if="label"
:for="id" :for="id"
class="text-xl flex items-center" class="text-xl flex items-center gap-2 text-primary-500"
:class="labelClass" :class="labelClass"
> >
<span <span
@@ -25,7 +25,7 @@
:step="step" :step="step"
:disabled="disabled" :disabled="disabled"
v-bind="attrs" v-bind="attrs"
class="border-b border-black text-xl bg-transparent w-16" class="border-b border-black text-xl bg-transparent w-16 text-primary-500"
:class="[ :class="[
isEmpty ? 'text-neutral-400' : 'text-black', isEmpty ? 'text-neutral-400' : 'text-black',
disabled ? 'cursor-not-allowed' : 'cursor-text', disabled ? 'cursor-not-allowed' : 'cursor-text',
@@ -74,14 +74,41 @@ const emit = defineEmits<{
const attrs = useAttrs() const attrs = useAttrs()
const isEmpty = computed(() => props.modelValue === null || props.modelValue === undefined || props.modelValue === '') const isEmpty = computed(() => props.modelValue === null || props.modelValue === undefined || props.modelValue === '')
const toNumberOrNull = (value: number | string | undefined) => {
if (value === undefined || value === '') {
return null
}
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
const onInput = (event: Event) => { const onInput = (event: Event) => {
const target = event.target as HTMLInputElement const target = event.target as HTMLInputElement
if (target.value === '') { if (target.value === '') {
emit('update:modelValue', null) emit('update:modelValue', null)
return return
} }
const numeric = Math.max(0, Number(target.value)) const parsed = Number(target.value)
emit('update:modelValue', Number.isNaN(numeric) ? null : numeric) if (!Number.isFinite(parsed)) {
emit('update:modelValue', null)
return
}
const min = toNumberOrNull(props.min)
const max = toNumberOrNull(props.max)
let numeric = parsed
if (min !== null) {
numeric = Math.max(min, numeric)
} else {
numeric = Math.max(0, numeric)
}
if (max !== null) {
numeric = Math.min(max, numeric)
}
target.value = String(numeric)
emit('update:modelValue', numeric)
} }
const onKeydown = (event: KeyboardEvent) => { const onKeydown = (event: KeyboardEvent) => {

View File

@@ -0,0 +1,93 @@
<template>
<div :class="['flex flex-col', wrapperClass]">
<label
v-if="label"
class="font-bold uppercase text-xl text-primary-500"
:class="labelClass"
>
{{ label }}
</label>
<div
role="radiogroup"
:aria-label="label || id || 'radio-group'"
:class="['flex items-center gap-6 mt-1', groupClass]"
>
<label
v-for="option in options"
:key="String(option.value)"
:for="`${id || 'radio'}-${option.value}`"
class="flex items-center gap-2 text-primary-500"
:class="itemClass"
>
<input
:id="`${id || 'radio'}-${option.value}`"
type="radio"
:name="name || id || 'radio-group'"
:value="String(option.value)"
:checked="String(modelValue ?? '') === String(option.value)"
:disabled="disabled"
v-bind="attrs"
class="h-4 w-4 border-slate-300 text-primary-500 focus:ring-primary-500"
:class="[
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
inputClass
]"
@change="onChange"
>
<span class="text-xl" :class="optionLabelClass">
{{ option.label }}
</span>
</label>
</div>
</div>
</template>
<script setup lang="ts">
import { useAttrs } from 'vue'
type RadioOption = {
value: string | number
label: string
}
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
id?: string
name?: string
label?: string
modelValue: string | number | null | undefined
options: RadioOption[]
disabled?: boolean
wrapperClass?: string
labelClass?: string
groupClass?: string
itemClass?: string
inputClass?: string
optionLabelClass?: string
}>(),
{
name: '',
label: '',
disabled: false,
wrapperClass: '',
labelClass: '',
groupClass: '',
itemClass: '',
inputClass: '',
optionLabelClass: ''
}
)
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void
}>()
const attrs = useAttrs()
const onChange = (event: Event) => {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
</script>

View File

@@ -3,7 +3,7 @@
<label <label
v-if="label" v-if="label"
:for="id" :for="id"
class="font-bold uppercase text-xl mb-2" class="font-bold uppercase text-xl text-primary-500"
:class="labelClass" :class="labelClass"
> >
{{ label }} {{ label }}
@@ -13,7 +13,7 @@
:value="modelValue ?? ''" :value="modelValue ?? ''"
:disabled="disabled || loading" :disabled="disabled || loading"
v-bind="attrs" v-bind="attrs"
class="border-b border-black justify-self-start text-xl pb-[6px] bg-transparent" class="border-b border-black justify-self-start text-xl text-primary-500 py-[6px] bg-transparent"
:class="[ :class="[
isEmpty ? 'text-neutral-400' : 'text-black', isEmpty ? 'text-neutral-400' : 'text-black',
disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer', disabled || loading ? 'cursor-not-allowed' : 'cursor-pointer',

View File

@@ -1,22 +0,0 @@
<template>
<div
:class="['animate-pulse', rounded, customClass]"
:style="{ width, height, background }"
/>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
width?: string
height?: string
rounded?: string
background?: string
customClass?: string
}>(), {
width: '50%',
height: '1rem',
rounded: 'rounded-md',
background: '#e5e7eb',
customClass: ''
})
</script>

View File

@@ -3,7 +3,7 @@
<label <label
v-if="label" v-if="label"
:for="id" :for="id"
class="font-bold uppercase text-xl mb-2" class="font-bold uppercase text-xl text-primary-500"
:class="labelClass" :class="labelClass"
> >
{{ label }} {{ label }}
@@ -16,7 +16,7 @@
:maxlength="maxlength" :maxlength="maxlength"
:disabled="disabled" :disabled="disabled"
v-bind="attrs" v-bind="attrs"
class="border-b border-black text-xl pb-[6px] bg-transparent" class="border-b border-black text-xl py-[6px] bg-transparent text-primary-500"
:class="[ :class="[
isEmpty ? 'text-neutral-400' : 'text-black', isEmpty ? 'text-neutral-400' : 'text-black',
disabled ? 'cursor-not-allowed' : 'cursor-text', disabled ? 'cursor-not-allowed' : 'cursor-text',

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col">
<label :for="inputId" class="font-bold uppercase text-xl mb-2">{{ label }}</label> <label :for="inputId" class="font-bold uppercase text-xl text-primary-500">{{ label }}</label>
<div class="flex items-end gap-8"> <div class="flex items-end gap-8">
<input <input
:id="inputId" :id="inputId"
@@ -9,7 +9,7 @@
type="text" type="text"
:maxlength="maxLength" :maxlength="maxLength"
:placeholder="placeholderText" :placeholder="placeholderText"
class="border-b border-black flex-1 min-w-0 text-xl uppercase h-[30px]" class="border-b border-black flex-1 min-w-0 text-xl text-primary-500 uppercase h-[36px] py-[6px]"
@input="handleInput" @input="handleInput"
/> />
<UiCheckbox <UiCheckbox

View File

@@ -4,7 +4,7 @@
<div <div
v-for="(label, index) in labels" v-for="(label, index) in labels"
:key="label" :key="label"
class="absolute top-0 whitespace-nowrap" class="absolute top-0 whitespace-nowrap text-primary-500"
:class="labelClass(index)" :class="labelClass(index)"
:style="positionStyle(index)" :style="positionStyle(index)"
> >

View File

@@ -1,9 +1,10 @@
export enum StepLabel { export enum StepLabel {
Reception = 'Réception', Reception = 'Réception',
GrossWeighing = 'Pesée à plein', GrossWeighing = 'Pesée à plein',
Selection = 'Sélection réceptionnées', Selection = 'Sélection réception',
TareWeighing = 'Pesée à vide', TareWeighing = 'Pesée à vide',
Shipment = 'Expédition', Shipment = 'Expédition',
ShipmentLoading = 'Chargement',
} }
export const RECEPTION_STEP_LABELS = [ export const RECEPTION_STEP_LABELS = [
@@ -16,5 +17,6 @@ export const RECEPTION_STEP_LABELS = [
export const SHIPMENT_STEP_LABELS = [ export const SHIPMENT_STEP_LABELS = [
StepLabel.Shipment, StepLabel.Shipment,
StepLabel.TareWeighing, StepLabel.TareWeighing,
StepLabel.ShipmentLoading,
StepLabel.GrossWeighing, StepLabel.GrossWeighing,
] ]

View File

@@ -1,106 +0,0 @@
<template>
<div class="min-h-screen text-neutral-900 grid grid-rows-[85px,1fr]">
<!-- HEADER -->
<header class="bg-primary-500 z-50 h-[85px]">
<div class="h-full w-full px-6 grid grid-cols-[auto,1fr,auto] items-center gap-8">
<NuxtLink to="/" class="grid place-items-center">
<span class="grid place-items-center bg-white text-xl font-bold uppercase text-primary-500 p-4">
LOGO
</span>
</NuxtLink>
<nav class="text-2xl font-bold uppercase text-white"></nav>
<NuxtLink
to="/"
class="text-xl font-bold uppercase text-white transition hover:opacity-80 justify-self-end"
>
Quitter le panel admin
</NuxtLink>
</div>
</header>
<div class="grid grid-cols-[16rem,1fr] h-[calc(100vh-85px)] min-h-0">
<aside class="bg-primary-500 text-white min-h-0 flex flex-col justify-between">
<div class="flex flex-col gap-4 p-4 font-bold text-xl">
<!-- Liste des liens à ajouter ci-dessous -->
<NuxtLink
to="/admin/dashboard"
custom v-slot="{ href, navigate, isExactActive }">
<a :href="href"
@click="navigate"
:class="isExactActive ? 'opacity-100' : 'opacity-50'">
Tableau de bord
</a>
</NuxtLink>
<NuxtLink
to="/admin/supplier/supplier-list"
custom v-slot="{ href, navigate }">
<a :href="href"
@click="navigate"
:class="route.path.startsWith('/admin/supplier') ? 'opacity-100' : 'opacity-50'">
Fournisseur
</a>
</NuxtLink>
<NuxtLink
to="/admin/carrier/carrier-list"
custom v-slot="{ href, navigate }">
<a :href="href"
@click="navigate"
:class="route.path.startsWith('/admin/carrier') ? 'opacity-100' : 'opacity-50'">
Transporteur
</a>
</NuxtLink>
<NuxtLink to="/admin/user/list" custom v-slot="{ href, navigate }">
<a
:href="href"
@click="navigate"
:class="route.path.startsWith('/admin/user') ? 'opacity-100' : 'opacity-50'"
>
Utilisateurs
</a>
</NuxtLink>
<NuxtLink to="/admin/customer/customer-list">
Client
</NuxtLink>
</div>
<div class="p-4">
<button
@click="handleLogout"
class="w-full bg-red-600 hover:bg-red-700 py-2 rounded font-bold"
>
Déconnexion
</button>
<p class="font-bold text-white text-center pt-2">
v{{ version }}
</p>
</div>
</aside>
<main class="min-h-0 overflow-auto px-12 py-12 ">
<div class="w-full ">
<slot/>
</div>
</main>
</div>
</div>
</template>
<script setup lang="ts">
import {useAuthStore} from '~/stores/auth'
const auth = useAuthStore()
const {version} = useAppVersion()
const route = useRoute()
const handleLogout = async () => {
try {
await auth.logout()
} finally {
await navigateTo('/login')
}
}
</script>

View File

@@ -1,54 +1,147 @@
<template> <template>
<div class="min-h-screen text-neutral-900 grid grid-rows-[85px,1fr]"> <div class="min-h-screen text-neutral-900 flex flex-col">
<header class="w-full border-b border-neutral-200 bg-primary-500"> <!-- HEADER -->
<div class="flex w-full items-center justify-center px-6 py-4"> <header class="w-full bg-primary-500 py-5 px-6">
<div class="flex w-full items-center justify-between">
<!-- Burger (mobile) -->
<button <button
type="button" type="button"
class="inline-flex items-center justify-center text-3xl text-white md:hidden" class="inline-flex items-center justify-center text-3xl text-white md:hidden"
aria-label="Ouvrir le menu" aria-label="Ouvrir le menu"
@click="toggleMenu" @click="toggleMenu"
> >
<span aria-hidden="true" class="flex items-center"><Icon name="mdi:menu" size="44"/></span> <span aria-hidden="true" class="flex items-center">
<Icon name="mdi:menu" size="44"/>
</span>
</button> </button>
<nav class="ml-4 hidden items-center gap-8 text-2xl font-bold uppercase text-white md:flex">
<NuxtLink to="/" custom v-slot="{ href, navigate, isExactActive }"> <!-- Logo -->
<NuxtLink to="/" class="shrink-0">
<span class="flex items-center justify-center bg-white text-xl font-bold uppercase px-6 py-4">
LOGO
</span>
</NuxtLink>
<!-- NAV centré (desktop) -->
<nav
class="hidden md:flex flex-1 items-center justify-center gap-8 text-xl font-bold uppercase text-white"
>
<NuxtLink to="/" custom v-slot="{ href, navigate }">
<a <a
:href="href" :href="href"
@click="navigate" @click="navigate"
:class="isExactActive ? 'opacity-100' : 'opacity-50'" :class="route.path === '/'
? 'opacity-100'
: 'opacity-65 hover:opacity-100 transition'"
> >
Accueil Accueil
</a> </a>
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
to="/admin/dashboard" custom v-slot="{ href, navigate, isExactActive }"
v-if="auth.isAdmin" v-if="auth.isAdmin"
to="/admin/supplier/supplier-list"
custom
v-slot="{ href, navigate }"
> >
<a <a
:href="href" :href="href"
@click="navigate" @click="navigate"
:class="isExactActive ? 'opacity-100' : 'opacity-50'" :class="route.path.startsWith('/admin/supplier')
? 'opacity-100'
: 'opacity-65 hover:opacity-100 transition'"
> >
Admin Fournisseurs
</a>
</NuxtLink>
<NuxtLink
v-if="auth.isAdmin"
to="/admin/carrier/carrier-list"
custom
v-slot="{ href, navigate }"
>
<a
:href="href"
@click="navigate"
:class="route.path.startsWith('/admin/carrier')
? 'opacity-100'
: 'opacity-65 hover:opacity-100 transition'"
>
Transporteurs
</a>
</NuxtLink>
<NuxtLink
v-if="auth.isAdmin"
to="/admin/user/list"
custom
v-slot="{ href, navigate }"
>
<a
:href="href"
@click="navigate"
:class="route.path.startsWith('/admin/user')
? 'opacity-100'
: 'opacity-65 hover:opacity-100 transition'"
>
Utilisateurs
</a>
</NuxtLink>
<NuxtLink
v-if="auth.isAdmin"
to="/admin/customer/customer-list"
custom
v-slot="{ href, navigate }"
>
<a
:href="href"
@click="navigate"
:class="route.path.startsWith('/admin/customer')
? 'opacity-100'
: 'opacity-65 hover:opacity-100 transition'"
>
Clients
</a> </a>
</NuxtLink> </NuxtLink>
</nav> </nav>
<NuxtLink to="/" class="flex flex-1 items-center justify-center gap-3">
<span <!-- Spacer mobile (pour centrer visuellement le header si besoin) -->
class="flex items-center justify-center bg-white text-xl font-bold uppercase text-primary-500 p-4"
>
LOGO
</span>
</NuxtLink>
<div class="w-[44px] md:hidden"></div> <div class="w-[44px] md:hidden"></div>
<button
type="button" <!-- User dropdown à droite (desktop) -->
class="ml-auto hidden text-xl font-bold uppercase text-white transition hover:opacity-80 md:inline-flex" <div v-if="auth.isAuthenticated" class="ml-auto relative hidden md:flex items-center text-white group">
@click="handleLogout" <button
> type="button"
Déconnexion class="inline-flex items-center py-2 -my-2 text-xl leading-none transition hover:opacity-80"
</button> aria-haspopup="true"
>
<span class="capitalize font-bold">{{ userDisplayName }}</span>
<span class="ml-[6px] inline-flex items-center font-bold transition-transform group-hover:rotate-180 group-focus-within:rotate-180">
<Icon name="mdi:chevron-down" size="20"/>
</span>
</button>
<div
class="absolute right-0 top-full z-10 w-56 rounded-md bg-primary-500 py-2 border-neutral-300 border shadow-lg
opacity-0 invisible pointer-events-none transition
group-hover:opacity-100 group-hover:visible group-hover:pointer-events-auto
group-focus-within:opacity-100 group-focus-within:visible group-focus-within:pointer-events-auto"
role="menu"
>
<button
type="button"
class="w-full px-4 py-2 text-left text-sm font-semibold text-white opacity-85 hover:opacity-100 transition"
@click="handleLogout"
>
Déconnexion
</button>
</div>
</div>
</div> </div>
<!-- Overlay (mobile) -->
<transition <transition
enter-active-class="transition duration-200 ease-out" enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0" enter-from-class="opacity-0"
@@ -63,6 +156,8 @@
@click="closeMenu" @click="closeMenu"
/> />
</transition> </transition>
<!-- Drawer (mobile) -->
<transition <transition
enter-active-class="transition duration-200 ease-out" enter-active-class="transition duration-200 ease-out"
enter-from-class="-translate-x-full" enter-from-class="-translate-x-full"
@@ -73,9 +168,7 @@
> >
<aside <aside
v-if="isMenuOpen" v-if="isMenuOpen"
class="fixed left-0 top-0 z-50 h-full w-full bg-primary-600 px-6 pb-8 pt-6 text-white shadow-xl md:hidden" class="fixed left-0 top-0 z-50 h-full w-full bg-primary-500 px-6 pb-8 pt-6 text-white shadow-xl md:hidden"
role="dialog"
aria-modal="true"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-2xl font-bold uppercase">Menu</span> <span class="text-2xl font-bold uppercase">Menu</span>
@@ -88,12 +181,27 @@
<Icon name="mdi:close" size="44"/> <Icon name="mdi:close" size="44"/>
</button> </button>
</div> </div>
<nav class="mt-8 flex flex-col gap-6 text-xl font-bold uppercase"> <nav class="mt-8 flex flex-col gap-6 text-xl font-bold uppercase">
<NuxtLink to="/" class="opacity-100" @click="closeMenu">Accueil</NuxtLink> <NuxtLink to="/admin/dashboard" @click="closeMenu">Accueil</NuxtLink>
<NuxtLink v-if="auth.isAdmin" to="/admin/supplier/supplier-list" @click="closeMenu">
Fournisseurs
</NuxtLink>
<NuxtLink v-if="auth.isAdmin" to="/admin/carrier/carrier-list" @click="closeMenu">
Transporteurs
</NuxtLink>
<NuxtLink v-if="auth.isAdmin" to="/admin/user/list" @click="closeMenu">
Utilisateurs
</NuxtLink>
<NuxtLink v-if="auth.isAdmin" to="/admin/customer/customer-list" @click="closeMenu">
Clients
</NuxtLink>
</nav> </nav>
<button <button
v-if="auth.isAuthenticated"
type="button" type="button"
class="mt-5 text-xl font-bold uppercase" class="mt-6 text-xl font-bold uppercase"
@click="handleLogout" @click="handleLogout"
> >
Déconnexion Déconnexion
@@ -101,10 +209,10 @@
</aside> </aside>
</transition> </transition>
</header> </header>
<main class="mx-auto w-full max-w-[1280px]"> <main class="mx-auto w-full max-w-[1280px] mt-16">
<slot/> <slot/>
</main> </main>
<footer class="w-full mt-8 bg-primary-500 p-6"> <footer class="w-full mt-auto bg-primary-500 px-6 py-3">
<p class="font-bold text-white text-right">v{{ version }}</p> <p class="font-bold text-white text-right">v{{ version }}</p>
</footer> </footer>
</div> </div>
@@ -115,9 +223,12 @@ import {useAuthStore} from '~/stores/auth'
const route = useRoute() const route = useRoute()
const auth = useAuthStore() const auth = useAuthStore()
const isMenuOpen = ref(false)
const {version} = useAppVersion() const {version} = useAppVersion()
const isMenuOpen = ref(false)
const userDisplayName = computed(() => auth.user?.username ?? 'Utilisateur')
const closeMenu = () => { const closeMenu = () => {
isMenuOpen.value = false isMenuOpen.value = false
} }

View File

@@ -1,19 +1,19 @@
<template> <template>
<form @submit.prevent="validate"> <form @submit.prevent="validate">
<div class="flex items-center justify-between "> <div class="flex items-center justify-between">
<h1 class="text-3xl font-bold uppercase"> <h1 class="text-3xl font-bold uppercase">
{{ route.params.id ? 'Modifier transporteur' : 'Ajout transporteur' }} {{ route.params.id ? 'Modifier transporteur' : 'Ajout transporteur' }}
</h1> </h1>
<button <UiButton
type="submit" type="submit"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end" class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] justify-self-end"
>Enregistrer >Enregistrer
</button> </UiButton>
</div> </div>
<div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 mb-16"> <div class="grid grid-cols-2 items-start gap-y-8 gap-x-40 py-12">
<UiTextInput <UiTextInput
label = "nom du fournisseur" label = "nom du fournisseur"
id="carrier-name" id="carrier-name"
@@ -33,19 +33,27 @@
<script setup lang="ts"> <script setup lang="ts">
import {createCarrier, getCarrier, updateCarrier} from "~/services/carrier"; import {createCarrier, getCarrier, updateCarrier} from "~/services/carrier";
import type {CarrierData, CarrierFormData} from "~/services/dto/carrier-data"; import type {CarrierData, CarrierFormData} from "~/services/dto/carrier-data";
import {computed} from "vue";
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const idCarrier = Number(route.params.id) const idCarrier = computed(() => resolveId(route.params.id))
const isLoading = ref(false) const isLoading = ref(false)
const isHydrating = ref(false) const isHydrating = ref(false)
const resolveId = (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 form = reactive<CarrierFormData>({ const form = reactive<CarrierFormData>({
code:'', code:'',
name:'' name:''
}) })
definePageMeta({ definePageMeta({
layout: 'admin' layout: 'default'
}) })
const hydrateFromUser = (carrier: CarrierData | null) => { const hydrateFromUser = (carrier: CarrierData | null) => {
@@ -59,7 +67,7 @@ const hydrateFromUser = (carrier: CarrierData | null) => {
} }
watch( watch(
() => idCarrier, () => idCarrier.value,
async (id) => { async (id) => {
if (id === null) { if (id === null) {
return return
@@ -85,8 +93,8 @@ async function validate() {
} }
if(idCarrier){ if(idCarrier.value){
await updateCarrier(idCarrier, basePayload) await updateCarrier(idCarrier.value, basePayload)
navigate() navigate()
return return
} }

View File

@@ -1,11 +1,13 @@
<template> <template>
<div class="flex items-center justify-between "> <div class="flex items-center justify-between ">
<h1 class="text-3xl font-bold uppercase">listes des transporteurs</h1> <h1 class="text-3xl font-bold uppercase text-primary-500">listes des transporteurs</h1>
<NuxtLink <NuxtLink
to="/admin/carrier" to="/admin/carrier"
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]" class="inline-flex items-center justify-center gap-2 text-xl uppercase bg-primary-500 text-white h-[50px] px-8 rounded"
>Ajouter >
<Icon name="mdi:plus" size="28" />
Ajouter
</NuxtLink> </NuxtLink>
</div> </div>
@@ -42,10 +44,10 @@ const goToCarrier = (id: number) => {
} }
definePageMeta({ definePageMeta({
layout: 'admin' layout: 'default'
}) })
onMounted(async () => { onMounted(async () => {
carrierList.value = await getCarrierList() carrierList.value = await getCarrierList(false)
}) })
</script> </script>

View File

@@ -5,13 +5,13 @@
{{ customerId ? "Modifications du client" : "Ajout d'un client" }} {{ customerId ? "Modifications du client" : "Ajout d'un client" }}
</h1> </h1>
<button <UiButton
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]"
type="submit" type="submit"
:disabled="isLoading || !auth.isAdmin" :disabled="isLoading || !auth.isAdmin"
> >
{{ customerId ? "Sauvegarder" : "Ajouter" }} {{ customerId ? "Sauvegarder" : "Ajouter" }}
</button> </UiButton>
</div> </div>
<div class="grid grid-cols-2 gap-y-8 gap-x-80 mb-10 py-12"> <div class="grid grid-cols-2 gap-y-8 gap-x-80 mb-10 py-12">
@@ -23,14 +23,14 @@
<div class="mx-24 mb-4 py-6 border-t border-black"></div> <div class="mx-24 mb-4 py-6 border-t border-black"></div>
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h2 class="text-3xl font-bold uppercase">Adresses client</h2> <h2 class="text-3xl font-bold uppercase">Adresses client</h2>
<button <UiButton
type="button" type="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]"
:disabled="customerId === null || !auth.isAdmin" :disabled="customerId === null || !auth.isAdmin"
@click="goToAddAddress" @click="goToAddAddress"
> >
Ajouter Ajouter
</button> </UiButton>
</div> </div>
<div class="overflow-x-auto mb-10"> <div class="overflow-x-auto mb-10">
<table class="w-full border-collapse"> <table class="w-full border-collapse">
@@ -80,7 +80,7 @@ import {createCustomer, getCustomer, updateCustomer} from "~/services/customer"
import type {CustomerData, CustomerFormData, CustomerPayload} from "~/services/dto/customer-data" import type {CustomerData, CustomerFormData, CustomerPayload} from "~/services/dto/customer-data"
import {useAuthStore} from "~/stores/auth" import {useAuthStore} from "~/stores/auth"
definePageMeta({layout: "admin"}) definePageMeta({layout: "default"})
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -8,7 +8,7 @@ import { createAddress, getAddress, updateAddress } from "~/services/address"
import { getCustomer, updateCustomer } from "~/services/customer" import { getCustomer, updateCustomer } from "~/services/customer"
import type { CustomerData } from "~/services/dto/customer-data" import type { CustomerData } from "~/services/dto/customer-data"
definePageMeta({ layout: "admin" }) definePageMeta({ layout: "default" })
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -1,12 +1,13 @@
<template> <template>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h1 class="text-3xl font-bold uppercase">Liste des Clients</h1> <h1 class="text-3xl font-bold uppercase text-primary-500">Liste des Clients</h1>
<NuxtLink <NuxtLink
to="/admin/customer" to="/admin/customer"
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]" class="inline-flex items-center justify-center gap-2 text-xl uppercase bg-primary-500 text-white h-[50px] px-8 rounded-md"
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'" :class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
@click="handleAddClick" @click="handleAddClick"
> >
<Icon name="mdi:plus" size="28" />
Ajouter Ajouter
</NuxtLink> </NuxtLink>
</div> </div>
@@ -93,7 +94,7 @@ import { getCustomerList } from "~/services/customer"
import type { CustomerData } from "~/services/dto/customer-data" import type { CustomerData } from "~/services/dto/customer-data"
import { useAuthStore } from "~/stores/auth" import { useAuthStore } from "~/stores/auth"
definePageMeta({ layout: "admin" }) definePageMeta({ layout: "default" })
const customerList = ref<CustomerData[]>([]) const customerList = ref<CustomerData[]>([])
const router = useRouter() const router = useRouter()

View File

@@ -2,6 +2,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
layout: 'admin' layout: 'default'
}) })
</script> </script>

View File

@@ -5,13 +5,13 @@
{{ supplierId ? "Modifications du fournisseur" : "Ajout d'un fournisseur" }} {{ supplierId ? "Modifications du fournisseur" : "Ajout d'un fournisseur" }}
</h1> </h1>
<button <UiButton
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]"
type="submit" type="submit"
:disabled="isLoading || !auth.isAdmin" :disabled="isLoading || !auth.isAdmin"
> >
{{ supplierId ? "Sauvegarder" : "Ajouter" }} {{ supplierId ? "Sauvegarder" : "Ajouter" }}
</button> </UiButton>
</div> </div>
<div class="grid grid-cols-2 gap-y-8 gap-x-80 mb-10 py-12"> <div class="grid grid-cols-2 gap-y-8 gap-x-80 mb-10 py-12">
@@ -23,14 +23,14 @@
<div class="mx-24 mb-4 py-6 border-t border-black"></div> <div class="mx-24 mb-4 py-6 border-t border-black"></div>
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h2 class="text-3xl font-bold uppercase">Adresses fournisseur</h2> <h2 class="text-3xl font-bold uppercase">Adresses fournisseur</h2>
<button <UiButton
type="button" type="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]"
:disabled="supplierId === null || !auth.isAdmin" :disabled="supplierId === null || !auth.isAdmin"
@click="goToAddAddress" @click="goToAddAddress"
> >
Ajouter Ajouter
</button> </UiButton>
</div> </div>
<div class="overflow-x-auto mb-10"> <div class="overflow-x-auto mb-10">
<table class="w-full border-collapse"> <table class="w-full border-collapse">
@@ -80,7 +80,7 @@ import {createSupplier, getSupplier, updateSupplier} from "~/services/supplier"
import type {SupplierData, SupplierFormData, SupplierPayload} from "~/services/dto/supplier-data" import type {SupplierData, SupplierFormData, SupplierPayload} from "~/services/dto/supplier-data"
import {useAuthStore} from "~/stores/auth" import {useAuthStore} from "~/stores/auth"
definePageMeta({layout: "admin"}) definePageMeta({layout: "default"})
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -8,7 +8,7 @@ import {createAddress, getAddress, updateAddress} from "~/services/address";
import {getSupplier, updateSupplier} from "~/services/supplier"; import {getSupplier, updateSupplier} from "~/services/supplier";
import type {SupplierData} from "~/services/dto/supplier-data"; import type {SupplierData} from "~/services/dto/supplier-data";
definePageMeta({ layout: "admin" }) definePageMeta({ layout: "default" })
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

View File

@@ -1,12 +1,13 @@
<template> <template>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h1 class="text-3xl font-bold uppercase">Liste des fournisseurs</h1> <h1 class="text-3xl font-bold uppercase text-primary-500">Liste des fournisseurs</h1>
<NuxtLink <NuxtLink
to="/admin/supplier" to="/admin/supplier"
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]" class="inline-flex items-center justify-center gap-2 text-xl uppercase bg-primary-500 text-white h-[50px] px-8 rounded"
:class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'" :class="auth.isAdmin ? '' : 'cursor-not-allowed opacity-60'"
@click="handleAddClick" @click="handleAddClick"
> >
<Icon name="mdi:plus" size="28" />
Ajouter Ajouter
</NuxtLink> </NuxtLink>
</div> </div>
@@ -89,7 +90,7 @@ import { getSupplierList } from "~/services/supplier"
import type { SupplierData } from "~/services/dto/supplier-data" import type { SupplierData } from "~/services/dto/supplier-data"
import { useAuthStore } from "~/stores/auth" import { useAuthStore } from "~/stores/auth"
definePageMeta({ layout: "admin" }) definePageMeta({ layout: "default" })
const supplierList = ref<SupplierData[]>([]) const supplierList = ref<SupplierData[]>([])
const router = useRouter() const router = useRouter()

View File

@@ -5,15 +5,15 @@
<h1 class="text-3xl font-bold uppercase"> <h1 class="text-3xl font-bold uppercase">
{{ userId ? "Modifications de l'utilisateur" : "Ajout d'un utilisateur" }} {{ userId ? "Modifications de l'utilisateur" : "Ajout d'un utilisateur" }}
</h1> </h1>
<button <UiButton
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]"
type="submit" type="submit"
> >
{{ userId ? 'Sauvegarder' : 'Ajouter' }} {{ userId ? 'Sauvegarder' : 'Ajouter' }}
</button> </UiButton>
</div> </div>
<div class="grid gap-y-16 gap-x-40 mb-16"> <div class="grid gap-y-16 gap-x-40 py-12">
<UiTextInput <UiTextInput
id="user-name" id="user-name"
v-model="form.username" v-model="form.username"
@@ -39,7 +39,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
layout: 'admin' layout: 'default'
}) })
import {computed, reactive, ref, watch} from 'vue' import {computed, reactive, ref, watch} from 'vue'

View File

@@ -1,10 +1,11 @@
<template> <template>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h1 class="text-3xl font-bold uppercase">Liste des utilisateurs</h1> <h1 class="text-3xl font-bold uppercase text-primary-500">Liste des utilisateurs</h1>
<NuxtLink <NuxtLink
class="flex items-center justify-center text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]" class="inline-flex items-center justify-center gap-2 text-xl uppercase bg-primary-500 text-white h-[50px] px-8 rounded-md"
@click="router.push('/admin/user/')" @click="router.push('/admin/user/')"
> >
<Icon name="mdi:plus" size="28" />
Ajouter Ajouter
</NuxtLink> </NuxtLink>
@@ -28,7 +29,7 @@
{{ user.username }} {{ user.username }}
</div> </div>
<div> <div>
{{ user.roles?.join(', ') || ' ---' }} {{ getRoleLabels(user.roles) }}
</div> </div>
</div> </div>
</div> </div>
@@ -38,19 +39,31 @@
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
layout: 'admin' layout: 'default'
}) })
import type {UserData} from "~/services/dto/user-data"; import type {UserData} from "~/services/dto/user-data";
import {getAdminUsers, getUsers} from "~/services/auth"; import {getAdminUsers} from "~/services/auth";
import {ROLE} from "~/utils/constants";
const userList = ref<UserData[]>([]) const userList = ref<UserData[]>([])
const router = useRouter() const router = useRouter()
const roleLabelByValue = new Map(ROLE.map((role) => [role.value, role.label]))
const goToUser = (id: number) => { const goToUser = (id: number) => {
router.push(`/admin/user/${id}`) router.push(`/admin/user/${id}`)
} }
const getRoleLabels = (roles?: string[]) => {
if (!roles || roles.length === 0) {
return ' ---'
}
return roles
.map((role) => roleLabelByValue.get(role) ?? role)
.join(', ')
}
onMounted(async () => { onMounted(async () => {
userList.value = await getAdminUsers() userList.value = await getAdminUsers()
}) })

View File

@@ -1,15 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
</script> </script>
<template> <template>
<div class="flex flex-wrap justify-center mt-8 gap-8 mb-8 md:mb-0"> <div class="flex flex-wrap justify-center pb-16 gap-12">
<card-link label="NOUVELLE RÉCEPTION" link="/reception" iconName="mdi:truck-outline" /> <card-link label="NOUVELLE RÉCEPTION" link="/reception" iconName="mdi:truck-outline" />
<card-link label="NOUVELLE EXPÉDITION" link="/shipment" iconName="mdi:truck-fast-outline" /> <card-link label="NOUVELLE EXPÉDITION" link="/shipment" iconName="mdi:truck-fast-outline" />
<card-link label="PLAN DE SITE" link="/" iconName="mdi:warehouse" /> <card-link label="PLAN DE SITE" link="/" iconName="material-symbols:warehouse-outline-rounded" />
<card-link label="RÉCEPTIONS EN ATTENTE" link="/reception/waiting-reception" iconName="mdi:truck-remove-outline" /> <card-link link="/reception/waiting-reception" iconName="mdi:truck-remove-outline">
<card-link label="EXPÉDITIONS EN ATTENTE" link="/shipment/waiting-shipment" iconName="mdi:truck-cargo-container" /> <template #label>
<card-link label="CASES" link="/" iconName="mdi:cube-outline" /> Réceptions<br>EN ATTENTE
</template>
</card-link>
<card-link link="/shipment/waiting-shipment" iconName="mdi:truck-cargo-container">
<template #label>
EXPÉDITIONS<br>EN ATTENTE
</template>
</card-link>
<card-link label="CASES" link="/" iconName="material-symbols:bottom-sheets-outline" />
<card-link label="RÉCEPTIONS FINIES" link="/reception/finish-reception" iconName="mdi:truck-check-outline" /> <card-link label="RÉCEPTIONS FINIES" link="/reception/finish-reception" iconName="mdi:truck-check-outline" />
<card-link label="EXPÉDITIONS FINIES" link="/shipment/finish-shipment" iconName="mdi:truck-delivery-outline" /> <card-link label="EXPÉDITIONS FINIES" link="/shipment/finish-shipment" iconName="mdi:truck-delivery-outline" />
<card-link label="PASSEPORT DU BOVIN" link="/" iconName="mdi:cow" /> <card-link link="/" iconName="mdi:cow">
<template #label>
PASSEPORT<br>DU BOVIN
</template>
</card-link>
</div> </div>
</template> </template>

View File

@@ -39,13 +39,13 @@
/> />
</div> </div>
<button <UiButton
type="submit" type="submit"
class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-60" class="w-full rounded-md bg-primary-500 px-4 py-2 text-base font-semibold text-white transition hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-60"
:disabled="isSubmitting" :disabled="isSubmitting"
> >
Connexion Connexion
</button> </UiButton>
<p class="font-bold">v{{ version }}</p> <p class="font-bold">v{{ version }}</p>
</form> </form>
</div> </div>

View File

@@ -1,29 +1,28 @@
<template> <template>
<div> <div class="flex justify-between h-[52px] mb-[80px]">
<div class="flex justify-between h-[52px] mt-6 mb-[80px]"> <div class="flex flex-1 mr-16">
<div class="flex flex-1 mr-16"> <UiStepper
<UiStepper :labels="RECEPTION_STEP_LABELS"
:labels="RECEPTION_STEP_LABELS" :current-step="storeReception?.currentStep ?? 0"
:current-step="storeReception?.currentStep ?? 0" @select="handleStepSelect"
@select="handleStepSelect" />
/>
</div>
<button
type="button"
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
@click="saveAndHold"
>Mettre en attente</button>
</div> </div>
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/> <UiButton
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/> type="button"
<ReceptionProductReceived class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
v-if="storeReception?.currentStep === 2 && @click="saveAndHold"
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.MERCHANDISES"/> >Mettre en attente
<ReceptionBovineReceived </UiButton>
v-if="storeReception?.currentStep === 2 &&
receptionStore.current?.receptionType?.code === RECEPTION_TYPE_CODES.BOVINS"/>
<ReceptionWeight v-if="storeReception?.currentStep !== null && storeReception?.currentStep >= 3" mode="tare"/>
</div> </div>
<ReceptionForm v-if="!storeReception || storeReception.currentStep === 0"/>
<ReceptionWeight v-if="storeReception?.currentStep === 1" mode="gross"/>
<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"/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -1,10 +1,10 @@
<template> <template>
<skeletonTable v-if="isPageLoading"/> <div class="flex items-center justify-start gap-10">
<div v-else class="ps-20"> <Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
<div class="flex items-center justify-start gap-10"> <h1 class="text-3xl font-bold uppercase text-primary-500">listes des réceptions finie</h1>
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44"/> </div>
<h1 class="text-3xl font-bold uppercase">listes des réceptions finie</h1>
</div> <div class="px-[86px]">
<div class="mt-6 border border-slate-200 mb-16 "> <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 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>Numéro</div>
@@ -27,7 +27,7 @@
<div>{{ reception.supplier?.name }}</div> <div>{{ reception.supplier?.name }}</div>
<div>{{ reception.address?.fullAddress }}</div> <div>{{ reception.address?.fullAddress }}</div>
<div>{{ reception.receptionType?.label }}</div> <div>{{ reception.receptionType?.label }}</div>
<div>{{ formatWeighing(reception, 'gross') }} | {{ formatWeighing(reception, 'tare') }}</div> <div>{{ formatWeighing(reception) }}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -36,16 +36,20 @@
<script setup lang="ts"> <script setup lang="ts">
import type {ReceptionData} from "~/services/dto/reception-data"; import type {ReceptionData} from "~/services/dto/reception-data";
import {getReceptionList} from "~/services/reception"; import {getReceptionList} from "~/services/reception";
import type {ShipmentData} from "~/services/dto/shipment-data";
const receptionList = ref<ReceptionData[]>() const receptionList = ref<ReceptionData[]>()
const router = useRouter() const router = useRouter()
const isPageLoading = ref(true)
const formatWeighing = (reception: ReceptionData, type: 'gross' | 'tare') => { const formatWeighing = (reception: ReceptionData) => {
const entry = reception.weights?.find((weight) => weight.type === type) const gross = reception.weights?.find((weight) => weight.type === 'gross')?.weight
if (!entry || entry.weight == null || entry.dsd == null) { const tare = reception.weights?.find((weight) => weight.type === 'tare')?.weight
if (gross == null || tare == null) {
return '—' return '—'
} }
return `${entry.weight} kg`
return `${gross - tare} kg`
} }
const goToReception = (id: number) => { const goToReception = (id: number) => {
@@ -54,6 +58,5 @@ const goToReception = (id: number) => {
onMounted(async () => { onMounted(async () => {
receptionList.value = await getReceptionList(true) receptionList.value = await getReceptionList(true)
isPageLoading.value = false
}) })
</script> </script>

View File

@@ -115,14 +115,14 @@
/> />
</div> </div>
<div class="flex justify-center mb-2"> <div class="flex justify-center mb-2">
<button <UiButton
v-if="auth.isAdmin" v-if="auth.isAdmin"
type="submit" type="submit"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] mb-16" class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] mb-16"
> >
Enregistrer Enregistrer
</button> </UiButton>
</div> </div>
<div class="flex justify-evenly gap-y-8 gap-x-40 mb-8 border-b border-slate-400"> <div class="flex justify-evenly gap-y-8 gap-x-40 mb-8 border-b border-slate-400">
<h1 <h1

View File

@@ -1,13 +1,13 @@
<template> <template>
<div class="flex items-center justify-between "> <div class="flex items-center justify-between">
<div class="flex items-center gap-10"> <div class="flex items-center gap-10">
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44" /> <Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
<h1 class="text-3xl font-bold uppercase">listes des réceptions en attente</h1> <h1 class="text-3xl font-bold uppercase text-primary-500">listes des réceptions en attente</h1>
</div> </div>
</div> </div>
<div class="ps-20 " > <div class="px-[86px]">
<div class="mt-6 border border-slate-200 mb-16 "> <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 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>Fournisseur</div>
<div>Adresse</div> <div>Adresse</div>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div class="flex justify-between h-[52px] mt-6 mb-[80px]"> <div class="flex justify-between h-[52px] mb-[80px]">
<div class="flex flex-1 mr-16"> <div class="flex flex-1 mr-16">
<UiStepper <UiStepper
:labels="SHIPMENT_STEP_LABELS" :labels="SHIPMENT_STEP_LABELS"
@@ -9,16 +9,17 @@
/> />
</div> </div>
<button <UiButton
type="button" type="button"
class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center" class="flex flex-col justify-center uppercase text-xl bg-black text-white h-[50px] w-[272px] text-center"
@click="saveAndHold" @click="saveAndHold"
>Mettre en attente >Mettre en attente
</button> </UiButton>
</div> </div>
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/> <ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/>
<ShipmentWeight v-if="storeShipment?.currentStep === 1" mode="gross"/> <ShipmentWeight v-if="storeShipment?.currentStep === 1" mode="gross"/>
<ShipmentWeight v-if="storeShipment?.currentStep >= 2" mode="tare"/> <ShipmentLoading v-if="storeShipment?.currentStep === 2"/>
<ShipmentWeight v-if="storeShipment?.currentStep === 3" mode="tare"/>
</div> </div>
</template> </template>

View File

@@ -1,11 +1,10 @@
<template> <template>
<skeletonTable v-if="isPageLoading"/>
<div v-else class="ps-20 ">
<div class="flex items-center justify-start gap-10"> <div class="flex items-center justify-start gap-10">
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44"/> <Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
<h1 class="text-3xl font-bold uppercase">listes des expéditions finie</h1> <h1 class="text-3xl font-bold uppercase text-primary-500">listes des expéditions finie</h1>
</div> </div>
<div class="px-[86px]">
<div class="mt-6 border border-slate-200 mb-16 "> <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 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>Numéro</div>
@@ -22,11 +21,11 @@
class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200" class="grid grid-cols-6 gap-4 px-4 py-3 text-sm hover:bg-slate-50 cursor-pointer border-t border-slate-200"
role="button" role="button"
tabindex="0" tabindex="0"
@click="goToshipment(shipment.id)" @click="goShipment(shipment.id)"
> >
<div>{{ shipment.identificationNumber }}</div> <div>{{ shipment.identificationNumber }}</div>
<div>{{ shipment.shipmentDate }}</div> <div>{{ shipment.shipmentDate }}</div>
<div>{{ shipment.customer?.label }}</div> <div>{{ shipment.customer?.name }}</div>
<div>{{ shipment.address?.fullAddress }}</div> <div>{{ shipment.address?.fullAddress }}</div>
<div> <div>
<template v-if="formatBovinShipmentLines(shipment).length"> <template v-if="formatBovinShipmentLines(shipment).length">
@@ -39,7 +38,7 @@
</div> </div>
</template> </template>
</div> </div>
<div>{{ formatWeighing(shipment, 'gross') }} | {{ formatWeighing(shipment, 'tare') }}</div> <div>{{ formatWeighing(shipment) }}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -51,13 +50,16 @@ import {getShipmentList} from "~/services/shipment";
const shipmentList = ref<ShipmentData[]>() const shipmentList = ref<ShipmentData[]>()
const router = useRouter() const router = useRouter()
const isPageLoading = ref(true)
const formatWeighing = (shipment: ShipmentData, type: 'gross' | 'tare') => { const formatWeighing = (shipment: ShipmentData) => {
const entry = shipment.weights?.find((weight) => weight.type === type) const gross = shipment.weights?.find((weight) => weight.type === 'gross')?.weight
if (!entry || entry.weight == null || entry.dsd == null) { const tare = shipment.weights?.find((weight) => weight.type === 'tare')?.weight
if (gross == null || tare == null) {
return '' return ''
} }
return `${entry.weight} kg`
return `${gross - tare} kg`
} }
const formatBovinShipmentLines = (shipment: ShipmentData) => { const formatBovinShipmentLines = (shipment: ShipmentData) => {
@@ -72,13 +74,11 @@ const formatBovinShipmentLines = (shipment: ShipmentData) => {
}) })
} }
const goToshipment = (id: number) => { const goShipment = (id: number) => {
//router.push(`/shipment/update/${id}`) router.push(`/shipment/update/${id}`)
} }
onMounted(async () => { onMounted(async () => {
shipmentList.value = await getShipmentList(true) shipmentList.value = await getShipmentList(true)
isPageLoading.value = false
}) })
</script> </script>

View File

@@ -1,12 +1,12 @@
<template> <template>
<div class="flex items-center justify-between "> <div class="flex items-center justify-between">
<div class="flex items-center gap-10"> <div class="flex items-center gap-10">
<Icon @click="router.push('/')" name="gg:arrow-left-o" style="color: black" size="44"/> <Icon @click="router.push('/')" name="gg:arrow-left-o" size="44" class="cursor-pointer text-primary-500"/>
<h1 class="text-3xl font-bold uppercase">listes des expéditions en attente</h1> <h1 class="text-3xl font-bold uppercase text-primary-500">listes des expéditions en attente</h1>
</div> </div>
</div> </div>
<div class="ps-20 "> <div class="px-[86px]">
<div class="mt-6 border border-slate-200 mb-16 "> <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 class="grid grid-cols-5 gap-4 bg-slate-100 px-4 py-3 text-sm font-semibold uppercase tracking-wide">
<div>Client</div> <div>Client</div>

View File

@@ -6,6 +6,7 @@ export interface AddressData {
postalCode: string postalCode: string
city: string city: string
countryCode: string countryCode: string
fullAddress: string
} }
export interface AddressFormData { export interface AddressFormData {

View File

@@ -8,16 +8,7 @@ export default <Partial<Config>>{
}, },
colors: { colors: {
primary: { primary: {
50: '#f6f9ea', 500: '#456452',
100: '#eaf2cf',
200: '#d6e3a4',
300: '#c1d47a',
400: '#afc85a',
500: '#9ebb43',
600: '#7e9735',
700: '#607228',
800: '#414d1a',
900: '#24290d'
} }
} }
} }

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260213114000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Allow only one bovin_shipment row per shipment.';
}
public function up(Schema $schema): void
{
// Keep one row per shipment (latest id), required before adding unique index.
$this->addSql(<<<'SQL'
DELETE FROM bovin_shipment bs
USING (
SELECT id, ROW_NUMBER() OVER (PARTITION BY shipment_id ORDER BY id DESC) AS rn
FROM bovin_shipment
WHERE shipment_id IS NOT NULL
) d
WHERE bs.id = d.id
AND d.rn > 1
SQL);
$this->addSql('DROP INDEX IF EXISTS uniq_bovin_shipment');
$this->addSql('CREATE UNIQUE INDEX uniq_bovin_shipment_one_type ON bovin_shipment (shipment_id)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX IF EXISTS uniq_bovin_shipment_one_type');
$this->addSql('CREATE UNIQUE INDEX uniq_bovin_shipment ON bovin_shipment (shipment_id, shipment_type_id)');
}
}

View File

@@ -168,6 +168,7 @@ class SeedCommand extends Command
['label' => 'Foin', 'code' => 'FOIN'], ['label' => 'Foin', 'code' => 'FOIN'],
['label' => 'Paille', 'code' => 'PAILLE'], ['label' => 'Paille', 'code' => 'PAILLE'],
['label' => 'Granule', 'code' => 'GRANULE'], ['label' => 'Granule', 'code' => 'GRANULE'],
['label' => 'Autres', 'code' => 'AUTRES'],
]; ];
foreach ($merchandiseTypes as $type) { foreach ($merchandiseTypes as $type) {
$this->upsertByCode(MerchandiseType::class, $type['code'], static function (MerchandiseType $entity) use ($type) { $this->upsertByCode(MerchandiseType::class, $type['code'], static function (MerchandiseType $entity) use ($type) {
@@ -250,8 +251,8 @@ class SeedCommand extends Command
private function seedShipmentTypes(): void private function seedShipmentTypes(): void
{ {
$shipmentTypes = [ $shipmentTypes = [
['label' => 'Bovin de boucherie', 'code' => 'BDB'], ['label' => 'Boucherie', 'code' => 'BDB'],
['label' => "Bovin d'équarrissage", 'code' => 'BE'], ['label' => quarrissage', 'code' => 'BE'],
]; ];
foreach ($shipmentTypes as $type) { foreach ($shipmentTypes as $type) {
$this->upsertByCode(ShipmentType::class, $type['code'], static function (ShipmentType $entity) use ($type) { $this->upsertByCode(ShipmentType::class, $type['code'], static function (ShipmentType $entity) use ($type) {
@@ -539,10 +540,10 @@ class SeedCommand extends Command
'addresses' => [ 'addresses' => [
[ [
'label' => 'Les producteurs de la marche (LPM)', 'label' => 'Les producteurs de la marche (LPM)',
'street' => 'Rue de Nexon', 'street' => 'Malonze',
'street2' => null, 'street2' => null,
'postalCode' => '87000', 'postalCode' => '23300',
'city' => 'LIMOGES', 'city' => 'LA SOUTERRAINE',
'countryCode' => 'FR', 'countryCode' => 'FR',
], ],
], ],
@@ -564,15 +565,15 @@ class SeedCommand extends Command
], ],
[ [
'name' => 'TERRENA', 'name' => 'TERRENA',
'phone' => '02.51.67.17.98', 'phone' => '02.40.98.90.00',
'email' => null, 'email' => 'scouillaud@terrena.fr',
'addresses' => [ 'addresses' => [
[ [
'label' => 'TERRENA', 'label' => 'TERRENA',
'street' => 'La Blanchardière', 'street' => 'LA NOELLE',
'street2' => null, 'street2' => 'BP 20199',
'postalCode' => '44522', 'postalCode' => '44155',
'city' => 'MESANGER', 'city' => 'ANCENIS CEDEX',
'countryCode' => 'FR', 'countryCode' => 'FR',
], ],
], ],

View File

@@ -26,6 +26,7 @@ class ReferenceFixtures extends Fixture
['label' => 'Foin', 'code' => 'FOIN'], ['label' => 'Foin', 'code' => 'FOIN'],
['label' => 'Paille', 'code' => 'PAILLE'], ['label' => 'Paille', 'code' => 'PAILLE'],
['label' => 'Granule', 'code' => 'GRANULE'], ['label' => 'Granule', 'code' => 'GRANULE'],
['label' => 'Autres', 'code' => 'AUTRES'],
]; ];
foreach ($merchandiseTypes as $type) { foreach ($merchandiseTypes as $type) {
$merchandiseType = new MerchandiseType() $merchandiseType = new MerchandiseType()
@@ -377,10 +378,10 @@ class ReferenceFixtures extends Fixture
'addresses' => [ 'addresses' => [
[ [
'label' => 'Les producteurs de la marche (LPM)', 'label' => 'Les producteurs de la marche (LPM)',
'street' => 'Rue de Nexon', 'street' => 'Malonze',
'street2' => null, 'street2' => null,
'postalCode' => '87000', 'postalCode' => '23300',
'city' => 'LIMOGES', 'city' => 'LA SOUTERRAINE',
'countryCode' => 'FR', 'countryCode' => 'FR',
], ],
], ],
@@ -402,15 +403,15 @@ class ReferenceFixtures extends Fixture
], ],
[ [
'name' => 'TERRENA', 'name' => 'TERRENA',
'phone' => '02.51.67.17.98', 'phone' => '02.40.98.90.00',
'email' => null, 'email' => 'scouillaud@terrena.fr',
'addresses' => [ 'addresses' => [
[ [
'label' => 'TERRENA', 'label' => 'TERRENA',
'street' => 'La Blanchardière', 'street' => 'LA NOELLE',
'street2' => null, 'street2' => 'BP 20199',
'postalCode' => '44522', 'postalCode' => '44155',
'city' => 'MESANGER', 'city' => 'ANCENIS CEDEX',
'countryCode' => 'FR', 'countryCode' => 'FR',
], ],
], ],

View File

@@ -18,7 +18,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity] #[ORM\Entity]
#[ApiFilter(SearchFilter::class, properties: ['shipment' => 'exact'])] #[ApiFilter(SearchFilter::class, properties: ['shipment' => 'exact'])]
#[ORM\UniqueConstraint(name: 'uniq_bovin_shipment', columns: ['shipment_id', 'shipment_type_id'])] #[ORM\UniqueConstraint(name: 'uniq_bovin_shipment_one_type', columns: ['shipment_id'])]
#[ORM\Table(name: 'bovin_shipment')] #[ORM\Table(name: 'bovin_shipment')]
#[ApiResource( #[ApiResource(
operations: [ operations: [

View File

@@ -328,7 +328,7 @@ class Shipment
return; return;
} }
$number = sprintf('P-BR-%04d', $this->id); $number = sprintf('P-BL-%04d', $this->id);
$this->identificationNumber = $number; $this->identificationNumber = $number;
$args->getObjectManager() $args->getObjectManager()

View File

@@ -26,11 +26,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
), ),
new GetCollection( new GetCollection(
normalizationContext: ['groups' => ['supplier:read']], normalizationContext: ['groups' => ['supplier:read']],
), security: "is_granted('ROLE_USER')"
new GetCollection(
uriTemplate: '/admin/suppliers',
normalizationContext: ['groups' => ['supplier:read']],
security: "is_granted('ROLE_ADMIN')"
), ),
new Post( new Post(
normalizationContext: ['groups' => ['supplier:read']], normalizationContext: ['groups' => ['supplier:read']],

View File

@@ -174,7 +174,7 @@
<td style="width:30%; text-align:right; vertical-align:top; font-size: 14px;"> <td style="width:30%; text-align:right; vertical-align:top; font-size: 14px;">
<div style="display:inline-block; width:75mm; line-height:1.3;"> <div style="display:inline-block; width:75mm; line-height:1.3;">
<strong>{{ shipment.customer ? shipment.customer.label : '-' }}</strong><br> <strong>{{ shipment.customer ? shipment.customer.name : '-' }}</strong><br>
<span>{{ shipment.address ? shipment.address.street : '' }}</span><br> <span>{{ shipment.address ? shipment.address.street : '' }}</span><br>
{% if shipment.address and shipment.address.street2 %} {% if shipment.address and shipment.address.street2 %}
<span>{{ shipment.address.street2 }}</span><br> <span>{{ shipment.address.street2 }}</span><br>
@@ -198,7 +198,7 @@
</tr> </tr>
<tr> <tr>
<td style="width:55%; text-align:center;"> <td style="width:55%; text-align:center;">
{{ shipment.customer ? shipment.customer.code : '-' }} {{ shipment.customer ? shipment.customer.name : '-' }}
</td> </td>
<td style="width:20%; text-align:center; white-space:nowrap;"> <td style="width:20%; text-align:center; white-space:nowrap;">
{{ shipment.shipmentDate|date('d/m/Y') }} {{ shipment.shipmentDate|date('d/m/Y') }}
@@ -276,7 +276,7 @@
<td style="width:60%; padding-right:8mm; vertical-align:top;"> <td style="width:60%; padding-right:8mm; vertical-align:top;">
<div class="meta"> <div class="meta">
<p>Transporteur : {{ shipment.carrier ? shipment.carrier.name : '-' }}</p> <p>Transporteur : {{ shipment.carrier ? shipment.carrier.name : '-' }}</p>
<p>Mode de livraison : {{ shipment.truck ? shipment.truck.name : '-' }}</p> <p>Mode d'expédition : {{ shipment.truck ? shipment.truck.name : '-' }}</p>
<p>Immatriculation : {{ shipment.licencePlate ?? '-' }}</p> <p>Immatriculation : {{ shipment.licencePlate ?? '-' }}</p>
</div> </div>
</td> </td>