Files
yourpart3/frontend/src/components/falukant/RevenueSection.vue
Torsten Schulz (local) 1118a691b9
All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
feat(falukant): enhance product pricing and nobility advancement features
- Updated the `getAllProductPricesInRegion` method in `FalukantService` to accept additional parameters for network worth and branch ID, improving pricing calculations based on user branches.
- Enhanced the nobility advancement logic in `NobilityView` to display unmet requirements clearly, providing users with better feedback on advancement conditions.
- Refactored the revenue calculation in `ProductionSection` to utilize a cached product prices object, optimizing performance and reducing redundant API calls.
- Updated localization files to include new translations for attack sources across multiple languages, enhancing the user experience for diverse audiences.
- Removed obsolete C++ worker references and streamlined the retention logic for production logs, ensuring efficient data management.
2026-04-09 09:08:32 +02:00

252 lines
7.9 KiB
Vue
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}: ${formatPrice(city.price)}`"
>
<span class="city-name">{{ city.regionName }}</span>
<span class="city-price-value">({{ formatPrice(city.price) }})</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);
},
},
};
</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;
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-primary-green);
color: #000;
}
.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>