Implement empty transport feature in DirectorInfo component

- Added functionality to allow directors to initiate empty transports without products, enhancing logistics management.
- Introduced a new transport form in the DirectorInfo component, enabling selection of vehicle types and target branches.
- Updated the i18n localization files to include new translations for the empty transport feature.
- Enhanced the BranchView to pass vehicle and branch data to the DirectorInfo component, ensuring proper functionality.
- This update aims to improve user experience and streamline transport operations within the application.
This commit is contained in:
Torsten Schulz (local)
2025-12-04 14:48:55 +01:00
parent e5ef334f7c
commit 98dea7dd39
6 changed files with 382 additions and 83 deletions

View File

@@ -116,6 +116,59 @@
</div>
</div>
</div>
<!-- Transport ohne Produkte (nur wenn Direktor vorhanden und mayStartTransport aktiviert) -->
<div v-if="director && director.mayStartTransport" class="director-transport-section">
<h4>{{ $t('falukant.branch.director.emptyTransport.title') }}</h4>
<p class="transport-description">
{{ $t('falukant.branch.director.emptyTransport.description') }}
</p>
<div class="transport-form">
<label>
{{ $t('falukant.branch.director.emptyTransport.vehicleType') }}
<select v-model.number="emptyTransportForm.vehicleTypeId" @change="loadEmptyTransportRoute">
<option :value="null" disabled>{{ $t('falukant.branch.director.emptyTransport.selectVehicle') }}</option>
<option v-for="vt in vehicleTypeOptions()" :key="vt.id" :value="vt.id">
{{ $t(`falukant.branch.vehicles.${vt.tr}`) }} ({{ vt.count }} × {{ vt.capacity }})
</option>
</select>
</label>
<label>
{{ $t('falukant.branch.director.emptyTransport.targetBranch') }}
<select v-model.number="emptyTransportForm.targetBranchId" @change="loadEmptyTransportRoute">
<option :value="null" disabled>{{ $t('falukant.branch.director.emptyTransport.selectTarget') }}</option>
<option v-for="tb in targetBranchOptions" :key="tb.id" :value="tb.id">
{{ tb.label }}
</option>
</select>
</label>
<div v-if="emptyTransportForm.costLabel" class="transport-cost">
{{ $t('falukant.branch.director.emptyTransport.cost', { cost: emptyTransportForm.costLabel }) }}
</div>
<div class="transport-route" v-if="emptyTransportForm.durationLabel">
<div>
{{ $t('falukant.branch.director.emptyTransport.duration', { duration: emptyTransportForm.durationLabel }) }}
</div>
<div>
{{ $t('falukant.branch.director.emptyTransport.arrival', { datetime: emptyTransportForm.etaLabel }) }}
</div>
<div v-if="emptyTransportForm.routeNames && emptyTransportForm.routeNames.length">
{{ $t('falukant.branch.director.emptyTransport.route') }}:
{{ emptyTransportForm.routeNames.join(' ') }}
</div>
</div>
<button
@click="createEmptyTransport"
:disabled="!emptyTransportForm.vehicleTypeId || !emptyTransportForm.targetBranchId"
>
{{ $t('falukant.branch.director.emptyTransport.create') }}
</button>
</div>
</div>
</div>
<NewDirectorDialog ref="newDirectorDialog" />
</template>
@@ -126,7 +179,11 @@ import NewDirectorDialog from '@/dialogues/falukant/NewDirectorDialog.vue';
export default {
name: "DirectorInfo",
props: { branchId: { type: Number, required: true } },
props: {
branchId: { type: Number, required: true },
vehicles: { type: Array, default: () => [] },
branches: { type: Array, default: () => [] },
},
components: {
NewDirectorDialog
},
@@ -135,6 +192,18 @@ export default {
director: null,
showNewDirectorDialog: false,
editIncome: null,
emptyTransportForm: {
vehicleTypeId: null,
targetBranchId: null,
distance: null,
durationHours: null,
eta: null,
durationLabel: '',
etaLabel: '',
routeNames: [],
cost: 0.1,
costLabel: '',
},
};
},
async mounted() {
@@ -212,6 +281,140 @@ export default {
teachDirector() {
alert(this.$t('falukant.branch.director.teachAlert'));
},
vehicleTypeOptions() {
const groups = {};
for (const v of this.vehicles || []) {
if (v.status !== 'available' || !v.type || !v.type.id) continue;
const id = v.type.id;
if (!groups[id]) {
groups[id] = {
id,
tr: v.type.tr,
capacity: v.type.capacity,
speed: v.type.speed,
count: 0,
};
}
groups[id].count += 1;
}
return Object.values(groups);
},
targetBranchOptions() {
return (this.branches || [])
.filter(b => ['store', 'fullstack'].includes(b.branchTypeLabelTr))
.filter(b => b.id !== this.branchId)
.map(b => ({
id: b.id,
label: `${b.cityName} ${b.type}`,
}));
},
async loadEmptyTransportRoute() {
this.emptyTransportForm.distance = null;
this.emptyTransportForm.durationHours = null;
this.emptyTransportForm.eta = null;
this.emptyTransportForm.durationLabel = '';
this.emptyTransportForm.etaLabel = '';
this.emptyTransportForm.routeNames = [];
const vType = this.vehicleTypeOptions().find(v => v.id === this.emptyTransportForm.vehicleTypeId);
const targetBranch = (this.branches || []).find(b => b.id === this.emptyTransportForm.targetBranchId);
const sourceBranch = (this.branches || []).find(b => b.id === this.branchId);
if (!vType || !targetBranch || !sourceBranch) {
return;
}
try {
const { data } = await apiClient.get('/api/falukant/transports/route', {
params: {
sourceRegionId: sourceBranch.regionId,
targetRegionId: targetBranch.regionId,
vehicleTypeId: vType.id,
},
});
if (data && data.totalDistance != null) {
const distance = data.totalDistance;
const speed = vType.speed || 1;
const hours = distance / speed;
this.emptyTransportForm.distance = distance;
this.emptyTransportForm.durationHours = hours;
const now = new Date();
const etaMs = now.getTime() + hours * 60 * 60 * 1000;
const etaDate = new Date(etaMs);
const fullHours = Math.floor(hours);
const minutes = Math.round((hours - fullHours) * 60);
const parts = [];
if (fullHours > 0) parts.push(`${fullHours} h`);
if (minutes > 0) parts.push(`${minutes} min`);
this.emptyTransportForm.durationLabel = parts.length ? parts.join(' ') : '0 min';
this.emptyTransportForm.etaLabel = etaDate.toLocaleString();
this.emptyTransportForm.routeNames = (data.regions || []).map(r => r.name);
}
// Kosten für leeren Transport: 0.1
this.emptyTransportForm.cost = 0.1;
this.emptyTransportForm.costLabel = this.formatMoney(0.1);
} catch (error) {
console.error('Error loading transport route:', error);
this.emptyTransportForm.distance = null;
this.emptyTransportForm.durationHours = null;
this.emptyTransportForm.eta = null;
this.emptyTransportForm.durationLabel = '';
this.emptyTransportForm.etaLabel = '';
this.emptyTransportForm.routeNames = [];
}
},
formatMoney(amount) {
if (amount == null) return '';
try {
return amount.toLocaleString(undefined, {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});
} catch (e) {
return String(amount);
}
},
async createEmptyTransport() {
if (!this.emptyTransportForm.vehicleTypeId || !this.emptyTransportForm.targetBranchId) {
return;
}
try {
await apiClient.post('/api/falukant/transports', {
branchId: this.branchId,
vehicleTypeId: this.emptyTransportForm.vehicleTypeId,
productId: null,
quantity: 0,
targetBranchId: this.emptyTransportForm.targetBranchId,
});
// Formular zurücksetzen
this.emptyTransportForm = {
vehicleTypeId: null,
targetBranchId: null,
distance: null,
durationHours: null,
eta: null,
durationLabel: '',
etaLabel: '',
routeNames: [],
cost: 0.1,
costLabel: '',
};
alert(this.$t('falukant.branch.director.emptyTransport.success'));
this.$emit('transportCreated');
} catch (error) {
console.error('Error creating empty transport:', error);
alert(this.$t('falukant.branch.director.emptyTransport.error'));
}
},
},
};
</script>
@@ -284,4 +487,50 @@ export default {
border: 1px solid #ddd;
padding: 4px 6px;
}
.director-transport-section {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #ddd;
}
.transport-description {
margin-bottom: 1rem;
color: #666;
font-style: italic;
}
.transport-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.transport-form label {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.transport-form select {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.transport-cost {
font-weight: bold;
color: #333;
}
.transport-route {
padding: 0.75rem;
background-color: #f5f5f5;
border-radius: 4px;
font-size: 0.9em;
}
.transport-route > div {
margin-bottom: 0.25rem;
}
</style>