Files
yourpart3/frontend/src/components/falukant/RevenueSection.vue

260 lines
8.4 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="revenue-section">
<h3>
<button @click="toggleRevenueTable">
{{ $t('falukant.branch.revenue.title') }}
{{ isRevenueTableOpen ? '' : '' }}
</button>
</h3>
<div v-if="isRevenueTableOpen" class="revenue-table">
<table>
<thead>
<tr>
<th>{{ $t('falukant.branch.revenue.product') }}</th>
<th>{{ $t('falukant.branch.revenue.knowledge') }}</th>
<th>{{ $t('falukant.branch.revenue.absolute') }}</th>
<th>{{ $t('falukant.branch.revenue.perMinute') }}</th>
<th>{{ $t('falukant.branch.revenue.profitAbsolute') }}</th>
<th>{{ $t('falukant.branch.revenue.profitPerMinute') }}</th>
<th>{{ $t('falukant.branch.revenue.betterPrices') }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="product in sortedProductsByProfitPerMinute"
:key="product.id"
:class="{ highlight: product.id === topProductByProfitPerMinute?.id }"
>
<td>{{ $t(`falukant.product.${product.labelTr}`) }}</td>
<td>{{ product.knowledges && product.knowledges[0] ? product.knowledges[0].knowledge : 0 }}</td>
<td>{{ calculateProductRevenue(product).absolute }}</td>
<td>{{ calculateProductRevenue(product).perMinute }}</td>
<td>{{ calculateProductProfit(product).absolute }}</td>
<td>{{ calculateProductProfit(product).perMinute }}</td>
<td>
<div v-if="getBetterPrices(product.id) && getBetterPrices(product.id).length > 0" class="price-cities">
<template v-for="(city, idx) in getBetterPrices(product.id)" :key="city.regionId">
<span
v-if="idx > 0"
class="city-price-sep"
aria-hidden="true"
>, </span>
<span
:class="['city-price', getCityPriceClass(city.branchType)]"
:title="`${city.regionName}: ${formatCityPrice(city, product)}`"
>
<span class="city-name">{{ city.regionName }}</span>
<span class="city-price-value">({{ formatCityPrice(city, product) }})</span>
</span>
</template>
</div>
<span v-else class="no-better-prices"></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
import apiClient from '@/utils/axios.js';
export default {
name: "RevenueSection",
props: {
products: { type: Array, required: true },
calculateProductRevenue: { type: Function, required: true },
calculateProductProfit: { type: Function, required: true },
currentRegionId: { type: Number, default: null },
},
data() {
return {
isRevenueTableOpen: false,
loadingPrices: new Set(),
betterPricesMap: {}, // Map von productId zu betterPrices Array
};
},
computed: {
/** Gleiche Metrik wie Daemon: Gewinn/Minute ≈ (Erlös/Stück Stückkosten) / Produktionszeit; höchste zuerst. */
sortedProductsByProfitPerMinute() {
if (!this.products || this.products.length === 0) return [];
return [...this.products].sort((a, b) => {
const pb = parseFloat(this.calculateProductProfit(b).perMinute);
const pa = parseFloat(this.calculateProductProfit(a).perMinute);
if (pb !== pa) return pb - pa;
return (a.labelTr || '').localeCompare(b.labelTr || '');
});
},
topProductByProfitPerMinute() {
const list = this.sortedProductsByProfitPerMinute;
return list.length ? list[0] : null;
},
},
async mounted() {
if (this.isRevenueTableOpen) {
await this.loadPricesForAllProducts();
}
},
watch: {
isRevenueTableOpen(newVal) {
if (newVal && this.currentRegionId !== null) {
this.loadPricesForAllProducts();
}
},
products: {
handler(newProducts, oldProducts) {
// Leere betterPricesMap wenn sich die Produktliste ändert
if (oldProducts && oldProducts.length > 0) {
this.betterPricesMap = {};
}
if (this.isRevenueTableOpen && this.currentRegionId !== null) {
this.loadPricesForAllProducts();
}
},
deep: true
},
currentRegionId(newVal, oldVal) {
// Leere betterPricesMap wenn sich die Region ändert
if (oldVal !== null && oldVal !== undefined) {
this.betterPricesMap = {};
}
if (this.isRevenueTableOpen && newVal !== null) {
this.loadPricesForAllProducts();
}
}
},
methods: {
toggleRevenueTable() {
this.isRevenueTableOpen = !this.isRevenueTableOpen;
if (this.isRevenueTableOpen) {
this.loadPricesForAllProducts();
}
},
async loadPricesForAllProducts() {
if (this.currentRegionId === null || this.currentRegionId === undefined || !this.products.length) {
return;
}
const items = this.products.map((product) => ({
productId: product.id,
currentPrice: parseFloat(this.calculateProductRevenue(product).absolute)
}));
try {
const { data } = await apiClient.post('/api/falukant/products/prices-in-cities-batch', {
currentRegionId: this.currentRegionId,
items
});
this.betterPricesMap = { ...this.betterPricesMap };
for (const product of this.products) {
this.betterPricesMap[product.id] = (data && data[product.id]) ? data[product.id] : [];
}
} catch (error) {
console.error('Error loading prices for products:', error);
for (const product of this.products) {
this.betterPricesMap = { ...this.betterPricesMap, [product.id]: [] };
}
}
},
getBetterPrices(productId) {
return this.betterPricesMap[productId] || [];
},
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);
},
formatCityPrice(city, product) {
const absolute = Number(city?.price || 0);
const productionTime = Number(product?.productionTime || 0);
const perMinute = Number.isFinite(Number(city?.grossPerMinute))
? Number(city.grossPerMinute)
: (productionTime > 0 ? (absolute / productionTime) : 0);
return `${this.formatPrice(absolute)} / ${this.formatPrice(perMinute)}`;
},
},
};
</script>
<style scoped>
.revenue-section {
border: 1px solid var(--color-border);
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
.revenue-section button {
background: none;
border: none;
color: var(--color-primary);
cursor: pointer;
font-size: 1em;
text-decoration: underline;
}
.revenue-table {
margin-top: 10px;
overflow-x: auto;
}
.revenue-table table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 8px;
border: 1px solid var(--color-border);
}
.highlight {
background-color: #dfffd6;
}
.price-cities {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.15em 0.35em;
line-height: 1.35;
min-width: 0;
}
.city-price-sep {
color: #666;
user-select: none;
}
.city-price {
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.85em;
cursor: help;
display: inline-flex;
align-items: center;
gap: 0.3em;
}
.city-name {
font-weight: 500;
}
.city-price-value {
font-size: 0.9em;
opacity: 0.9;
}
.city-price-green {
background-color: var(--color-secondary);
color: var(--color-text-primary);
}
.city-price-orange {
background-color: var(--color-primary-orange);
color: #000;
}
.city-price-red {
background-color: #FF6B6B;
color: #fff;
}
.no-better-prices {
color: #999;
font-style: italic;
}
</style>