[#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
|
||||
* [#317] Admin modification creation transporteur
|
||||
* [#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
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ import type {DriverData} from '~/services/dto/driver-data'
|
||||
import {getDriverList} from '~/services/driver'
|
||||
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||
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 type {ReceptionFormData} from "~/services/dto/reception-data";
|
||||
|
||||
@@ -183,7 +183,7 @@ const selectedCarrier = computed(() =>
|
||||
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||
)
|
||||
// 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é
|
||||
const supplierAddresses = computed(() => {
|
||||
const supplierId = Number(form.supplierId)
|
||||
|
||||
@@ -74,7 +74,9 @@ const printReceipt = async () => {
|
||||
}
|
||||
|
||||
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.
|
||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<form @submit.prevent="validate">
|
||||
<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
|
||||
id="shipment-user"
|
||||
label="Nom de l'utisateur"
|
||||
v-model="form.userId"
|
||||
label="Nom de l'utilisateur"
|
||||
:options="users.map((user) => ({
|
||||
value: String(user.id),
|
||||
label: user.username
|
||||
@@ -12,14 +14,14 @@
|
||||
:loading="isLoadingUsers"
|
||||
wrapper-class="col-start-1 row-start-2"
|
||||
/>
|
||||
|
||||
<!-- Date de l'éxpedition -->
|
||||
<UiDateInput
|
||||
id="shipment-date"
|
||||
v-model="form.shipmentDate"
|
||||
label="Date du jour"
|
||||
wrapper-class="col-start-1 row-start-3"
|
||||
/>
|
||||
|
||||
<!-- Type d'expédition -->
|
||||
<div class="col-start-1 row-start-4">
|
||||
<label class="font-bold uppercase text-xl mb-2 block">
|
||||
Type d'expédition
|
||||
@@ -40,10 +42,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Client -->
|
||||
<UiSelect
|
||||
id="reception-driver"
|
||||
id="shipment-customer"
|
||||
v-model="form.customerId"
|
||||
label="Client"
|
||||
:options="customers.map((customer) => ({
|
||||
@@ -53,21 +54,18 @@
|
||||
:loading="isLoadingCustomers"
|
||||
wrapper-class="col-start-1 row-start-5"
|
||||
/>
|
||||
|
||||
<!-- Adresse du client -->
|
||||
<UiSelect
|
||||
id="reception-driver"
|
||||
id="shipment-address"
|
||||
v-model="form.addressId"
|
||||
:options="customerAddresses.map((address) => ({
|
||||
value: String(address.id),
|
||||
label: address.fullAddress
|
||||
}))"
|
||||
:options="customerAddressOptions"
|
||||
:disabled="isLoadingCustomers || customerAddresses.length === 0"
|
||||
label="Adresse"
|
||||
wrapper-class="col-start-2 row-start-1"
|
||||
/>
|
||||
|
||||
<!-- Camion -->
|
||||
<UiSelect
|
||||
id="reception-driver"
|
||||
id="shipment-truck"
|
||||
v-model="form.truckId"
|
||||
label="Camion"
|
||||
:options="trucks.map((truck) => ({
|
||||
@@ -77,9 +75,9 @@
|
||||
:loading="isLoadingTrucks"
|
||||
wrapper-class="col-start-2 row-start-2"
|
||||
/>
|
||||
|
||||
<!-- Transporteur -->
|
||||
<UiSelect
|
||||
id="reception-driver"
|
||||
id="shipment-carrier"
|
||||
v-model="form.carrierId"
|
||||
label="Transporteur"
|
||||
:options="carriers.map((carrier) => ({
|
||||
@@ -88,9 +86,9 @@
|
||||
}))"
|
||||
wrapper-class="col-start-2 row-start-3"
|
||||
/>
|
||||
|
||||
<!-- Chauffeur (LIOT) -->
|
||||
<UiSelect
|
||||
id="reception-driver"
|
||||
id="shipment-driver"
|
||||
v-model="form.driverId"
|
||||
label="Nom du chauffeur si LIOT"
|
||||
:options="filteredDrivers.map((driver) => ({
|
||||
@@ -100,7 +98,7 @@
|
||||
:loading="isLoadingDrivers"
|
||||
wrapper-class="col-start-2 row-start-4"
|
||||
/>
|
||||
|
||||
<!-- Plaque d'immatriculation (hors LIOT) -->
|
||||
<div v-if="!isLiotCarrier" class="col-start-2 row-start-5">
|
||||
<UiLicensePlateInput
|
||||
v-model="form.licencePlate"
|
||||
@@ -110,7 +108,7 @@
|
||||
<!-- Immatriculation (LIOT) -->
|
||||
<UiSelect
|
||||
v-if="isLiotCarrier"
|
||||
id="reception-vehicle"
|
||||
id="shipment-vehicle"
|
||||
v-model="form.vehicleId"
|
||||
label="Immatriculation"
|
||||
: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 {DriverData} from '~/services/dto/driver-data'
|
||||
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||
import type {AddressData} from '~/services/dto/address-data'
|
||||
import {getUsers} from '~/services/auth'
|
||||
import {getCustomerList} from '~/services/customer'
|
||||
import {getTruckList} from '~/services/truck'
|
||||
@@ -146,10 +145,10 @@ import {getCarrierList} from '~/services/carrier'
|
||||
import {getVehicleList} from '~/services/vehicle'
|
||||
import {getDriverList} from '~/services/driver'
|
||||
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 {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 {getShipmentTypeList} from "~/services/shipment-type";
|
||||
import {
|
||||
@@ -167,6 +166,7 @@ const drivers = ref<DriverData[]>([])
|
||||
const vehicles = ref<VehicleData[]>([])
|
||||
|
||||
const isLoadingUsers = ref(false)
|
||||
const isLoadingShipmentTypes = ref(false)
|
||||
const isLoadingCustomers = ref(false)
|
||||
const isLoadingTrucks = ref(false)
|
||||
const isLoadingCarriers = ref(false)
|
||||
@@ -183,8 +183,7 @@ const bovineShipment = ref<ShipmentTypeData[]>([])
|
||||
const selectedCarrier = computed(() =>
|
||||
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>({
|
||||
userId: '',
|
||||
@@ -197,22 +196,30 @@ const form = reactive<ShipmentFormData>({
|
||||
vehicleId: '',
|
||||
licencePlate: '',
|
||||
})
|
||||
|
||||
const customerAddresses = computed(() => {
|
||||
// Adresses liées au client sélectionné
|
||||
const customerAddresses = computed<AddressData[]>(() => {
|
||||
const customerId = Number(form.customerId)
|
||||
if (!Number.isFinite(customerId)) {
|
||||
return []
|
||||
}
|
||||
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[]>(() => {
|
||||
if (!form.carrierId) {
|
||||
return []
|
||||
}
|
||||
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[]>(() => {
|
||||
if (!form.carrierId) {
|
||||
return []
|
||||
@@ -223,8 +230,7 @@ const filteredVehicles = computed<VehicleData[]>(() => {
|
||||
(!form.truckId || String(vehicle.truck?.id) === form.truckId)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
// Chargement des données pour les selects
|
||||
const loadUsers = async () => {
|
||||
isLoadingUsers.value = true
|
||||
try {
|
||||
@@ -235,11 +241,11 @@ const loadUsers = async () => {
|
||||
}
|
||||
|
||||
const loadShipmentType = async () => {
|
||||
isLoadingUsers.value = true
|
||||
isLoadingShipmentTypes.value = true
|
||||
try {
|
||||
bovineShipment.value = await getShipmentTypeList()
|
||||
} finally {
|
||||
isLoadingUsers.value = false
|
||||
isLoadingShipmentTypes.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +258,6 @@ const loadCustomers = async () => {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const loadTrucks = async () => {
|
||||
isLoadingTrucks.value = true
|
||||
try {
|
||||
@@ -261,7 +266,6 @@ const loadTrucks = async () => {
|
||||
isLoadingTrucks.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadCarriers = async () => {
|
||||
isLoadingCarriers.value = true
|
||||
try {
|
||||
@@ -270,7 +274,6 @@ const loadCarriers = async () => {
|
||||
isLoadingCarriers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadVehicles = async () => {
|
||||
isLoadingVehicles.value = true
|
||||
try {
|
||||
@@ -279,7 +282,6 @@ const loadVehicles = async () => {
|
||||
isLoadingVehicles.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadDrivers = async () => {
|
||||
isLoadingDrivers.value = true
|
||||
try {
|
||||
@@ -288,8 +290,6 @@ const loadDrivers = async () => {
|
||||
isLoadingDrivers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// On met le user connecté par défaut dans le select
|
||||
const setDefaultUser = () => {
|
||||
if (form.userId) {
|
||||
@@ -299,7 +299,7 @@ const setDefaultUser = () => {
|
||||
form.userId = String(authStore.user.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Chargement initial des données
|
||||
onMounted(async () => {
|
||||
await loadShipmentType()
|
||||
await loadUsers()
|
||||
@@ -311,27 +311,37 @@ onMounted(async () => {
|
||||
await authStore.ensureSession()
|
||||
setDefaultUser()
|
||||
})
|
||||
|
||||
// Hydrate le formulaire depuis l'expédition en cours
|
||||
watch(
|
||||
() => shipmentStore.current,
|
||||
(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) {
|
||||
bovineQuantities.value = {}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
const next: Record<string, number | null> = {}
|
||||
for (const entry of shipment.bovinShipments) {
|
||||
const typeId = entry.shipmentType?.id
|
||||
if (!typeId) {
|
||||
continue
|
||||
}
|
||||
if (!typeId) continue
|
||||
next[String(typeId)] = entry.nbBovinSend ?? null
|
||||
}
|
||||
bovineQuantities.value = next
|
||||
}
|
||||
isHydrating.value = false
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
// Ajuste driver/vehicle quand le transporteur change (logique LIOT)
|
||||
watch(
|
||||
() => [form.customerId, customers.value],
|
||||
@@ -356,11 +366,8 @@ watch(
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
// Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT)
|
||||
watch(
|
||||
() => form.carrierId,
|
||||
() => {
|
||||
const applyLiotDefaults = () => {
|
||||
if (isHydrating.value) {
|
||||
return
|
||||
}
|
||||
@@ -380,10 +387,22 @@ watch(
|
||||
if (filteredVehicles.value.length === 1) {
|
||||
form.vehicleId = String(filteredVehicles.value[0].id)
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => form.carrierId,
|
||||
() => {
|
||||
applyLiotDefaults()
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => isHydrating.value,
|
||||
(value) => {
|
||||
if (!value) {
|
||||
applyLiotDefaults()
|
||||
}
|
||||
}
|
||||
)
|
||||
// Récupère la plaque depuis le véhicule choisi (LIOT)
|
||||
watch(
|
||||
() => [form.truckId, form.carrierId, vehicles.value],
|
||||
@@ -407,7 +426,6 @@ watch(
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
// Auto-renseigne le véhicule si la plaque correspond (LIOT)
|
||||
watch(
|
||||
() => [form.vehicleId, form.carrierId, vehicles.value],
|
||||
@@ -427,7 +445,6 @@ watch(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [form.licencePlate, form.carrierId, vehicles.value],
|
||||
() => {
|
||||
@@ -442,7 +459,6 @@ watch(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const buildDesiredBovinShipments = () => {
|
||||
return bovineShipment.value
|
||||
.map((type) => {
|
||||
@@ -455,17 +471,20 @@ const buildDesiredBovinShipments = () => {
|
||||
})
|
||||
.filter((entry) => entry.quantity > 0)
|
||||
}
|
||||
|
||||
const syncBovinShipments = async (shipmentId: number) => {
|
||||
const syncBovinShipments = async (
|
||||
shipmentId: number,
|
||||
existing: Array<{ id?: number; nbBovinSend: number | null; shipmentType?: unknown }> = []
|
||||
) => {
|
||||
const shipmentIri = `/api/shipments/${shipmentId}`
|
||||
const existing = await getBovinShipmentList(shipmentIri)
|
||||
const desired = buildDesiredBovinShipments()
|
||||
const desiredByTypeId = new Map<number, number>()
|
||||
for (const entry of desired) {
|
||||
desiredByTypeId.set(entry.type.id, entry.quantity)
|
||||
}
|
||||
|
||||
for (const entry of existing) {
|
||||
if (!entry.id) {
|
||||
continue
|
||||
}
|
||||
const rawType = entry.shipmentType
|
||||
let typeId: number | null = null
|
||||
if (rawType && typeof rawType === 'object' && 'id' in rawType) {
|
||||
@@ -496,15 +515,15 @@ const syncBovinShipments = async (shipmentId: number) => {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Valide le formulaire et crée/met à jour l'expédition
|
||||
const validate = async () => {
|
||||
const buildPayload = () => {
|
||||
const normalizedLicensePlate = form.licencePlate.trim()
|
||||
const normalizedShipmentDate = form.shipmentDate.trim()
|
||||
const normalizedCustomerId = form.customerId.trim()
|
||||
const normalizedTruckId = form.truckId.trim()
|
||||
const normalizedCarrierId = form.carrierId.trim()
|
||||
|
||||
const normalizedDriverId = form.driverId.trim()
|
||||
const normalizedUserId = form.userId.trim()
|
||||
const normalizedAddressId = form.addressId.trim()
|
||||
const customerIri = normalizedCustomerId
|
||||
? `/api/customers/${normalizedCustomerId}`
|
||||
: null
|
||||
@@ -514,31 +533,73 @@ const validate = async () => {
|
||||
const carrierIri = normalizedCarrierId
|
||||
? `/api/carriers/${normalizedCarrierId}`
|
||||
: 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,
|
||||
shipmentDate: normalizedShipmentDate,
|
||||
customer: customerIri,
|
||||
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) {
|
||||
const created = await shipmentStore.createShipment({
|
||||
currentStep: 1,
|
||||
...payload
|
||||
})
|
||||
if (created) {
|
||||
await syncBovinShipments(created.id)
|
||||
await shipmentStore.loadShipment(created.id)
|
||||
await syncBovinShipments(created.id, shipmentStore.current?.bovinShipments ?? [])
|
||||
await router.push(`/shipment/${created.id}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const nextStep = shipmentStore.current.currentStep + 1
|
||||
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||
currentStep: nextStep,
|
||||
...payload
|
||||
})
|
||||
await syncBovinShipments(shipmentStore.current.id)
|
||||
await shipmentStore.loadShipment(shipmentStore.current.id)
|
||||
await syncBovinShipments(shipmentStore.current.id, shipmentStore.current?.bovinShipments ?? [])
|
||||
}
|
||||
</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 = () => {
|
||||
const api = useApi()
|
||||
const receptionStore = useReceptionStore()
|
||||
const currentReception = receptionStore.current
|
||||
|
||||
const printPdf = async (url: string): Promise<void> => {
|
||||
const blob = await api.getBlob(url);
|
||||
const printPdf = async (url: string, filename = 'document.pdf'): Promise<void> => {
|
||||
const blob = await api.getBlob(url)
|
||||
|
||||
const pdfBlob = blob.type === 'application/pdf'
|
||||
? 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;
|
||||
a.download = filename;
|
||||
a.style.display = 'none';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
const a = document.createElement('a')
|
||||
a.href = blobUrl
|
||||
a.download = filename
|
||||
a.style.display = 'none'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
// 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 {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type {Ref} from 'vue'
|
||||
import {computed, ref} from 'vue'
|
||||
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 {getWeight} from '~/services/reception'
|
||||
import {getWeightShipment} from '~/services/shipment'
|
||||
import {createWeight, updateWeight} from '~/services/weight'
|
||||
|
||||
export type WeighingMode = 'gross' | 'tare'
|
||||
@@ -14,6 +16,13 @@ type UseWeighingOptions = {
|
||||
loadReception?: (id: number) => Promise<ReceptionData | null>
|
||||
}
|
||||
|
||||
type UseWeighingShipmentOptions = {
|
||||
modeShipment: WeighingMode
|
||||
shipment: Ref<ShipmentData | null>
|
||||
updateShipment: (id: number, payload: ShipmentPayload) => Promise<ShipmentData | null>
|
||||
loadShipment?: (id: number) => Promise<ShipmentData | null>
|
||||
}
|
||||
|
||||
export const useWeighing = ({
|
||||
mode,
|
||||
reception,
|
||||
@@ -97,3 +106,87 @@ export const useWeighing = ({
|
||||
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.",
|
||||
"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": {
|
||||
"list": "Impossible de récupérer la liste des types de réception."
|
||||
},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<template>
|
||||
<AdminUserForm/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
|
||||
@@ -1,8 +1,125 @@
|
||||
<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>
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
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>
|
||||
|
||||
@@ -137,7 +137,7 @@ import type {DriverData} from '~/services/dto/driver-data'
|
||||
import {getDriverList} from '~/services/driver'
|
||||
import type {VehicleData} from '~/services/dto/vehicle-data'
|
||||
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 type {ReceptionData, ReceptionFormData} from "~/services/dto/reception-data";
|
||||
import {getReception} from "~/services/reception";
|
||||
@@ -185,7 +185,7 @@ const selectedCarrier = computed(() =>
|
||||
carriers.value.find((carrier) => String(carrier.id) === form.carrierId) ?? null
|
||||
)
|
||||
// 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é
|
||||
const supplierAddresses = computed(() => {
|
||||
const supplierId = Number(form.supplierId)
|
||||
|
||||
@@ -16,11 +16,10 @@
|
||||
>Mettre en attente
|
||||
</button>
|
||||
</div>
|
||||
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0"/>
|
||||
<button
|
||||
v-if="storeShipment?.currentStep === 1">
|
||||
TEST ETAPE 2
|
||||
</button>
|
||||
<ShipmentForm v-if="!storeShipment || storeShipment.currentStep === 0" ref="shipmentFormRef"/>
|
||||
<ShipmentWeight v-if="storeShipment?.currentStep === 1" mode="gross"/>
|
||||
<ShipmentWeight v-if="storeShipment?.currentStep >= 2" mode="tare"/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -28,9 +27,10 @@
|
||||
import {SHIPMENT_STEP_LABELS} from "~/constants/steps";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useShipmentStore} from "~/stores/shipment";
|
||||
|
||||
import { ref, watch } from 'vue'
|
||||
const shipmentStore = useShipmentStore()
|
||||
const {current: storeShipment} = storeToRefs(shipmentStore)
|
||||
const shipmentFormRef = ref<{ saveDraft: () => Promise<void> } | null>(null)
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -58,15 +58,9 @@ watch (
|
||||
)
|
||||
|
||||
const saveAndHold = async () => {
|
||||
if (!shipmentStore.current) {
|
||||
await router.push('/')
|
||||
return
|
||||
if (shipmentFormRef.value) {
|
||||
await shipmentFormRef.value.saveDraft()
|
||||
}
|
||||
await shipmentStore.updateShipment(shipmentStore.current.id, {
|
||||
currentStep: shipmentStore.current.currentStep,
|
||||
licencePlate: shipmentStore.current.licencePlate,
|
||||
shipmentDate: shipmentStore.current.shipmentDate
|
||||
})
|
||||
await router.push('/')
|
||||
}
|
||||
const handleStepSelect = async (step: number) => {
|
||||
|
||||
@@ -25,6 +25,16 @@ export type ShipmentData = {
|
||||
truck?: TruckData | null
|
||||
customer?: CustomerData | 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 = {
|
||||
@@ -48,4 +58,8 @@ export type ShipmentPayload = {
|
||||
truck?: string | null
|
||||
customer?: 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[]> {
|
||||
const api = useApi()
|
||||
const response = await api.get<ShipmentTypeListResponse>('shipment_types', {}, {
|
||||
toastErrorKey: 'errors.shipment_type.list'
|
||||
toastErrorKey: 'errors.shipmentType.list'
|
||||
})
|
||||
|
||||
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()
|
||||
return api.get<WeightData>('shipments/weigh', {}, {
|
||||
toastErrorKey: 'errors.shipment.weigh'
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useApi } from '~/composables/useApi'
|
||||
import type { WeightEntryData } from '~/services/dto/reception-data'
|
||||
|
||||
export type WeightPayload = {
|
||||
reception: string
|
||||
reception?: string
|
||||
shipment?: string
|
||||
type: 'gross' | 'tare'
|
||||
dsd: number | null
|
||||
weight: number | null
|
||||
|
||||
@@ -12,6 +12,6 @@ export const ROLE = [
|
||||
{ label: 'Administrateur', value: 'ROLE_ADMIN' },
|
||||
{ label: 'Utilisateur', value: 'ROLE_USER' }
|
||||
]
|
||||
export const SUPLLIER_CODE = {
|
||||
export const SUPPLIER_CODE = {
|
||||
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
|
||||
{
|
||||
public function __construct(
|
||||
#[Groups(['reception:weigh:read'])]
|
||||
#[Groups(['reception:weigh:read', 'shipment:weigh:read'])]
|
||||
private ?int $dsd,
|
||||
#[Groups(['reception:weigh:read'])]
|
||||
#[Groups(['reception:weigh:read', 'shipment:weigh:read'])]
|
||||
private ?float $weight,
|
||||
#[Groups(['reception:weigh:read'])]
|
||||
#[Groups(['reception:weigh:read', 'shipment:weigh:read'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
private ?DateTimeImmutable $weighedAt = null,
|
||||
) {}
|
||||
|
||||
@@ -31,7 +31,7 @@ class Address
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['address:read', 'supplier:read'])]
|
||||
#[Groups(['address:read', 'supplier:read', 'customer:read', 'shipment:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 120)]
|
||||
@@ -64,9 +64,16 @@ class Address
|
||||
#[ORM\ManyToMany(targetEntity: Supplier::class, mappedBy: 'addresses')]
|
||||
private Collection $suppliers;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Shipment>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Shipment::class, mappedBy: 'address')]
|
||||
private Collection $shipments;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->suppliers = new ArrayCollection();
|
||||
$this->shipments = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -146,7 +153,7 @@ class Address
|
||||
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
|
||||
{
|
||||
$parts = array_filter([
|
||||
@@ -165,4 +172,34 @@ class Address
|
||||
{
|
||||
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\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['driver:read', 'reception:read'])]
|
||||
#[Groups(['driver:read', 'reception:read', 'shipment:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 180)]
|
||||
#[Groups(['driver:read', 'reception:read'])]
|
||||
#[Groups(['driver:read', 'reception:read', 'shipment:read'])]
|
||||
private string $name = '';
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
|
||||
@@ -10,15 +10,21 @@ use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
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 Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Event\PostPersistEventArgs;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Context;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ORM\Table(name: 'shipment')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
@@ -38,27 +44,28 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||
normalizationContext: ['groups' => ['shipment:read']],
|
||||
denormalizationContext: ['groups' => ['shipment:write']],
|
||||
),
|
||||
// new Get(
|
||||
// uriTemplate: '/shipments/weigh',
|
||||
// openapi: new OpenApiOperation(
|
||||
// summary: 'Fetch the current weight reading',
|
||||
// description: 'Queries the pont-bascule and returns the weight data.',
|
||||
// ),
|
||||
// normalizationContext: ['groups' => ['shipment:weigh:read']],
|
||||
// output: PontBasculeReading::class,
|
||||
// provider: shipmentWeighingProvider::class,
|
||||
// ),
|
||||
// new Get(
|
||||
// uriTemplate: '/shipments/{id}/receipt',
|
||||
// requirements: ['id' => '\d+'],
|
||||
// openapi: new OpenApiOperation(
|
||||
// summary: 'Render a shipment receipt',
|
||||
// description: 'Returns a PDF receipt for the shipment.',
|
||||
// ),
|
||||
// output: false,
|
||||
// provider: shipmentReceiptProvider::class,
|
||||
// ),
|
||||
new Get(
|
||||
uriTemplate: '/shipments/weigh',
|
||||
openapi: new OpenApiOperation(
|
||||
summary: 'Fetch the current weight reading',
|
||||
description: 'Queries the pont-bascule and returns the weight data.',
|
||||
),
|
||||
normalizationContext: ['groups' => ['shipment:weigh:read']],
|
||||
output: PontBasculeReading::class,
|
||||
provider: ShipmentWeighingProvider::class,
|
||||
),
|
||||
new Get(
|
||||
uriTemplate: '/shipments/{id}/receipt',
|
||||
requirements: ['id' => '\d+'],
|
||||
openapi: new OpenApiOperation(
|
||||
summary: 'Render a shipment receipt',
|
||||
description: 'Returns a PDF receipt for the shipment.',
|
||||
),
|
||||
output: false,
|
||||
provider: ShipmentReceiptProvider::class,
|
||||
),
|
||||
],
|
||||
security: "is_granted('ROLE_USER')",
|
||||
)]
|
||||
class Shipment
|
||||
{
|
||||
@@ -84,6 +91,12 @@ class Shipment
|
||||
#[Groups(['shipment:read', 'shipment:write'])]
|
||||
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')]
|
||||
#[Groups(['shipment:read', 'shipment:write'])]
|
||||
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||
@@ -102,19 +115,46 @@ class Shipment
|
||||
private ?Truck $truck = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
#[Groups(['shipment:read', 'shipment:write'])]
|
||||
#[ApiProperty(readableLink: true)]
|
||||
private ?Customer $customer = null;
|
||||
|
||||
/**
|
||||
* @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'])]
|
||||
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()
|
||||
{
|
||||
$this->bovinShipments = new ArrayCollection();
|
||||
$this->weights = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -157,6 +197,22 @@ class Shipment
|
||||
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
|
||||
{
|
||||
$this->isValid = $isValid;
|
||||
@@ -182,16 +238,6 @@ class Shipment
|
||||
$this->carrier = $carrier;
|
||||
}
|
||||
|
||||
public function getVehicle(): ?Vehicle
|
||||
{
|
||||
return $this->vehicle;
|
||||
}
|
||||
|
||||
public function setVehicle(?Vehicle $vehicle): void
|
||||
{
|
||||
$this->vehicle = $vehicle;
|
||||
}
|
||||
|
||||
public function getTruck(): ?Truck
|
||||
{
|
||||
return $this->truck;
|
||||
@@ -222,24 +268,99 @@ class Shipment
|
||||
$this->bovinShipments = $bovinShipments;
|
||||
}
|
||||
|
||||
public function addPelletBuilding(BovinShipment $bovinShipments): self
|
||||
public function addBovinShipment(BovinShipment $bovinShipment): self
|
||||
{
|
||||
if (!$this->bovinShipments->contains($bovinShipments)) {
|
||||
$this->bovinShipments->add($bovinShipments);
|
||||
$bovinShipments->setReception($this);
|
||||
if (!$this->bovinShipments->contains($bovinShipment)) {
|
||||
$this->bovinShipments->add($bovinShipment);
|
||||
$bovinShipment->setShipment($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removePelletBuilding(BovinShipment $bovinShipments): self
|
||||
public function removeBovinShipment(BovinShipment $bovinShipment): self
|
||||
{
|
||||
if ($this->bovinShipments->removeElement($bovinShipments)) {
|
||||
if ($bovinShipments->getReception() === $this) {
|
||||
$bovinShipments->setReception(null);
|
||||
if ($this->bovinShipments->removeElement($bovinShipment)) {
|
||||
if ($bovinShipment->getShipment() === $this) {
|
||||
$bovinShipment->setShipment(null);
|
||||
}
|
||||
}
|
||||
|
||||
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\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['truck:read', 'vehicle:read', 'reception:read'])]
|
||||
#[Groups(['truck:read', 'vehicle:read', 'reception:read', 'shipment:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 180)]
|
||||
#[Groups(['truck:read', 'vehicle:read', 'reception:read'])]
|
||||
#[Groups(['truck:read', 'vehicle:read', 'reception:read', 'shipment:read'])]
|
||||
private string $name = '';
|
||||
|
||||
public function getId(): ?int
|
||||
|
||||
@@ -61,11 +61,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[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;
|
||||
|
||||
#[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 = '';
|
||||
|
||||
#[ORM\Column(type: 'json')]
|
||||
|
||||
@@ -35,36 +35,46 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
security: "is_granted('ROLE_USER')",
|
||||
)]
|
||||
#[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
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['reception:read', 'weight:read'])]
|
||||
#[Groups(['reception:read', 'shipment:read', 'weight:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'weights')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
#[Groups(['weight:read', 'weight:write'])]
|
||||
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)]
|
||||
#[Groups(['reception:read', 'weight:read', 'weight:write'])]
|
||||
#[Groups(['reception:read', 'shipment:read', 'weight:read', 'weight:write'])]
|
||||
#[Assert\PositiveOrZero]
|
||||
private ?int $dsd = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
#[Groups(['reception:read', 'weight:read', 'weight:write'])]
|
||||
#[Groups(['reception:read', 'shipment:read', 'weight:read', 'weight:write'])]
|
||||
#[Assert\PositiveOrZero]
|
||||
private ?int $weight = null;
|
||||
|
||||
#[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'])]
|
||||
private ?DateTimeImmutable $weighedAt = null;
|
||||
|
||||
#[ORM\Column(length: 10)]
|
||||
#[Groups(['reception:read', 'weight:read', 'weight:write'])]
|
||||
#[Groups(['reception:read', 'shipment:read', 'weight:read', 'weight:write'])]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Choice(choices: ['gross', 'tare'])]
|
||||
private string $type = 'gross';
|
||||
@@ -90,6 +100,22 @@ class Weight
|
||||
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
|
||||
{
|
||||
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 @@
|
||||
|
||||
|
|
||||
<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>{{ reception.supplier.name }}</strong><br>
|
||||
<span>{{ reception.address.street }}</span><br>
|
||||
{% if reception.address.street2 %}
|
||||
<strong>{{ reception.supplier ? reception.supplier.name : '-' }}</strong><br>
|
||||
<span>{{ reception.address ? reception.address.street : '' }}</span><br>
|
||||
{% if reception.address and reception.address.street2 %}
|
||||
<span>{{ reception.address.street2 }}</span><br>
|
||||
{% endif %}
|
||||
{% if reception.address %}
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if reception.supplier.email %}
|
||||
{% if reception.supplier and reception.supplier.email %}
|
||||
<span>{{ reception.supplier.email}}</span><br>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -168,7 +170,9 @@
|
||||
<th style="width:25%; text-align:center; white-space:nowrap;">N° réception</th>
|
||||
</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;">
|
||||
{{ reception.receptionDate|date('d/m/Y') }}
|
||||
</td>
|
||||
@@ -189,13 +193,11 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:75%;">
|
||||
<strong>{{ reception.receptionType.label }}</strong><br><br>
|
||||
|
||||
<strong>{{ reception.receptionType ? reception.receptionType.label : '-' }}</strong><br><br>
|
||||
<div class="bigtable-notes">
|
||||
{% set grossWeight = null %}
|
||||
{% set tareWeight = null %}
|
||||
|
||||
{% for weight in reception.weights %}
|
||||
{% for weight in reception.weights|default([]) %}
|
||||
{% 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>
|
||||
@@ -219,22 +221,33 @@
|
||||
<tr class="border-bottom">
|
||||
<td>
|
||||
<strong>
|
||||
{% if reception.merchandiseType %}
|
||||
{{ reception.merchandiseType.label }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
Type de bovins
|
||||
</strong>
|
||||
<br><br>
|
||||
|
||||
<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 %}
|
||||
<p><strong>Précision</strong> : {{ reception.merchandiseDetail }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if reception.merchandiseType and reception.merchandiseType.code == 'GRANULE' %}
|
||||
{% set pelletGroups = {} %}
|
||||
{% for selection in reception.pelletBuildings %}
|
||||
{% for selection in reception.pelletBuildings|default([]) %}
|
||||
{% set pelletLabel = selection.pelletType.label %}
|
||||
{% if pelletGroups[pelletLabel] is not defined %}
|
||||
{% set pelletGroups = pelletGroups|merge({ (pelletLabel): [] }) %}
|
||||
@@ -251,7 +264,7 @@
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% set buildingLabels = [] %}
|
||||
{% for building in reception.buildings %}
|
||||
{% for building in reception.buildings|default([]) %}
|
||||
{% set buildingLabels = buildingLabels|merge([building.label]) %}
|
||||
{% endfor %}
|
||||
{% if buildingLabels %}
|
||||
@@ -260,6 +273,7 @@
|
||||
<p>Aucun bâtiment renseigné.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
@@ -273,9 +287,9 @@
|
||||
<tr>
|
||||
<td style="width:60%; padding-right:8mm; vertical-align:top;">
|
||||
<div class="meta">
|
||||
Transporteur : <strong>{{ reception.carrier.name }}</strong><br>
|
||||
Mode de livraison : <strong>{{ reception.truck.name }}</strong><br>
|
||||
Immatriculation : <strong>{{ reception.licensePlate }}</strong><br><br>
|
||||
Transporteur : <strong>{{ reception.carrier ? reception.carrier.name : '-' }}</strong><br>
|
||||
Mode de livraison : <strong>{{ reception.truck ? reception.truck.name : '-' }}</strong><br>
|
||||
Immatriculation : <strong>{{ reception.licensePlate ?? '-' }}</strong><br><br>
|
||||
</div>
|
||||
</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
Pourquoi ce fichier est modifié ?