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:
@@ -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 user = await getFalukantUserOrFail(hashedUserId);
|
||||||
|
|
||||||
const sourceBranch = await Branch.findOne({
|
const sourceBranch = await Branch.findOne({
|
||||||
@@ -754,14 +754,61 @@ class FalukantService extends BaseService {
|
|||||||
const targetRegionId = targetBranch.regionId;
|
const targetRegionId = targetBranch.regionId;
|
||||||
const now = new Date();
|
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) {
|
if (!type) {
|
||||||
throw new Error('Vehicle type not found');
|
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);
|
type = await VehicleType.findByPk(vehicleTypeId);
|
||||||
if (!route) {
|
if (!type) {
|
||||||
throw new PreconditionError('noRoute');
|
throw new Error('Vehicle type not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Freie Fahrzeuge dieses Typs in der Quell-Region
|
// 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 || [];
|
const t = v.transports || [];
|
||||||
return t.length === 0;
|
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) {
|
if (!freeVehicles.length) {
|
||||||
throw new PreconditionError('noVehiclesAvailable');
|
throw new PreconditionError('noVehiclesAvailable');
|
||||||
|
|||||||
@@ -295,13 +295,26 @@
|
|||||||
"mode": "Art",
|
"mode": "Art",
|
||||||
"speed": "Geschwindigkeit",
|
"speed": "Geschwindigkeit",
|
||||||
"availableFrom": "Verfügbar ab",
|
"availableFrom": "Verfügbar ab",
|
||||||
"status": "Status"
|
"status": "Status",
|
||||||
|
"actions": "Aktionen"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"inUse": "In Benutzung (mit Transport verknüpft)",
|
"inUse": "In Benutzung (mit Transport verknüpft)",
|
||||||
"building": "Im Bau",
|
"building": "Im Bau",
|
||||||
"free": "Verfügbar"
|
"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": {
|
"stocktype": {
|
||||||
"wood": "Holzlager",
|
"wood": "Holzlager",
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
<th>{{ $t('falukant.branch.transport.table.speed') }}</th>
|
<th>{{ $t('falukant.branch.transport.table.speed') }}</th>
|
||||||
<th>{{ $t('falukant.branch.transport.table.availableFrom') }}</th>
|
<th>{{ $t('falukant.branch.transport.table.availableFrom') }}</th>
|
||||||
<th>{{ $t('falukant.branch.transport.table.status') }}</th>
|
<th>{{ $t('falukant.branch.transport.table.status') }}</th>
|
||||||
|
<th>{{ $t('falukant.branch.transport.table.actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -106,9 +107,36 @@
|
|||||||
{{ $t('falukant.branch.transport.status.free') }}
|
{{ $t('falukant.branch.transport.status.free') }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
</div>
|
||||||
<p v-else class="no-vehicles">
|
<p v-else class="no-vehicles">
|
||||||
{{ $t('falukant.branch.transport.noVehicles') }}
|
{{ $t('falukant.branch.transport.noVehicles') }}
|
||||||
@@ -121,6 +149,32 @@
|
|||||||
:region-id="selectedBranch?.regionId"
|
:region-id="selectedBranch?.regionId"
|
||||||
@bought="handleVehiclesBought"
|
@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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -180,11 +234,30 @@ export default {
|
|||||||
{ value: 'storage', label: 'falukant.branch.tabs.storage' },
|
{ value: 'storage', label: 'falukant.branch.tabs.storage' },
|
||||||
{ value: 'transport', label: 'falukant.branch.tabs.transport' },
|
{ value: 'transport', label: 'falukant.branch.tabs.transport' },
|
||||||
],
|
],
|
||||||
|
sendVehicleDialog: {
|
||||||
|
show: false,
|
||||||
|
vehicleId: null,
|
||||||
|
vehicleIds: null,
|
||||||
|
vehicleTypeId: null,
|
||||||
|
targetBranchId: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['socket', 'daemonSocket']),
|
...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() {
|
async mounted() {
|
||||||
@@ -506,6 +579,81 @@ export default {
|
|||||||
this.$refs.storageSection?.loadStorageData();
|
this.$refs.storageSection?.loadStorageData();
|
||||||
this.$refs.saleSection?.loadTransports();
|
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>
|
</script>
|
||||||
@@ -514,4 +662,112 @@ export default {
|
|||||||
h2 {
|
h2 {
|
||||||
padding-top: 20px;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user