661 lines
24 KiB
Vue
661 lines
24 KiB
Vue
<template>
|
||
<div class="sale-section">
|
||
<!-- Inventar-Tabelle -->
|
||
<div v-if="inventory.length > 0" class="inventory-table">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>{{ $t('falukant.branch.sale.region') }}</th>
|
||
<th>{{ $t('falukant.branch.sale.product') }}</th>
|
||
<th>{{ $t('falukant.branch.sale.quality') }}</th>
|
||
<th>{{ $t('falukant.branch.sale.quantity') }}</th>
|
||
<th>{{ $t('falukant.branch.sale.sell') }}</th>
|
||
<th>Bessere Preise</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(item, index) in inventory" :key="`${item.region.id}-${item.product.id}-${item.quality}`">
|
||
<td>{{ item.region.name }}</td>
|
||
<td>{{ $t(`falukant.product.${item.product.labelTr}`) }}</td>
|
||
<td>{{ item.quality }}</td>
|
||
<td>{{ item.totalQuantity }}</td>
|
||
<td>
|
||
<input type="number" v-model.number="item.sellQuantity" :min="1" :max="item.totalQuantity" :disabled="sellingItemIndex === index" />
|
||
<button @click="sellItem(index)" :disabled="sellingItemIndex === index || sellingAll">
|
||
{{ sellingItemIndex === index ? $t('falukant.branch.sale.selling') : $t('falukant.branch.sale.sellButton') }}
|
||
</button>
|
||
</td>
|
||
<td>
|
||
<div v-if="item.betterPrices && item.betterPrices.length > 0" class="price-cities">
|
||
<span v-for="city in item.betterPrices" :key="city.regionId"
|
||
:class="['city-price', getCityPriceClass(city.branchType)]"
|
||
:title="`${city.regionName}: ${formatPrice(city.price)}`">
|
||
{{ city.regionName }}
|
||
</span>
|
||
</div>
|
||
<span v-else class="no-better-prices">—</span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<button @click="sellAll" :disabled="sellingAll || sellingItemIndex !== null">
|
||
{{ sellingAll ? $t('falukant.branch.sale.selling') : $t('falukant.branch.sale.sellAllButton') }}
|
||
</button>
|
||
<div v-if="sellAllStatus" class="sell-all-status" :class="sellAllStatus.type">
|
||
{{ sellAllStatus.message }}
|
||
</div>
|
||
</div>
|
||
<div v-else>
|
||
<p>{{ $t('falukant.branch.sale.noInventory') }}</p>
|
||
</div>
|
||
|
||
<!-- Transport anlegen (nur wenn Inventar vorhanden) -->
|
||
<div class="transport-form" v-if="inventory.length > 0">
|
||
<h4>{{ $t('falukant.branch.sale.transportTitle') }}</h4>
|
||
<div class="transport-row">
|
||
<label>
|
||
{{ $t('falukant.branch.sale.transportSource') }}
|
||
<select v-model.number="transportForm.sourceKey" @change="() => { recalcMaxQuantity(); loadRouteInfo(); }">
|
||
<option :value="null" disabled>{{ $t('falukant.branch.sale.transportSourcePlaceholder') }}</option>
|
||
<option v-for="opt in inventoryOptions()" :key="opt.key" :value="opt.key">
|
||
{{ opt.label }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label>
|
||
{{ $t('falukant.branch.sale.transportVehicle') }}
|
||
<select v-model.number="transportForm.vehicleTypeId" @change="() => { recalcMaxQuantity(); loadRouteInfo(); }">
|
||
<option :value="null" disabled>{{ $t('falukant.branch.sale.transportVehiclePlaceholder') }}</option>
|
||
<option v-for="vt in vehicleTypeOptions()" :key="vt.id" :value="vt.id">
|
||
{{ $t(`falukant.branch.vehicles.${vt.tr}`) }} ({{ vt.count }} × {{ vt.capacity }}) - {{ speedLabel(vt.speed) }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label>
|
||
{{ $t('falukant.branch.sale.transportTarget') }}
|
||
<select v-model.number="transportForm.targetBranchId" @change="loadRouteInfo">
|
||
<option :value="null" disabled>{{ $t('falukant.branch.sale.transportTargetPlaceholder') }}</option>
|
||
<option v-for="tb in targetBranchOptions()" :key="tb.id" :value="tb.id">
|
||
{{ tb.label }}
|
||
</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label>
|
||
{{ $t('falukant.branch.sale.transportQuantity') }}
|
||
<input
|
||
type="number"
|
||
v-model.number="transportForm.quantity"
|
||
:min="1"
|
||
:max="transportForm.maxQuantity || 0"
|
||
@input="recalcCost"
|
||
/>
|
||
<span v-if="transportForm.maxQuantity">
|
||
({{ $t('falukant.branch.sale.transportMax', { max: transportForm.maxQuantity }) }})
|
||
</span>
|
||
</label>
|
||
|
||
<div v-if="transportForm.costLabel">
|
||
{{ $t('falukant.branch.sale.transportCost', { cost: transportForm.costLabel }) }}
|
||
</div>
|
||
|
||
<button
|
||
@click="createTransport"
|
||
:disabled="
|
||
transportForm.sourceKey === null ||
|
||
transportForm.sourceKey === undefined ||
|
||
!transportForm.vehicleTypeId ||
|
||
!transportForm.targetBranchId ||
|
||
!transportForm.maxQuantity ||
|
||
!transportForm.quantity
|
||
"
|
||
>
|
||
{{ $t('falukant.branch.sale.transportCreate') }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="transport-route" v-if="transportForm.durationLabel">
|
||
<div>
|
||
{{ $t('falukant.branch.sale.transportDuration', { duration: transportForm.durationLabel }) }}
|
||
</div>
|
||
<div>
|
||
{{ $t('falukant.branch.sale.transportArrival', { datetime: transportForm.etaLabel }) }}
|
||
</div>
|
||
<div v-if="transportForm.routeNames && transportForm.routeNames.length">
|
||
{{ $t('falukant.branch.sale.transportRoute') }}:
|
||
{{ transportForm.routeNames.join(' → ') }}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Laufende Transporte (immer im Inventar-Tab sichtbar, auch ohne Inventar) -->
|
||
<div class="running-transports" v-if="runningTransports.length">
|
||
<h5>{{ $t('falukant.branch.sale.runningTransportsTitle') }}</h5>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>{{ $t('falukant.branch.sale.runningDirection') }}</th>
|
||
<th>{{ $t('falukant.branch.sale.runningProduct') }}</th>
|
||
<th>{{ $t('falukant.branch.sale.runningQuantity') }}</th>
|
||
<th>{{ $t('falukant.branch.sale.runningSource') }}</th>
|
||
<th>{{ $t('falukant.branch.sale.runningTarget') }}</th>
|
||
<th>{{ $t('falukant.branch.sale.runningEta') }}</th>
|
||
<th>{{ $t('falukant.branch.sale.runningRemaining') }}</th>
|
||
<th>{{ $t('falukant.branch.sale.runningVehicleCount') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(group, index) in groupedTransports" :key="`group-${index}`">
|
||
<td>
|
||
{{ group.direction === 'outgoing'
|
||
? $t('falukant.branch.sale.runningDirectionOut')
|
||
: $t('falukant.branch.sale.runningDirectionIn') }}
|
||
</td>
|
||
<td>
|
||
<span v-if="group.product && group.product.labelTr">
|
||
{{ $t(`falukant.product.${group.product.labelTr}`) }}
|
||
</span>
|
||
<span v-else class="no-product">
|
||
{{ $t('falukant.branch.sale.runningNoProduct') }}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<span v-if="group.product && group.totalQuantity > 0">{{ group.totalQuantity }}</span>
|
||
<span v-else>—</span>
|
||
</td>
|
||
<td>{{ group.sourceRegion?.name }}</td>
|
||
<td>{{ group.targetRegion?.name }}</td>
|
||
<td>{{ formatEta({ eta: group.eta }) }}</td>
|
||
<td>{{ formatRemaining({ eta: group.eta }) }}</td>
|
||
<td>{{ group.vehicleCount }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import apiClient from '@/utils/axios.js';
|
||
export default {
|
||
name: "SaleSection",
|
||
props: {
|
||
branchId: { type: Number, required: true },
|
||
vehicles: { type: Array, default: () => [] },
|
||
branches: { type: Array, default: () => [] },
|
||
},
|
||
data() {
|
||
return {
|
||
inventory: [],
|
||
sellingItemIndex: null,
|
||
sellingAll: false,
|
||
sellAllStatus: null,
|
||
transportForm: {
|
||
sourceKey: null,
|
||
vehicleTypeId: null,
|
||
targetBranchId: null,
|
||
quantity: 0,
|
||
maxQuantity: 0,
|
||
distance: null,
|
||
durationHours: null,
|
||
eta: null,
|
||
durationLabel: '',
|
||
etaLabel: '',
|
||
routeNames: [],
|
||
cost: null,
|
||
costLabel: '',
|
||
},
|
||
runningTransports: [],
|
||
nowTs: Date.now(),
|
||
_transportTimer: null,
|
||
loadingPrices: new Set(),
|
||
};
|
||
},
|
||
computed: {
|
||
groupedTransports() {
|
||
const groups = new Map();
|
||
|
||
for (const transport of this.runningTransports) {
|
||
// Erstelle einen Schlüssel für die Gruppierung
|
||
// Wichtig: Restzeit nicht in den Schlüssel einbeziehen, da sie sich ständig ändert
|
||
const productId = transport.product?.id || null;
|
||
const productLabelTr = transport.product?.labelTr || null;
|
||
const sourceId = transport.sourceRegion?.id || null;
|
||
const targetId = transport.targetRegion?.id || null;
|
||
const direction = transport.direction;
|
||
// ETA als Zeitstempel für Gruppierung (auf Sekunden genau, um kleine Unterschiede zu ignorieren)
|
||
const eta = transport.eta ? Math.floor(new Date(transport.eta).getTime() / 1000) : null;
|
||
|
||
// Gruppierungsschlüssel: alle relevanten Eigenschaften außer Restzeit
|
||
const key = `${direction}-${productId}-${productLabelTr}-${sourceId}-${targetId}-${eta}`;
|
||
|
||
if (!groups.has(key)) {
|
||
groups.set(key, {
|
||
direction: transport.direction,
|
||
product: transport.product,
|
||
sourceRegion: transport.sourceRegion,
|
||
targetRegion: transport.targetRegion,
|
||
eta: transport.eta,
|
||
vehicleCount: 0,
|
||
totalQuantity: 0,
|
||
transports: [],
|
||
});
|
||
}
|
||
|
||
const group = groups.get(key);
|
||
group.vehicleCount += 1;
|
||
if (transport.product && transport.size > 0) {
|
||
group.totalQuantity += transport.size || 0;
|
||
}
|
||
group.transports.push(transport);
|
||
}
|
||
|
||
// Sortiere nach ETA (früheste zuerst)
|
||
return Array.from(groups.values()).sort((a, b) => {
|
||
if (!a.eta && !b.eta) return 0;
|
||
if (!a.eta) return 1;
|
||
if (!b.eta) return -1;
|
||
return new Date(a.eta).getTime() - new Date(b.eta).getTime();
|
||
});
|
||
},
|
||
},
|
||
async mounted() {
|
||
await this.loadInventory();
|
||
await this.loadTransports();
|
||
this._transportTimer = setInterval(() => {
|
||
this.nowTs = Date.now();
|
||
}, 1000);
|
||
await this.loadPricesForInventory();
|
||
},
|
||
beforeUnmount() {
|
||
if (this._transportTimer) {
|
||
clearInterval(this._transportTimer);
|
||
this._transportTimer = null;
|
||
}
|
||
},
|
||
methods: {
|
||
speedLabel(value) {
|
||
// Muss in methods liegen (Vue3): in computed wäre es ein Getter und keine aufrufbare Funktion.
|
||
const key = value == null ? 'unknown' : String(value);
|
||
const tKey = `falukant.branch.transport.speed.${key}`;
|
||
const translated = this.$t(tKey);
|
||
if (!translated || translated === tKey) return value;
|
||
return translated;
|
||
},
|
||
async loadInventory() {
|
||
try {
|
||
const response = await apiClient.get(`/api/falukant/inventory/${this.branchId}`);
|
||
this.inventory = response.data.map(item => ({
|
||
...item,
|
||
sellQuantity: item.totalQuantity,
|
||
// Vue3: besserPrices direkt als Property setzen (statt this.$set)
|
||
betterPrices: Array.isArray(item.betterPrices) ? item.betterPrices : [],
|
||
}));
|
||
await this.loadPricesForInventory();
|
||
} catch (error) {
|
||
console.error('Error loading inventory:', error);
|
||
}
|
||
},
|
||
async loadPricesForInventory() {
|
||
for (const item of this.inventory) {
|
||
const itemKey = `${item.region.id}-${item.product.id}-${item.quality}`;
|
||
if (this.loadingPrices.has(itemKey)) continue;
|
||
this.loadingPrices.add(itemKey);
|
||
try {
|
||
// Aktueller Preis basierend auf sellCost
|
||
const currentPrice = item.product.sellCost || 0;
|
||
const { data } = await apiClient.get('/api/falukant/products/prices-in-cities', {
|
||
params: {
|
||
productId: item.product.id,
|
||
currentPrice: currentPrice
|
||
}
|
||
});
|
||
// Vue3: direkte Zuweisung ist reaktiv
|
||
item.betterPrices = Array.isArray(data) ? data : [];
|
||
} catch (error) {
|
||
console.error(`Error loading prices for item ${itemKey}:`, error);
|
||
item.betterPrices = [];
|
||
} finally {
|
||
this.loadingPrices.delete(itemKey);
|
||
}
|
||
}
|
||
},
|
||
getCityPriceClass(branchType) {
|
||
if (branchType === 'store') return 'city-price-green';
|
||
if (branchType === 'production') return 'city-price-orange';
|
||
return 'city-price-red';
|
||
},
|
||
formatPrice(price) {
|
||
return new Intl.NumberFormat(navigator.language, {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
}).format(price);
|
||
},
|
||
async sellItem(index) {
|
||
if (this.sellingItemIndex !== null || this.sellingAll) return;
|
||
|
||
const item = this.inventory[index];
|
||
const quantityToSell = item.sellQuantity || item.totalQuantity;
|
||
this.sellingItemIndex = index;
|
||
|
||
try {
|
||
await apiClient.post(`/api/falukant/sell`, {
|
||
branchId: this.branchId,
|
||
productId: item.product.id,
|
||
quantity: quantityToSell,
|
||
quality: item.quality,
|
||
});
|
||
// UI sofort freigeben (Label/Disabled zurücksetzen), dann Inventory refreshen
|
||
this.sellingItemIndex = null;
|
||
await this.loadInventory();
|
||
} catch (error) {
|
||
alert(this.$t('falukant.branch.sale.sellError'));
|
||
} finally {
|
||
this.sellingItemIndex = null;
|
||
}
|
||
},
|
||
async sellAll() {
|
||
if (this.sellingAll || this.sellingItemIndex !== null) return;
|
||
|
||
this.sellingAll = true;
|
||
this.sellAllStatus = null;
|
||
|
||
try {
|
||
const response = await apiClient.post(`/api/falukant/sell/all`, { branchId: this.branchId });
|
||
const revenue = response.data?.revenue || 0;
|
||
// UI sofort freigeben + Status setzen, danach Inventory refreshen
|
||
this.sellingAll = false;
|
||
this.sellAllStatus = {
|
||
type: 'success',
|
||
message: this.$t('falukant.branch.sale.sellAllSuccess', { revenue: this.formatMoney(revenue) })
|
||
};
|
||
// Inventory neu laden nach erfolgreichem Verkauf
|
||
await this.loadInventory();
|
||
} catch (error) {
|
||
// UI sofort freigeben + Fehlerstatus setzen
|
||
this.sellingAll = false;
|
||
this.sellAllStatus = {
|
||
type: 'error',
|
||
message: this.$t('falukant.branch.sale.sellAllError')
|
||
};
|
||
} finally {
|
||
// Falls noch nicht freigegeben (z.B. wenn ein unerwarteter Fehler vor Response passiert)
|
||
this.sellingAll = false;
|
||
// Status nach 5 Sekunden löschen
|
||
setTimeout(() => {
|
||
this.sellAllStatus = null;
|
||
}, 5000);
|
||
}
|
||
},
|
||
inventoryOptions() {
|
||
return this.inventory.map((item, index) => ({
|
||
key: index,
|
||
label: `${this.$t(`falukant.product.${item.product.labelTr}`)} (Q${item.quality}, ${item.totalQuantity})`,
|
||
productId: item.product.id,
|
||
totalQuantity: item.totalQuantity,
|
||
}));
|
||
},
|
||
vehicleTypeOptions() {
|
||
const groups = {};
|
||
for (const v of this.vehicles) {
|
||
if (v.status !== 'available' || !v.type || !v.type.id) continue;
|
||
const id = v.type.id;
|
||
if (!groups[id]) {
|
||
groups[id] = {
|
||
id,
|
||
tr: v.type.tr,
|
||
capacity: v.type.capacity,
|
||
speed: v.type.speed,
|
||
count: 0,
|
||
};
|
||
}
|
||
groups[id].count += 1;
|
||
}
|
||
return Object.values(groups);
|
||
},
|
||
targetBranchOptions() {
|
||
return (this.branches || [])
|
||
.filter(b => ['store', 'fullstack'].includes(b.branchTypeLabelTr))
|
||
// aktuelle Niederlassung darf nicht als Ziel angeboten werden
|
||
.filter(b => b.id !== this.branchId)
|
||
.map(b => ({
|
||
id: b.id,
|
||
label: `${b.cityName} – ${b.type}`,
|
||
}));
|
||
},
|
||
recalcMaxQuantity() {
|
||
const source = this.inventoryOptions().find(o => o.key === this.transportForm.sourceKey);
|
||
const vType = this.vehicleTypeOptions().find(v => v.id === this.transportForm.vehicleTypeId);
|
||
if (!source || !vType) {
|
||
this.transportForm.maxQuantity = 0;
|
||
this.transportForm.quantity = 0;
|
||
this.recalcCost();
|
||
return;
|
||
}
|
||
const maxByInventory = source.totalQuantity;
|
||
const maxByVehicles = vType.capacity * vType.count;
|
||
const max = Math.min(maxByInventory, maxByVehicles);
|
||
this.transportForm.maxQuantity = max;
|
||
if (!this.transportForm.quantity || this.transportForm.quantity > max) {
|
||
this.transportForm.quantity = max;
|
||
}
|
||
this.recalcCost();
|
||
},
|
||
recalcCost() {
|
||
const idx = this.transportForm.sourceKey;
|
||
if (idx === null || idx === undefined) {
|
||
this.transportForm.cost = null;
|
||
this.transportForm.costLabel = '';
|
||
return;
|
||
}
|
||
const item = this.inventory[idx];
|
||
const qty = this.transportForm.quantity || 0;
|
||
if (!item || !item.product || item.product.sellCost == null || qty <= 0) {
|
||
this.transportForm.cost = null;
|
||
this.transportForm.costLabel = '';
|
||
return;
|
||
}
|
||
const unitValue = item.product.sellCost || 0;
|
||
const totalValue = unitValue * qty;
|
||
const cost = Math.max(0.1, totalValue * 0.01);
|
||
this.transportForm.cost = cost;
|
||
this.transportForm.costLabel = this.formatMoney(cost);
|
||
},
|
||
formatMoney(amount) {
|
||
if (amount == null) return '';
|
||
try {
|
||
return amount.toLocaleString(undefined, {
|
||
minimumFractionDigits: 1,
|
||
maximumFractionDigits: 1,
|
||
});
|
||
} catch (e) {
|
||
return String(amount);
|
||
}
|
||
},
|
||
async loadRouteInfo() {
|
||
this.transportForm.distance = null;
|
||
this.transportForm.durationHours = null;
|
||
this.transportForm.eta = null;
|
||
this.transportForm.durationLabel = '';
|
||
this.transportForm.etaLabel = '';
|
||
this.transportForm.routeNames = [];
|
||
|
||
const sourceOpt = this.inventoryOptions().find(o => o.key === this.transportForm.sourceKey);
|
||
const vType = this.vehicleTypeOptions().find(v => v.id === this.transportForm.vehicleTypeId);
|
||
const targetBranch = (this.branches || []).find(b => b.id === this.transportForm.targetBranchId);
|
||
|
||
if (!sourceOpt || !vType || !targetBranch) {
|
||
return;
|
||
}
|
||
|
||
const sourceBranch = (this.branches || []).find(b => b.id === this.branchId);
|
||
if (!sourceBranch) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const { data } = await apiClient.get('/api/falukant/transports/route', {
|
||
params: {
|
||
sourceRegionId: sourceBranch.regionId,
|
||
targetRegionId: targetBranch.regionId,
|
||
vehicleTypeId: vType.id,
|
||
},
|
||
});
|
||
if (data && data.totalDistance != null) {
|
||
const distance = data.totalDistance;
|
||
const speed = vType.speed || 1;
|
||
const hours = distance / speed;
|
||
|
||
this.transportForm.distance = distance;
|
||
this.transportForm.durationHours = hours;
|
||
|
||
const now = new Date();
|
||
const etaMs = now.getTime() + hours * 60 * 60 * 1000;
|
||
const etaDate = new Date(etaMs);
|
||
|
||
const fullHours = Math.floor(hours);
|
||
const minutes = Math.round((hours - fullHours) * 60);
|
||
const parts = [];
|
||
if (fullHours > 0) parts.push(`${fullHours} h`);
|
||
if (minutes > 0) parts.push(`${minutes} min`);
|
||
this.transportForm.durationLabel = parts.length ? parts.join(' ') : '0 min';
|
||
this.transportForm.etaLabel = etaDate.toLocaleString();
|
||
|
||
this.transportForm.routeNames = (data.regions || []).map(r => r.name);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading transport route:', error);
|
||
this.transportForm.distance = null;
|
||
this.transportForm.durationHours = null;
|
||
this.transportForm.eta = null;
|
||
this.transportForm.durationLabel = '';
|
||
this.transportForm.etaLabel = '';
|
||
this.transportForm.routeNames = [];
|
||
}
|
||
},
|
||
async createTransport() {
|
||
const source = this.inventoryOptions().find(o => o.key === this.transportForm.sourceKey);
|
||
if (!source) return;
|
||
try {
|
||
await apiClient.post('/api/falukant/transports', {
|
||
branchId: this.branchId,
|
||
vehicleTypeId: this.transportForm.vehicleTypeId,
|
||
productId: source.productId,
|
||
quantity: this.transportForm.quantity,
|
||
targetBranchId: this.transportForm.targetBranchId,
|
||
});
|
||
await this.loadInventory();
|
||
await this.loadTransports();
|
||
alert(this.$t('falukant.branch.sale.transportStarted'));
|
||
this.$emit('transportCreated');
|
||
} catch (error) {
|
||
console.error('Error creating transport:', error);
|
||
alert(this.$t('falukant.branch.sale.transportError'));
|
||
}
|
||
},
|
||
async loadTransports() {
|
||
try {
|
||
const { data } = await apiClient.get(`/api/falukant/transports/branch/${this.branchId}`);
|
||
this.runningTransports = Array.isArray(data) ? data : [];
|
||
} catch (error) {
|
||
console.error('Error loading transports:', error);
|
||
this.runningTransports = [];
|
||
}
|
||
},
|
||
formatEta(transport) {
|
||
if (!transport || !transport.eta) return '';
|
||
const etaDate = new Date(transport.eta);
|
||
if (Number.isNaN(etaDate.getTime())) return '';
|
||
return etaDate.toLocaleString();
|
||
},
|
||
formatRemaining(transport) {
|
||
if (!transport || !transport.eta) return '';
|
||
const etaMs = new Date(transport.eta).getTime();
|
||
if (Number.isNaN(etaMs)) return '';
|
||
let diff = Math.floor((etaMs - this.nowTs) / 1000);
|
||
if (diff <= 0) {
|
||
return this.$t('falukant.branch.production.noProductions') ? '0s' : '0s';
|
||
}
|
||
const hours = Math.floor(diff / 3600);
|
||
diff %= 3600;
|
||
const minutes = Math.floor(diff / 60);
|
||
const seconds = diff % 60;
|
||
const parts = [];
|
||
if (hours > 0) parts.push(`${hours}h`);
|
||
if (minutes > 0 || hours > 0) parts.push(`${minutes}m`);
|
||
parts.push(`${seconds}s`);
|
||
return parts.join(' ');
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.sale-section {
|
||
border: 1px solid #ccc;
|
||
margin: 10px 0;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
}
|
||
.inventory-table table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
.inventory-table th,
|
||
.inventory-table td {
|
||
padding: 2px 3px;
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
/* Größerer Abstand zwischen den Spalten der Transport-Tabelle */
|
||
.running-transports table {
|
||
border-collapse: separate;
|
||
border-spacing: 16px 0; /* horizontaler Abstand zwischen Spalten */
|
||
}
|
||
.price-cities {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.3em;
|
||
}
|
||
.city-price {
|
||
padding: 0.2em 0.4em;
|
||
border-radius: 3px;
|
||
font-size: 0.85em;
|
||
cursor: help;
|
||
}
|
||
.city-price-green {
|
||
background-color: #90EE90;
|
||
color: #000;
|
||
}
|
||
.city-price-orange {
|
||
background-color: #FFA500;
|
||
color: #000;
|
||
}
|
||
.city-price-red {
|
||
background-color: #FF6B6B;
|
||
color: #fff;
|
||
}
|
||
.no-better-prices {
|
||
color: #999;
|
||
font-style: italic;
|
||
}
|
||
.sell-all-status {
|
||
margin-top: 10px;
|
||
padding: 8px;
|
||
border-radius: 4px;
|
||
}
|
||
.sell-all-status.success {
|
||
background-color: #d4edda;
|
||
color: #155724;
|
||
border: 1px solid #c3e6cb;
|
||
}
|
||
.sell-all-status.error {
|
||
background-color: #f8d7da;
|
||
color: #721c24;
|
||
border: 1px solid #f5c6cb;
|
||
}
|
||
</style>
|