Files
yourpart3/frontend/src/components/falukant/DirectorInfo.vue
Torsten Schulz (local) 0a3864f0a9 Optimize falukantService and DirectorInfo component for improved performance and user experience
- Refactored proposal handling in falukantService to load existing proposals before cleaning expired ones, reducing unnecessary database queries.
- Enhanced fetchProposals method with explicit joins for better performance and added a limit to avoid excessive data retrieval.
- Updated DirectorInfo component to reload data after hiring a director, ensuring the UI reflects the latest information.
2026-01-22 10:15:45 +01:00

577 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="director-info">
<div v-if="!director || director === null">
<button @click="openNewDirectorDialog">
{{ $t('falukant.branch.director.actions.new') }}
</button>
</div>
<div v-else class="director-info-container">
<!-- Linke Seite: Stammdaten & Wissen (aus /falukant/directors) -->
<div class="director-main">
<h3 class="director-name">
{{ $t('falukant.titles.' + director.character.gender + '.' + director.character.nobleTitle.labelTr) }}
{{ director.character.definedFirstName.name }} {{ director.character.definedLastName.name }}
</h3>
<p class="director-meta">
{{ $t('falukant.director.age') }}:
{{ director.character.age }}
<span v-if="director.region"> {{ director.region }}</span>
</p>
<div
v-if="director.character.knowledges && director.character.knowledges.length"
class="knowledge-panel"
>
<h4>{{ $t('falukant.director.knowledge.title') }}</h4>
<div class="table-container">
<table class="knowledge-table">
<thead>
<tr>
<th>{{ $t('falukant.director.product') }}</th>
<th>{{ $t('falukant.director.knowledge.knowledge') }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in director.character.knowledges"
:key="item.productId"
>
<td>{{ $t(`falukant.product.${item.productType.labelTr}`) }}</td>
<td>{{ item.knowledge }} %</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Rechte Seite: Aktionen, Einkommen, Rechte -->
<div class="director-actions">
<div class="field">
<label>
{{ $t('falukant.branch.director.satisfaction') }}:
<span>{{ director.satisfaction }} %</span>
</label>
</div>
<div class="field">
<label>
{{ $t('falukant.branch.director.income') }}:
<input type="number" v-model.number="editIncome" />
</label>
<span
v-if="director.wishedIncome != null"
class="link"
@click="setWishedIncome"
>
({{ $t('falukant.director.wishedIncome') }}:
{{ director.wishedIncome }})
</span>
<div v-if="incomeUpdateSuccess" class="income-update-success">
{{ $t('falukant.branch.director.incomeUpdated') }}
</div>
</div>
<div class="field">
<button @click="updateDirector">
{{ $t('falukant.director.updateButton') }}
</button>
</div>
<div class="field toggles">
<label>
<input
type="checkbox"
v-model="director.mayProduce"
@change="saveSetting('mayProduce', director.mayProduce)"
/>
{{ $t('falukant.branch.director.produce') }}
</label>
<label>
<input
type="checkbox"
v-model="director.maySell"
@change="saveSetting('maySell', director.maySell)"
/>
{{ $t('falukant.branch.director.sell') }}
</label>
<label>
<input
type="checkbox"
v-model="director.mayStartTransport"
@change="saveSetting('mayStartTransport', director.mayStartTransport)"
/>
{{ $t('falukant.branch.director.starttransport') }}
</label>
<label>
<input
type="checkbox"
v-model="director.mayRepairVehicles"
@change="saveSetting('mayRepairVehicles', director.mayRepairVehicles)"
/>
{{ $t('falukant.branch.director.repairVehicles') }}
</label>
</div>
<div class="field">
<button @click="fireDirector">
{{ $t('falukant.branch.director.fire') }}
</button>
</div>
<div class="field">
<button @click="teachDirector">
{{ $t('falukant.branch.director.teach') }}
</button>
</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 }}) - {{ speedLabel(vt.speed) }}
</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" @directorHired="handleDirectorHired" />
</template>
<script>
import apiClient from '@/utils/axios.js';
import NewDirectorDialog from '@/dialogues/falukant/NewDirectorDialog.vue';
export default {
name: "DirectorInfo",
props: {
branchId: { type: Number, required: true },
vehicles: { type: Array, default: () => [] },
branches: { type: Array, default: () => [] },
},
components: {
NewDirectorDialog
},
data() {
return {
director: null,
showNewDirectorDialog: false,
editIncome: null,
incomeUpdateSuccess: false,
emptyTransportForm: {
vehicleTypeId: null,
targetBranchId: null,
distance: null,
durationHours: null,
eta: null,
durationLabel: '',
etaLabel: '',
routeNames: [],
cost: 0.1,
costLabel: '',
},
};
},
async mounted() {
await this.loadDirector();
},
methods: {
async refresh() {
await this.loadDirector();
},
async loadDirector() {
try {
const response = await apiClient.get(`/api/falukant/director/${this.branchId}`);
const data = response.data;
if (
!data ||
(Array.isArray(data) && data.length === 0) ||
typeof data.director === 'undefined' ||
data.director === null
) {
this.director = null;
this.editIncome = null;
} else {
this.director = data.director;
this.editIncome = this.director.income;
}
} catch (error) {
console.error('Error loading director:', error);
this.director = null;
this.editIncome = null;
}
},
async saveSetting(settingKey, value) {
if (!this.director) return;
try {
await apiClient.post(`/api/falukant/director/settings`, {
branchId: this.branchId,
directorId: this.director.id,
settingKey,
value,
});
} catch (error) {
console.error(`Error saving setting ${settingKey}:`, error);
}
},
speedLabel(value) {
const key = value == null ? 'unknown' : String(value);
const tKey = `falukant.branch.transport.speed.${key}`;
const translated = this.$t(tKey);
if (!translated || translated === tKey) return value;
return translated;
},
openNewDirectorDialog() {
console.log('openNewDirectorDialog');
this.$refs.newDirectorDialog.open(this.branchId);
},
async handleDirectorHired() {
// Nach dem Einstellen eines Direktors die Daten neu laden
await this.loadDirector();
},
async updateDirector() {
if (!this.director || this.editIncome == null) return;
try {
await apiClient.post(`/api/falukant/directors`, {
directorId: this.director.id,
income: this.editIncome,
});
await this.loadDirector();
// Erfolgsnachricht anzeigen
this.incomeUpdateSuccess = true;
// Nach 3 Sekunden automatisch ausblenden
setTimeout(() => {
this.incomeUpdateSuccess = false;
}, 3000);
} catch (error) {
console.error('Error updating director:', error);
}
},
setWishedIncome() {
if (!this.director || this.director.wishedIncome == null) return;
this.editIncome = this.director.wishedIncome;
},
fireDirector() {
alert(this.$t('falukant.branch.director.fireAlert'));
},
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>
<style scoped>
.director-info {
border: 1px solid #ccc;
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
.director-info-container {
display: flex;
gap: 1rem;
}
.director-main {
flex: 2;
}
.director-actions {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.director-name {
margin: 0 0 0.25rem 0;
}
.director-meta {
margin: 0 0 0.75rem 0;
font-style: italic;
}
.field {
margin-bottom: 0.5rem;
}
.field label {
display: flex;
align-items: center;
gap: 0.5rem;
}
.income-update-success {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
border-radius: 4px;
font-size: 0.9em;
}
.toggles label {
display: block;
}
.link {
color: #007bff;
cursor: pointer;
margin-left: 0.5rem;
}
.table-container {
max-height: 40vh;
overflow-y: auto;
}
.knowledge-table {
width: 100%;
border-collapse: collapse;
}
.knowledge-table th,
.knowledge-table td {
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>