Files
yourpart3/frontend/src/views/falukant/BranchView.vue
Torsten Schulz (local) 5ebcc581f9 feat(falukant): add character visual representation and settings
- Introduced FalukantCharacterVisual component to display character portraits and 3D figures based on user settings.
- Updated DirectorInfo, ChurchView, PoliticsView, and other components to utilize the new character visual component.
- Added visual settings management for users, allowing selection between portrait styles and display modes (portraits, 3D, or both).
- Implemented age rules for engagement and marriage in the backend, ensuring compliance with specified age limits.
- Created a new settings view for Falukant to manage visual preferences.
- Added new images for medieval character portraits.
- Updated translations for new settings and features in multiple languages.
2026-07-24 10:55:19 +02:00

1617 lines
64 KiB
Vue
Executable File
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="falukant-branch-view">
<StatusBar ref="statusBar" />
<div class="falukant-branch">
<section class="branch-hero surface-card">
<div>
<span class="branch-kicker">{{ $t('falukant.branch.heroEyebrow') }}</span>
<h2>{{ $t('falukant.branch.title') }}</h2>
<p>{{ $t('falukant.branch.heroIntro') }}</p>
<div class="branch-hero__meta">
<span class="branch-hero__badge">
{{ $t('falukant.branch.currentCertificate') }}: {{ currentCertificate ?? '---' }}
</span>
</div>
</div>
</section>
<section class="branch-certificate surface-card">
<div class="branch-certificate__header">
<div>
<h3>{{ $t('falukant.branch.certificate.title') }}</h3>
<p>{{ $t('falukant.branch.certificate.description') }}</p>
</div>
<span class="branch-hero__badge">
{{ $t('falukant.branch.currentCertificate') }}: {{ currentCertificate ?? 1 }}
</span>
</div>
<div class="branch-certificate__grid">
<article class="branch-certificate__block">
<h4>{{ $t('falukant.branch.certificate.currentUnlocks') }}</h4>
<p>{{ currentCertificateProducts.join(', ') }}</p>
</article>
<article v-if="nextCertificateProducts.length" class="branch-certificate__block">
<h4>{{ $t('falukant.branch.certificate.nextUnlocks', { level: nextCertificateLevel }) }}</h4>
<p>{{ nextCertificateProducts.join(', ') }}</p>
</article>
</div>
</section>
<section
v-if="debtorsPrison.active"
class="branch-debt-warning surface-card"
:class="{ 'is-prison': debtorsPrison.inDebtorsPrison }"
>
<strong>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.bank.debtorsPrison.titlePrison')
: $t('falukant.bank.debtorsPrison.titleWarning') }}
</strong>
<p>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.branch.debtorsPrison.branchLocked')
: $t('falukant.branch.debtorsPrison.branchRisk') }}
</p>
</section>
<BranchSelection
:branches="branches"
:selectedBranch="selectedBranch"
:blocked="debtorsPrison.inDebtorsPrison"
:blocked-reason="debtorsPrison.inDebtorsPrison ? $t('falukant.branch.debtorsPrison.selectionBlocked') : ''"
@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"
:visual-settings="visualSettings"
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"
:current-certificate="currentCertificate"
:product-prices-cache="productPricesCache"
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>
<label>
{{ $t('falukant.branch.transport.guardCount') }}
<input v-model.number="sendVehicleDialog.guardCount" type="number" min="0" max="20" />
</label>
<p class="transport-guards-hint">
{{ $t('falukant.branch.transport.guardHint', { cost: formatMoney((sendVehicleDialog.guardCount || 0) * 4) }) }}
</p>
<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';
import { showError, showSuccess, showApiError } from '@/utils/feedback.js';
import {
FALUKANT_VISUAL_DEFAULTS,
loadFalukantVisualSettings,
} from '@/utils/falukantVisualSettings.js';
const CERTIFICATE_PRODUCT_LEVELS = [
{ level: 1, products: ['fish', 'meat', 'leather', 'wood', 'stone', 'milk', 'cheese', 'bread', 'wheat', 'grain', 'carrot'] },
{ level: 2, products: ['beer', 'iron', 'copper', 'spices', 'salt', 'sugar', 'vinegar', 'cotton', 'wine'] },
{ level: 3, products: ['gold', 'diamond', 'furniture', 'clothing'] },
{ level: 4, products: ['jewelry', 'painting', 'book', 'weapon', 'armor', 'shield'] },
{ level: 5, products: ['horse', 'ox'] },
];
// Stückkosten wie backend/utils/falukant/falukantProductEconomy.js (bei Änderungen dort mitziehen).
const PRODUCTION_COST_BASE = 6.0;
const PRODUCTION_COST_PER_PRODUCT_CATEGORY = 1.0;
const PRODUCTION_HEADROOM_DISCOUNT_PER_STEP = 0.035;
const PRODUCTION_HEADROOM_DISCOUNT_CAP = 0.14;
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 }
productNetMetricsCache: {}, // Netto-Metriken vom Backend: { productId: { netPerPiece, netPerMinute, ... } }
/** Cache-Schlüssel: Region + ob MAX(worth) über alle Filialregionen (bei Fahrzeug) */
productPricesCacheKey: null,
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,
guardCount: 0,
},
repairVehicleDialog: {
show: false,
vehicle: null,
repairCost: null,
buildTimeMinutes: null,
},
repairAllVehiclesDialog: {
show: false,
vehicleIds: [],
totalCost: null,
},
branchTaxes: null,
branchTaxesLoading: false,
branchTaxesError: null,
currentCertificate: null,
debtorsPrison: {
active: false,
inDebtorsPrison: false
},
pendingBranchRefresh: null,
visualSettings: { ...FALUKANT_VISUAL_DEFAULTS },
};
},
computed: {
...mapState(['socket', 'daemonSocket', 'user']),
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);
},
currentCertificateProducts() {
const certificate = Number(this.currentCertificate || 1);
return CERTIFICATE_PRODUCT_LEVELS
.filter((entry) => entry.level <= certificate)
.flatMap((entry) => entry.products)
.map((productKey) => this.$t(`falukant.product.${productKey}`));
},
nextCertificateLevel() {
const current = Number(this.currentCertificate || 1);
return current < CERTIFICATE_PRODUCT_LEVELS.length ? current + 1 : null;
},
nextCertificateProducts() {
const nextEntry = CERTIFICATE_PRODUCT_LEVELS.find((entry) => entry.level === this.nextCertificateLevel);
return nextEntry
? nextEntry.products.map((productKey) => this.$t(`falukant.product.${productKey}`))
: [];
},
},
async mounted() {
this.visualSettings = await loadFalukantVisualSettings(this.user?.id);
await this.loadBranches();
const branchId = this.$route.params.branchId;
await this.loadCurrentCertificate();
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('falukantUpdateDebt', (data) => this.handleEvent({ event: 'falukantUpdateDebt', ...data }));
this.socket.on('falukantUpdateProductionCertificate', (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...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 }));
this.socket.on('directorchanged', (data) => this.handleEvent({ event: 'directorchanged', ...(data || {}) }));
}
// Load taxes for the initially selected branch (if any)
await this.loadBranchTaxes();
},
beforeUnmount() {
if (this.pendingBranchRefresh) {
clearTimeout(this.pendingBranchRefresh);
this.pendingBranchRefresh = null;
}
// 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('falukantUpdateDebt');
this.socket.off('falukantUpdateProductionCertificate');
this.socket.off('falukantBranchUpdate');
this.socket.off('transport_arrived');
this.socket.off('inventory_updated');
this.socket.off('directorchanged');
}
},
watch: {
activeTab(newVal) {
if (newVal === 'taxes') {
this.loadBranchTaxes();
}
},
selectedBranch: {
handler(newBranch) {
// if taxes tab is active, refresh when branch changes
if (this.activeTab === 'taxes' && newBranch && newBranch.id) {
this.loadBranchTaxes();
}
},
deep: false
}
},
methods: {
calculateProductionPieceCost(productCategory) {
const category = Math.max(1, Number(productCategory) || 1);
const certificate = Math.max(1, Number(this.currentCertificate) || 1);
const raw = PRODUCTION_COST_BASE + (category * PRODUCTION_COST_PER_PRODUCT_CATEGORY);
const headroom = Math.max(0, certificate - category);
const discount = Math.min(
headroom * PRODUCTION_HEADROOM_DISCOUNT_PER_STEP,
PRODUCTION_HEADROOM_DISCOUNT_CAP
);
return raw * (1 - discount);
},
matchesCurrentUser(eventData) {
if (eventData?.user_id == null) {
return true;
}
const currentIds = [this.user?.id, this.user?.hashedId]
.filter(Boolean)
.map((value) => String(value));
return currentIds.includes(String(eventData.user_id));
},
queueBranchRefresh() {
if (this.pendingBranchRefresh) {
clearTimeout(this.pendingBranchRefresh);
}
this.pendingBranchRefresh = setTimeout(async () => {
this.pendingBranchRefresh = null;
this.$refs.statusBar?.fetchStatus();
await this.loadCurrentCertificate();
await this.loadProducts();
if (this.selectedBranch) {
await this.loadVehicles();
this.productPricesCacheKey = null;
await this.loadProductPricesForCurrentBranch();
}
this.$refs.productionSection?.loadProductions();
this.$refs.productionSection?.loadStorage();
this.$refs.storageSection?.loadStorageData();
this.$refs.saleSection?.loadInventory();
if (this.$refs.revenueSection) {
this.$refs.revenueSection.products = this.products;
this.$refs.revenueSection.refresh && this.$refs.revenueSection.refresh();
}
this.$refs.directorInfo?.refresh?.();
}, 120);
},
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 loadCurrentCertificate() {
try {
const result = await apiClient.get('/api/falukant/user');
this.currentCertificate = result.data?.certificate ?? null;
this.debtorsPrison = result.data?.debtorsPrison || {
active: false,
inDebtorsPrison: false
};
} catch (error) {
console.error('Error loading certificate:', 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;
}
},
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();
});
// 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 = {};
this.productNetMetricsCache = {};
this.productPricesCacheKey = null;
return;
}
const useNetworkWorth = Array.isArray(this.vehicles) && this.vehicles.length > 0;
const cacheKey = `${this.selectedBranch.regionId}:${useNetworkWorth ? 'net' : 'loc'}:${this.selectedBranch.id}`;
if (this.productPricesCacheKey === cacheKey && Object.keys(this.productPricesCache).length > 0) {
return;
}
try {
const params = { regionId: this.selectedBranch.regionId };
if (useNetworkWorth) {
params.networkWorth = '1';
params.branchId = this.selectedBranch.id;
}
const { data } = await apiClient.get('/api/falukant/products/prices-in-region', { params });
this.productPricesCache = data.prices || {};
this.productNetMetricsCache = data.netMetrics || {};
this.productPricesCacheKey = cacheKey;
} catch (error) {
console.error(`Error loading product prices for region ${this.selectedBranch.regionId}:`, error);
// Fallback: Lade Preise einzeln (alte Methode)
console.warn('[BranchView] Falling back to individual product price requests');
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 (err) {
console.error(`Error loading price for product ${product.id}:`, err);
// Fallback auf Standard-Berechnung
const knowledgeFactor = product.knowledges?.[0]?.knowledge || 0;
const maxPrice = product.sellCost;
const minPrice = maxPrice * 0.7;
prices[product.id] = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
}
}
this.productPricesCache = prices;
this.productNetMetricsCache = {};
this.productPricesCacheKey = cacheKey;
}
},
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() {
if (this.debtorsPrison.inDebtorsPrison) {
showError(this, this.$t('falukant.branch.debtorsPrison.selectionBlocked'));
return;
}
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;
if (this.debtorsPrison.inDebtorsPrison) {
showError(this, this.$t('falukant.branch.debtorsPrison.selectionBlocked'));
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);
showError(this, 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.7;
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 netMetrics = this.productNetMetricsCache?.[product.id];
if (netMetrics && Number.isFinite(Number(netMetrics.netPerPiece)) && Number.isFinite(Number(netMetrics.netPerMinute))) {
return {
absolute: Number(netMetrics.netPerPiece).toFixed(2),
perMinute: Number(netMetrics.netPerMinute).toFixed(2),
};
}
const { absolute: revenueAbsoluteStr, perMinute: revenuePerMinuteStr }
= this.calculateProductRevenue(product);
const revenueAbsolute = parseFloat(revenueAbsoluteStr);
const costPerUnit = this.calculateProductionPieceCost(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 this.$t('falukant.conditionBand.excellent');
if (v >= 72) return this.$t('falukant.conditionBand.veryGood');
if (v >= 54) return this.$t('falukant.conditionBand.good');
if (v >= 39) return this.$t('falukant.conditionBand.moderate');
if (v >= 22) return this.$t('falukant.conditionBand.bad');
if (v >= 6) return this.$t('falukant.conditionBand.veryBad');
if (v >= 1) return this.$t('falukant.conditionBand.catastrophic');
return this.$t('falukant.conditionBand.unknown');
},
speedLabel(value) {
if (value == null) return this.$t('falukant.branch.transport.speed.unknown') || '—';
if (typeof value === 'object') {
const k = value.tr ?? value.id ?? 'unknown';
const tKey = `falukant.branch.transport.speed.${k}`;
const t = this.$t(tKey);
return (t && t !== tKey) ? t : String(k);
}
const key = String(value);
const tKey = `falukant.branch.transport.speed.${key}`;
const translated = this.$t(tKey);
return (!translated || translated === tKey) ? key : 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) {
if (!this.matchesCurrentUser(eventData)) {
return;
}
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':
case 'directorchanged':
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 'falukantUpdateDebt':
case 'falukantUpdateProductionCertificate':
case 'falukantBranchUpdate':
this.queueBranchRefresh();
break;
case 'knowledge_update':
this.queueBranchRefresh();
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:
break;
}
},
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,
guardCount: 0,
};
},
openSendAllVehiclesDialog(vehicleTypeId, vehicleList) {
this.sendVehicleDialog = {
show: true,
vehicleId: null,
vehicleIds: vehicleList.map(v => v.id),
vehicleTypeId: vehicleTypeId,
targetBranchId: null,
success: false,
guardCount: 0,
};
},
closeSendVehicleDialog() {
this.sendVehicleDialog = {
show: false,
vehicleId: null,
vehicleIds: null,
vehicleTypeId: null,
targetBranchId: null,
success: false,
guardCount: 0,
};
},
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) {
showError(this, this.$t('falukant.branch.transport.selectTargetError'));
return;
}
try {
const payload = {
branchId: this.selectedBranch.id,
targetBranchId: this.sendVehicleDialog.targetBranchId,
productId: null,
quantity: 0,
guardCount: this.sendVehicleDialog.guardCount || 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 {
showError(this, 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);
showApiError(this, error, 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();
showSuccess(this, 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');
showError(this, errorMessage);
}
},
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();
showSuccess(this, 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');
showError(this, errorMessage);
}
},
},
};
</script>
<style scoped lang="scss">
.falukant-branch {
max-width: var(--content-max-width);
margin: 0 auto;
padding-bottom: 24px;
}
.branch-hero {
padding: 24px 26px;
margin-bottom: 16px;
background:
radial-gradient(circle at top right, rgba(248, 162, 43, 0.16), transparent 28%),
linear-gradient(180deg, rgba(255, 250, 243, 0.98) 0%, rgba(247, 238, 224, 0.98) 100%);
}
.branch-kicker {
display: inline-block;
margin-bottom: 10px;
padding: 4px 10px;
border-radius: 999px;
background: rgba(248, 162, 43, 0.14);
color: #8a5411;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.branch-hero p {
margin: 0;
color: var(--color-text-secondary);
}
.branch-debt-warning {
margin-bottom: 16px;
padding: 16px 18px;
border: 1px solid rgba(180, 120, 40, 0.32);
background: linear-gradient(180deg, rgba(255, 244, 223, 0.95), rgba(255, 250, 239, 0.98));
}
.branch-debt-warning.is-prison {
border-color: rgba(146, 57, 40, 0.4);
background: linear-gradient(180deg, rgba(255, 232, 225, 0.96), rgba(255, 245, 241, 0.98));
}
.branch-debt-warning p {
margin: 6px 0 0;
color: var(--color-text-secondary);
}
.branch-hero__meta {
margin-top: 12px;
}
.branch-hero__badge {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border: 1px solid rgba(138, 84, 17, 0.16);
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
color: #7a4b12;
font-size: 0.9rem;
font-weight: 600;
}
.branch-certificate {
margin-bottom: 16px;
padding: 18px;
}
.branch-certificate__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 14px;
}
.branch-certificate__header p {
margin: 6px 0 0;
color: var(--color-text-secondary);
}
.branch-certificate__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.branch-certificate__block {
padding: 14px 16px;
border-radius: var(--radius-lg);
background: rgba(138, 84, 17, 0.06);
}
.branch-certificate__block h4 {
margin: 0 0 8px;
}
.branch-certificate__block p {
margin: 0;
color: var(--color-text-secondary);
}
.branch-tab-content {
margin-top: 16px;
padding: 18px;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
background: rgba(255, 252, 247, 0.86);
box-shadow: var(--shadow-soft);
}
.branch-tab-pane {
min-height: 0;
}
.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: rgba(255,255,255,0.98);
padding: 2rem;
border-radius: var(--radius-lg);
min-width: 400px;
max-width: 600px;
box-shadow: var(--shadow-medium);
}
.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>