diff --git a/CHANGELOG.md b/CHANGELOG.md index 308b37e..816a8d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/frontend/components/reception/reception-form.vue b/frontend/components/reception/reception-form.vue index 063a363..93d2daf 100644 --- a/frontend/components/reception/reception-form.vue +++ b/frontend/components/reception/reception-form.vue @@ -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) diff --git a/frontend/components/reception/reception-weight.vue b/frontend/components/reception/reception-weight.vue index 7e624d7..5547810 100644 --- a/frontend/components/reception/reception-weight.vue +++ b/frontend/components/reception/reception-weight.vue @@ -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)) diff --git a/frontend/components/shipment/shipment-form.vue b/frontend/components/shipment/shipment-form.vue index 2e50dae..1eb026a 100644 --- a/frontend/components/shipment/shipment-form.vue +++ b/frontend/components/shipment/shipment-form.vue @@ -1,10 +1,12 @@ - Éxpedition + Expédition + - + - + Type d'expédition @@ -40,10 +42,9 @@ - - + - + - + - + - + - + ([]) const vehicles = ref([]) 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([]) 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({ userId: '', @@ -197,22 +196,30 @@ const form = reactive({ vehicleId: '', licencePlate: '', }) - -const customerAddresses = computed(() => { +// Adresses liées au client sélectionné +const customerAddresses = computed(() => { 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(() => { 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(() => { if (!form.carrierId) { return [] @@ -223,8 +230,7 @@ const filteredVehicles = computed(() => { (!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 - } - const next: Record = {} - for (const entry of shipment.bovinShipments) { - const typeId = entry.shipmentType?.id - if (!typeId) { - continue + } else { + const next: Record = {} + for (const entry of shipment.bovinShipments) { + const typeId = entry.shipmentType?.id + if (!typeId) continue + next[String(typeId)] = entry.nbBovinSend ?? null } - next[String(typeId)] = entry.nbBovinSend ?? null + bovineQuantities.value = next } - bovineQuantities.value = next + isHydrating.value = false }, {immediate: true} ) - // Ajuste driver/vehicle quand le transporteur change (logique LIOT) watch( () => [form.customerId, customers.value], @@ -356,34 +366,43 @@ watch( }, {immediate: true} ) - // Valide/auto-sélectionne le véhicule selon camion + transporteur (LIOT) +const applyLiotDefaults = () => { + if (isHydrating.value) { + return + } + if (!form.carrierId) { + form.driverId = '' + form.vehicleId = '' + return + } + if (!isLiotCarrier.value) { + form.driverId = '' + form.vehicleId = '' + return + } + if (filteredDrivers.value.length === 1) { + form.driverId = String(filteredDrivers.value[0].id) + } + if (filteredVehicles.value.length === 1) { + form.vehicleId = String(filteredVehicles.value[0].id) + } +} watch( () => form.carrierId, () => { - if (isHydrating.value) { - return - } - if (!form.carrierId) { - form.driverId = '' - form.vehicleId = '' - return - } - if (!isLiotCarrier.value) { - form.driverId = '' - form.vehicleId = '' - return - } - if (filteredDrivers.value.length === 1) { - form.driverId = String(filteredDrivers.value[0].id) - } - if (filteredVehicles.value.length === 1) { - form.vehicleId = String(filteredVehicles.value[0].id) - } + 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() 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 ?? []) } diff --git a/frontend/components/shipment/shipment-weight.vue b/frontend/components/shipment/shipment-weight.vue new file mode 100644 index 0000000..07bcf47 --- /dev/null +++ b/frontend/components/shipment/shipment-weight.vue @@ -0,0 +1,101 @@ + + + + {{ title }} + + Pont-bascule connecté + + + + + + {{ displayWeight }} kg + + + + + + {{ displayWeight !== null ? 'refaire une pesee' : 'peser' }} + Valider la pesée + Générer le bon + + + + 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 @@ - - - - - {{ userId ? "Modifications de l'utilisateur" : "Ajout d'un utilisateur" }} - - - {{ userId ? 'Sauvegarder' : 'Ajouter' }} - - - - - - - - - - - - - - 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..25adbea 100644 --- a/frontend/composables/useWeighing.ts +++ b/frontend/composables/useWeighing.ts @@ -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 } +type UseWeighingShipmentOptions = { + modeShipment: WeighingMode + shipment: Ref + updateShipment: (id: number, payload: ShipmentPayload) => Promise + loadShipment?: (id: number) => Promise +} + export const useWeighing = ({ mode, reception, @@ -97,3 +106,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/i18n/locales/fr.json b/frontend/i18n/locales/fr.json index 6b7acba..4ce3287 100644 --- a/frontend/i18n/locales/fr.json +++ b/frontend/i18n/locales/fr.json @@ -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." }, diff --git a/frontend/pages/admin/dashboard.vue b/frontend/pages/admin/dashboard.vue index 788fdd1..28f3f30 100644 --- a/frontend/pages/admin/dashboard.vue +++ b/frontend/pages/admin/dashboard.vue @@ -1,7 +1,5 @@ - - diff --git a/frontend/pages/reception/update/[[id]].vue b/frontend/pages/reception/update/[[id]].vue index e5b28a7..b0a6cc6 100644 --- a/frontend/pages/reception/update/[[id]].vue +++ b/frontend/pages/reception/update/[[id]].vue @@ -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) diff --git a/frontend/pages/shipment/[[id]].vue b/frontend/pages/shipment/[[id]].vue index e4a1ef2..476e445 100644 --- a/frontend/pages/shipment/[[id]].vue +++ b/frontend/pages/shipment/[[id]].vue @@ -16,11 +16,10 @@ >Mettre en attente - - - TEST ETAPE 2 - + + + +
Pont-bascule connecté