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.
This commit is contained in:
Torsten Schulz (local)
2025-12-05 12:49:37 +01:00
parent 085b851925
commit 5ad27a87e5
3 changed files with 355 additions and 29 deletions

View File

@@ -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,14 +754,61 @@ class FalukantService extends BaseService {
const targetRegionId = targetBranch.regionId;
const now = new Date();
const type = await VehicleType.findByPk(vehicleTypeId);
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');
}
const route = await computeShortestRoute(type.transportMode, sourceRegionId, targetRegionId);
if (!route) {
throw new PreconditionError('noRoute');
type = await VehicleType.findByPk(vehicleTypeId);
if (!type) {
throw new Error('Vehicle type not found');
}
// Freie Fahrzeuge dieses Typs in der Quell-Region
@@ -782,10 +829,20 @@ class FalukantService extends BaseService {
],
});
const freeVehicles = vehicles.filter((v) => {
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);
if (!route) {
throw new PreconditionError('noRoute');
}
if (!freeVehicles.length) {
throw new PreconditionError('noVehiclesAvailable');

View File

@@ -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",

View File

@@ -83,6 +83,7 @@
<th>{{ $t('falukant.branch.transport.table.speed') }}</th>
<th>{{ $t('falukant.branch.transport.table.availableFrom') }}</th>
<th>{{ $t('falukant.branch.transport.table.status') }}</th>
<th>{{ $t('falukant.branch.transport.table.actions') }}</th>
</tr>
</thead>
<tbody>
@@ -106,9 +107,36 @@
{{ $t('falukant.branch.transport.status.free') }}
</span>
</td>
<td>
<button
v-if="v.status === 'available'"
@click="openSendVehicleDialog(v.id)"
:title="$t('falukant.branch.transport.sendSingle')"
>
{{ $t('falukant.branch.transport.send') }}
</button>
<span v-else class="no-action"></span>
</td>
</tr>
</tbody>
</table>
<!-- Buttons zum Versenden aller freien Fahrzeuge eines Typs -->
<div class="send-all-vehicles" v-if="freeVehiclesByType && Object.keys(freeVehiclesByType).length > 0">
<h4>{{ $t('falukant.branch.transport.sendAllFree') }}</h4>
<div class="send-all-buttons">
<button
v-for="(vehicleList, vehicleTypeId) in freeVehiclesByType"
:key="vehicleTypeId"
@click="openSendAllVehiclesDialog(parseInt(vehicleTypeId), vehicleList)"
>
{{ $t('falukant.branch.transport.sendAllOfType', {
type: $t(`falukant.branch.vehicles.${vehicleList[0].type.tr}`),
count: vehicleList.length
}) }}
</button>
</div>
</div>
</div>
<p v-else class="no-vehicles">
{{ $t('falukant.branch.transport.noVehicles') }}
@@ -121,6 +149,32 @@
:region-id="selectedBranch?.regionId"
@bought="handleVehiclesBought"
/>
<!-- Dialog zum Versenden von Fahrzeugen -->
<div v-if="sendVehicleDialog.show" class="modal-overlay" @click.self="closeSendVehicleDialog">
<div class="modal-content">
<h3>{{ $t('falukant.branch.transport.sendVehiclesTitle') }}</h3>
<div class="send-vehicle-form">
<label>
{{ $t('falukant.branch.transport.selectTargetBranch') }}
<select v-model.number="sendVehicleDialog.targetBranchId">
<option :value="null" disabled>{{ $t('falukant.branch.transport.selectTarget') }}</option>
<option v-for="tb in targetBranchOptions()" :key="tb.id" :value="tb.id">
{{ tb.label }}
</option>
</select>
</label>
<div class="modal-buttons">
<button @click="sendVehicles" :disabled="!sendVehicleDialog.targetBranchId">
{{ $t('falukant.branch.transport.send') }}
</button>
<button @click="closeSendVehicleDialog">
{{ $t('falukant.branch.transport.cancel') }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -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'));
}
},
},
};
</script>
@@ -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;
}
</style>