diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index d829d01..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml index a873179..a081fbb 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -15,6 +15,7 @@ + diff --git a/.idea/workspace.xml b/.idea/workspace.xml index a3262d4..42ee722 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,11 +4,28 @@ - + + + + + + + + + + + + - - + + + + + + + + + + + + + + + @@ -757,14 +781,23 @@ - - - - - - - - + + + + + file://$PROJECT_DIR$/src/Entity/ReceptionPelletBuilding.php + 6 + + + file://$PROJECT_DIR$/frontend/services/shipment.ts + + + + @@ -778,4 +811,4 @@ - + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cb58947..9631bd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,12 @@ Ajouter dans le fichier .env du frontend * [#315] Creation page admin utilisateur * [#317] Admin modification creation transporteur * [#318] Affichage modification reception terminée +* [#320] Affichage modification reception terminée suite +* [#271] Créer une nouvelle expédition (étape 1) +* [#272] Créer une nouvelle expédition (étape 2) +* [#273] Créer une nouvelle expédition (étape 3) +* [#256] Créer une nouvelle réception (étape 3 - bovin) +* [#314] Création d'une page d'administration : listing des utilisateurs * [#313] Admin modification creation fournisseur ### Changed diff --git a/config/version.yaml b/config/version.yaml index 7878495..a75c41e 100644 --- a/config/version.yaml +++ b/config/version.yaml @@ -1,2 +1,2 @@ parameters: - app.version: '0.0.37' + app.version: '0.0.39' diff --git a/frontend/components/reception/reception-form.vue b/frontend/components/reception/reception-form.vue index 063a363..52ec0d3 100644 --- a/frontend/components/reception/reception-form.vue +++ b/frontend/components/reception/reception-form.vue @@ -123,6 +123,7 @@ + diff --git a/frontend/components/reception/update-merchandise.vue b/frontend/components/reception/update-merchandise.vue new file mode 100644 index 0000000..e3c72a6 --- /dev/null +++ b/frontend/components/reception/update-merchandise.vue @@ -0,0 +1,257 @@ + + + diff --git a/frontend/components/reception/update-weight.vue b/frontend/components/reception/update-weight.vue new file mode 100644 index 0000000..2f97c5c --- /dev/null +++ b/frontend/components/reception/update-weight.vue @@ -0,0 +1,74 @@ + + + diff --git a/frontend/components/shipment/shipment-form.vue b/frontend/components/shipment/shipment-form.vue new file mode 100644 index 0000000..1eb026a --- /dev/null +++ b/frontend/components/shipment/shipment-form.vue @@ -0,0 +1,605 @@ + + diff --git a/frontend/components/shipment/shipment-weight.vue b/frontend/components/shipment/shipment-weight.vue new file mode 100644 index 0000000..cf4d7f0 --- /dev/null +++ b/frontend/components/shipment/shipment-weight.vue @@ -0,0 +1,101 @@ + + + diff --git a/frontend/components/ui/UiNumberInput.vue b/frontend/components/ui/UiNumberInput.vue index aa5407f..3ebc130 100644 --- a/frontend/components/ui/UiNumberInput.vue +++ b/frontend/components/ui/UiNumberInput.vue @@ -3,7 +3,7 @@ @@ -24,7 +25,7 @@ :step="step" :disabled="disabled" v-bind="attrs" - class="border-b border-black text-xl bg-transparent w-48" + class="border-b border-black text-xl bg-transparent w-12" :class="[ isEmpty ? 'text-neutral-400' : 'text-black', disabled ? 'cursor-not-allowed' : 'cursor-text', diff --git a/frontend/components/user/user-form.vue b/frontend/components/user/user-form.vue deleted file mode 100644 index 137d299..0000000 --- a/frontend/components/user/user-form.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - diff --git a/frontend/composables/usePdfPrinter.ts b/frontend/composables/usePdfPrinter.ts index ff6a057..3e34846 100644 --- a/frontend/composables/usePdfPrinter.ts +++ b/frontend/composables/usePdfPrinter.ts @@ -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 => { - const blob = await api.getBlob(url); + const printPdf = async (url: string, filename = 'document.pdf'): Promise => { + 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 { diff --git a/frontend/composables/useWeighing.ts b/frontend/composables/useWeighing.ts index 9aea037..f632b5b 100644 --- a/frontend/composables/useWeighing.ts +++ b/frontend/composables/useWeighing.ts @@ -3,23 +3,20 @@ import {computed, ref} from 'vue' import type {ReceptionData, ReceptionPayload, WeightEntryData} from '~/services/dto/reception-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' +import type {UseWeighingShipmentOptions, UseWeighingOptions} from '~/services/weight' +import type {WeightShipmentEntryData} from "~/services/dto/shipment-data"; export type WeighingMode = 'gross' | 'tare' -type UseWeighingOptions = { - mode: WeighingMode - reception: Ref - updateReception: (id: number, payload: ReceptionPayload) => Promise - loadReception?: (id: number) => Promise -} export const useWeighing = ({ - mode, - reception, - updateReception, - loadReception -}: UseWeighingOptions) => { + mode, + reception, + updateReception, + loadReception + }: UseWeighingOptions) => { const weightData = ref(null) const isFetching = ref(false) @@ -97,3 +94,87 @@ export const useWeighing = ({ saveWeight } } + +export const useWeighingShipment = ({ + modeShipment, + shipment, + updateShipment, + loadShipment + }: UseWeighingShipmentOptions) => { + const weightData = ref(null) + const isFetching = ref(false) + + const currentWeightEntry = computed(() => { + 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 + } +} diff --git a/frontend/constants/steps.ts b/frontend/constants/steps.ts index 2e51937..d42ae66 100644 --- a/frontend/constants/steps.ts +++ b/frontend/constants/steps.ts @@ -2,7 +2,8 @@ export enum StepLabel { Reception = 'Réception', GrossWeighing = 'Pesée à plein', Selection = 'Sélection réceptionnées', - TareWeighing = 'Pesée à vide' + TareWeighing = 'Pesée à vide', + Shipment = 'Expédition', } export const RECEPTION_STEP_LABELS = [ @@ -11,3 +12,9 @@ export const RECEPTION_STEP_LABELS = [ StepLabel.Selection, StepLabel.TareWeighing ] + +export const SHIPMENT_STEP_LABELS = [ + StepLabel.Shipment, + StepLabel.TareWeighing, + StepLabel.GrossWeighing, +] diff --git a/frontend/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 87c0d3d..a210e30 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -12,8 +12,27 @@ "fetch": "Impossible de récupérer la réception.", "create": "Impossible de créer la réception.", "update": "Impossible de mettre à jour la réception.", + "weight": "Impossible de récupérer la pesée." + }, + "weight": { + "update": "Impossible de mettre à jour la pesée" + }, + "shipment": { + "list": "Impossible de récupérer la liste des éxpeditions.", + "fetch": "Impossible de récupérer l'éxpeditions.", + "create": "Impossible de créer l'éxpeditions.", + "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." }, @@ -70,7 +89,6 @@ "fetch": "Impossible de récupérer les données du transporteur", "update": "Impossible de mettre à jour le transporteur", "create": "Impossible de créer le transporteur" - }, "driver": { "list": "Impossible de récupérer la liste des chauffeurs." @@ -90,6 +108,9 @@ "reception": { "update": "Réception mise à jour avec succès." }, + "shipment": { + "update": "Éxpedition mise à jour avec succès." + }, "supplier": { "create": "Fournisseur créé avec succès.", "update": "Fournisseur mis à jour avec succès." @@ -98,10 +119,6 @@ "create": "Adresse créée avec succès.", "update": "Adresse mise à jour avec succès." }, - "customer": { - "create": "Client créé avec succès.", - "update": "Client mis à jour avec succès." - }, "auth": { "update": "Utilisateur mis à jour avec succès.", "create": "Utilisateur créé avec succès.", @@ -111,6 +128,9 @@ "carrier": { "update": "Transporteur mis à jour", "create": "Transporteur créé" + }, + "weight": { + "update": "Pesée mis à jour" } } } diff --git a/frontend/pages/admin/carrier/[[id]].vue b/frontend/pages/admin/carrier/[[id]].vue index 702a3f7..350c627 100644 --- a/frontend/pages/admin/carrier/[[id]].vue +++ b/frontend/pages/admin/carrier/[[id]].vue @@ -14,7 +14,6 @@
- - - diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue index b7e90a4..52d3da7 100644 --- a/frontend/pages/index.vue +++ b/frontend/pages/index.vue @@ -3,7 +3,7 @@ @@ -137,10 +160,13 @@ 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"; +import UpdateWeight from "~/components/reception/update-weight.vue"; +import UpdateMerchandise from "~/components/reception/update-merchandise.vue"; +import UpdateBovin from "~/components/reception/update-bovin.vue"; const router = useRouter() const receptionStore = useReceptionStore() @@ -179,13 +205,15 @@ const idReception = Number(route.params.id) const receptionLoad = await getReception(idReception) const receptionType = receptionLoad.receptionType const auth = useAuthStore() +const isBtWeight = ref(true) +const isMerchandise = ref(receptionType.code === 'MARCHANDISES') // Transporteur sélectionné dans le formulaire 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) diff --git a/frontend/pages/shipment/[[id]].vue b/frontend/pages/shipment/[[id]].vue new file mode 100644 index 0000000..476e445 --- /dev/null +++ b/frontend/pages/shipment/[[id]].vue @@ -0,0 +1,82 @@ + + diff --git a/frontend/services/bovin-shipment.ts b/frontend/services/bovin-shipment.ts new file mode 100644 index 0000000..d14e855 --- /dev/null +++ b/frontend/services/bovin-shipment.ts @@ -0,0 +1,50 @@ +import { useApi } from '~/composables/useApi' +import type { BovinShipmentData } from '~/services/dto/bovin-shipment-data' +import type { ShipmentBovinePayload, BovinShipmentListResponse } from '~/services/dto/bovin-shipment-data' + +export async function getBovinShipmentList( + shipmentIri: string +): Promise { + const api = useApi() + const response = await api.get( + 'bovin_shipments', + { shipment: shipmentIri }, + { + toastErrorKey: 'errors.shipmentBovine.list' + } + ) + + if (Array.isArray(response)) { + return response + } + if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) { + return response['hydra:member'] + } + return [] +} + +export async function createShipmentBovine( + payload: ShipmentBovinePayload +): Promise { + const api = useApi() + return api.post('bovin_shipments', payload, { + toastErrorKey: 'errors.shipmentBovine.create' + }) +} + +export async function deleteShipmentBovine(id: number): Promise { + const api = useApi() + await api.delete(`bovin_shipments/${id}`, {}, { + toastErrorKey: 'errors.shipmentBovine.delete' + }) +} + +export async function updateShipmentBovine( + id: number, + payload: Partial +): Promise { + const api = useApi() + return api.patch(`bovin_shipments/${id}`, payload, { + toastErrorKey: 'errors.shipmentBovine.update' + }) +} diff --git a/frontend/services/customer.ts b/frontend/services/customer.ts new file mode 100644 index 0000000..04c8faa --- /dev/null +++ b/frontend/services/customer.ts @@ -0,0 +1,23 @@ +import { useApi } from '~/composables/useApi' +import type { CustomerData } from '~/services/dto/customer-data' + +export type CustomerListResponse = + | CustomerData[] + | { 'hydra:member'?: CustomerData[] } + +export async function getCustomerList(): Promise { + const api = useApi() + const response = await api.get('customers', {}, { + toastErrorKey: 'errors.customer.list' + }) + + if (Array.isArray(response)) { + return response + } + + if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) { + return response['hydra:member'] + } + + return [] +} diff --git a/frontend/services/dto/bovin-shipment-data.ts b/frontend/services/dto/bovin-shipment-data.ts new file mode 100644 index 0000000..6bbca73 --- /dev/null +++ b/frontend/services/dto/bovin-shipment-data.ts @@ -0,0 +1,18 @@ +import type {ShipmentTypeData} from "~/services/dto/shipment-type-data"; + +export interface BovinShipmentData { + id: number + nbBovinSend: number | null + shipment?: string | null + shipmentType?: ShipmentTypeData | null +} + +export type ShipmentBovinePayload = { + nbBovinSend: number + shipment: string + shipmentType: string +} + +export type BovinShipmentListResponse = + | BovinShipmentData[] + | { 'hydra:member'?: BovinShipmentData[] } diff --git a/frontend/services/dto/customer-data.ts b/frontend/services/dto/customer-data.ts new file mode 100644 index 0000000..57d6f7d --- /dev/null +++ b/frontend/services/dto/customer-data.ts @@ -0,0 +1,8 @@ +import type { AddressData } from "~/services/dto/address-data" + +export interface CustomerData { + id: number + label: string + code?: string | null + addresses?: AddressData[] | null +} diff --git a/frontend/services/dto/reception-data.ts b/frontend/services/dto/reception-data.ts index 2ed9a77..b81bfe9 100644 --- a/frontend/services/dto/reception-data.ts +++ b/frontend/services/dto/reception-data.ts @@ -41,6 +41,14 @@ export interface WeightEntryData { weighedAt: string | null } +export interface WeightFormData { + id: number + weight: number + type: 'gross' | 'tare' +} + + + export type ReceptionPayload = { licensePlate?: string | null receptionDate?: string @@ -72,3 +80,14 @@ export type ReceptionFormData = { driverId: string vehicleId: string } + +export type ReceptionFormWeight = { + weights: WeightFormData[] +} + +export interface ReceptionUpdatePayload { + weights: { + id: number + weight: number + }[] +} diff --git a/frontend/services/dto/shipment-data.ts b/frontend/services/dto/shipment-data.ts new file mode 100644 index 0000000..a78016a --- /dev/null +++ b/frontend/services/dto/shipment-data.ts @@ -0,0 +1,65 @@ +import type {CarrierData} from '~/services/dto/carrier-data' +import type {TruckData} from '~/services/dto/truck-data' +import type {CustomerData} from '~/services/dto/customer-data' + +export interface ShipmentTypeData { + id: number + label: string + code: string +} + +export interface BovinShipmentData { + id?: number + shipmentType?: ShipmentTypeData | string | null + nbBovinSend: number | null +} + +export type ShipmentData = { + id: number + identificationNumber?: string | null + licencePlate: string | null + shipmentDate: string + currentStep: number + isValid: boolean + carrier?: CarrierData | null + 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 = { + userId: string, + shipmentDate: string, + customerId: string, + addressId: string, + truckId: string, + carrierId: string, + driverId: string, + vehicleId: string, + licencePlate: string, +} + +export type ShipmentPayload = { + licencePlate?: string | null + shipmentDate?: string + currentStep?: number + isValid?: boolean + carrier?: string | null + truck?: string | null + customer?: string | null + bovinShipments?: string[] | null + address?: string | null + user?: string | null + driver?: string | null + +} diff --git a/frontend/services/dto/shipment-type-data.ts b/frontend/services/dto/shipment-type-data.ts new file mode 100644 index 0000000..e1c66db --- /dev/null +++ b/frontend/services/dto/shipment-type-data.ts @@ -0,0 +1,5 @@ +export interface ShipmentTypeData { + id: number + label: string + code: string +} diff --git a/frontend/services/shipment-type.ts b/frontend/services/shipment-type.ts new file mode 100644 index 0000000..17cc328 --- /dev/null +++ b/frontend/services/shipment-type.ts @@ -0,0 +1,24 @@ +import { useApi } from '~/composables/useApi' +import type {ShipmentTypeData} from "~/services/dto/shipment-type-data"; + +export type ShipmentTypeListResponse = + | ShipmentTypeData[] + | { 'hydra:member'?: ShipmentTypeData[] } + + +export async function getShipmentTypeList(): Promise { + const api = useApi() + const response = await api.get('shipment_types', {}, { + toastErrorKey: 'errors.shipmentType.list' + }) + + if (Array.isArray(response)) { + return response + } + + if (response && typeof response === 'object' && Array.isArray(response['hydra:member'])) { + return response['hydra:member'] + } + + return [] +} diff --git a/frontend/services/shipment.ts b/frontend/services/shipment.ts new file mode 100644 index 0000000..d701048 --- /dev/null +++ b/frontend/services/shipment.ts @@ -0,0 +1,40 @@ +import {useApi} from '~/composables/useApi' +import type {ShipmentData, ShipmentPayload} from '~/services/dto/shipment-data' +import type {WeightData} from '~/services/dto/weight-data' + +export async function getShipmentList(isValid: boolean|null = null) { + const api = useApi() + const query = isValid !== null ? { isValid: isValid} : {} + return api.get('shipments', query, { + toastErrorKey: 'errors.shipment.list' + }) +} + +export async function getShipment(id: number) { + const api = useApi() + return api.get(`shipments/${id}`, {}, { + toastErrorKey: 'errors.shipment.fetch' + }) +} + +export async function createShipment(payload: ShipmentPayload = {}) { + const api = useApi() + return api.post('shipments', payload, { + toastErrorKey: 'errors.shipment.create' + }) +} + +export async function updateShipment(id: number, payload: ShipmentPayload) { + const api = useApi() + return api.patch(`shipments/${id}`, payload, { + toastErrorKey: 'errors.shipment.update', + toastSuccessKey: 'success.shipment.update' + }) +} + +export async function getWeightShipment(): Promise { + const api = useApi() + return api.get('shipments/weigh', {}, { + toastErrorKey: 'errors.shipment.weigh' + }) +} diff --git a/frontend/services/weight.ts b/frontend/services/weight.ts index b8ba1f7..6c9243c 100644 --- a/frontend/services/weight.ts +++ b/frontend/services/weight.ts @@ -1,8 +1,12 @@ import { useApi } from '~/composables/useApi' -import type { WeightEntryData } from '~/services/dto/reception-data' +import type {ReceptionData, ReceptionPayload, WeightEntryData} from '~/services/dto/reception-data' +import type {Ref} from "vue"; +import type {ShipmentData, ShipmentPayload} from "~/services/dto/shipment-data"; +import type {WeighingMode} from "~/composables/useWeighing"; export type WeightPayload = { - reception: string + reception?: string + shipment?: string type: 'gross' | 'tare' dsd: number | null weight: number | null @@ -16,5 +20,22 @@ export async function createWeight(payload: WeightPayload) { export async function updateWeight(id: number, payload: Partial) { const api = useApi() - return api.patch(`weights/${id}`, payload) + return api.patch(`weights/${id}`, payload,{ + toastErrorKey: 'errors.weight.update', + toastSuccessKey: 'success.weight.update' + }) +} + +export type UseWeighingShipmentOptions = { + modeShipment: WeighingMode + shipment: Ref + updateShipment: (id: number, payload: ShipmentPayload) => Promise + loadShipment?: (id: number) => Promise +} + +export type UseWeighingOptions = { + mode: WeighingMode + reception: Ref + updateReception: (id: number, payload: ReceptionPayload) => Promise + loadReception?: (id: number) => Promise } diff --git a/frontend/stores/shipment.ts b/frontend/stores/shipment.ts new file mode 100644 index 0000000..a92b919 --- /dev/null +++ b/frontend/stores/shipment.ts @@ -0,0 +1,58 @@ +import { defineStore } from 'pinia' +import type {ShipmentData, ShipmentPayload} from "~/services/dto/shipment-data"; +import {createShipment, getShipment, updateShipment} from "~/services/shipment"; + +const isShipmentData = (value: unknown): value is ShipmentData => { + return Boolean(value && typeof value === 'object' && 'id' in value) +} +export const useShipmentStore = defineStore('shipment', { + state: () => ({ + current: null as ShipmentData | null, + isLoading: false + }), + actions: { + setCurrent(shipment: ShipmentData | null) { + this.current = shipment + }, + clearCurrent() { + this.current = null + }, + async loadShipment(id: number) { + this.isLoading = true + const result = await getShipment(id).finally(() => { + this.isLoading = false + }) + if (!isShipmentData(result)) { + this.current = null + return null + } + + this.current = result + return result + }, + async createShipment(payload: ShipmentPayload = {}) { + this.isLoading = true + const result = await createShipment(payload).finally(() => { + this.isLoading = false + }) + if (!isShipmentData(result)) { + return null + } + + this.current = result + return result + }, + async updateShipment(id: number, payload: ShipmentPayload) { + this.isLoading = true + const result = await updateShipment(id, payload).finally(() => { + this.isLoading = false + }) + if (!isShipmentData(result)) { + return null + } + + this.current = result + return result + } + } +}) diff --git a/frontend/utils/constants.ts b/frontend/utils/constants.ts index 719b4f6..2f741b8 100644 --- a/frontend/utils/constants.ts +++ b/frontend/utils/constants.ts @@ -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' } diff --git a/makefile b/makefile index 0856c61..8065f4b 100644 --- a/makefile +++ b/makefile @@ -79,7 +79,7 @@ migration-migrate: $(SYMFONY_CONSOLE) --no-interaction doctrine:migrations:migrate --allow-no-migration fixtures: - $(SYMFONY_CONSOLE) doctrine:fixtures:load + $(SYMFONY_CONSOLE) --no-interaction doctrine:fixtures:load # Attention, supprime votre bdd local db-reset: diff --git a/migrations/Version20260204101625.php b/migrations/Version20260204101625.php new file mode 100644 index 0000000..9dd7038 --- /dev/null +++ b/migrations/Version20260204101625.php @@ -0,0 +1,64 @@ +addSql('CREATE TABLE bovin_shipment (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, nb_bovin_send INT NOT NULL, shipment_id INT DEFAULT NULL, shipment_type_id INT DEFAULT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_7049F4507BE036FC ON bovin_shipment (shipment_id)'); + $this->addSql('CREATE INDEX IDX_7049F4502EE48A36 ON bovin_shipment (shipment_type_id)'); + $this->addSql('CREATE TABLE customer (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(255) NOT NULL, code VARCHAR(255) NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE TABLE customer_address (customer_id INT NOT NULL, address_id INT NOT NULL, PRIMARY KEY (customer_id, address_id))'); + $this->addSql('CREATE INDEX IDX_1193CB3F9395C3F3 ON customer_address (customer_id)'); + $this->addSql('CREATE INDEX IDX_1193CB3FF5B7AF75 ON customer_address (address_id)'); + $this->addSql('CREATE TABLE shipment (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, licence_plate VARCHAR(255) NOT NULL, identification_number VARCHAR(20) DEFAULT NULL, current_step INT DEFAULT 0 NOT NULL, is_valid BOOLEAN NOT NULL, shipment_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, carrier_id INT DEFAULT NULL, vehicle_id INT DEFAULT NULL, truck_id INT DEFAULT NULL, customer_id INT DEFAULT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_2CB20DC347639A5 ON shipment (identification_number)'); + $this->addSql('CREATE INDEX IDX_2CB20DC21DFC797 ON shipment (carrier_id)'); + $this->addSql('CREATE INDEX IDX_2CB20DC545317D1 ON shipment (vehicle_id)'); + $this->addSql('CREATE INDEX IDX_2CB20DCC6957CCE ON shipment (truck_id)'); + $this->addSql('CREATE INDEX IDX_2CB20DC9395C3F3 ON shipment (customer_id)'); + $this->addSql('CREATE TABLE shipment_type (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, label VARCHAR(255) NOT NULL, code VARCHAR(255) NOT NULL, PRIMARY KEY (id))'); + $this->addSql('ALTER TABLE bovin_shipment ADD CONSTRAINT FK_7049F4507BE036FC FOREIGN KEY (shipment_id) REFERENCES shipment (id)'); + $this->addSql('ALTER TABLE bovin_shipment ADD CONSTRAINT FK_7049F4502EE48A36 FOREIGN KEY (shipment_type_id) REFERENCES shipment_type (id)'); + $this->addSql('ALTER TABLE customer_address ADD CONSTRAINT FK_1193CB3F9395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE customer_address ADD CONSTRAINT FK_1193CB3FF5B7AF75 FOREIGN KEY (address_id) REFERENCES address (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DC21DFC797 FOREIGN KEY (carrier_id) REFERENCES carrier (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DC545317D1 FOREIGN KEY (vehicle_id) REFERENCES vehicle (id)'); + $this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DCC6957CCE FOREIGN KEY (truck_id) REFERENCES truck (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE shipment ADD CONSTRAINT FK_2CB20DC9395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE bovin_shipment DROP CONSTRAINT FK_7049F4507BE036FC'); + $this->addSql('ALTER TABLE bovin_shipment DROP CONSTRAINT FK_7049F4502EE48A36'); + $this->addSql('ALTER TABLE customer_address DROP CONSTRAINT FK_1193CB3F9395C3F3'); + $this->addSql('ALTER TABLE customer_address DROP CONSTRAINT FK_1193CB3FF5B7AF75'); + $this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DC21DFC797'); + $this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DC545317D1'); + $this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DCC6957CCE'); + $this->addSql('ALTER TABLE shipment DROP CONSTRAINT FK_2CB20DC9395C3F3'); + $this->addSql('DROP TABLE bovin_shipment'); + $this->addSql('DROP TABLE customer'); + $this->addSql('DROP TABLE customer_address'); + $this->addSql('DROP TABLE shipment'); + $this->addSql('DROP TABLE shipment_type'); + } +} diff --git a/migrations/Version20260204102423.php b/migrations/Version20260204102423.php new file mode 100644 index 0000000..7db4507 --- /dev/null +++ b/migrations/Version20260204102423.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE shipment DROP CONSTRAINT fk_2cb20dc545317d1'); + $this->addSql('DROP INDEX idx_2cb20dc545317d1'); + $this->addSql('ALTER TABLE shipment DROP vehicle_id'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE shipment ADD vehicle_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE shipment ADD CONSTRAINT fk_2cb20dc545317d1 FOREIGN KEY (vehicle_id) REFERENCES vehicle (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX idx_2cb20dc545317d1 ON shipment (vehicle_id)'); + } +} diff --git a/migrations/Version20260211075656.php b/migrations/Version20260211075656.php new file mode 100644 index 0000000..6bc612f --- /dev/null +++ b/migrations/Version20260211075656.php @@ -0,0 +1,49 @@ +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'); + } +} diff --git a/migrations/Version20260211123000.php b/migrations/Version20260211123000.php new file mode 100644 index 0000000..32ba355 --- /dev/null +++ b/migrations/Version20260211123000.php @@ -0,0 +1,38 @@ +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'); + } +} diff --git a/src/Dto/PontBasculeReading.php b/src/Dto/PontBasculeReading.php index 648c792..22c6c52 100644 --- a/src/Dto/PontBasculeReading.php +++ b/src/Dto/PontBasculeReading.php @@ -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, ) {} diff --git a/src/Entity/Address.php b/src/Entity/Address.php index 29ec41c..b0cc38c 100644 --- a/src/Entity/Address.php +++ b/src/Entity/Address.php @@ -43,31 +43,31 @@ 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)] - #[Groups(['address:read', 'supplier:read', 'reception:read', 'address:write'])] + #[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])] private string $label = ''; #[ORM\Column(length: 180)] - #[Groups(['address:read', 'supplier:read', 'reception:read', 'address:write'])] + #[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])] private string $street = ''; #[ORM\Column(name: 'street2', length: 180, nullable: true)] - #[Groups(['address:read', 'supplier:read', 'reception:read', 'address:write'])] + #[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])] private ?string $street2 = null; #[ORM\Column(name: 'postal_code', length: 20)] - #[Groups(['address:read', 'supplier:read', 'reception:read', 'address:write'])] + #[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])] private string $postalCode = ''; #[ORM\Column(length: 120)] - #[Groups(['address:read', 'supplier:read', 'reception:read', 'address:write'])] + #[Groups(['address:read', 'supplier:read', 'reception:read', 'customer:read', 'shipment:read', 'address:write'])] private string $city = ''; #[ORM\Column(name: 'country_code', length: 2)] - #[Groups(['address:read', 'supplier:read', 'address:write'])] + #[Groups(['address:read', 'supplier:read', 'customer:read', 'address:write'])] private string $countryCode = ''; /** @@ -76,9 +76,16 @@ class Address #[ORM\ManyToMany(targetEntity: Supplier::class, mappedBy: 'addresses')] private Collection $suppliers; + /** + * @var Collection + */ + #[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 @@ -158,7 +165,7 @@ class Address return $this; } - #[Groups(['address:read', 'supplier:read', 'reception:read'])] + #[Groups(['address:read', 'supplier:read', 'reception:read', 'shipment:read', 'customer:read'])] public function getFullAddress(): string { $parts = array_filter([ @@ -177,4 +184,34 @@ class Address { return $this->suppliers; } + + /** + * @return Collection + */ + 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; + } } diff --git a/src/Entity/BovinShipment.php b/src/Entity/BovinShipment.php new file mode 100644 index 0000000..6a7bf4e --- /dev/null +++ b/src/Entity/BovinShipment.php @@ -0,0 +1,101 @@ + 'exact'])] +#[ORM\UniqueConstraint(name: 'uniq_bovin_shipment', columns: ['shipment_id', 'shipment_type_id'])] +#[ORM\Table(name: 'bovin_shipment')] +#[ApiResource( + operations: [ + new Get( + requirements: ['id' => '\d+'], + normalizationContext: ['groups' => ['shipment-bovine:read']], + ), + new GetCollection( + normalizationContext: ['groups' => ['shipment-bovine:read']], + ), + + new Post( + normalizationContext: ['groups' => ['shipment-bovine:read']], + denormalizationContext: ['groups' => ['shipment-bovine:write']], + ), + new Patch( + normalizationContext: ['groups' => ['shipment-bovine:read']], + denormalizationContext: ['groups' => ['shipment-bovine:write']], + ), + new Delete(), + ], + security: "is_granted('ROLE_USER')", +)] +class BovinShipment +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['shipment:read', 'shipment-bovine:read'])] + private ?int $id = null; + + #[ORM\ManyToOne(inversedBy: 'bovinShipments')] + #[Groups(['shipment-bovine:read', 'shipment-bovine:write'])] + #[ApiProperty(readableLink: true)] + private ?Shipment $shipment = null; + + #[ORM\ManyToOne] + #[Groups(['shipment:read', 'shipment-bovine:write', 'shipment-bovine:read'])] + #[ApiProperty(readableLink: true)] + private ?ShipmentType $shipmentType = null; + + #[ORM\Column] + #[Groups(['shipment:read', 'shipment-bovine:write', 'shipment-bovine:read'])] + private ?int $nbBovinSend = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getShipment(): ?Shipment + { + return $this->shipment; + } + + public function setShipment(?Shipment $shipment): void + { + $this->shipment = $shipment; + } + + public function getShipmentType(): ?ShipmentType + { + return $this->shipmentType; + } + + public function setShipmentType(?ShipmentType $shipmentType): void + { + $this->shipmentType = $shipmentType; + } + + public function getNbBovinSend(): ?int + { + return $this->nbBovinSend; + } + + public function setNbBovinSend(?int $nbBovinSend): void + { + $this->nbBovinSend = $nbBovinSend; + } +} diff --git a/src/Entity/Carrier.php b/src/Entity/Carrier.php index 048fd51..90c9a29 100644 --- a/src/Entity/Carrier.php +++ b/src/Entity/Carrier.php @@ -42,15 +42,15 @@ class Carrier #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] - #[Groups(['carrier:read', 'driver:read', 'vehicle:read', 'reception:read'])] + #[Groups(['carrier:read', 'driver:read', 'vehicle:read', 'reception:read', 'shipment:read'])] private ?int $id = null; #[ORM\Column(length: 180)] - #[Groups(['carrier:read', 'carrier:write', 'driver:read', 'vehicle:read', 'reception:read'])] + #[Groups(['carrier:read', 'carrier:write', 'driver:read', 'vehicle:read', 'reception:read', 'shipment:read'])] private string $name = ''; #[ORM\Column(length: 30, nullable: true)] - #[Groups(['carrier:read', 'carrier:write', 'driver:read', 'vehicle:read', 'reception:read'])] + #[Groups(['carrier:read', 'carrier:write', 'driver:read', 'vehicle:read', 'reception:read', 'shipment:read'])] private ?string $code = null; public function getId(): ?int diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php new file mode 100644 index 0000000..ad6e09c --- /dev/null +++ b/src/Entity/Customer.php @@ -0,0 +1,94 @@ + '\d+'], + normalizationContext: ['groups' => ['customer:read']], + ), + new GetCollection( + normalizationContext: ['groups' => ['customer:read']], + ), + ], + security: "is_granted('ROLE_USER')", +)] +class Customer +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['shipment:read', 'customer:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['customer:read', 'shipment:read'])] + private ?string $label = null; + + #[ORM\Column(length: 255)] + #[Groups(['customer:read', 'shipment:read'])] + private ?string $code = null; + + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Address::class, inversedBy: 'customers')] + #[ORM\JoinTable(name: 'customer_address')] + #[Groups(['customer:read'])] + #[ApiProperty(readableLink: true)] + private Collection $addresses; + + public function __construct() + { + $this->addresses = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(?string $label): void + { + $this->label = $label; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(?string $code): void + { + $this->code = $code; + } + + public function getAddresses(): Collection + { + return $this->addresses; + } + + public function setAddresses(Collection $addresses): void + { + $this->addresses = $addresses; + } +} diff --git a/src/Entity/Driver.php b/src/Entity/Driver.php index 418ae08..5010b61 100644 --- a/src/Entity/Driver.php +++ b/src/Entity/Driver.php @@ -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] diff --git a/src/Entity/Shipment.php b/src/Entity/Shipment.php new file mode 100644 index 0000000..8bcaed4 --- /dev/null +++ b/src/Entity/Shipment.php @@ -0,0 +1,366 @@ + '\d+'], + normalizationContext: ['groups' => ['shipment:read']], + ), + new GetCollection( + normalizationContext: ['groups' => ['shipment:read']], + ), + new Post( + normalizationContext: ['groups' => ['shipment:read']], + denormalizationContext: ['groups' => ['shipment:write']], + ), + new Patch( + requirements: ['id' => '\d+'], + 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, + ), + ], + security: "is_granted('ROLE_USER')", +)] +class Shipment +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['shipment:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['shipment:read', 'shipment:write'])] + private ?string $licencePlate = null; + + #[ORM\Column(length: 20, unique: true, nullable: true)] + #[Groups(['shipment:read'])] + private ?string $identificationNumber = null; + + #[ORM\Column(options: ['default' => 0])] + #[Groups(['shipment:read', 'shipment:write'])] + private int $currentStep = 0; + + #[ORM\Column] + #[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'])] + private ?DateTimeImmutable $shipmentDate = null; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: true)] + #[Groups(['shipment:read', 'shipment:write'])] + #[ApiProperty(readableLink: true)] + private ?Carrier $carrier = null; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: true)] + #[Groups(['shipment:read', 'shipment:write'])] + #[ApiProperty(readableLink: true)] + private ?Truck $truck = null; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: true)] + #[Groups(['shipment:read', 'shipment:write'])] + #[ApiProperty(readableLink: true)] + private ?Customer $customer = null; + + /** + * @var Collection + */ + #[ORM\OneToMany( + targetEntity: BovinShipment::class, + mappedBy: 'shipment', + cascade: ['persist', 'remove'], + orphanRemoval: true + )] + #[Groups(['shipment:read', 'shipment:write'])] + private Collection $bovinShipments; + + /** + * @var Collection + */ + #[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 + { + return $this->id; + } + + public function getLicencePlate(): ?string + { + return $this->licencePlate; + } + + public function setLicencePlate(?string $licencePlate): void + { + $this->licencePlate = $licencePlate; + } + + public function getIdentificationNumber(): ?string + { + return $this->identificationNumber; + } + + public function setIdentificationNumber(?string $identificationNumber): void + { + $this->identificationNumber = $identificationNumber; + } + + public function getCurrentStep(): int + { + return $this->currentStep; + } + + public function setCurrentStep(int $currentStep): void + { + $this->currentStep = $currentStep; + } + + public function getIsValid(): ?bool + { + 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; + } + + public function getShipmentDate(): ?DateTimeImmutable + { + return $this->shipmentDate; + } + + public function setShipmentDate(?DateTimeImmutable $shipmentDate): void + { + $this->shipmentDate = $shipmentDate; + } + + public function getCarrier(): ?Carrier + { + return $this->carrier; + } + + public function setCarrier(?Carrier $carrier): void + { + $this->carrier = $carrier; + } + + public function getTruck(): ?Truck + { + return $this->truck; + } + + public function setTruck(?Truck $truck): void + { + $this->truck = $truck; + } + + public function getCustomer(): ?Customer + { + return $this->customer; + } + + public function setCustomer(?Customer $customer): void + { + $this->customer = $customer; + } + + public function getBovinShipments(): Collection + { + return $this->bovinShipments; + } + + public function setBovinShipments(Collection $bovinShipments): void + { + $this->bovinShipments = $bovinShipments; + } + + public function addBovinShipment(BovinShipment $bovinShipment): self + { + if (!$this->bovinShipments->contains($bovinShipment)) { + $this->bovinShipments->add($bovinShipment); + $bovinShipment->setShipment($this); + } + + return $this; + } + + public function removeBovinShipment(BovinShipment $bovinShipment): self + { + if ($this->bovinShipments->removeElement($bovinShipment)) { + if ($bovinShipment->getShipment() === $this) { + $bovinShipment->setShipment(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + 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; + } +} diff --git a/src/Entity/ShipmentType.php b/src/Entity/ShipmentType.php new file mode 100644 index 0000000..0be6bc8 --- /dev/null +++ b/src/Entity/ShipmentType.php @@ -0,0 +1,71 @@ + '\d+'], + normalizationContext: ['groups' => ['shipment-type:read']], + ), + new GetCollection( + normalizationContext: ['groups' => ['shipment-type:read']], + ), + ], + security: "is_granted('ROLE_USER')", +)] +class ShipmentType +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['shipment-type:read', 'shipment:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['shipment-type:read', 'shipment:read'])] + private ?string $label = null; + + #[ORM\Column(length: 255)] + #[Groups(['shipment-type:read', 'shipment:read'])] + private ?string $code = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): static + { + $this->label = $label; + + return $this; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } +} diff --git a/src/Entity/Truck.php b/src/Entity/Truck.php index 2b89798..d8c3b42 100644 --- a/src/Entity/Truck.php +++ b/src/Entity/Truck.php @@ -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 diff --git a/src/Entity/User.php b/src/Entity/User.php index 13b672e..f45ccd6 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -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')] diff --git a/src/Entity/Weight.php b/src/Entity/Weight.php index 0a695ff..3da8576 100644 --- a/src/Entity/Weight.php +++ b/src/Entity/Weight.php @@ -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; diff --git a/src/State/ShipmentReceiptProvider.php b/src/State/ShipmentReceiptProvider.php new file mode 100644 index 0000000..6dab4f5 --- /dev/null +++ b/src/State/ShipmentReceiptProvider.php @@ -0,0 +1,63 @@ +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.'"', + ]); + } +} diff --git a/src/State/ShipmentWeighingProvider.php b/src/State/ShipmentWeighingProvider.php new file mode 100644 index 0000000..8a14d63 --- /dev/null +++ b/src/State/ShipmentWeighingProvider.php @@ -0,0 +1,30 @@ +pontBasculeService->fetch(); + } catch (PontBasculeException $exception) { + throw new HttpException(500, $exception->getMessage(), $exception); + } + + return $result; + } +} diff --git a/templates/reception_voucher.html.twig b/templates/reception_voucher.html.twig index 6c94ccd..138fbc4 100644 --- a/templates/reception_voucher.html.twig +++ b/templates/reception_voucher.html.twig @@ -141,16 +141,18 @@
- {{ reception.supplier.name }}
- {{ reception.address.street }}
- {% if reception.address.street2 %} + {{ reception.supplier ? reception.supplier.name : '-' }}
+ {{ reception.address ? reception.address.street : '' }}
+ {% if reception.address and reception.address.street2 %} {{ reception.address.street2 }}
{% endif %} - {{ reception.address.postalCode }} {{ reception.address.city }}
- {% if reception.supplier.phone %} + {% if reception.address %} + {{ reception.address.postalCode }} {{ reception.address.city }}
+ {% endif %} + {% if reception.supplier and reception.supplier.phone %} {{ reception.supplier.phone }}
{% endif %} - {% if reception.supplier.email %} + {% if reception.supplier and reception.supplier.email %} {{ reception.supplier.email}}
{% endif %}
@@ -168,7 +170,9 @@ N° réception - {{ reception.supplier.name }} + + {{ reception.supplier ? reception.supplier.name : '-' }} + {{ reception.receptionDate|date('d/m/Y') }} @@ -189,13 +193,11 @@ - {{ reception.receptionType.label }}

- + {{ reception.receptionType ? reception.receptionType.label : '-' }}

{% set grossWeight = null %} {% set tareWeight = null %} - - {% for weight in reception.weights %} + {% for weight in reception.weights|default([]) %} {% if weight.type == 'gross' %} {% set grossWeight = weight %}

Poids à plein : {{ grossWeight.weight }}kg (pesée n°{{ grossWeight.dsd }} {{ grossWeight.weighedAt|date('d/m/Y H:i:s') }})

@@ -219,45 +221,57 @@ - {% if reception.merchandiseType %} - {{ reception.merchandiseType.label }} - {% else %} - - - {% endif %} + Type de bovins

- {% if reception.merchandiseType and reception.merchandiseType.code == 'AUTRES' and reception.merchandiseDetail %} -

Précision : {{ reception.merchandiseDetail }}

- {% endif %} - - {% if reception.merchandiseType and reception.merchandiseType.code == 'GRANULE' %} - {% set pelletGroups = {} %} - {% for selection in reception.pelletBuildings %} - {% set pelletLabel = selection.pelletType.label %} - {% if pelletGroups[pelletLabel] is not defined %} - {% set pelletGroups = pelletGroups|merge({ (pelletLabel): [] }) %} - {% endif %} - {% set pelletGroups = pelletGroups|merge({ - (pelletLabel): pelletGroups[pelletLabel]|merge([selection.building.label]) - }) %} - {% endfor %} - - {% for pelletLabel, buildingLabels in pelletGroups %} -

{{ pelletLabel }} : {{ buildingLabels|join(', ') }}

+ {% if reception.receptionType and reception.receptionType.code == 'BOVINS' %} + {% if reception.bovinesTypes is not empty %} + {% for entry in reception.bovinesTypes %} +

+ {{ entry.bovineType ? entry.bovineType.label : '-' }} : {{ entry.quantity ?? 0 }} +

+ {% endfor %} {% else %} -

Aucun dépôt de granulés renseigné.

- {% endfor %} +

-

+ {% endif %} + + {% if reception.bovineDetail %} +

Autres : {{ reception.bovineDetail }}

+ {% endif %} {% else %} - {% set buildingLabels = [] %} - {% for building in reception.buildings %} - {% set buildingLabels = buildingLabels|merge([building.label]) %} - {% endfor %} - {% if buildingLabels %} -

Ferme : {{ buildingLabels|join(', ') }}

+ {% if reception.merchandiseType and reception.merchandiseType.code == 'AUTRES' and reception.merchandiseDetail %} +

Précision : {{ reception.merchandiseDetail }}

+ {% endif %} + + {% if reception.merchandiseType and reception.merchandiseType.code == 'GRANULE' %} + {% set pelletGroups = {} %} + {% for selection in reception.pelletBuildings|default([]) %} + {% set pelletLabel = selection.pelletType.label %} + {% if pelletGroups[pelletLabel] is not defined %} + {% set pelletGroups = pelletGroups|merge({ (pelletLabel): [] }) %} + {% endif %} + {% set pelletGroups = pelletGroups|merge({ + (pelletLabel): pelletGroups[pelletLabel]|merge([selection.building.label]) + }) %} + {% endfor %} + + {% for pelletLabel, buildingLabels in pelletGroups %} +

{{ pelletLabel }} : {{ buildingLabels|join(', ') }}

+ {% else %} +

Aucun dépôt de granulés renseigné.

+ {% endfor %} {% else %} -

Aucun bâtiment renseigné.

+ {% set buildingLabels = [] %} + {% for building in reception.buildings|default([]) %} + {% set buildingLabels = buildingLabels|merge([building.label]) %} + {% endfor %} + {% if buildingLabels %} +

Ferme : {{ buildingLabels|join(', ') }}

+ {% else %} +

Aucun bâtiment renseigné.

+ {% endif %} {% endif %} {% endif %}
@@ -273,9 +287,9 @@
- Transporteur : {{ reception.carrier.name }}
- Mode de livraison : {{ reception.truck.name }}
- Immatriculation : {{ reception.licensePlate }}

+ Transporteur : {{ reception.carrier ? reception.carrier.name : '-' }}
+ Mode de livraison : {{ reception.truck ? reception.truck.name : '-' }}
+ Immatriculation : {{ reception.licensePlate ?? '-' }}

diff --git a/templates/shipment_voucher.html.twig b/templates/shipment_voucher.html.twig new file mode 100644 index 0000000..eec1245 --- /dev/null +++ b/templates/shipment_voucher.html.twig @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + +
+ + + + +
+ SCEA LES NAUDS
+ 14 Allée d’Argenson
+ Z.I Nord – Secteur Est
+ 86100 CHATELLERAULT
+ Tel. : 05 49 20 09 10
+ Email : lpc.contacts@lpc-liot.fr
+ RCS Châtellerault B 444 262 455 +
+
+
+ {{ shipment.customer ? shipment.customer.label : '-' }}
+ {{ shipment.address ? shipment.address.street : '' }}
+ {% if shipment.address and shipment.address.street2 %} + {{ shipment.address.street2 }}
+ {% endif %} + {% if shipment.address %} + {{ shipment.address.postalCode }} {{ shipment.address.city }}
+ {% endif %} +
+
+ +
BON D'EXPEDITION
+ + + + + + + + + + + + + +
Code clientDateN° expédition
+ {{ shipment.customer ? shipment.customer.code : '-' }} + + {{ shipment.shipmentDate|date('d/m/Y') }} + + {{ shipment.identificationNumber ?? '-' }} +
+ +
+ + + + + + + + + + {% set grossWeight = null %} + {% set tareWeight = null %} + + + + + + + + + +
DésignationQté expédiée (kg)
+ Expédition

+
+ {% for weight in shipment.weights %} + {% if weight.type == 'gross' %} + {% set grossWeight = weight %} +

Poids à plein : {{ grossWeight.weight }}kg (pesée + n°{{ grossWeight.dsd }} {{ grossWeight.weighedAt|date('d/m/Y H:i:s') }})

+ {% elseif weight.type == 'tare' %} + {% set tareWeight = weight %} +

Poids à vide : {{ tareWeight.weight }}kg (pesée + n°{{ tareWeight.dsd }} {{ tareWeight.weighedAt|date('d/m/Y H:i:s') }})

+ {% endif %} + {% endfor %} +
+
+ {% if grossWeight and tareWeight %} + {{ grossWeight.weight - tareWeight.weight }} + {% else %} + 0 + {% endif %} +
+ Bovin

+
+ {% if shipment.bovinShipments is not empty %} + {% for entry in shipment.bovinShipments %} +

+ {{ entry.shipmentType ? entry.shipmentType.label : '-' }} : + {{ entry.nbBovinSend ?? 0 }} +

+ {% endfor %} + {% else %} +

-

+ {% endif %} +
+ +
+
+
+ + + + + + + + + + + +