Files
yourpart3/frontend/src/components/falukant/RevenueSection.vue
Torsten Schulz (local) d6ea09b3e2 Enhance RevenueSection UI and streamline price loading logic
- Updated the display of city prices in the RevenueSection component to include both city names and formatted price values, improving user experience.
- Removed unnecessary console logs from the loadPricesForAllProducts method to clean up the code and reduce clutter, while maintaining essential functionality.
- Simplified the getBetterPrices method by eliminating redundant logging, enhancing code clarity and performance.
2025-12-03 16:30:10 +01:00

237 lines
7.6 KiB
Vue

<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>Bessere Preise</th>
</tr>
</thead>
<tbody>
<tr v-for="product in products" :key="product.id" :class="{ highlight: product.id === productWithMaxRevenuePerMinute?.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">
<span v-for="city in getBetterPrices(product.id)" :key="city.regionId"
:class="['city-price', getCityPriceClass(city.branchType)]"
:title="`${city.regionName}: ${formatPrice(city.price)}`">
<span class="city-name">{{ city.regionName }}</span>
<span class="city-price-value">({{ formatPrice(city.price) }})</span>
</span>
</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: {
productWithMaxRevenuePerMinute() {
if (!this.products || this.products.length === 0) return null;
return this.products.reduce((maxProduct, currentProduct) => {
const currentRevenue = parseFloat(this.calculateProductRevenue(currentProduct).perMinute);
const maxRevenue = maxProduct ? parseFloat(this.calculateProductRevenue(maxProduct).perMinute) : 0;
return currentRevenue > maxRevenue ? currentProduct : maxProduct;
}, 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) {
return;
}
for (const product of this.products) {
if (this.loadingPrices.has(product.id)) continue;
this.loadingPrices.add(product.id);
try {
// Verwende den gerundeten Preis aus calculateProductRevenue (wie gewollt)
const currentPrice = parseFloat(this.calculateProductRevenue(product).absolute);
const { data } = await apiClient.get('/api/falukant/products/prices-in-cities', {
params: {
productId: product.id,
currentPrice: currentPrice,
currentRegionId: this.currentRegionId
}
});
// Speichere betterPrices in einem separaten Map, nicht auf dem product Objekt
// In Vue 3 müssen wir ein neues Objekt erstellen, um die Reaktivität zu triggern
this.betterPricesMap = {
...this.betterPricesMap,
[product.id]: data || []
};
} catch (error) {
console.error(`Error loading prices for product ${product.id}:`, error);
this.betterPricesMap = {
...this.betterPricesMap,
[product.id]: []
};
} finally {
this.loadingPrices.delete(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);
},
},
};
</script>
<style scoped>
.revenue-section {
border: 1px solid #ccc;
margin: 10px 0;
border-radius: 4px;
padding: 10px;
}
.revenue-section button {
background: none;
border: none;
color: #007bff;
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 #ddd;
}
.highlight {
background-color: #dfffd6;
}
.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;
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: #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;
}
</style>