1184 lines
46 KiB
Vue
1184 lines
46 KiB
Vue
<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">
|
||
<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"
|
||
/>
|
||
<RevenueSection
|
||
:products="products"
|
||
:calculateProductRevenue="calculateProductRevenue"
|
||
:calculateProductProfit="calculateProductProfit"
|
||
:currentRegionId="selectedBranch?.regionId"
|
||
ref="revenueSection"
|
||
/>
|
||
</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: '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,
|
||
},
|
||
};
|
||
},
|
||
|
||
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 }));
|
||
}
|
||
},
|
||
|
||
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');
|
||
}
|
||
},
|
||
|
||
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 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(() => {
|
||
this.$refs.directorInfo?.refresh();
|
||
this.$refs.saleSection?.loadInventory();
|
||
this.$refs.saleSection?.loadTransports();
|
||
this.$refs.productionSection?.loadProductions();
|
||
this.$refs.productionSection?.loadStorage();
|
||
this.$refs.storageSection?.loadStorageData();
|
||
this.$refs.revenueSection?.refresh && this.$refs.revenueSection.refresh();
|
||
});
|
||
|
||
// 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;
|
||
},
|
||
|
||
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) {
|
||
const v = Number(value) || 0;
|
||
if (v >= 95) return 'Ausgezeichnet'; // 95–100
|
||
if (v >= 72) return 'Sehr gut'; // 72–94
|
||
if (v >= 54) return 'Gut'; // 54–71
|
||
if (v >= 39) return 'Mäßig'; // 39–53
|
||
if (v >= 22) return 'Schlecht'; // 22–38
|
||
if (v >= 6) return 'Sehr schlecht'; // 6–21
|
||
if (v >= 1) return 'Katastrophal'; // 1–5
|
||
return 'Unbekannt';
|
||
},
|
||
|
||
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();
|
||
alert(this.$t('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');
|
||
alert(errorMessage);
|
||
}
|
||
},
|
||
|
||
closeRepairVehicleDialog() {
|
||
this.repairVehicleDialog = {
|
||
show: false,
|
||
vehicle: null,
|
||
repairCost: null,
|
||
buildTimeMinutes: null,
|
||
};
|
||
},
|
||
|
||
formatMoney(amount) {
|
||
if (amount == null) return '';
|
||
try {
|
||
return amount.toLocaleString(undefined, {
|
||
minimumFractionDigits: 0,
|
||
maximumFractionDigits: 0,
|
||
});
|
||
} 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();
|
||
alert(this.$t('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');
|
||
alert(errorMessage);
|
||
}
|
||
},
|
||
},
|
||
};
|
||
</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>
|