feat : ajout d'une page de creation d'une expedition

This commit is contained in:
2026-02-11 16:12:33 +01:00
parent 0181d72144
commit 5f4139fde3
31 changed files with 1292 additions and 356 deletions

View File

@@ -34,6 +34,9 @@ Ajouter dans le fichier .env du frontend
* [#315] Creation page admin utilisateur * [#315] Creation page admin utilisateur
* [#317] Admin modification creation transporteur * [#317] Admin modification creation transporteur
* [#318] Affichage modification reception terminée * [#318] Affichage modification reception terminée
* [#271] Créer une nouvelle expédition (étape 1)
* [#256] Créer une nouvelle réception (étape 3 - bovin)
* [#314] Création d'une page d'administration : listing des utilisateurs
### Changed ### Changed

View File

@@ -142,7 +142,7 @@ import type {DriverData} from '~/services/dto/driver-data'
import {getDriverList} from '~/services/driver' import {getDriverList} from '~/services/driver'
import type {VehicleData} from '~/services/dto/vehicle-data' import type {VehicleData} from '~/services/dto/vehicle-data'
import {getVehicleList} from '~/services/vehicle' import {getVehicleList} from '~/services/vehicle'
import {RECEPTION_TYPE_CODES, SUPLLIER_CODE} from "~/utils/constants"; 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";
@@ -183,7 +183,7 @@ const selectedCarrier = computed(() =>
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
) )
// Indique si le transporteur est LIOT // Indique si le transporteur est LIOT
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPLLIER_CODE.LIOT) const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
// Adresses disponibles pour le fournisseur sélectionné // Adresses disponibles pour le fournisseur sélectionné
const supplierAddresses = computed(() => { const supplierAddresses = computed(() => {
const supplierId = Number(form.supplierId) const supplierId = Number(form.supplierId)

View File

@@ -74,7 +74,9 @@ const printReceipt = async () => {
} }
await saveWeight() await saveWeight()
await printPdf(`/receptions/${receptionStore.current.id}/receipt`) const reception = receptionStore.current
const filename = `${reception.identificationNumber ?? reception.id}_${reception.supplier?.name ?? 'fournisseur'}_${reception.licensePlate ?? 'immat'}.pdf`
await printPdf(`/receptions/${reception.id}/receipt`, filename)
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir. // Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
await new Promise((resolve) => setTimeout(resolve, 600)) await new Promise((resolve) => setTimeout(resolve, 600))

View File

@@ -1,10 +1,12 @@
<template> <template>
<form @submit.prevent="validate"> <form @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">Éxpedition</h1> <h1 class="font-bold text-5xl uppercase col-start-1 row-start-1">Expédition</h1>
<!-- Nom de l'utilisateur -->
<UiSelect <UiSelect
id="shipment-user" id="shipment-user"
label="Nom de l'utisateur" v-model="form.userId"
label="Nom de l'utilisateur"
:options="users.map((user) => ({ :options="users.map((user) => ({
value: String(user.id), value: String(user.id),
label: user.username label: user.username
@@ -12,14 +14,14 @@
:loading="isLoadingUsers" :loading="isLoadingUsers"
wrapper-class="col-start-1 row-start-2" wrapper-class="col-start-1 row-start-2"
/> />
<!-- Date de l'éxpedition -->
<UiDateInput <UiDateInput
id="shipment-date" id="shipment-date"
v-model="form.shipmentDate" v-model="form.shipmentDate"
label="Date du jour" label="Date du jour"
wrapper-class="col-start-1 row-start-3" wrapper-class="col-start-1 row-start-3"
/> />
<!-- Type d'expédition -->
<div class="col-start-1 row-start-4"> <div class="col-start-1 row-start-4">
<label class="font-bold uppercase text-xl mb-2 block"> <label class="font-bold uppercase text-xl mb-2 block">
Type d'expédition Type d'expédition
@@ -40,10 +42,9 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Client -->
<UiSelect <UiSelect
id="reception-driver" id="shipment-customer"
v-model="form.customerId" v-model="form.customerId"
label="Client" label="Client"
:options="customers.map((customer) => ({ :options="customers.map((customer) => ({
@@ -53,21 +54,18 @@
:loading="isLoadingCustomers" :loading="isLoadingCustomers"
wrapper-class="col-start-1 row-start-5" wrapper-class="col-start-1 row-start-5"
/> />
<!-- Adresse du client -->
<UiSelect <UiSelect
id="reception-driver" id="shipment-address"
v-model="form.addressId" v-model="form.addressId"
:options="customerAddresses.map((address) => ({ :options="customerAddressOptions"
value: String(address.id),
label: address.fullAddress
}))"
:disabled="isLoadingCustomers || customerAddresses.length === 0" :disabled="isLoadingCustomers || customerAddresses.length === 0"
label="Adresse" label="Adresse"
wrapper-class="col-start-2 row-start-1" wrapper-class="col-start-2 row-start-1"
/> />
<!-- Camion -->
<UiSelect <UiSelect
id="reception-driver" id="shipment-truck"
v-model="form.truckId" v-model="form.truckId"
label="Camion" label="Camion"
:options="trucks.map((truck) => ({ :options="trucks.map((truck) => ({
@@ -77,9 +75,9 @@
:loading="isLoadingTrucks" :loading="isLoadingTrucks"
wrapper-class="col-start-2 row-start-2" wrapper-class="col-start-2 row-start-2"
/> />
<!-- Transporteur -->
<UiSelect <UiSelect
id="reception-driver" id="shipment-carrier"
v-model="form.carrierId" v-model="form.carrierId"
label="Transporteur" label="Transporteur"
:options="carriers.map((carrier) => ({ :options="carriers.map((carrier) => ({
@@ -88,9 +86,9 @@
}))" }))"
wrapper-class="col-start-2 row-start-3" wrapper-class="col-start-2 row-start-3"
/> />
<!-- Chauffeur (LIOT) -->
<UiSelect <UiSelect
id="reception-driver" id="shipment-driver"
v-model="form.driverId" v-model="form.driverId"
label="Nom du chauffeur si LIOT" label="Nom du chauffeur si LIOT"
:options="filteredDrivers.map((driver) => ({ :options="filteredDrivers.map((driver) => ({
@@ -100,7 +98,7 @@
:loading="isLoadingDrivers" :loading="isLoadingDrivers"
wrapper-class="col-start-2 row-start-4" wrapper-class="col-start-2 row-start-4"
/> />
<!-- 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-5">
<UiLicensePlateInput <UiLicensePlateInput
v-model="form.licencePlate" v-model="form.licencePlate"
@@ -110,7 +108,7 @@
<!-- Immatriculation (LIOT) --> <!-- Immatriculation (LIOT) -->
<UiSelect <UiSelect
v-if="isLiotCarrier" v-if="isLiotCarrier"
id="reception-vehicle" id="shipment-vehicle"
v-model="form.vehicleId" v-model="form.vehicleId"
label="Immatriculation" label="Immatriculation"
:options="filteredVehicles.map((vehicle) => ({ :options="filteredVehicles.map((vehicle) => ({
@@ -139,6 +137,7 @@ import type {TruckData} from '~/services/dto/truck-data'
import type {CarrierData} from '~/services/dto/carrier-data' import type {CarrierData} from '~/services/dto/carrier-data'
import type {DriverData} from '~/services/dto/driver-data' import type {DriverData} from '~/services/dto/driver-data'
import type {VehicleData} from '~/services/dto/vehicle-data' import type {VehicleData} from '~/services/dto/vehicle-data'
import type {AddressData} from '~/services/dto/address-data'
import {getUsers} from '~/services/auth' import {getUsers} from '~/services/auth'
import {getCustomerList} from '~/services/customer' import {getCustomerList} from '~/services/customer'
import {getTruckList} from '~/services/truck' import {getTruckList} from '~/services/truck'
@@ -146,10 +145,10 @@ import {getCarrierList} from '~/services/carrier'
import {getVehicleList} from '~/services/vehicle' import {getVehicleList} from '~/services/vehicle'
import {getDriverList} from '~/services/driver' import {getDriverList} from '~/services/driver'
import type {ShipmentFormData} from '~/services/dto/shipment-data' import type {ShipmentFormData} from '~/services/dto/shipment-data'
import {SUPLLIER_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 {ref} 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 {
@@ -167,6 +166,7 @@ const drivers = ref<DriverData[]>([])
const vehicles = ref<VehicleData[]>([]) const vehicles = ref<VehicleData[]>([])
const isLoadingUsers = ref(false) const isLoadingUsers = ref(false)
const isLoadingShipmentTypes = ref(false)
const isLoadingCustomers = ref(false) const isLoadingCustomers = ref(false)
const isLoadingTrucks = ref(false) const isLoadingTrucks = ref(false)
const isLoadingCarriers = ref(false) const isLoadingCarriers = ref(false)
@@ -183,8 +183,7 @@ const bovineShipment = ref<ShipmentTypeData[]>([])
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
) )
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPLLIER_CODE.LIOT) const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
const form = reactive<ShipmentFormData>({ const form = reactive<ShipmentFormData>({
userId: '', userId: '',
@@ -197,22 +196,30 @@ const form = reactive<ShipmentFormData>({
vehicleId: '', vehicleId: '',
licencePlate: '', licencePlate: '',
}) })
// Adresses liées au client sélectionné
const customerAddresses = computed(() => { const customerAddresses = computed<AddressData[]>(() => {
const customerId = Number(form.customerId) const customerId = Number(form.customerId)
if (!Number.isFinite(customerId)) { if (!Number.isFinite(customerId)) {
return [] return []
} }
return customers.value.find((customer) => customer.id === customerId)?.addresses ?? [] return customers.value.find((customer) => customer.id === customerId)?.addresses ?? []
}) })
// Options pour le select des adresses du client
const customerAddressOptions = computed(() =>
customerAddresses.value
.map((address) => ({
value: String(address.id),
label: address.fullAddress
}))
)
// Chauffeurs liés au transporteur sélectionné (LIOT)
const filteredDrivers = computed<DriverData[]>(() => { const filteredDrivers = computed<DriverData[]>(() => {
if (!form.carrierId) { if (!form.carrierId) {
return [] return []
} }
return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId) return drivers.value.filter((driver) => String(driver.carrier?.id) === form.carrierId)
}) })
// Véhicules liés au transporteur + camion sélectionnés (LIOT)
const filteredVehicles = computed<VehicleData[]>(() => { const filteredVehicles = computed<VehicleData[]>(() => {
if (!form.carrierId) { if (!form.carrierId) {
return [] return []
@@ -223,8 +230,7 @@ const filteredVehicles = computed<VehicleData[]>(() => {
(!form.truckId || String(vehicle.truck?.id) === form.truckId) (!form.truckId || String(vehicle.truck?.id) === form.truckId)
) )
}) })
// Chargement des données pour les selects
const loadUsers = async () => { const loadUsers = async () => {
isLoadingUsers.value = true isLoadingUsers.value = true
try { try {
@@ -235,11 +241,11 @@ const loadUsers = async () => {
} }
const loadShipmentType = async () => { const loadShipmentType = async () => {
isLoadingUsers.value = true isLoadingShipmentTypes.value = true
try { try {
bovineShipment.value = await getShipmentTypeList() bovineShipment.value = await getShipmentTypeList()
} finally { } finally {
isLoadingUsers.value = false isLoadingShipmentTypes.value = false
} }
} }
@@ -252,7 +258,6 @@ const loadCustomers = async () => {
} }
} }
const loadTrucks = async () => { const loadTrucks = async () => {
isLoadingTrucks.value = true isLoadingTrucks.value = true
try { try {
@@ -261,7 +266,6 @@ const loadTrucks = async () => {
isLoadingTrucks.value = false isLoadingTrucks.value = false
} }
} }
const loadCarriers = async () => { const loadCarriers = async () => {
isLoadingCarriers.value = true isLoadingCarriers.value = true
try { try {
@@ -270,7 +274,6 @@ const loadCarriers = async () => {
isLoadingCarriers.value = false isLoadingCarriers.value = false
} }
} }
const loadVehicles = async () => { const loadVehicles = async () => {
isLoadingVehicles.value = true isLoadingVehicles.value = true
try { try {
@@ -279,7 +282,6 @@ const loadVehicles = async () => {
isLoadingVehicles.value = false isLoadingVehicles.value = false
} }
} }
const loadDrivers = async () => { const loadDrivers = async () => {
isLoadingDrivers.value = true isLoadingDrivers.value = true
try { try {
@@ -288,8 +290,6 @@ const loadDrivers = async () => {
isLoadingDrivers.value = false isLoadingDrivers.value = false
} }
} }
// On met le user connecté par défaut dans le select // On met le user connecté par défaut dans le select
const setDefaultUser = () => { const setDefaultUser = () => {
if (form.userId) { if (form.userId) {
@@ -299,7 +299,7 @@ const setDefaultUser = () => {
form.userId = String(authStore.user.id) form.userId = String(authStore.user.id)
} }
} }
// Chargement initial des données
onMounted(async () => { onMounted(async () => {
await loadShipmentType() await loadShipmentType()
await loadUsers() await loadUsers()
@@ -311,27 +311,37 @@ onMounted(async () => {
await authStore.ensureSession() await authStore.ensureSession()
setDefaultUser() setDefaultUser()
}) })
// Hydrate le formulaire depuis l'expédition en cours
watch( watch(
() => shipmentStore.current, () => shipmentStore.current,
(shipment) => { (shipment) => {
isHydrating.value = true
form.licencePlate = shipment?.licencePlate ?? ''
form.shipmentDate = shipment?.shipmentDate ?? new Date().toISOString().slice(0, 10)
form.userId = shipment?.user?.id ? String(shipment.user.id) :
form.userId
form.customerId = shipment?.customer?.id ?
String(shipment.customer.id) : ''
form.addressId = shipment?.address?.id ? String(shipment.address.id) : ''
form.truckId = shipment?.truck?.id ? String(shipment.truck.id) : ''
form.carrierId = shipment?.carrier?.id ? String(shipment.carrier.id) : ''
form.driverId = shipment?.driver?.id ? String(shipment.driver.id) : ''
form.vehicleId = shipment?.vehicle?.id ? String(shipment.vehicle.id) : ''
if (!shipment || !shipment.bovinShipments) { if (!shipment || !shipment.bovinShipments) {
bovineQuantities.value = {} bovineQuantities.value = {}
return } else {
}
const next: Record<string, number | null> = {} const next: Record<string, number | null> = {}
for (const entry of shipment.bovinShipments) { for (const entry of shipment.bovinShipments) {
const typeId = entry.shipmentType?.id const typeId = entry.shipmentType?.id
if (!typeId) { if (!typeId) continue
continue
}
next[String(typeId)] = entry.nbBovinSend ?? null next[String(typeId)] = entry.nbBovinSend ?? null
} }
bovineQuantities.value = next bovineQuantities.value = next
}
isHydrating.value = false
}, },
{immediate: true} {immediate: true}
) )
// Ajuste driver/vehicle quand le transporteur change (logique LIOT) // Ajuste driver/vehicle quand le transporteur change (logique LIOT)
watch( watch(
() => [form.customerId, customers.value], () => [form.customerId, customers.value],
@@ -356,11 +366,8 @@ watch(
}, },
{immediate: true} {immediate: true}
) )
// Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT) // Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT)
watch( const applyLiotDefaults = () => {
() => form.carrierId,
() => {
if (isHydrating.value) { if (isHydrating.value) {
return return
} }
@@ -380,10 +387,22 @@ watch(
if (filteredVehicles.value.length === 1) { if (filteredVehicles.value.length === 1) {
form.vehicleId = String(filteredVehicles.value[0].id) form.vehicleId = String(filteredVehicles.value[0].id)
} }
}
watch(
() => form.carrierId,
() => {
applyLiotDefaults()
}, },
{immediate: true} {immediate: true}
) )
watch(
() => isHydrating.value,
(value) => {
if (!value) {
applyLiotDefaults()
}
}
)
// Récupère la plaque depuis le véhicule choisi (LIOT) // Récupère la plaque depuis le véhicule choisi (LIOT)
watch( watch(
() => [form.truckId, form.carrierId, vehicles.value], () => [form.truckId, form.carrierId, vehicles.value],
@@ -407,7 +426,6 @@ watch(
}, },
{immediate: true} {immediate: true}
) )
// Auto-renseigne le véhicule si la plaque correspond (LIOT) // Auto-renseigne le véhicule si la plaque correspond (LIOT)
watch( watch(
() => [form.vehicleId, form.carrierId, vehicles.value], () => [form.vehicleId, form.carrierId, vehicles.value],
@@ -427,7 +445,6 @@ watch(
} }
} }
) )
watch( watch(
() => [form.licencePlate, form.carrierId, vehicles.value], () => [form.licencePlate, form.carrierId, vehicles.value],
() => { () => {
@@ -442,7 +459,6 @@ watch(
} }
} }
) )
const buildDesiredBovinShipments = () => { const buildDesiredBovinShipments = () => {
return bovineShipment.value return bovineShipment.value
.map((type) => { .map((type) => {
@@ -455,17 +471,20 @@ const buildDesiredBovinShipments = () => {
}) })
.filter((entry) => entry.quantity > 0) .filter((entry) => entry.quantity > 0)
} }
const syncBovinShipments = async (
const syncBovinShipments = async (shipmentId: number) => { shipmentId: number,
existing: Array<{ id?: number; nbBovinSend: number | null; shipmentType?: unknown }> = []
) => {
const shipmentIri = `/api/shipments/${shipmentId}` const shipmentIri = `/api/shipments/${shipmentId}`
const existing = await getBovinShipmentList(shipmentIri)
const desired = buildDesiredBovinShipments() const desired = buildDesiredBovinShipments()
const desiredByTypeId = new Map<number, number>() const desiredByTypeId = new Map<number, number>()
for (const entry of desired) { for (const entry of desired) {
desiredByTypeId.set(entry.type.id, entry.quantity) desiredByTypeId.set(entry.type.id, entry.quantity)
} }
for (const entry of existing) { for (const entry of existing) {
if (!entry.id) {
continue
}
const rawType = entry.shipmentType const rawType = entry.shipmentType
let typeId: number | null = null let typeId: number | null = null
if (rawType && typeof rawType === 'object' && 'id' in rawType) { if (rawType && typeof rawType === 'object' && 'id' in rawType) {
@@ -496,15 +515,15 @@ const syncBovinShipments = async (shipmentId: number) => {
}) })
} }
} }
const buildPayload = () => {
// Valide le formulaire et crée/met à jour l'expédition
const validate = async () => {
const normalizedLicensePlate = form.licencePlate.trim() const normalizedLicensePlate = form.licencePlate.trim()
const normalizedShipmentDate = form.shipmentDate.trim() const normalizedShipmentDate = form.shipmentDate.trim()
const normalizedCustomerId = form.customerId.trim() const normalizedCustomerId = form.customerId.trim()
const normalizedTruckId = form.truckId.trim() const normalizedTruckId = form.truckId.trim()
const normalizedCarrierId = form.carrierId.trim() const normalizedCarrierId = form.carrierId.trim()
const normalizedDriverId = form.driverId.trim()
const normalizedUserId = form.userId.trim()
const normalizedAddressId = form.addressId.trim()
const customerIri = normalizedCustomerId const customerIri = normalizedCustomerId
? `/api/customers/${normalizedCustomerId}` ? `/api/customers/${normalizedCustomerId}`
: null : null
@@ -514,31 +533,73 @@ const validate = async () => {
const carrierIri = normalizedCarrierId const carrierIri = normalizedCarrierId
? `/api/carriers/${normalizedCarrierId}` ? `/api/carriers/${normalizedCarrierId}`
: null : null
const userIri = normalizedUserId
? `/api/users/${normalizedUserId}`
: null
const driverIri = normalizedDriverId
? `/api/drivers/${normalizedDriverId}`
: null
const addressIri = normalizedAddressId
? `/api/addresses/${normalizedAddressId}`
: null
const payload = { return {
licencePlate: normalizedLicensePlate, licencePlate: normalizedLicensePlate,
shipmentDate: normalizedShipmentDate, shipmentDate: normalizedShipmentDate,
customer: customerIri, customer: customerIri,
truck: truckIri, truck: truckIri,
carrier: carrierIri carrier: carrierIri,
driver: driverIri,
user: userIri,
address: addressIri
} }
}
const saveDraft = async () => {
const payload = buildPayload()
if (!shipmentStore.current) {
const created = await shipmentStore.createShipment({
currentStep: 0,
...payload
})
if (created) {
await syncBovinShipments(created.id, [])
}
return
}
await shipmentStore.updateShipment(shipmentStore.current.id, {
currentStep: shipmentStore.current.currentStep,
...payload
})
await syncBovinShipments(
shipmentStore.current.id,
shipmentStore.current?.bovinShipments ?? []
)
}
defineExpose({saveDraft})
// Valide le formulaire et crée/met à jour l'expédition
const validate = async () => {
const payload = buildPayload()
if (!shipmentStore.current) { if (!shipmentStore.current) {
const created = await shipmentStore.createShipment({ const created = await shipmentStore.createShipment({
currentStep: 1, currentStep: 1,
...payload ...payload
}) })
if (created) { if (created) {
await syncBovinShipments(created.id) await shipmentStore.loadShipment(created.id)
await syncBovinShipments(created.id, shipmentStore.current?.bovinShipments ?? [])
await router.push(`/shipment/${created.id}`) await router.push(`/shipment/${created.id}`)
} }
return return
} }
const nextStep = shipmentStore.current.currentStep + 1 const nextStep = shipmentStore.current.currentStep + 1
await shipmentStore.updateShipment(shipmentStore.current.id, { await shipmentStore.updateShipment(shipmentStore.current.id, {
currentStep: nextStep, currentStep: nextStep,
...payload ...payload
}) })
await syncBovinShipments(shipmentStore.current.id) await shipmentStore.loadShipment(shipmentStore.current.id)
await syncBovinShipments(shipmentStore.current.id, shipmentStore.current?.bovinShipments ?? [])
} }
</script> </script>

View File

@@ -0,0 +1,101 @@
<template>
<div class="flex justify-center">
<div class="flex flex-col items-center w-[660px]">
<h1 class="font-bold text-5xl uppercase">{{ title }}</h1>
<!--@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>
<div
v-if="showLoadingBox"
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
<UiLoadingDots />
</div>
<div v-else-if="displayWeight !== null" class="w-full">
<div
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[25px] text-4xl">
{{ displayWeight }} kg
</div>
</div>
</div>
</div>
<div class="flex justify-center mt-[54px]">
<button
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
@click="fetchWeight"
>{{ displayWeight !== null ? 'refaire une pesee' : 'peser' }}</button>
<button
v-if="displayWeight !== null && !showGenerateReceipt"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
@click="saveWeight"
>Valider la pesée</button>
<button
v-if="showGenerateReceipt"
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px] ml-4"
@click="printReceipt"
>Générer le bon</button>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useWeighingShipment } from '~/composables/useWeighing'
import { usePdfPrinter } from '~/composables/usePdfPrinter'
import { useShipmentStore } from '~/stores/shipment'
const props = defineProps<{
mode: 'gross' | 'tare'
}>()
const router = useRouter()
const shipmentStore = useShipmentStore()
const { current: storeShipment } = storeToRefs(shipmentStore)
const { printPdf } = usePdfPrinter()
const {
displayWeight,
title,
showLoadingBox,
fetchWeight,
saveWeight
} = useWeighingShipment({
modeShipment: props.mode,
shipment: storeShipment,
updateShipment: shipmentStore.updateShipment,
loadShipment: shipmentStore.loadShipment
})
// Affiche le bouton de génération du bon à l'étape tare
const showGenerateReceipt = computed(
() => props.mode === 'tare' && displayWeight.value !== null
)
// Génère le bon d'expédition, puis clôture l'expédition
const printReceipt = async () => {
if (!import.meta.client || !shipmentStore.current) {
return
}
await saveWeight()
const shipment = shipmentStore.current
const filename = `${shipment.identificationNumber ?? shipment.id}_${shipment.customer?.label ?? 'client'}_${shipment.licencePlate ?? 'immat'}.pdf`
await printPdf(`/shipments/${shipment.id}/receipt`, filename)
// Laisse le temps a la boite de dialogue d'impression de s'ouvrir.
await new Promise((resolve) => setTimeout(resolve, 600))
const result = await shipmentStore.updateShipment(shipmentStore.current.id, {
isValid: true
})
if (!result) {
return
}
/* shipmentStore.clearCurrent()
await router.push('/')*/
}
// Récupère le poids dès l'arrivée sur l'écran
onMounted(() => {
if (displayWeight.value === null) {
fetchWeight()
}
})
</script>

View File

@@ -1,123 +0,0 @@
<template>
<form @submit.prevent="validate">
<div
class="flex items-center justify-between gap-10">
<h1 class="text-3xl font-bold uppercase">
{{ userId ? "Modifications de l'utilisateur" : "Ajout d'un utilisateur" }}
</h1>
<button
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
type="submit"
>
{{ userId ? 'Sauvegarder' : 'Ajouter' }}
</button>
</div>
<div class="grid gap-y-16 gap-x-40 mb-16">
<UiTextInput
id="user-name"
v-model="form.username"
label="Nom de l'utilisateur"
/>
<UiSelect
id="user-role"
v-model="form.role"
label="Rôle de l'utilisateur"
:options="ROLE"
/>
<UiTextInput
id="user-password"
v-model="form.password"
label="Mot de passe"
type="password"
/>
</div>
</form>
</template>
<script setup lang="ts">
import {computed, reactive, ref, watch} from 'vue'
import {ROLE} from '~/utils/constants'
import {createUser, updateUser, getUser} from '~/services/auth'
import type {UserData, UserFormData} from '~/services/dto/user-data'
const route = useRoute()
const router = useRouter()
const userId = computed(() => resolveUserId(route.params.id))
const isLoading = ref(false)
const isHydrating = ref(false)
const resolveUserId = (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<UserFormData>({
username: '',
password: '',
role: ''
})
const hydrateFromUser = (user: UserData | null) => {
if (!user) {
return
}
isHydrating.value = true
form.username = user.username ?? ''
const roles = user.roles ?? []
const hasAdmin = roles.includes("ROLE_ADMIN")
form.role = hasAdmin ? "ROLE_ADMIN" : "ROLE_USER"
form.password = ''
isHydrating.value = false
}
watch(
() => userId.value,
async (id) => {
if (id === null) {
return
}
isLoading.value = true
try {
const user = await getUser(id)
hydrateFromUser(user)
} finally {
isLoading.value = false
}
},
{immediate: true}
)
async function validate() {
const normalizedUsername = form.username.trim()
const normalizedRole = form.role.trim()
const normalizedPassword = form.password.trim()
const basePayload = {
username: normalizedUsername,
roles: normalizedRole ? [normalizedRole] : undefined,
password: normalizedPassword || undefined
}
if (userId.value) {
await updateUser(userId.value, basePayload)
await router.push(`/admin/user/list/`)
return
}
const created = await createUser(basePayload)
if (created) {
await router.push(`/admin/user/list/`)
}
}
</script>

View File

@@ -1,30 +1,26 @@
import {useApi} from '~/composables/useApi' import { useApi } from '~/composables/useApi'
export const usePdfPrinter = () => { export const usePdfPrinter = () => {
const api = useApi() const api = useApi()
const receptionStore = useReceptionStore()
const currentReception = receptionStore.current
const printPdf = async (url: string): Promise<void> => { const printPdf = async (url: string, filename = 'document.pdf'): Promise<void> => {
const blob = await api.getBlob(url); const blob = await api.getBlob(url)
const pdfBlob = blob.type === 'application/pdf' const pdfBlob = blob.type === 'application/pdf'
? blob ? blob
: new Blob([blob], { type: 'application/pdf' }); : new Blob([blob], { type: 'application/pdf' })
const blobUrl = URL.createObjectURL(pdfBlob); const blobUrl = URL.createObjectURL(pdfBlob)
const filename = `${currentReception.identificationNumber}_${currentReception.supplier.name}_${currentReception.licensePlate}.pdf`; const a = document.createElement('a')
a.href = blobUrl
const a = document.createElement('a'); a.download = filename
a.href = blobUrl; a.style.display = 'none'
a.download = filename; document.body.appendChild(a)
a.style.display = 'none'; a.click()
document.body.appendChild(a); a.remove()
a.click();
a.remove();
// L'ouverture dans un nouvel onglet déclenche un 2e PDF sans le nom personnalisé. // L'ouverture dans un nouvel onglet déclenche un 2e PDF sans le nom personnalisé.
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000); setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000)
} }
return { return {

View File

@@ -1,8 +1,10 @@
import type {Ref} from 'vue' import type {Ref} from 'vue'
import {computed, ref} from 'vue' import {computed, ref} from 'vue'
import type {ReceptionData, ReceptionPayload, WeightEntryData} from '~/services/dto/reception-data' import type {ReceptionData, ReceptionPayload, WeightEntryData} from '~/services/dto/reception-data'
import type {ShipmentData, ShipmentPayload, WeightShipmentEntryData } from '~/services/dto/shipment-data'
import type {WeightData} from '~/services/dto/weight-data' import type {WeightData} from '~/services/dto/weight-data'
import {getWeight} from '~/services/reception' import {getWeight} from '~/services/reception'
import {getWeightShipment} from '~/services/shipment'
import {createWeight, updateWeight} from '~/services/weight' import {createWeight, updateWeight} from '~/services/weight'
export type WeighingMode = 'gross' | 'tare' export type WeighingMode = 'gross' | 'tare'
@@ -14,6 +16,13 @@ type UseWeighingOptions = {
loadReception?: (id: number) => Promise<ReceptionData | null> loadReception?: (id: number) => Promise<ReceptionData | null>
} }
type UseWeighingShipmentOptions = {
modeShipment: WeighingMode
shipment: Ref<ShipmentData | null>
updateShipment: (id: number, payload: ShipmentPayload) => Promise<ShipmentData | null>
loadShipment?: (id: number) => Promise<ShipmentData | null>
}
export const useWeighing = ({ export const useWeighing = ({
mode, mode,
reception, reception,
@@ -97,3 +106,87 @@ export const useWeighing = ({
saveWeight saveWeight
} }
} }
export const useWeighingShipment = ({
modeShipment,
shipment,
updateShipment,
loadShipment
}: UseWeighingShipmentOptions) => {
const weightData = ref<WeightData | null>(null)
const isFetching = ref(false)
const currentWeightEntry = computed<WeightShipmentEntryData | null>(() => {
const weights = shipment.value?.weights ?? []
return weights.find((entry) => entry.type === modeShipment) ?? null
})
const displayWeight = computed(() => weightData.value?.weight ?? currentWeightEntry.value?.weight ?? null)
const displayDsd = computed(() => weightData.value?.dsd ?? currentWeightEntry.value?.dsd ?? '-')
const title = computed(() => (modeShipment === 'gross' ? 'Pesée à plein' : 'Pesée à vide'))
const showLoadingBox = computed(
() => isFetching.value || (displayWeight.value === null && currentWeightEntry.value === null)
)
const fetchWeight = async () => {
isFetching.value = true
weightData.value = await getWeightShipment().finally(() => {
isFetching.value = false
})
}
const saveWeight = async () => {
if (!shipment.value) {
return
}
const existingEntry = currentWeightEntry.value
const baseDsd = weightData.value?.dsd ?? existingEntry?.dsd ?? null
const baseWeight = weightData.value?.weight ?? existingEntry?.weight ?? null
const baseWeighedAt = weightData.value?.weighedAt ?? existingEntry?.weighedAt ?? null
if (baseWeight === null) {
return
}
if (existingEntry?.id) {
await updateWeight(existingEntry.id, {
type: modeShipment,
dsd: baseDsd,
weight: baseWeight,
weighedAt: baseWeighedAt
})
} else {
await createWeight({
shipment: `api/shipments/${shipment.value.id}`,
type: modeShipment,
dsd: baseDsd,
weight: baseWeight,
weighedAt: baseWeighedAt
})
}
const nextStep = modeShipment === 'tare'
? shipment.value.currentStep
: shipment.value.currentStep + 1
await updateShipment(shipment.value.id, {
currentStep: nextStep,
isValid: shipment.value.isValid
})
if (loadShipment) {
await loadShipment(shipment.value.id)
}
}
return {
weightData,
currentWeightEntry,
displayWeight,
displayDsd,
title,
showLoadingBox,
fetchWeight,
saveWeight
}
}

View File

@@ -21,6 +21,15 @@
"update": "Impossible de mettre à jour l'éxpeditions.", "update": "Impossible de mettre à jour l'éxpeditions.",
"weigh": "Impossible de récupérer la pesée." "weigh": "Impossible de récupérer la pesée."
}, },
"shipmentBovine": {
"list": "Impossible de récupérer la liste des bovins de l'éxpedition.",
"create": "Impossible d'enregistrer le bovin.",
"delete": "Impossible de supprimer le bovin.",
"update": "Impossible de mettre à jour le bovin."
},
"shipmentType": {
"list": "Impossible de récupérer la liste des types d'éxpedition."
},
"receptionType": { "receptionType": {
"list": "Impossible de récupérer la liste des types de réception." "list": "Impossible de récupérer la liste des types de réception."
}, },

View File

@@ -1,7 +1,5 @@
<template> <template>
<AdminUserForm/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
layout: 'admin' layout: 'admin'

View File

@@ -1,8 +1,125 @@
<template> <template>
<UserForm/> <form @submit.prevent="validate">
<div
class="flex items-center justify-between gap-10">
<h1 class="text-3xl font-bold uppercase">
{{ userId ? "Modifications de l'utilisateur" : "Ajout d'un utilisateur" }}
</h1>
<button
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
type="submit"
>
{{ userId ? 'Sauvegarder' : 'Ajouter' }}
</button>
</div>
<div class="grid gap-y-16 gap-x-40 mb-16">
<UiTextInput
id="user-name"
v-model="form.username"
label="Nom de l'utilisateur"
/>
<UiSelect
id="user-role"
v-model="form.role"
label="Rôle de l'utilisateur"
:options="ROLE"
/>
<UiTextInput
id="user-password"
v-model="form.password"
label="Mot de passe"
type="password"
/>
</div>
</form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
definePageMeta({ definePageMeta({
layout: 'admin' layout: 'admin'
}) })
import {computed, reactive, ref, watch} from 'vue'
import {ROLE} from '~/utils/constants'
import {createUser, updateUser, getUser} from '~/services/auth'
import type {UserData, UserFormData} from '~/services/dto/user-data'
const route = useRoute()
const router = useRouter()
const userId = computed(() => resolveUserId(route.params.id))
const isLoading = ref(false)
const isHydrating = ref(false)
const resolveUserId = (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<UserFormData>({
username: '',
password: '',
role: ''
})
const hydrateFromUser = (user: UserData | null) => {
if (!user) {
return
}
isHydrating.value = true
form.username = user.username ?? ''
const roles = user.roles ?? []
const hasAdmin = roles.includes("ROLE_ADMIN")
form.role = hasAdmin ? "ROLE_ADMIN" : "ROLE_USER"
form.password = ''
isHydrating.value = false
}
watch(
() => userId.value,
async (id) => {
if (id === null) {
return
}
isLoading.value = true
try {
const user = await getUser(id)
hydrateFromUser(user)
} finally {
isLoading.value = false
}
},
{immediate: true}
)
async function validate() {
const normalizedUsername = form.username.trim()
const normalizedRole = form.role.trim()
const normalizedPassword = form.password.trim()
const basePayload = {
username: normalizedUsername,
roles: normalizedRole ? [normalizedRole] : undefined,
password: normalizedPassword || undefined
}
if (userId.value) {
await updateUser(userId.value, basePayload)
await router.push(`/admin/user/list/`)
return
}
const created = await createUser(basePayload)
if (created) {
await router.push(`/admin/user/list/`)
}
}
</script> </script>

View File

@@ -137,7 +137,7 @@ import type {DriverData} from '~/services/dto/driver-data'
import {getDriverList} from '~/services/driver' import {getDriverList} from '~/services/driver'
import type {VehicleData} from '~/services/dto/vehicle-data' import type {VehicleData} from '~/services/dto/vehicle-data'
import {getVehicleList} from '~/services/vehicle' import {getVehicleList} from '~/services/vehicle'
import {SUPLLIER_CODE} from "~/utils/constants"; import {SUPPLIER_CODE} from "~/utils/constants";
import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine"; import {deleteReceptionBovine, getReceptionBovineList} from "~/services/reception-bovine";
import type {ReceptionData, ReceptionFormData} from "~/services/dto/reception-data"; import type {ReceptionData, ReceptionFormData} from "~/services/dto/reception-data";
import {getReception} from "~/services/reception"; import {getReception} from "~/services/reception";
@@ -185,7 +185,7 @@ const selectedCarrier = computed(() =>
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
) )
// Indique si le transporteur est LIOT // Indique si le transporteur est LIOT
const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPLLIER_CODE.LIOT) const isLiotCarrier = computed(() => selectedCarrier.value?.code === SUPPLIER_CODE.LIOT)
// Adresses disponibles pour le fournisseur sélectionné // Adresses disponibles pour le fournisseur sélectionné
const supplierAddresses = computed(() => { const supplierAddresses = computed(() => {
const supplierId = Number(form.supplierId) const supplierId = Number(form.supplierId)

View File

@@ -16,11 +16,10 @@
>Mettre en attente >Mettre en attente
</button> </button>
</div> </div>
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0"/> <ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/>
<button <ShipmentWeight v-if="storeShipment?.currentStep === 1" mode="gross"/>
v-if="storeShipment?.currentStep === 1"> <ShipmentWeight v-if="storeShipment?.currentStep >= 2" mode="tare"/>
TEST ETAPE 2
</button>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -28,9 +27,10 @@
import {SHIPMENT_STEP_LABELS} from "~/constants/steps"; import {SHIPMENT_STEP_LABELS} from "~/constants/steps";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import {useShipmentStore} from "~/stores/shipment"; import {useShipmentStore} from "~/stores/shipment";
import { ref, watch } from 'vue'
const shipmentStore = useShipmentStore() const shipmentStore = useShipmentStore()
const {current: storeShipment} = storeToRefs(shipmentStore) const {current: storeShipment} = storeToRefs(shipmentStore)
const shipmentFormRef = ref<{ saveDraft: () => Promise<void> } | null>(null)
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -58,15 +58,9 @@ watch (
) )
const saveAndHold = async () => { const saveAndHold = async () => {
if (!shipmentStore.current) { if (shipmentFormRef.value) {
await router.push('/') await shipmentFormRef.value.saveDraft()
return
} }
await shipmentStore.updateShipment(shipmentStore.current.id, {
currentStep: shipmentStore.current.currentStep,
licencePlate: shipmentStore.current.licencePlate,
shipmentDate: shipmentStore.current.shipmentDate
})
await router.push('/') await router.push('/')
} }
const handleStepSelect = async (step: number) => { const handleStepSelect = async (step: number) => {

View File

@@ -25,6 +25,16 @@ export type ShipmentData = {
truck?: TruckData | null truck?: TruckData | null
customer?: CustomerData | null customer?: CustomerData | null
bovinShipments?: BovinShipmentData[] | null bovinShipments?: BovinShipmentData[] | null
weights?: WeightShipmentEntryData[] | null
}
export interface WeightShipmentEntryData {
id?: number
type: 'gross' | 'tare'
dsd: number | null
weight: number | null
weighedAt: string | null
} }
export type ShipmentFormData = { export type ShipmentFormData = {
@@ -48,4 +58,8 @@ export type ShipmentPayload = {
truck?: string | null truck?: string | null
customer?: string | null customer?: string | null
bovinShipments?: string[] | null bovinShipments?: string[] | null
address?: string | null
user?: string | null
driver?: string | null
} }

View File

@@ -9,7 +9,7 @@ export type ShipmentTypeListResponse =
export async function getShipmentTypeList(): Promise<ShipmentTypeData[]> { export async function getShipmentTypeList(): Promise<ShipmentTypeData[]> {
const api = useApi() const api = useApi()
const response = await api.get<ShipmentTypeListResponse>('shipment_types', {}, { const response = await api.get<ShipmentTypeListResponse>('shipment_types', {}, {
toastErrorKey: 'errors.shipment_type.list' toastErrorKey: 'errors.shipmentType.list'
}) })
if (Array.isArray(response)) { if (Array.isArray(response)) {

View File

@@ -32,7 +32,7 @@ export async function updateShipment(id: number, payload: ShipmentPayload) {
}) })
} }
export async function getWeight(): Promise<WeightData> { export async function getWeightShipment(): Promise<WeightData> {
const api = useApi() const api = useApi()
return api.get<WeightData>('shipments/weigh', {}, { return api.get<WeightData>('shipments/weigh', {}, {
toastErrorKey: 'errors.shipment.weigh' toastErrorKey: 'errors.shipment.weigh'

View File

@@ -2,7 +2,8 @@ import { useApi } from '~/composables/useApi'
import type { WeightEntryData } from '~/services/dto/reception-data' import type { WeightEntryData } from '~/services/dto/reception-data'
export type WeightPayload = { export type WeightPayload = {
reception: string reception?: string
shipment?: string
type: 'gross' | 'tare' type: 'gross' | 'tare'
dsd: number | null dsd: number | null
weight: number | null weight: number | null

View File

@@ -12,6 +12,6 @@ export const ROLE = [
{ label: 'Administrateur', value: 'ROLE_ADMIN' }, { label: 'Administrateur', value: 'ROLE_ADMIN' },
{ label: 'Utilisateur', value: 'ROLE_USER' } { label: 'Utilisateur', value: 'ROLE_USER' }
] ]
export const SUPLLIER_CODE = { export const SUPPLIER_CODE = {
LIOT: 'LIOT' LIOT: 'LIOT'
} }

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260211075656 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE UNIQUE INDEX uniq_bovin_shipment ON bovin_shipment (shipment_id, shipment_type_id)');
$this->addSql('ALTER TABLE shipment ADD user_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE shipment ADD driver_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE shipment ADD address_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCA76ED395 FOREIGN KEY (user_id) REFERENCES public."user" (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCC3423909 FOREIGN KEY (driver_id) REFERENCES driver (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCF5B7AF75 FOREIGN KEY (address_id) REFERENCES address (id) NOT DEFERRABLE');
$this->addSql('CREATE INDEX IDX_2CB20DCA76ED395 ON shipment (user_id)');
$this->addSql('CREATE INDEX IDX_2CB20DCC3423909 ON shipment (driver_id)');
$this->addSql('CREATE INDEX IDX_2CB20DCF5B7AF75 ON shipment (address_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP INDEX uniq_bovin_shipment');
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCA76ED395');
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCC3423909');
$this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCF5B7AF75');
$this->addSql('DROP INDEX IDX_2CB20DCA76ED395');
$this->addSql('DROP INDEX IDX_2CB20DCC3423909');
$this->addSql('DROP INDEX IDX_2CB20DCF5B7AF75');
$this->addSql('ALTER TABLE shipment DROP user_id');
$this->addSql('ALTER TABLE shipment DROP driver_id');
$this->addSql('ALTER TABLE shipment DROP address_id');
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260211123000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Allow weight to belong to reception or shipment.';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE weight ALTER COLUMN reception_id DROP NOT NULL');
$this->addSql('ALTER TABLE weight ADD shipment_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE weight ADD CONSTRAINT FK_WEIGHT_SHIPMENT FOREIGN KEY (shipment_id) REFERENCES shipment (id) NOT DEFERRABLE');
$this->addSql('CREATE INDEX IDX_WEIGHT_SHIPMENT ON weight (shipment_id)');
$this->addSql('CREATE UNIQUE INDEX uniq_weight_reception_type ON weight (reception_id, type)');
$this->addSql('CREATE UNIQUE INDEX uniq_weight_shipment_type ON weight (shipment_id, type)');
$this->addSql('ALTER TABLE weight ADD CONSTRAINT chk_weight_reception_or_shipment CHECK ((reception_id IS NOT NULL AND shipment_id IS NULL) OR (reception_id IS NULL AND shipment_id IS NOT NULL))');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE weight DROP CONSTRAINT chk_weight_reception_or_shipment');
$this->addSql('DROP INDEX uniq_weight_shipment_type');
$this->addSql('DROP INDEX uniq_weight_reception_type');
$this->addSql('DROP INDEX IDX_WEIGHT_SHIPMENT');
$this->addSql('ALTER TABLE weight DROP CONSTRAINT FK_WEIGHT_SHIPMENT');
$this->addSql('ALTER TABLE weight DROP shipment_id');
$this->addSql('ALTER TABLE weight ALTER COLUMN reception_id SET NOT NULL');
}
}

View File

@@ -12,11 +12,11 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
final readonly class PontBasculeReading final readonly class PontBasculeReading
{ {
public function __construct( public function __construct(
#[Groups(['reception:weigh:read'])] #[Groups(['reception:weigh:read', 'shipment:weigh:read'])]
private ?int $dsd, private ?int $dsd,
#[Groups(['reception:weigh:read'])] #[Groups(['reception:weigh:read', 'shipment:weigh:read'])]
private ?float $weight, private ?float $weight,
#[Groups(['reception:weigh:read'])] #[Groups(['reception:weigh:read', 'shipment:weigh:read'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeImmutable $weighedAt = null, private ?DateTimeImmutable $weighedAt = null,
) {} ) {}

View File

@@ -31,7 +31,7 @@ class Address
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column] #[ORM\Column]
#[Groups(['address:read', 'supplier:read'])] #[Groups(['address:read', 'supplier:read', 'customer:read', 'shipment:read'])]
private ?int $id = null; private ?int $id = null;
#[ORM\Column(length: 120)] #[ORM\Column(length: 120)]
@@ -64,9 +64,16 @@ class Address
#[ORM\ManyToMany(targetEntity: Supplier::class, mappedBy: 'addresses')] #[ORM\ManyToMany(targetEntity: Supplier::class, mappedBy: 'addresses')]
private Collection $suppliers; private Collection $suppliers;
/**
* @var Collection<int, Shipment>
*/
#[ORM\OneToMany(targetEntity: Shipment::class, mappedBy: 'address')]
private Collection $shipments;
public function __construct() public function __construct()
{ {
$this->suppliers = new ArrayCollection(); $this->suppliers = new ArrayCollection();
$this->shipments = new ArrayCollection();
} }
public function getId(): ?int public function getId(): ?int
@@ -146,7 +153,7 @@ class Address
return $this; return $this;
} }
#[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read'])] #[Groups(['address:read', 'supplier:read', 'reception:read', 'shipment:read', 'customer:read'])]
public function getFullAddress(): string public function getFullAddress(): string
{ {
$parts = array_filter([ $parts = array_filter([
@@ -165,4 +172,34 @@ class Address
{ {
return $this->suppliers; return $this->suppliers;
} }
/**
* @return Collection<int, Shipment>
*/
public function getShipments(): Collection
{
return $this->shipments;
}
public function addShipment(Shipment $shipment): static
{
if (!$this->shipments->contains($shipment)) {
$this->shipments->add($shipment);
$shipment->setAddress($this);
}
return $this;
}
public function removeShipment(Shipment $shipment): static
{
if ($this->shipments->removeElement($shipment)) {
// set the owning side to null (unless already changed)
if ($shipment->getAddress() === $this) {
$shipment->setAddress(null);
}
}
return $this;
}
} }

View File

@@ -30,11 +30,11 @@ class Driver
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column] #[ORM\Column]
#[Groups(['driver:read', 'reception:read'])] #[Groups(['driver:read', 'reception:read', 'shipment:read'])]
private ?int $id = null; private ?int $id = null;
#[ORM\Column(length: 180)] #[ORM\Column(length: 180)]
#[Groups(['driver:read', 'reception:read'])] #[Groups(['driver:read', 'reception:read', 'shipment:read'])]
private string $name = ''; private string $name = '';
#[ORM\ManyToOne] #[ORM\ManyToOne]

View File

@@ -10,15 +10,21 @@ use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
use App\Dto\PontBasculeReading;
use App\State\ShipmentReceiptProvider;
use App\State\ShipmentWeighingProvider;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Context; use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups; use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
#[ORM\Entity] #[ORM\Entity]
#[ORM\HasLifecycleCallbacks]
#[ORM\Table(name: 'shipment')] #[ORM\Table(name: 'shipment')]
#[ApiResource( #[ApiResource(
operations: [ operations: [
@@ -38,27 +44,28 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
normalizationContext: ['groups' => ['shipment:read']], normalizationContext: ['groups' => ['shipment:read']],
denormalizationContext: ['groups' => ['shipment:write']], denormalizationContext: ['groups' => ['shipment:write']],
), ),
// new Get( new Get(
// uriTemplate: '/shipments/weigh', uriTemplate: '/shipments/weigh',
// openapi: new OpenApiOperation( openapi: new OpenApiOperation(
// summary: 'Fetch the current weight reading', summary: 'Fetch the current weight reading',
// description: 'Queries the pont-bascule and returns the weight data.', description: 'Queries the pont-bascule and returns the weight data.',
// ), ),
// normalizationContext: ['groups' => ['shipment:weigh:read']], normalizationContext: ['groups' => ['shipment:weigh:read']],
// output: PontBasculeReading::class, output: PontBasculeReading::class,
// provider: shipmentWeighingProvider::class, provider: ShipmentWeighingProvider::class,
// ), ),
// new Get( new Get(
// uriTemplate: '/shipments/{id}/receipt', uriTemplate: '/shipments/{id}/receipt',
// requirements: ['id' => '\d+'], requirements: ['id' => '\d+'],
// openapi: new OpenApiOperation( openapi: new OpenApiOperation(
// summary: 'Render a shipment receipt', summary: 'Render a shipment receipt',
// description: 'Returns a PDF receipt for the shipment.', description: 'Returns a PDF receipt for the shipment.',
// ), ),
// output: false, output: false,
// provider: shipmentReceiptProvider::class, provider: ShipmentReceiptProvider::class,
// ), ),
], ],
security: "is_granted('ROLE_USER')",
)] )]
class Shipment class Shipment
{ {
@@ -84,6 +91,12 @@ class Shipment
#[Groups(['shipment:read', 'shipment:write'])] #[Groups(['shipment:read', 'shipment:write'])]
private bool $isValid = false; private bool $isValid = false;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true)]
#[Groups(['shipment:read', 'shipment:write'])]
#[ApiProperty(readableLink: true)]
private ?User $user = null;
#[ORM\Column(name: 'shipment_date', type: 'datetime_immutable')] #[ORM\Column(name: 'shipment_date', type: 'datetime_immutable')]
#[Groups(['shipment:read', 'shipment:write'])] #[Groups(['shipment:read', 'shipment:write'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
@@ -102,19 +115,46 @@ class Shipment
private ?Truck $truck = null; private ?Truck $truck = null;
#[ORM\ManyToOne] #[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true)]
#[Groups(['shipment:read', 'shipment:write'])] #[Groups(['shipment:read', 'shipment:write'])]
#[ApiProperty(readableLink: true)]
private ?Customer $customer = null; private ?Customer $customer = null;
/** /**
* @var Collection<int, BovinShipment> * @var Collection<int, BovinShipment>
*/ */
#[ORM\OneToMany(targetEntity: BovinShipment::class, mappedBy: 'shipment')] #[ORM\OneToMany(
targetEntity: BovinShipment::class,
mappedBy: 'shipment',
cascade: ['persist', 'remove'],
orphanRemoval: true
)]
#[Groups(['shipment:read', 'shipment:write'])] #[Groups(['shipment:read', 'shipment:write'])]
private Collection $bovinShipments; private Collection $bovinShipments;
/**
* @var Collection<int, Weight>
*/
#[ORM\OneToMany(targetEntity: Weight::class, mappedBy: 'shipment', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[Groups(['shipment:read'])]
private Collection $weights;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true)]
#[Groups(['shipment:read', 'shipment:write'])]
#[ApiProperty(readableLink: true)]
private ?Driver $driver = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: true)]
#[Groups(['shipment:read', 'shipment:write'])]
#[ApiProperty(readableLink: true)]
private ?Address $address = null;
public function __construct() public function __construct()
{ {
$this->bovinShipments = new ArrayCollection(); $this->bovinShipments = new ArrayCollection();
$this->weights = new ArrayCollection();
} }
public function getId(): ?int public function getId(): ?int
@@ -157,6 +197,22 @@ class Shipment
return $this->isValid; return $this->isValid;
} }
#[Groups(['shipment:read'])]
public function isValid(): bool
{
return $this->isValid;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): void
{
$this->user = $user;
}
public function setIsValid(?bool $isValid): void public function setIsValid(?bool $isValid): void
{ {
$this->isValid = $isValid; $this->isValid = $isValid;
@@ -182,16 +238,6 @@ class Shipment
$this->carrier = $carrier; $this->carrier = $carrier;
} }
public function getVehicle(): ?Vehicle
{
return $this->vehicle;
}
public function setVehicle(?Vehicle $vehicle): void
{
$this->vehicle = $vehicle;
}
public function getTruck(): ?Truck public function getTruck(): ?Truck
{ {
return $this->truck; return $this->truck;
@@ -222,24 +268,99 @@ class Shipment
$this->bovinShipments = $bovinShipments; $this->bovinShipments = $bovinShipments;
} }
public function addPelletBuilding(BovinShipment $bovinShipments): self public function addBovinShipment(BovinShipment $bovinShipment): self
{ {
if (!$this->bovinShipments->contains($bovinShipments)) { if (!$this->bovinShipments->contains($bovinShipment)) {
$this->bovinShipments->add($bovinShipments); $this->bovinShipments->add($bovinShipment);
$bovinShipments->setReception($this); $bovinShipment->setShipment($this);
} }
return $this; return $this;
} }
public function removePelletBuilding(BovinShipment $bovinShipments): self public function removeBovinShipment(BovinShipment $bovinShipment): self
{ {
if ($this->bovinShipments->removeElement($bovinShipments)) { if ($this->bovinShipments->removeElement($bovinShipment)) {
if ($bovinShipments->getReception() === $this) { if ($bovinShipment->getShipment() === $this) {
$bovinShipments->setReception(null); $bovinShipment->setShipment(null);
} }
} }
return $this; return $this;
} }
/**
* @return Collection<int, Weight>
*/
public function getWeights(): Collection
{
return $this->weights;
}
public function addWeight(Weight $weight): void
{
if (!$this->weights->contains($weight)) {
$this->weights->add($weight);
$weight->setShipment($this);
}
}
public function removeWeight(Weight $weight): void
{
if ($this->weights->removeElement($weight)) {
if ($weight->getShipment() === $this) {
$weight->setShipment(null);
}
}
}
#[ORM\PostPersist]
public function initializeIdentificationNumber(PostPersistEventArgs $args): void
{
if (null !== $this->identificationNumber) {
return;
}
if (null === $this->id) {
return;
}
$number = sprintf('P-BR-%04d', $this->id);
$this->identificationNumber = $number;
$args->getObjectManager()
->getConnection()
->executeStatement(
'UPDATE shipment SET identification_number = :number WHERE id = :id',
[
'number' => $number,
'id' => $this->id,
]
)
;
}
public function getDriver(): ?Driver
{
return $this->driver;
}
public function setDriver(?Driver $driver): static
{
$this->driver = $driver;
return $this;
}
public function getAddress(): ?Address
{
return $this->address;
}
public function setAddress(?Address $address): static
{
$this->address = $address;
return $this;
}
} }

View File

@@ -29,11 +29,11 @@ class Truck
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column] #[ORM\Column]
#[Groups(['truck:read', 'vehicle:read', 'reception:read'])] #[Groups(['truck:read', 'vehicle:read', 'reception:read', 'shipment:read'])]
private ?int $id = null; private ?int $id = null;
#[ORM\Column(length: 180)] #[ORM\Column(length: 180)]
#[Groups(['truck:read', 'vehicle:read', 'reception:read'])] #[Groups(['truck:read', 'vehicle:read', 'reception:read', 'shipment:read'])]
private string $name = ''; private string $name = '';
public function getId(): ?int public function getId(): ?int

View File

@@ -61,11 +61,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')] #[ORM\Column(type: 'integer')]
#[Groups(['user:read', 'user-login:read', 'reception:read'])] #[Groups(['user:read', 'user-login:read', 'reception:read', 'shipment:read'])]
private ?int $id = null; private ?int $id = null;
#[ORM\Column(length: 180, unique: true)] #[ORM\Column(length: 180, unique: true)]
#[Groups(['user:read', 'user:write', 'user-login:read', 'reception:read'])] #[Groups(['user:read', 'user:write', 'user-login:read', 'reception:read', 'shipment:read'])]
private string $username = ''; private string $username = '';
#[ORM\Column(type: 'json')] #[ORM\Column(type: 'json')]

View File

@@ -35,36 +35,46 @@ use Symfony\Component\Validator\Constraints as Assert;
security: "is_granted('ROLE_USER')", security: "is_granted('ROLE_USER')",
)] )]
#[UniqueEntity(fields: ['reception', 'type'], message: 'A weighing already exists for this type.')] #[UniqueEntity(fields: ['reception', 'type'], message: 'A weighing already exists for this type.')]
#[UniqueEntity(fields: ['shipment', 'type'], message: 'A weighing already exists for this type.')]
#[Assert\Expression(
'(this.getReception() !== null and this.getShipment() === null) or (this.getReception() === null and this.getShipment() !== null)',
message: 'Either reception or shipment must be set, but not both.'
)]
class Weight class Weight
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
#[ORM\Column] #[ORM\Column]
#[Groups(['reception:read', 'weight:read'])] #[Groups(['reception:read', 'shipment:read', 'weight:read'])]
private ?int $id = null; private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'weights')] #[ORM\ManyToOne(inversedBy: 'weights')]
#[ORM\JoinColumn(nullable: false)] #[ORM\JoinColumn(nullable: true)]
#[Groups(['weight:read', 'weight:write'])] #[Groups(['weight:read', 'weight:write'])]
private ?Reception $reception = null; private ?Reception $reception = null;
#[ORM\ManyToOne(inversedBy: 'weights')]
#[ORM\JoinColumn(nullable: true)]
#[Groups(['weight:read', 'weight:write'])]
private ?Shipment $shipment = null;
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
#[Groups(['reception:read', 'weight:read', 'weight:write'])] #[Groups(['reception:read', 'shipment:read', 'weight:read', 'weight:write'])]
#[Assert\PositiveOrZero] #[Assert\PositiveOrZero]
private ?int $dsd = null; private ?int $dsd = null;
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
#[Groups(['reception:read', 'weight:read', 'weight:write'])] #[Groups(['reception:read', 'shipment:read', 'weight:read', 'weight:write'])]
#[Assert\PositiveOrZero] #[Assert\PositiveOrZero]
private ?int $weight = null; private ?int $weight = null;
#[ORM\Column(type: 'datetime_immutable', nullable: true)] #[ORM\Column(type: 'datetime_immutable', nullable: true)]
#[Groups(['reception:read', 'weight:read', 'weight:write'])] #[Groups(['reception:read', 'shipment:read', 'weight:read', 'weight:write'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])] #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
private ?DateTimeImmutable $weighedAt = null; private ?DateTimeImmutable $weighedAt = null;
#[ORM\Column(length: 10)] #[ORM\Column(length: 10)]
#[Groups(['reception:read', 'weight:read', 'weight:write'])] #[Groups(['reception:read', 'shipment:read', 'weight:read', 'weight:write'])]
#[Assert\NotBlank] #[Assert\NotBlank]
#[Assert\Choice(choices: ['gross', 'tare'])] #[Assert\Choice(choices: ['gross', 'tare'])]
private string $type = 'gross'; private string $type = 'gross';
@@ -90,6 +100,22 @@ class Weight
return $this; return $this;
} }
public function getShipment(): ?Shipment
{
return $this->shipment;
}
public function setShipment(?Shipment $shipment): self
{
$this->shipment = $shipment;
if (null !== $shipment && !$shipment->getWeights()->contains($this)) {
$shipment->addWeight($this);
}
return $this;
}
public function getDsd(): ?int public function getDsd(): ?int
{ {
return $this->dsd; return $this->dsd;

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\Shipment;
use Doctrine\ORM\EntityManagerInterface;
use Dompdf\Dompdf;
use Dompdf\Options;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
final readonly class ShipmentReceiptProvider implements ProviderInterface
{
public function __construct(
private Environment $twig,
private EntityManagerInterface $entityManager,
) {}
/**
* @throws RuntimeError
* @throws SyntaxError
* @throws LoaderError
*/
public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response
{
$id = $uriVariables['id'] ?? null;
if (null === $id) {
throw new NotFoundHttpException('Shipment not found.');
}
$shipment = $this->entityManager->getRepository(Shipment::class)->find($id);
if (!$shipment instanceof Shipment) {
throw new NotFoundHttpException('Shipment not found.');
}
$options = new Options();
$options->set('isRemoteEnabled', true);
$dompdf = new Dompdf($options);
$html = $this->twig->render('shipment_voucher.html.twig', [
'shipment' => $shipment,
]);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4');
$dompdf->render();
$filename = sprintf('bon-expedition-%d.pdf', $shipment->getId());
return new Response($dompdf->output(), Response::HTTP_OK, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="'.$filename.'"',
]);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Dto\PontBasculeReading;
use App\Exception\PontBasculeException;
use App\Service\PontBasculeService;
use Symfony\Component\HttpKernel\Exception\HttpException;
final readonly class ShipmentWeighingProvider implements ProviderInterface
{
public function __construct(
private PontBasculeService $pontBasculeService,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?PontBasculeReading
{
try {
$result = $this->pontBasculeService->fetch();
} catch (PontBasculeException $exception) {
throw new HttpException(500, $exception->getMessage(), $exception);
}
return $result;
}
}

View File

@@ -141,16 +141,18 @@
<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>{{ reception.supplier.name }}</strong><br> <strong>{{ reception.supplier ? reception.supplier.name : '-' }}</strong><br>
<span>{{ reception.address.street }}</span><br> <span>{{ reception.address ? reception.address.street : '' }}</span><br>
{% if reception.address.street2 %} {% if reception.address and reception.address.street2 %}
<span>{{ reception.address.street2 }}</span><br> <span>{{ reception.address.street2 }}</span><br>
{% endif %} {% endif %}
{% if reception.address %}
<span>{{ reception.address.postalCode }} {{ reception.address.city }}</span><br> <span>{{ reception.address.postalCode }} {{ reception.address.city }}</span><br>
{% if reception.supplier.phone %} {% endif %}
{% if reception.supplier and reception.supplier.phone %}
<span>{{ reception.supplier.phone }}</span><br> <span>{{ reception.supplier.phone }}</span><br>
{% endif %} {% endif %}
{% if reception.supplier.email %} {% if reception.supplier and reception.supplier.email %}
<span>{{ reception.supplier.email}}</span><br> <span>{{ reception.supplier.email}}</span><br>
{% endif %} {% endif %}
</div> </div>
@@ -168,7 +170,9 @@
<th style="width:25%; text-align:center; white-space:nowrap;">N° réception</th> <th style="width:25%; text-align:center; white-space:nowrap;">N° réception</th>
</tr> </tr>
<tr> <tr>
<td style="width:55%; text-align:center;">{{ reception.supplier.name }}</td> <td style="width:55%; text-align:center;">
{{ reception.supplier ? reception.supplier.name : '-' }}
</td>
<td style="width:20%; text-align:center; white-space:nowrap;"> <td style="width:20%; text-align:center; white-space:nowrap;">
{{ reception.receptionDate|date('d/m/Y') }} {{ reception.receptionDate|date('d/m/Y') }}
</td> </td>
@@ -189,13 +193,11 @@
<tbody> <tbody>
<tr> <tr>
<td style="width:75%;"> <td style="width:75%;">
<strong>{{ reception.receptionType.label }}</strong><br><br> <strong>{{ reception.receptionType ? reception.receptionType.label : '-' }}</strong><br><br>
<div class="bigtable-notes"> <div class="bigtable-notes">
{% set grossWeight = null %} {% set grossWeight = null %}
{% set tareWeight = null %} {% set tareWeight = null %}
{% for weight in reception.weights|default([]) %}
{% for weight in reception.weights %}
{% if weight.type == 'gross' %} {% if weight.type == 'gross' %}
{% set grossWeight = weight %} {% set grossWeight = weight %}
<p>Poids à plein : {{ grossWeight.weight }}kg (pesée n°{{ grossWeight.dsd }} {{ grossWeight.weighedAt|date('d/m/Y H:i:s') }})</p> <p>Poids à plein : {{ grossWeight.weight }}kg (pesée n°{{ grossWeight.dsd }} {{ grossWeight.weighedAt|date('d/m/Y H:i:s') }})</p>
@@ -219,22 +221,33 @@
<tr class="border-bottom"> <tr class="border-bottom">
<td> <td>
<strong> <strong>
{% if reception.merchandiseType %} Type de bovins
{{ reception.merchandiseType.label }}
{% else %}
-
{% endif %}
</strong> </strong>
<br><br> <br><br>
<div class="bigtable-notes"> <div class="bigtable-notes">
{% if reception.receptionType and reception.receptionType.code == 'BOVINS' %}
{% if reception.bovinesTypes is not empty %}
{% for entry in reception.bovinesTypes %}
<p>
{{ entry.bovineType ? entry.bovineType.label : '-' }} : {{ entry.quantity ?? 0 }}
</p>
{% endfor %}
{% else %}
<p>-</p>
{% endif %}
{% if reception.bovineDetail %}
<p>Autres : {{ reception.bovineDetail }}</p>
{% endif %}
{% else %}
{% if reception.merchandiseType and reception.merchandiseType.code == 'AUTRES' and reception.merchandiseDetail %} {% if reception.merchandiseType and reception.merchandiseType.code == 'AUTRES' and reception.merchandiseDetail %}
<p><strong>Précision</strong> : {{ reception.merchandiseDetail }}</p> <p><strong>Précision</strong> : {{ reception.merchandiseDetail }}</p>
{% endif %} {% endif %}
{% if reception.merchandiseType and reception.merchandiseType.code == 'GRANULE' %} {% if reception.merchandiseType and reception.merchandiseType.code == 'GRANULE' %}
{% set pelletGroups = {} %} {% set pelletGroups = {} %}
{% for selection in reception.pelletBuildings %} {% for selection in reception.pelletBuildings|default([]) %}
{% set pelletLabel = selection.pelletType.label %} {% set pelletLabel = selection.pelletType.label %}
{% if pelletGroups[pelletLabel] is not defined %} {% if pelletGroups[pelletLabel] is not defined %}
{% set pelletGroups = pelletGroups|merge({ (pelletLabel): [] }) %} {% set pelletGroups = pelletGroups|merge({ (pelletLabel): [] }) %}
@@ -251,7 +264,7 @@
{% endfor %} {% endfor %}
{% else %} {% else %}
{% set buildingLabels = [] %} {% set buildingLabels = [] %}
{% for building in reception.buildings %} {% for building in reception.buildings|default([]) %}
{% set buildingLabels = buildingLabels|merge([building.label]) %} {% set buildingLabels = buildingLabels|merge([building.label]) %}
{% endfor %} {% endfor %}
{% if buildingLabels %} {% if buildingLabels %}
@@ -260,6 +273,7 @@
<p>Aucun bâtiment renseigné.</p> <p>Aucun bâtiment renseigné.</p>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %}
</div> </div>
</td> </td>
<td></td> <td></td>
@@ -273,9 +287,9 @@
<tr> <tr>
<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">
Transporteur : <strong>{{ reception.carrier.name }}</strong><br> Transporteur : <strong>{{ reception.carrier ? reception.carrier.name : '-' }}</strong><br>
Mode de livraison : <strong>{{ reception.truck.name }}</strong><br> Mode de livraison : <strong>{{ reception.truck ? reception.truck.name : '-' }}</strong><br>
Immatriculation : <strong>{{ reception.licensePlate }}</strong><br><br> Immatriculation : <strong>{{ reception.licensePlate ?? '-' }}</strong><br><br>
</div> </div>
</td> </td>

View File

@@ -0,0 +1,292 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<style>
@page {
margin: 56px 56px;
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
margin: 0;
color: #000;
}
p {
margin: 0;
}
em {
font-style: normal;
}
.company-block {
font-size: 14px;
text-align: left;
line-height: 1.3;
}
.box {
border: 1px solid #000;
border-radius: 10px;
padding: 10px;
font-size: 16px;
height: 80px;
}
.title {
text-align: center;
font-size: 18pt;
font-weight: 700;
margin: 64px 0 20px 0;
}
.info-table {
margin-bottom: 24px;
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.info-table th {
font-size: 16px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #333;
padding: 4px 6px;
vertical-align: top;
font-size: 12px;
}
th {
text-align: center;
font-weight: 700;
}
.layout, .layout td {
border: none !important;
padding: 0;
}
.bigtable-wrap {
border: 1px solid #000;
height: 360px;
margin-bottom: 10px;
}
.bigtable {
width: 100%;
height: 100%;
border: none;
border-collapse: collapse;
table-layout: fixed;
}
.bigtable th,
.bigtable td {
font-size: 16px;
border: 1px solid #333;
}
.bigtable thead th {
border-top: 0;
}
.bigtable tbody tr:last-child td {
border-bottom: 0;
}
.bigtable tr th:first-child,
.bigtable tr td:first-child {
border-left: 0;
}
.bigtable tr th:last-child,
.bigtable tr td:last-child {
border-right: 0;
}
.bigtable thead th {
border-bottom: 0;
}
.bigtable tbody tr:first-child td {
border-top: 1px solid #333;
}
.bigtable-notes {
font-size: 14px;
line-height: 1.25;
}
.border-bottom {
border-bottom: 1px solid #000;
}
.footer-block {
page-break-inside: avoid;
}
.signature-box {
height: 130px;
margin-bottom: 10px;
border: 0.5px solid #000;
padding: 6px 10px;
}
.meta {
font-size: 16px;
line-height: 1.35;
}
</style>
</head>
<body>
<!-- HEADER -->
<table class="layout" style="width:100%;">
<tr>
<td style="width:70%; vertical-align:top;">
<table class="layout" style="width:100%;">
<tr>
<td class="company-block" style="padding:0; border:none;">
<strong>SCEA LES NAUDS</strong><br>
14 Allée dArgenson<br>
Z.I Nord Secteur Est<br>
86100 CHATELLERAULT<br>
Tel. : 05 49 20 09 10<br>
Email : lpc.contacts@lpc-liot.fr<br>
RCS Châtellerault B 444 262 455
</td>
</tr>
</table>
</td>
<td style="width:30%; text-align:right; vertical-align:top; font-size: 14px;">
<div style="display:inline-block; width:75mm; line-height:1.3;">
<strong>{{ shipment.customer ? shipment.customer.label : '-' }}</strong><br>
<span>{{ shipment.address ? shipment.address.street : '' }}</span><br>
{% if shipment.address and shipment.address.street2 %}
<span>{{ shipment.address.street2 }}</span><br>
{% endif %}
{% if shipment.address %}
<span>{{ shipment.address.postalCode }} {{ shipment.address.city }}</span><br>
{% endif %}
</div>
</td>
</tr>
</table>
<div class="title">BON D'EXPEDITION</div>
<!-- INFOS (code/date/num) -->
<table class="info-table">
<tr>
<th style="width:55%; text-align:center;">Code client</th>
<th style="width:20%; text-align:center; white-space:nowrap;">Date</th>
<th style="width:25%; text-align:center; white-space:nowrap;">N° expédition</th>
</tr>
<tr>
<td style="width:55%; text-align:center;">
{{ shipment.customer ? shipment.customer.code : '-' }}
</td>
<td style="width:20%; text-align:center; white-space:nowrap;">
{{ shipment.shipmentDate|date('d/m/Y') }}
</td>
<td style="width:25%; text-align:center; white-space:nowrap;">
{{ shipment.identificationNumber ?? '-' }}
</td>
</tr>
</table>
<!-- GRAND TABLEAU -->
<div class="bigtable-wrap">
<table class="bigtable">
<thead>
<tr>
<th style="width:75%; text-align:center;">Désignation</th>
<th style="width:25%; text-align:center; white-space:nowrap;">Qté expédiée (kg)</th>
</tr>
</thead>
<tbody>
{% set grossWeight = null %}
{% set tareWeight = null %}
<tr>
<td style="width:75%;">
<strong>Expédition</strong><br><br>
<div class="bigtable-notes">
{% for weight in shipment.weights %}
{% if weight.type == 'gross' %}
{% set grossWeight = weight %}
<p>Poids à plein : {{ grossWeight.weight }}kg (pesée
{{ grossWeight.dsd }} {{ grossWeight.weighedAt|date('d/m/Y H:i:s') }})</p>
{% elseif weight.type == 'tare' %}
{% set tareWeight = weight %}
<p>Poids à vide : {{ tareWeight.weight }}kg (pesée
{{ tareWeight.dsd }} {{ tareWeight.weighedAt|date('d/m/Y H:i:s') }})</p>
{% endif %}
{% endfor %}
</div>
</td>
<td style="width:25%; text-align:center; white-space:nowrap;">
{% if grossWeight and tareWeight %}
{{ grossWeight.weight - tareWeight.weight }}
{% else %}
0
{% endif %}
</td>
</tr>
<tr class="border-bottom">
<td>
<strong>Bovin</strong><br><br>
<div class="bigtable-notes">
{% if shipment.bovinShipments is not empty %}
{% for entry in shipment.bovinShipments %}
<p>
{{ entry.shipmentType ? entry.shipmentType.label : '-' }} :
{{ entry.nbBovinSend ?? 0 }}
</p>
{% endfor %}
{% else %}
<p>-</p>
{% endif %}
</div>
</td>
<td style="width:25%; text-align:center; white-space:nowrap;">
</td>
</tr>
</tbody>
</table>
</div>
<!-- BAS : meta à gauche / signatures à droite -->
<table class="layout footer-block">
<tr>
<td style="width:60%; padding-right:8mm; vertical-align:top;">
<div class="meta">
<p>Transporteur : {{ shipment.carrier ? shipment.carrier.name : '-' }}</p>
<p>Mode de livraison : {{ shipment.truck ? shipment.truck.name : '-' }}</p>
<p>Immatriculation : {{ shipment.licencePlate ?? '-' }}</p>
</div>
</td>
<td style="width:40%; vertical-align:top;">
<div class="box signature-box">Signature les Nauds :</div>
<div class="box signature-box">Signature transporteur :</div>
</td>
</tr>
</table>
</body>
</html>