Add product price retrieval feature in cities

- Implemented a new endpoint in FalukantController to fetch product prices in various cities based on product ID and current price.
- Developed the corresponding service method in FalukantService to calculate and return prices, considering user knowledge and city branches.
- Updated frontend components (RevenueSection and SaleSection) to display better prices for products, including loading logic and UI enhancements for price visibility.
- Added styling for price indicators based on branch types to improve user experience.
This commit is contained in:
Torsten Schulz (local)
2025-12-01 16:42:54 +01:00
parent 8c8841705c
commit adc7132404
5 changed files with 260 additions and 0 deletions

View File

@@ -16,6 +16,7 @@
<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>
@@ -26,6 +27,16 @@
<td>{{ calculateProductRevenue(product).perMinute }}</td>
<td>{{ calculateProductProfit(product).absolute }}</td>
<td>{{ calculateProductProfit(product).perMinute }}</td>
<td>
<div v-if="product.betterPrices && product.betterPrices.length > 0" class="price-cities">
<span v-for="city in product.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>
@@ -34,6 +45,8 @@
</template>
<script>
import apiClient from '@/utils/axios.js';
export default {
name: "RevenueSection",
props: {
@@ -44,6 +57,7 @@
data() {
return {
isRevenueTableOpen: false,
loadingPrices: new Set(),
};
},
computed: {
@@ -56,9 +70,64 @@
}, null);
},
},
async mounted() {
if (this.isRevenueTableOpen) {
await this.loadPricesForAllProducts();
}
},
watch: {
isRevenueTableOpen(newVal) {
if (newVal) {
this.loadPricesForAllProducts();
}
},
products: {
handler() {
if (this.isRevenueTableOpen) {
this.loadPricesForAllProducts();
}
},
deep: true
}
},
methods: {
toggleRevenueTable() {
this.isRevenueTableOpen = !this.isRevenueTableOpen;
if (this.isRevenueTableOpen) {
this.loadPricesForAllProducts();
}
},
async loadPricesForAllProducts() {
for (const product of this.products) {
if (this.loadingPrices.has(product.id)) continue;
this.loadingPrices.add(product.id);
try {
const currentPrice = parseFloat(this.calculateProductRevenue(product).absolute);
const { data } = await apiClient.get('/api/falukant/products/prices-in-cities', {
params: {
productId: product.id,
currentPrice: currentPrice
}
});
this.$set(product, 'betterPrices', data || []);
} catch (error) {
console.error(`Error loading prices for product ${product.id}:`, error);
this.$set(product, 'betterPrices', []);
} finally {
this.loadingPrices.delete(product.id);
}
}
},
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);
},
},
};
@@ -94,5 +163,32 @@
.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;
}
.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>

View File

@@ -10,6 +10,7 @@
<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>
@@ -22,6 +23,16 @@
<input type="number" v-model.number="item.sellQuantity" :min="1" :max="item.totalQuantity" />
<button @click="sellItem(index)">{{ $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>
@@ -178,6 +189,7 @@
runningTransports: [],
nowTs: Date.now(),
_transportTimer: null,
loadingPrices: new Set(),
};
},
async mounted() {
@@ -186,6 +198,7 @@
this._transportTimer = setInterval(() => {
this.nowTs = Date.now();
}, 1000);
await this.loadPricesForInventory();
},
beforeUnmount() {
if (this._transportTimer) {
@@ -201,10 +214,45 @@
...item,
sellQuantity: item.totalQuantity,
}));
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
}
});
this.$set(item, 'betterPrices', data || []);
} catch (error) {
console.error(`Error loading prices for item ${itemKey}:`, error);
this.$set(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);
},
sellItem(index) {
const item = this.inventory[index];
const quantityToSell = item.sellQuantity || item.totalQuantity;
@@ -448,5 +496,32 @@
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;
}
</style>