All checks were successful
Deploy to production / deploy (push) Successful in 2m52s
- 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.
252 lines
7.9 KiB
Vue
252 lines
7.9 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>{{ $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>
|
||
|