Files
yourpart3/frontend/src/views/falukant/BranchView.vue
Torsten Schulz (local) 91f59062f5 Update BranchView to refresh active tab data on tab change and modify 3D model for female toddler character
- Enhanced the activeTab watcher to refresh data only when the selected branch changes and the tab is switched.
- Introduced a new refreshActiveTab method to load data for the currently active tab, improving data management and user experience.
- Updated the female toddler 3D model file for better integration in the application.
2026-01-12 08:07:50 +01:00

1328 lines
52 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="contenthidden">
<StatusBar ref="statusBar" />
<div class="contentscroll">
<h2>{{ $t('falukant.branch.title') }}</h2>
<BranchSelection
:branches="branches"
:selectedBranch="selectedBranch"
@branchSelected="onBranchSelected"
@createBranch="createBranch"
@upgradeBranch="upgradeBranch"
ref="branchSelection"
/>
<!-- Tab-Navigation für Inhalte der ausgewählten Niederlassung -->
<SimpleTabs
v-if="selectedBranch"
v-model="activeTab"
:tabs="tabs"
/>
<!-- Tab-Inhalte -->
<div v-if="selectedBranch" class="branch-tab-content">
<!-- Direktor -->
<div v-if="activeTab === 'director'" class="branch-tab-pane">
<DirectorInfo
:branchId="selectedBranch.id"
:vehicles="vehicles"
:branches="branches"
ref="directorInfo"
@transportCreated="handleTransportCreated"
/>
</div>
<!-- Inventar / Verkauf -->
<div v-else-if="activeTab === 'inventory'" class="branch-tab-pane">
<!-- Tax summary for inventory/sales -->
<div v-if="branchTaxes" class="branch-tax-summary">
<strong>{{ $t('falukant.branch.taxes.total') }}:</strong>
<span>{{ formatPercent(branchTaxes.total) }}</span>
</div>
<SaleSection
:branchId="selectedBranch.id"
:vehicles="vehicles"
:branches="branches"
ref="saleSection"
@transportCreated="handleTransportCreated"
/>
</div>
<!-- Produktion + Produkt-Erträge -->
<div v-else-if="activeTab === 'production'" class="branch-tab-pane">
<ProductionSection
:branchId="selectedBranch.id"
:products="products"
ref="productionSection"
/>
<!-- Tax summary for production -->
<div v-if="branchTaxes" class="branch-tax-summary">
<strong>{{ $t('falukant.branch.taxes.total') }}:</strong>
<span>{{ formatPercent(branchTaxes.total) }}</span>
</div>
<RevenueSection
:products="products"
:calculateProductRevenue="calculateProductRevenue"
:calculateProductProfit="calculateProductProfit"
:currentRegionId="selectedBranch?.regionId"
ref="revenueSection"
/>
</div>
<!-- Taxes Übersicht -->
<div v-else-if="activeTab === 'taxes'" class="branch-tab-pane">
<h3>{{ $t('falukant.branch.taxes.title') }}</h3>
<div v-if="branchTaxesLoading">
<p>{{ $t('falukant.branch.taxes.loading') }}</p>
</div>
<div v-else-if="branchTaxesError">
<p>{{ $t('falukant.branch.taxes.loadingError', { error: branchTaxesError }) }}</p>
<button @click="loadBranchTaxes">{{ $t('falukant.branch.taxes.retry') }}</button>
</div>
<div v-else-if="!branchTaxes || !branchTaxes.breakdown || branchTaxes.breakdown.length === 0">
<p>{{ $t('falukant.branch.taxes.noData') }}</p>
</div>
<div v-else>
<p>
<strong>{{ $t('falukant.branch.taxes.total') }}:</strong>
<span>{{ formatCurrency(branchTaxes.total) }}</span>
</p>
<table class="taxes-table">
<thead>
<tr>
<th>{{ $t('falukant.branch.taxes.table.region') }}</th>
<th>{{ $t('falukant.branch.taxes.table.taxPercent') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="r in branchTaxes.breakdown" :key="r.id">
<td>{{ r.name }}</td>
<td>{{ r.taxPercent }}%</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Lager -->
<div v-else-if="activeTab === 'storage'" class="branch-tab-pane">
<StorageSection :branchId="selectedBranch.id" ref="storageSection" />
</div>
<!-- Transportmittel -->
<div v-else-if="activeTab === 'transport'" class="branch-tab-pane">
<p>{{ $t('falukant.branch.transport.placeholder') }}</p>
<div class="vehicle-action-buttons">
<button @click="openBuyVehicleDialog">
{{ $t('falukant.branch.transport.buy') }}
</button>
<button
v-if="repairableVehiclesCount > 0"
@click="openRepairAllVehiclesDialog"
class="repair-all-button"
>
{{ $t('falukant.branch.transport.repairAll', { count: repairableVehiclesCount, cost: formatMoney(repairAllCost) }) }}
</button>
</div>
<div class="vehicle-overview" v-if="vehicles && vehicles.length">
<table class="vehicle-table">
<thead>
<tr>
<th>{{ $t('falukant.branch.transport.table.type') }}</th>
<th>{{ $t('falukant.branch.transport.table.capacity') }}</th>
<th>{{ $t('falukant.branch.transport.table.condition') }}</th>
<th>{{ $t('falukant.branch.transport.table.mode') }}</th>
<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>
<tr v-for="v in vehicles" :key="v.id">
<td>
{{ $t(`falukant.branch.vehicles.${v.type.tr}`) }}
</td>
<td>{{ v.type.capacity }}</td>
<td>{{ conditionLabel(v.condition) }}</td>
<td>{{ transportModeLabel(v.type.transportMode) }}</td>
<td>{{ speedLabel(v.type.speed) }}</td>
<td>{{ formatDateTime(v.availableFrom) }}</td>
<td>
<span v-if="v.status === 'travelling'">
{{ $t('falukant.branch.transport.status.inUse') }}
</span>
<span v-else-if="v.status === 'building'">
{{ $t('falukant.branch.transport.status.building') }}
</span>
<span v-else>
{{ $t('falukant.branch.transport.status.free') }}
</span>
</td>
<td>
<div class="vehicle-actions">
<button
v-if="v.status === 'available'"
@click="openSendVehicleDialog(v.id)"
:title="$t('falukant.branch.transport.sendSingle')"
>
{{ $t('falukant.branch.transport.send') }}
</button>
<button
v-if="v.status === 'available' && v.condition < 100"
@click="openRepairVehicleDialog(v)"
:title="$t('falukant.branch.transport.repairTooltip')"
class="repair-button"
>
{{ $t('falukant.branch.transport.repairWithCost', { cost: formatMoney(calculateRepairCost(v)) }) }}
</button>
<span v-if="v.status !== 'available'" class="no-action"></span>
</div>
</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') }}
</p>
</div>
</div>
<BuyVehicleDialog
v-if="selectedBranch"
ref="buyVehicleDialog"
: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 v-if="!sendVehicleDialog.success" 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 v-else class="send-vehicle-success">
<p>{{ $t('falukant.branch.transport.sendSuccess') }}</p>
<div class="modal-buttons">
<button @click="closeSendVehicleDialog">
{{ $t('falukant.branch.transport.close') }}
</button>
</div>
</div>
</div>
</div>
<!-- Dialog zum Reparieren aller Fahrzeuge -->
<div v-if="repairAllVehiclesDialog.show" class="modal-overlay" @click.self="closeRepairAllVehiclesDialog">
<div class="modal-content">
<h3>{{ $t('falukant.branch.transport.repairAllTitle') }}</h3>
<div class="repair-all-form">
<p>
{{ $t('falukant.branch.transport.repairAllDescription', {
count: repairAllVehiclesDialog.vehicleIds.length,
cost: formatMoney(repairAllVehiclesDialog.totalCost)
}) }}
</p>
<p class="repair-all-discount">
{{ $t('falukant.branch.transport.repairAllDiscount') }}
</p>
<div class="modal-buttons">
<button @click="repairAllVehicles" class="repair-confirm-button">
{{ $t('falukant.branch.transport.repairAllConfirm') }}
</button>
<button @click="closeRepairAllVehiclesDialog">
{{ $t('falukant.branch.transport.cancel') }}
</button>
</div>
</div>
</div>
</div>
<!-- Dialog zum Reparieren von Fahrzeugen -->
<div v-if="repairVehicleDialog.show" class="modal-overlay" @click.self="closeRepairVehicleDialog">
<div class="modal-content">
<h3>{{ $t('falukant.branch.transport.repairVehicleTitle') }}</h3>
<div class="repair-vehicle-form" v-if="repairVehicleDialog.vehicle">
<div class="repair-info">
<p>
<strong>{{ $t('falukant.branch.transport.repairVehicleType') }}:</strong>
{{ $t(`falukant.branch.vehicles.${repairVehicleDialog.vehicle.type.tr}`) }}
</p>
<p>
<strong>{{ $t('falukant.branch.transport.repairCurrentCondition') }}:</strong>
{{ repairVehicleDialog.vehicle.condition }}%
</p>
<p>
<strong>{{ $t('falukant.branch.transport.repairCost') }}:</strong>
{{ formatMoney(repairVehicleDialog.repairCost) }}
</p>
<p>
<strong>{{ $t('falukant.branch.transport.repairBuildTime') }}:</strong>
{{ formatBuildTime(repairVehicleDialog.buildTimeMinutes) }}
</p>
</div>
<div class="modal-buttons">
<button @click="repairVehicle" class="repair-confirm-button">
{{ $t('falukant.branch.transport.repairConfirm') }}
</button>
<button @click="closeRepairVehicleDialog">
{{ $t('falukant.branch.transport.cancel') }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusBar from '@/components/falukant/StatusBar.vue';
import BranchSelection from '@/components/falukant/BranchSelection.vue';
import SimpleTabs from '@/components/SimpleTabs.vue';
import DirectorInfo from '@/components/falukant/DirectorInfo.vue';
import SaleSection from '@/components/falukant/SaleSection.vue';
import ProductionSection from '@/components/falukant/ProductionSection.vue';
import StorageSection from '@/components/falukant/StorageSection.vue';
import RevenueSection from '@/components/falukant/RevenueSection.vue';
import BuyVehicleDialog from '@/dialogues/falukant/BuyVehicleDialog.vue';
import apiClient from '@/utils/axios.js';
import { mapState } from 'vuex';
export default {
name: "BranchView",
components: {
StatusBar,
BranchSelection,
SimpleTabs,
DirectorInfo,
SaleSection,
ProductionSection,
StorageSection,
RevenueSection,
BuyVehicleDialog,
},
watch: {
// Wenn sich der Daemon-Socket ändert (z.B. nach Login/Reconnect),
// Listener sauber entfernen/neu registrieren.
daemonSocket(newSocket, oldSocket) {
if (oldSocket) {
oldSocket.removeEventListener('message', this.handleDaemonMessage);
}
if (newSocket) {
newSocket.addEventListener('message', this.handleDaemonMessage);
}
}
},
data() {
return {
branches: [],
selectedBranch: null,
products: [],
vehicles: [],
activeTab: 'production',
productPricesCache: {}, // Cache für regionale Preise: { productId: price }
tabs: [
{ value: 'production', label: 'falukant.branch.tabs.production' },
{ value: 'inventory', label: 'falukant.branch.tabs.inventory' },
{ value: 'taxes', label: 'falukant.branch.tabs.taxes' },
{ value: 'director', label: 'falukant.branch.tabs.director' },
{ 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,
success: false,
},
repairVehicleDialog: {
show: false,
vehicle: null,
repairCost: null,
buildTimeMinutes: null,
},
repairAllVehiclesDialog: {
show: false,
vehicleIds: [],
totalCost: null,
},
branchTaxes: null,
branchTaxesLoading: false,
branchTaxesError: 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;
},
repairableVehicles() {
return (this.vehicles || []).filter(v =>
v.status === 'available' && v.condition < 100
);
},
repairableVehiclesCount() {
return this.repairableVehicles.length;
},
repairAllCost() {
const totalCost = this.repairableVehicles.reduce((sum, v) => {
return sum + this.calculateRepairCost(v);
}, 0);
// 10% Rabatt für Reparatur aller Fahrzeuge
return Math.round(totalCost * 0.9);
},
},
async mounted() {
await this.loadBranches();
const branchId = this.$route.params.branchId;
await this.loadProducts();
if (branchId) {
this.selectedBranch = this.branches.find(
b => b.id === parseInt(branchId, 10)
) || null;
} else {
this.selectMainBranch();
}
// Live-Socket-Events (Daemon WS)
if (this.daemonSocket) {
this.daemonSocket.addEventListener('message', this.handleDaemonMessage);
}
// Live-Socket-Events (Backend Socket.io)
if (this.socket) {
this.socket.on('falukantUpdateStatus', (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data }));
this.socket.on('falukantBranchUpdate', (data) => this.handleEvent({ event: 'falukantBranchUpdate', ...data }));
this.socket.on('transport_arrived', (data) => this.handleEvent({ event: 'transport_arrived', ...data }));
this.socket.on('inventory_updated', (data) => this.handleEvent({ event: 'inventory_updated', ...data }));
}
// Load taxes for the initially selected branch (if any)
await this.loadBranchTaxes();
},
beforeUnmount() {
// Daemon WebSocket: Listener entfernen (der Socket selbst wird beim Logout geschlossen)
if (this.daemonSocket) {
this.daemonSocket.removeEventListener('message', this.handleDaemonMessage);
}
if (this.socket) {
this.socket.off('falukantUpdateStatus');
this.socket.off('falukantBranchUpdate');
this.socket.off('transport_arrived');
this.socket.off('inventory_updated');
}
},
watch: {
activeTab(newVal, oldVal) {
// Nur neu laden, wenn der Tab wirklich gewechselt wurde und ein Branch ausgewählt ist
if (!this.selectedBranch || newVal === oldVal) return;
// Alle Tabs neu laden, wenn gewechselt wird
this.$nextTick(() => {
this.refreshActiveTab();
});
},
selectedBranch: {
handler(newBranch) {
// if taxes tab is active, refresh when branch changes
if (this.activeTab === 'taxes' && newBranch && newBranch.id) {
this.loadBranchTaxes();
}
},
deep: false
}
},
methods: {
async loadBranches() {
try {
const result = await apiClient.get('/api/falukant/branches');
this.branches = result.data.map(branch => ({
id: branch.id,
regionId: branch.regionId,
cityName: branch.region.name,
type: this.$t(`falukant.branch.types.${branch.branchType.labelTr}`),
branchTypeLabelTr: branch.branchType.labelTr,
isMainBranch: branch.isMainBranch,
weather: branch.weather,
}));
if (!this.selectedBranch) {
this.selectMainBranch();
}
} catch (error) {
console.error('Error loading branches:', error);
}
},
async loadProducts() {
try {
const productsResult = await apiClient.get('/api/falukant/products');
this.products = productsResult.data;
} catch (error) {
console.error('Error loading products:', error);
}
},
async loadBranchTaxes() {
if (!this.selectedBranch || !this.selectedBranch.id) {
this.branchTaxes = null;
this.branchTaxesError = null;
this.branchTaxesLoading = false;
return;
}
this.branchTaxesLoading = true;
this.branchTaxesError = null;
try {
const res = await apiClient.get(`/api/falukant/branches/${this.selectedBranch.id}/taxes`);
this.branchTaxes = res.data;
} catch (err) {
console.error('Failed to load branch taxes', err);
// Try to surface a useful error message
const remoteMsg = err?.response?.data?.error || err?.message || String(err);
this.branchTaxes = null;
this.branchTaxesError = remoteMsg;
} finally {
this.branchTaxesLoading = false;
}
},
refreshActiveTab() {
// Lade die Daten für den aktiven Tab neu
switch (this.activeTab) {
case 'director':
this.$refs.directorInfo?.refresh();
break;
case 'inventory':
this.$refs.saleSection?.loadInventory();
this.$refs.saleSection?.loadTransports();
break;
case 'production':
this.$refs.productionSection?.loadProductions();
this.$refs.productionSection?.loadStorage();
this.$refs.revenueSection?.refresh && this.$refs.revenueSection.refresh();
break;
case 'taxes':
this.loadBranchTaxes();
break;
case 'storage':
this.$refs.storageSection?.loadStorageData();
break;
case 'transport':
this.loadVehicles();
break;
}
},
async onBranchSelected(newBranch) {
this.selectedBranch = newBranch;
// Branches neu laden, um das Wetter zu aktualisieren
await this.loadBranches();
// Den ausgewählten Branch nach dem Neuladen wieder setzen
if (newBranch) {
this.selectedBranch = this.branches.find(b => b.id === newBranch.id) || newBranch;
}
await this.loadProducts();
await this.loadVehicles();
await this.loadProductPricesForCurrentBranch();
this.$nextTick(() => {
// Alle Tabs neu laden
this.refreshActiveTab();
});
// load tax info for this branch
this.loadBranchTaxes();
// Beim Initial-Laden sicherstellen, dass ein Tab-Inhalt sichtbar ist
if (this.selectedBranch && !this.activeTab) {
this.activeTab = 'director';
}
},
async loadProductPricesForCurrentBranch() {
if (!this.selectedBranch || !this.selectedBranch.regionId) {
this.productPricesCache = {};
return;
}
// Lade Preise für alle Produkte in der aktuellen Region
const prices = {};
for (const product of this.products) {
try {
const { data } = await apiClient.get('/api/falukant/products/price-in-region', {
params: {
productId: product.id,
regionId: this.selectedBranch.regionId
}
});
prices[product.id] = data.price;
} catch (error) {
console.error(`Error loading price for product ${product.id}:`, error);
// Fallback auf Standard-Berechnung
const knowledgeFactor = product.knowledges?.[0]?.knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.6;
prices[product.id] = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
}
}
this.productPricesCache = prices;
},
formatPercent(value) {
if (value === null || value === undefined) return '—';
// Ensure numeric and format with locale with max 2 fraction digits
const num = typeof value === 'string' ? parseFloat(value) : Number(value);
if (Number.isNaN(num)) return '—';
return num.toLocaleString(undefined, { maximumFractionDigits: 2 }) + '%';
},
async createBranch() {
await this.loadBranches();
// Nach dem Anlegen eines neuen Branches automatisch den
// zuletzt/neu erstellten Branch auswählen.
if (this.branches.length > 0) {
const newest = this.branches.reduce((acc, b) =>
!acc || b.id > acc.id ? b : acc,
null
);
if (newest) {
await this.onBranchSelected(newest);
}
}
},
async upgradeBranch() {
if (!this.selectedBranch) return;
try {
await apiClient.post('/api/falukant/branches/upgrade', {
branchId: this.selectedBranch.id,
});
await this.loadBranches();
// Ausgewählten Branch nach dem Upgrade neu setzen
const updated = this.branches.find(b => b.id === this.selectedBranch.id);
if (updated) {
await this.onBranchSelected(updated);
}
} catch (error) {
console.error('Error upgrading branch:', error);
alert(this.$t('falukant.branch.actions.upgradeAlert', { branchId: this.selectedBranch.id }));
}
},
selectMainBranch() {
const main = this.branches.find(b => b.isMainBranch) || null;
if (main && main !== this.selectedBranch) {
this.selectedBranch = main;
}
if (this.selectedBranch) {
this.loadVehicles();
}
if (this.selectedBranch && !this.activeTab) {
this.activeTab = 'director';
}
},
calculateProductRevenue(product) {
if (!product.knowledges || product.knowledges.length === 0) {
return { absolute: 0, perMinute: 0 };
}
// Verwende gecachten regionalen Preis, falls verfügbar
let revenuePerUnit;
if (this.productPricesCache[product.id] !== undefined) {
revenuePerUnit = this.productPricesCache[product.id];
} else {
// Fallback auf Standard-Berechnung
const knowledgeFactor = product.knowledges[0].knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.6;
revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
}
const perMinute = product.productionTime > 0
? revenuePerUnit / product.productionTime
: 0;
return {
absolute: revenuePerUnit.toFixed(2),
perMinute: perMinute.toFixed(2),
};
},
calculateProductProfit(product) {
const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr }
= this.calculateProductRevenue(product);
const revenueAbsolute = parseFloat(revenueAbsoluteStr);
const costPerUnit = 6 * product.category;
const profitAbsolute = revenueAbsolute - costPerUnit;
const costPerMinute = product.productionTime > 0
? costPerUnit / product.productionTime
: 0;
const profitPerMinute = parseFloat(revenuePerMinuteStr) - costPerMinute;
return {
absolute: profitAbsolute.toFixed(2),
perMinute: profitPerMinute.toFixed(2),
};
},
conditionLabel(value) {
// 0 ist ein gültiger Zustand (z.B. komplett kaputt) und darf nicht als "Unbekannt" enden.
if (value === null || value === undefined) return 'Unbekannt';
const v = Number(value);
if (!Number.isFinite(v)) return 'Unbekannt';
if (v >= 95) return 'Ausgezeichnet'; // 95100
if (v >= 72) return 'Sehr gut'; // 7294
if (v >= 54) return 'Gut'; // 5471
if (v >= 39) return 'Mäßig'; // 3953
if (v >= 22) return 'Schlecht'; // 2238
if (v >= 6) return 'Sehr schlecht'; // 621
if (v >= 1) return 'Katastrophal'; // 15
return 'Katastrophal'; // 0 oder kleiner
},
speedLabel(value) {
// Expect numeric speeds 1..4; provide localized labels as fallback to raw value
const key = value == null ? 'unknown' : String(value);
const tKey = `falukant.branch.transport.speed.${key}`;
const translated = this.$t(tKey);
// If translation returns the key (no translation found), fall back to the numeric value
if (!translated || translated === tKey) return value;
return translated;
},
transportModeLabel(mode) {
if (!mode) return '';
const key = String(mode);
const tKey = `falukant.branch.transport.modes.${key}`;
const translated = this.$t(tKey);
if (!translated || translated === tKey) return mode;
return translated;
},
async loadVehicles() {
if (!this.selectedBranch) return;
try {
const { data } = await apiClient.get('/api/falukant/vehicles', {
params: { regionId: this.selectedBranch.regionId },
});
this.vehicles = data || [];
} catch (error) {
console.error('Error loading vehicles:', error);
this.vehicles = [];
}
},
formatDateTime(value) {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
return date.toLocaleString();
},
handleEvent(eventData) {
switch (eventData.event) {
case 'production_ready':
this.$refs.productionSection?.loadProductions();
this.$refs.storageSection?.loadStorageData();
this.$refs.productionSection?.loadStorage();
this.$refs.saleSection?.loadInventory();
break;
case 'stock_change':
this.$refs.storageSection?.loadStorageData();
this.$refs.productionSection?.loadStorage();
this.$refs.saleSection?.loadInventory();
break;
case 'price_update':
this.$refs.revenueSection?.refresh();
break;
case 'director_death':
this.$refs.directorInfo?.loadDirector();
break;
case 'production_started':
this.$refs.productionSection?.loadProductions();
break;
case 'selled_items':
this.$refs.saleSection?.loadInventory();
this.$refs.storageSection?.loadStorageData();
this.$refs.productionSection?.loadStorage();
break;
case 'falukantUpdateStatus':
case 'falukantBranchUpdate':
if (this.$refs.statusBar) {
this.$refs.statusBar.fetchStatus();
}
if (this.$refs.productionSection) {
this.$refs.productionSection.loadProductions();
this.$refs.productionSection.loadStorage();
}
if (this.$refs.storageSection) {
this.$refs.storageSection.loadStorageData();
}
if (this.$refs.saleSection) {
this.$refs.saleSection.loadInventory();
}
break;
case 'knowledge_update':
this.loadProducts();
if (this.$refs.revenueSection) {
this.$refs.revenueSection.products = this.products;
this.$refs.revenueSection.refresh && this.$refs.revenueSection.refresh();
}
break;
case 'transport_arrived':
// Leerer Transport angekommen - Fahrzeug wurde zurückgeholt
if (eventData.empty && eventData.branch_id) {
// Nur aktualisieren, wenn der betroffene Branch ausgewählt ist
if (this.selectedBranch && this.selectedBranch.id === eventData.branch_id) {
// Fahrzeuge im Transport-Tab aktualisieren
this.loadVehicles();
// Laufende Transporte im Inventar-Tab aktualisieren
if (this.$refs.saleSection) {
this.$refs.saleSection.loadTransports();
}
}
}
break;
case 'transport_removed':
// Transport wurde entfernt (z.B. abgebrochen oder gelöscht)
if (eventData.branch_id) {
// Nur aktualisieren, wenn der betroffene Branch ausgewählt ist
if (this.selectedBranch && this.selectedBranch.id === eventData.branch_id) {
// Fahrzeuge im Transport-Tab aktualisieren
this.loadVehicles();
// Laufende Transporte im Inventar-Tab aktualisieren
if (this.$refs.saleSection) {
this.$refs.saleSection.loadInventory();
this.$refs.saleSection.loadTransports();
}
// Lager aktualisieren
if (this.$refs.storageSection) {
this.$refs.storageSection.loadStorageData();
}
if (this.$refs.productionSection) {
this.$refs.productionSection.loadStorage();
}
}
}
break;
case 'inventory_updated':
// Inventar wurde aktualisiert (durch Transporte)
if (eventData.branch_id) {
// Nur aktualisieren, wenn der betroffene Branch ausgewählt ist
if (this.selectedBranch && this.selectedBranch.id === eventData.branch_id) {
// Inventar im Inventar-Tab aktualisieren
if (this.$refs.saleSection) {
this.$refs.saleSection.loadInventory();
this.$refs.saleSection.loadTransports();
}
// Lager aktualisieren
if (this.$refs.storageSection) {
this.$refs.storageSection.loadStorageData();
}
if (this.$refs.productionSection) {
this.$refs.productionSection.loadStorage();
}
}
}
break;
default:
console.log('Unhandled event:', eventData);
}
},
handleDaemonMessage(event) {
if (event.data === 'ping') return;
try {
const message = JSON.parse(event.data);
this.handleEvent(message);
} catch (error) {
console.error('Error processing daemon message:', error);
}
},
openBuyVehicleDialog() {
if (!this.selectedBranch) return;
this.$refs.buyVehicleDialog?.open();
},
handleVehiclesBought() {
// Refresh status bar (for updated money) and potentially other data later
this.$refs.statusBar?.fetchStatus();
this.loadVehicles();
},
handleTransportCreated() {
this.loadVehicles();
this.$refs.storageSection?.loadStorageData();
this.$refs.saleSection?.loadTransports();
},
openSendVehicleDialog(vehicleId) {
this.sendVehicleDialog = {
show: true,
vehicleId: vehicleId,
vehicleIds: null,
vehicleTypeId: null,
targetBranchId: null,
success: false,
};
},
openSendAllVehiclesDialog(vehicleTypeId, vehicleList) {
this.sendVehicleDialog = {
show: true,
vehicleId: null,
vehicleIds: vehicleList.map(v => v.id),
vehicleTypeId: vehicleTypeId,
targetBranchId: null,
success: false,
};
},
closeSendVehicleDialog() {
this.sendVehicleDialog = {
show: false,
vehicleId: null,
vehicleIds: null,
vehicleTypeId: null,
targetBranchId: null,
success: false,
};
},
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.sendVehicleDialog.success = true;
} catch (error) {
console.error('Error sending vehicles:', error);
alert(this.$t('falukant.branch.transport.sendError'));
}
},
calculateRepairCost(vehicle) {
if (!vehicle || !vehicle.type || vehicle.condition >= 100) return 0;
const baseCost = vehicle.type.cost || 0;
return Math.round(baseCost * 0.8 * (100 - vehicle.condition) / 100);
},
openRepairVehicleDialog(vehicle) {
if (!vehicle || vehicle.status !== 'available' || vehicle.condition >= 100) {
return;
}
// Kosten berechnen: Baupreis * 0.8 * (100 - zustand) / 100
const repairCost = this.calculateRepairCost(vehicle);
const buildTimeMinutes = vehicle.type.buildTimeMinutes || 0;
this.repairVehicleDialog = {
show: true,
vehicle: vehicle,
repairCost: repairCost,
buildTimeMinutes: buildTimeMinutes,
};
},
openRepairAllVehiclesDialog() {
const repairableVehicles = this.repairableVehicles;
if (repairableVehicles.length === 0) {
return;
}
this.repairAllVehiclesDialog = {
show: true,
vehicleIds: repairableVehicles.map(v => v.id),
totalCost: this.repairAllCost,
};
},
closeRepairAllVehiclesDialog() {
this.repairAllVehiclesDialog = {
show: false,
vehicleIds: [],
totalCost: null,
};
},
async repairAllVehicles() {
if (!this.repairAllVehiclesDialog.vehicleIds || this.repairAllVehiclesDialog.vehicleIds.length === 0) {
return;
}
try {
await apiClient.post('/api/falukant/vehicles/repair-all', {
vehicleIds: this.repairAllVehiclesDialog.vehicleIds,
});
await this.loadVehicles();
this.closeRepairAllVehiclesDialog();
// Statt JS-alert: Dialog schließen und MessageDialog anzeigen
this.$root.$refs.messageDialog?.open('tr:falukant.branch.transport.repairAllSuccess');
this.$refs.statusBar?.fetchStatus();
} catch (error) {
console.error('Error repairing all vehicles:', error);
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairAllError');
// Bestätigungsdialog ebenfalls schließen und Fehler im MessageDialog anzeigen
this.closeRepairAllVehiclesDialog();
this.$root.$refs.messageDialog?.open(String(errorMessage), this.$t('error.title'));
}
},
closeRepairVehicleDialog() {
this.repairVehicleDialog = {
show: false,
vehicle: null,
repairCost: null,
buildTimeMinutes: null,
};
},
formatMoney(amount) {
if (amount == null) return '';
try {
return this.formatCurrency(amount);
} catch (e) {
return String(amount);
}
},
formatCurrency(amount) {
if (amount == null) return '';
try {
// Use euro currency formatting with locale-sensitive digits
return Number(amount).toLocaleString(undefined, { style: 'currency', currency: 'EUR' });
} catch (e) {
return String(amount);
}
},
formatBuildTime(minutes) {
if (!minutes || minutes === 0) {
return this.$t('falukant.branch.transport.repairBuildTimeInstant');
}
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
const parts = [];
if (hours > 0) {
parts.push(`${hours} ${this.$t('falukant.branch.transport.repairBuildTimeHours')}`);
}
if (mins > 0) {
parts.push(`${mins} ${this.$t('falukant.branch.transport.repairBuildTimeMinutes')}`);
}
return parts.length > 0 ? parts.join(' ') : '0 ' + this.$t('falukant.branch.transport.repairBuildTimeMinutes');
},
async repairVehicle() {
if (!this.repairVehicleDialog.vehicle) {
return;
}
try {
await apiClient.post(`/api/falukant/vehicles/${this.repairVehicleDialog.vehicle.id}/repair`);
await this.loadVehicles();
this.closeRepairVehicleDialog();
// Statt JS-alert: Dialog schließen und MessageDialog anzeigen
this.$root.$refs.messageDialog?.open('tr:falukant.branch.transport.repairSuccess');
this.$refs.statusBar?.fetchStatus();
} catch (error) {
console.error('Error repairing vehicle:', error);
const errorMessage = error.response?.data?.message || this.$t('falukant.branch.transport.repairError');
// Bestätigungsdialog ebenfalls schließen und Fehler im MessageDialog anzeigen
this.closeRepairVehicleDialog();
this.$root.$refs.messageDialog?.open(String(errorMessage), this.$t('error.title'));
}
},
},
};
</script>
<style scoped lang="scss">
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: #F9A22C;
color: #000000;
border: 1px solid #F9A22C;
border-radius: 4px;
cursor: pointer;
}
.send-all-buttons button:hover {
background-color: #fdf1db;
color: #7E471B;
border: 1px solid #7E471B;
}
.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;
}
.vehicle-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.repair-button {
background-color: #28a745;
color: white;
}
.repair-button:hover {
background-color: #218838;
}
.repair-vehicle-form {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.repair-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.repair-info p {
margin: 0;
}
.repair-confirm-button {
background-color: #28a745;
color: white;
}
.repair-confirm-button:hover {
background-color: #218838;
}
.send-vehicle-success {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
text-align: center;
}
.send-vehicle-success p {
margin: 0;
font-size: 1.1em;
color: #28a745;
}
.vehicle-action-buttons {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.repair-all-button {
background-color: #28a745;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.repair-all-button:hover {
background-color: #218838;
}
.repair-all-form {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.repair-all-discount {
color: #28a745;
font-weight: bold;
}
</style>