From 5ad27a87e570b9b585af127611728fee1645834c Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Fri, 5 Dec 2025 12:49:37 +0100 Subject: [PATCH] Enhance vehicle transport functionality in FalukantService and update UI components - Modified the createTransport method in FalukantService to support optional vehicleIds, allowing for more flexible vehicle selection. - Implemented logic to ensure that either specific vehicleIds or a vehicleTypeId must be provided, improving error handling for vehicle availability. - Updated the BranchView component to include new UI elements for sending vehicles, including buttons for sending single or multiple vehicles of the same type. - Added a modal dialog for selecting target branches when sending vehicles, enhancing user experience and streamlining transport operations. - Updated German localization files to include new translations related to vehicle actions and transport functionalities. --- backend/services/falukantService.js | 111 ++++++--- frontend/src/i18n/locales/de/falukant.json | 17 +- frontend/src/views/falukant/BranchView.vue | 256 +++++++++++++++++++++ 3 files changed, 355 insertions(+), 29 deletions(-) diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 1104174..c3b20f6 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -728,7 +728,7 @@ class FalukantService extends BaseService { }; } - async createTransport(hashedUserId, { branchId, vehicleTypeId, productId, quantity, targetBranchId }) { + async createTransport(hashedUserId, { branchId, vehicleTypeId, vehicleIds, productId, quantity, targetBranchId }) { const user = await getFalukantUserOrFail(hashedUserId); const sourceBranch = await Branch.findOne({ @@ -754,9 +754,89 @@ class FalukantService extends BaseService { const targetRegionId = targetBranch.regionId; const now = new Date(); - const type = await VehicleType.findByPk(vehicleTypeId); - if (!type) { - throw new Error('Vehicle type not found'); + let type; + let freeVehicles = []; + + // Wenn spezifische vehicleIds übergeben wurden, diese verwenden + if (vehicleIds && Array.isArray(vehicleIds) && vehicleIds.length > 0) { + const vehicles = await Vehicle.findAll({ + where: { + id: { [Op.in]: vehicleIds }, + falukantUserId: user.id, + regionId: sourceRegionId, + availableFrom: { [Op.lte]: now }, + }, + include: [ + { + model: Transport, + as: 'transports', + required: false, + attributes: ['id'], + }, + { + model: VehicleType, + as: 'type', + required: true, + }, + ], + }); + + freeVehicles = vehicles.filter((v) => { + const t = v.transports || []; + return t.length === 0; + }); + + if (freeVehicles.length === 0) { + throw new PreconditionError('noVehiclesAvailable'); + } + + // Alle Fahrzeuge müssen denselben Typ haben + const vehicleTypeIds = [...new Set(freeVehicles.map(v => v.vehicleTypeId))]; + if (vehicleTypeIds.length !== 1) { + throw new Error('All vehicles must be of the same type'); + } + + type = await VehicleType.findByPk(vehicleTypeIds[0]); + if (!type) { + throw new Error('Vehicle type not found'); + } + } else { + // Standard-Verhalten: Alle freien Fahrzeuge dieses Typs verwenden + if (!vehicleTypeId) { + throw new Error('Either vehicleTypeId or vehicleIds must be provided'); + } + + type = await VehicleType.findByPk(vehicleTypeId); + if (!type) { + throw new Error('Vehicle type not found'); + } + + // Freie Fahrzeuge dieses Typs in der Quell-Region + const vehicles = await Vehicle.findAll({ + where: { + falukantUserId: user.id, + regionId: sourceRegionId, + vehicleTypeId, + availableFrom: { [Op.lte]: now }, + }, + include: [ + { + model: Transport, + as: 'transports', + required: false, + attributes: ['id'], + }, + ], + }); + + freeVehicles = vehicles.filter((v) => { + const t = v.transports || []; + return t.length === 0; + }); + } + + if (!freeVehicles.length) { + throw new PreconditionError('noVehiclesAvailable'); } const route = await computeShortestRoute(type.transportMode, sourceRegionId, targetRegionId); @@ -764,29 +844,6 @@ class FalukantService extends BaseService { throw new PreconditionError('noRoute'); } - // Freie Fahrzeuge dieses Typs in der Quell-Region - const vehicles = await Vehicle.findAll({ - where: { - falukantUserId: user.id, - regionId: sourceRegionId, - vehicleTypeId, - availableFrom: { [Op.lte]: now }, - }, - include: [ - { - model: Transport, - as: 'transports', - required: false, - attributes: ['id'], - }, - ], - }); - - const freeVehicles = vehicles.filter((v) => { - const t = v.transports || []; - return t.length === 0; - }); - if (!freeVehicles.length) { throw new PreconditionError('noVehiclesAvailable'); } diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index 5d23299..2bf5c67 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -295,13 +295,26 @@ "mode": "Art", "speed": "Geschwindigkeit", "availableFrom": "Verfügbar ab", - "status": "Status" + "status": "Status", + "actions": "Aktionen" }, "status": { "inUse": "In Benutzung (mit Transport verknüpft)", "building": "Im Bau", "free": "Verfügbar" - } + }, + "send": "Versenden", + "sendSingle": "Einzelnes Fahrzeug versenden", + "sendAllFree": "Alle freien Fahrzeuge versenden", + "sendAllOfType": "{count} × {type} versenden", + "sendVehiclesTitle": "Fahrzeuge versenden", + "selectTargetBranch": "Ziel-Niederlassung", + "selectTarget": "Ziel-Niederlassung auswählen", + "selectTargetError": "Bitte wähle eine Ziel-Niederlassung aus.", + "noVehiclesSelected": "Keine Fahrzeuge ausgewählt.", + "sendSuccess": "Fahrzeuge erfolgreich versendet!", + "sendError": "Fehler beim Versenden der Fahrzeuge.", + "cancel": "Abbrechen" }, "stocktype": { "wood": "Holzlager", diff --git a/frontend/src/views/falukant/BranchView.vue b/frontend/src/views/falukant/BranchView.vue index df63cdc..d38b974 100644 --- a/frontend/src/views/falukant/BranchView.vue +++ b/frontend/src/views/falukant/BranchView.vue @@ -83,6 +83,7 @@ {{ $t('falukant.branch.transport.table.speed') }} {{ $t('falukant.branch.transport.table.availableFrom') }} {{ $t('falukant.branch.transport.table.status') }} + {{ $t('falukant.branch.transport.table.actions') }} @@ -106,9 +107,36 @@ {{ $t('falukant.branch.transport.status.free') }} + + + + + + +
+

{{ $t('falukant.branch.transport.sendAllFree') }}

+
+ +
+

{{ $t('falukant.branch.transport.noVehicles') }} @@ -121,6 +149,32 @@ :region-id="selectedBranch?.regionId" @bought="handleVehiclesBought" /> + + +

@@ -180,11 +234,30 @@ export default { { value: 'storage', label: 'falukant.branch.tabs.storage' }, { value: 'transport', label: 'falukant.branch.tabs.transport' }, ], + sendVehicleDialog: { + show: false, + vehicleId: null, + vehicleIds: null, + vehicleTypeId: null, + targetBranchId: null, + }, }; }, computed: { ...mapState(['socket', 'daemonSocket']), + freeVehiclesByType() { + const grouped = {}; + for (const v of this.vehicles || []) { + if (v.status !== 'available' || !v.type || !v.type.id) continue; + const typeId = v.type.id; + if (!grouped[typeId]) { + grouped[typeId] = []; + } + grouped[typeId].push(v); + } + return grouped; + }, }, async mounted() { @@ -506,6 +579,81 @@ export default { this.$refs.storageSection?.loadStorageData(); this.$refs.saleSection?.loadTransports(); }, + + openSendVehicleDialog(vehicleId) { + this.sendVehicleDialog = { + show: true, + vehicleId: vehicleId, + vehicleIds: null, + vehicleTypeId: null, + targetBranchId: null, + }; + }, + + openSendAllVehiclesDialog(vehicleTypeId, vehicleList) { + this.sendVehicleDialog = { + show: true, + vehicleId: null, + vehicleIds: vehicleList.map(v => v.id), + vehicleTypeId: vehicleTypeId, + targetBranchId: null, + }; + }, + + closeSendVehicleDialog() { + this.sendVehicleDialog = { + show: false, + vehicleId: null, + vehicleIds: null, + vehicleTypeId: null, + targetBranchId: null, + }; + }, + + targetBranchOptions() { + return (this.branches || []) + .filter(b => ['store', 'fullstack'].includes(b.branchTypeLabelTr)) + .filter(b => b.id !== this.selectedBranch?.id) + .map(b => ({ + id: b.id, + label: `${b.cityName} – ${b.type}`, + })); + }, + + async sendVehicles() { + if (!this.sendVehicleDialog.targetBranchId) { + alert(this.$t('falukant.branch.transport.selectTargetError')); + return; + } + + try { + const payload = { + branchId: this.selectedBranch.id, + targetBranchId: this.sendVehicleDialog.targetBranchId, + productId: null, + quantity: 0, + }; + + if (this.sendVehicleDialog.vehicleIds && this.sendVehicleDialog.vehicleIds.length > 0) { + payload.vehicleIds = this.sendVehicleDialog.vehicleIds; + } else if (this.sendVehicleDialog.vehicleId) { + payload.vehicleIds = [this.sendVehicleDialog.vehicleId]; + } else if (this.sendVehicleDialog.vehicleTypeId) { + payload.vehicleTypeId = this.sendVehicleDialog.vehicleTypeId; + } else { + alert(this.$t('falukant.branch.transport.noVehiclesSelected')); + return; + } + + await apiClient.post('/api/falukant/transports', payload); + await this.loadVehicles(); + this.closeSendVehicleDialog(); + alert(this.$t('falukant.branch.transport.sendSuccess')); + } catch (error) { + console.error('Error sending vehicles:', error); + alert(this.$t('falukant.branch.transport.sendError')); + } + }, }, }; @@ -514,4 +662,112 @@ export default { h2 { padding-top: 20px; } + +.send-all-vehicles { + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid #ddd; +} + +.send-all-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.send-all-buttons button { + padding: 0.5rem 1rem; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.send-all-buttons button:hover { + background-color: #0056b3; +} + +.no-action { + color: #999; + font-style: italic; +} + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background: white; + padding: 2rem; + border-radius: 8px; + min-width: 400px; + max-width: 600px; +} + +.send-vehicle-form { + display: flex; + flex-direction: column; + gap: 1rem; + margin-top: 1rem; +} + +.send-vehicle-form label { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.send-vehicle-form select { + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; +} + +.modal-buttons { + display: flex; + gap: 0.5rem; + justify-content: flex-end; + margin-top: 1rem; +} + +.modal-buttons button { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.modal-buttons button:first-child { + background-color: #007bff; + color: white; +} + +.modal-buttons button:first-child:hover { + background-color: #0056b3; +} + +.modal-buttons button:first-child:disabled { + background-color: #ccc; + cursor: not-allowed; +} + +.modal-buttons button:last-child { + background-color: #6c757d; + color: white; +} + +.modal-buttons button:last-child:hover { + background-color: #5a6268; +}