[#271]Créer une nouvelle expédition (étape 1) #12
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
101
frontend/components/shipment/shipment-weight.vue
Normal file
101
frontend/components/shipment/shipment-weight.vue
Normal 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>
|
||||||
@@ -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>
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
tristan
commented
Fait un type dans le dto wheight.ts Fait un type dans le dto wheight.ts
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AdminUserForm/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'admin'
|
layout: 'admin'
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
49
migrations/Version20260211075656.php
Normal file
49
migrations/Version20260211075656.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
38
migrations/Version20260211123000.php
Normal file
38
migrations/Version20260211123000.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
63
src/State/ShipmentReceiptProvider.php
Normal file
63
src/State/ShipmentReceiptProvider.php
Normal 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.'"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/State/ShipmentWeighingProvider.php
Normal file
30
src/State/ShipmentWeighingProvider.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -141,16 +141,18 @@
|
|||||||
|
|
||||||
|
tristan
commented
Pourquoi ce fichier est modifié ? Pourquoi ce fichier est modifié ?
|
|||||||
<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>
|
||||||
|
|
||||||
|
|||||||
292
templates/shipment_voucher.html.twig
Normal file
292
templates/shipment_voucher.html.twig
Normal 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 d’Argenson<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
|
||||||
|
n°{{ 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
|
||||||
|
n°{{ 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>
|
||||||
Reference in New Issue
Block a user
Supprime le commentaire