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:
@@ -144,6 +144,14 @@ class FalukantController {
|
|||||||
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
|
this.applyForElections = this._wrapWithUser((userId, req) => this.service.applyForElections(userId, req.body.electionIds));
|
||||||
|
|
||||||
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
this.getRegions = this._wrapWithUser((userId) => this.service.getRegions(userId));
|
||||||
|
this.getProductPricesInCities = this._wrapWithUser((userId, req) => {
|
||||||
|
const productId = parseInt(req.query.productId, 10);
|
||||||
|
const currentPrice = parseFloat(req.query.currentPrice);
|
||||||
|
if (Number.isNaN(productId) || Number.isNaN(currentPrice)) {
|
||||||
|
throw new Error('productId and currentPrice are required');
|
||||||
|
}
|
||||||
|
return this.service.getProductPricesInCities(userId, productId, currentPrice);
|
||||||
|
});
|
||||||
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
|
this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element));
|
||||||
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId));
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ router.post('/politics/elections', falukantController.vote);
|
|||||||
router.get('/politics/open', falukantController.getOpenPolitics);
|
router.get('/politics/open', falukantController.getOpenPolitics);
|
||||||
router.post('/politics/open', falukantController.applyForElections);
|
router.post('/politics/open', falukantController.applyForElections);
|
||||||
router.get('/cities', falukantController.getRegions);
|
router.get('/cities', falukantController.getRegions);
|
||||||
|
router.get('/products/prices-in-cities', falukantController.getProductPricesInCities);
|
||||||
router.get('/vehicles/types', falukantController.getVehicleTypes);
|
router.get('/vehicles/types', falukantController.getVehicleTypes);
|
||||||
router.post('/vehicles', falukantController.buyVehicles);
|
router.post('/vehicles', falukantController.buyVehicles);
|
||||||
router.get('/vehicles', falukantController.getVehicles);
|
router.get('/vehicles', falukantController.getVehicles);
|
||||||
|
|||||||
@@ -3518,6 +3518,86 @@ class FalukantService extends BaseService {
|
|||||||
return regions;
|
return regions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProductPricesInCities(hashedUserId, productId, currentPrice) {
|
||||||
|
const user = await this.getFalukantUserByHashedId(hashedUserId);
|
||||||
|
const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
|
||||||
|
if (!character) {
|
||||||
|
throw new Error(`No FalukantCharacter found for user with id ${user.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produkt abrufen
|
||||||
|
const product = await ProductType.findOne({ where: { id: productId } });
|
||||||
|
if (!product) {
|
||||||
|
throw new Error(`Product not found with id ${productId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knowledge für dieses Produkt abrufen
|
||||||
|
const knowledge = await Knowledge.findOne({
|
||||||
|
where: { characterId: character.id, productId: productId }
|
||||||
|
});
|
||||||
|
const knowledgeFactor = knowledge?.knowledge || 0;
|
||||||
|
|
||||||
|
// Alle Städte abrufen
|
||||||
|
const cities = await RegionData.findAll({
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: RegionType,
|
||||||
|
as: 'regionType',
|
||||||
|
where: { labelTr: 'city' },
|
||||||
|
attributes: ['labelTr']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Branch,
|
||||||
|
as: 'branches',
|
||||||
|
where: { falukantUserId: user.id },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: BranchType,
|
||||||
|
as: 'branchType',
|
||||||
|
attributes: ['labelTr']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
attributes: ['branchTypeId'],
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Für jede Stadt den Preis berechnen und Branch-Typ bestimmen
|
||||||
|
const results = [];
|
||||||
|
for (const city of cities) {
|
||||||
|
const priceInCity = calcSellPrice(product, knowledgeFactor);
|
||||||
|
|
||||||
|
// Nur Städte zurückgeben, wo der Preis höher ist
|
||||||
|
if (priceInCity > currentPrice) {
|
||||||
|
// Branch-Typ bestimmen
|
||||||
|
let branchType = null; // null = kein Branch
|
||||||
|
if (city.branches && city.branches.length > 0) {
|
||||||
|
// Finde den "besten" Branch-Typ (store/fullstack > production)
|
||||||
|
const branchTypes = city.branches.map(b => b.branchType?.labelTr).filter(Boolean);
|
||||||
|
if (branchTypes.includes('store') || branchTypes.includes('fullstack')) {
|
||||||
|
branchType = 'store'; // Grün
|
||||||
|
} else if (branchTypes.includes('production')) {
|
||||||
|
branchType = 'production'; // Orange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
regionId: city.id,
|
||||||
|
regionName: city.name,
|
||||||
|
price: priceInCity,
|
||||||
|
branchType: branchType // 'store' (grün), 'production' (orange), null (rot)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortiere nach Preis (höchster zuerst)
|
||||||
|
results.sort((a, b) => b.price - a.price);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
async renovate(hashedUserId, element) {
|
async renovate(hashedUserId, element) {
|
||||||
const user = await getFalukantUserOrFail(hashedUserId);
|
const user = await getFalukantUserOrFail(hashedUserId);
|
||||||
const house = await UserHouse.findOne({
|
const house = await UserHouse.findOne({
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<th>{{ $t('falukant.branch.revenue.perMinute') }}</th>
|
<th>{{ $t('falukant.branch.revenue.perMinute') }}</th>
|
||||||
<th>{{ $t('falukant.branch.revenue.profitAbsolute') }}</th>
|
<th>{{ $t('falukant.branch.revenue.profitAbsolute') }}</th>
|
||||||
<th>{{ $t('falukant.branch.revenue.profitPerMinute') }}</th>
|
<th>{{ $t('falukant.branch.revenue.profitPerMinute') }}</th>
|
||||||
|
<th>Bessere Preise</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -26,6 +27,16 @@
|
|||||||
<td>{{ calculateProductRevenue(product).perMinute }}</td>
|
<td>{{ calculateProductRevenue(product).perMinute }}</td>
|
||||||
<td>{{ calculateProductProfit(product).absolute }}</td>
|
<td>{{ calculateProductProfit(product).absolute }}</td>
|
||||||
<td>{{ calculateProductProfit(product).perMinute }}</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -34,6 +45,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import apiClient from '@/utils/axios.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "RevenueSection",
|
name: "RevenueSection",
|
||||||
props: {
|
props: {
|
||||||
@@ -44,6 +57,7 @@
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isRevenueTableOpen: false,
|
isRevenueTableOpen: false,
|
||||||
|
loadingPrices: new Set(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -56,9 +70,64 @@
|
|||||||
}, null);
|
}, 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: {
|
methods: {
|
||||||
toggleRevenueTable() {
|
toggleRevenueTable() {
|
||||||
this.isRevenueTableOpen = !this.isRevenueTableOpen;
|
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 {
|
.highlight {
|
||||||
background-color: #dfffd6;
|
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>
|
</style>
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
<th>{{ $t('falukant.branch.sale.quality') }}</th>
|
<th>{{ $t('falukant.branch.sale.quality') }}</th>
|
||||||
<th>{{ $t('falukant.branch.sale.quantity') }}</th>
|
<th>{{ $t('falukant.branch.sale.quantity') }}</th>
|
||||||
<th>{{ $t('falukant.branch.sale.sell') }}</th>
|
<th>{{ $t('falukant.branch.sale.sell') }}</th>
|
||||||
|
<th>Bessere Preise</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -22,6 +23,16 @@
|
|||||||
<input type="number" v-model.number="item.sellQuantity" :min="1" :max="item.totalQuantity" />
|
<input type="number" v-model.number="item.sellQuantity" :min="1" :max="item.totalQuantity" />
|
||||||
<button @click="sellItem(index)">{{ $t('falukant.branch.sale.sellButton') }}</button>
|
<button @click="sellItem(index)">{{ $t('falukant.branch.sale.sellButton') }}</button>
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -178,6 +189,7 @@
|
|||||||
runningTransports: [],
|
runningTransports: [],
|
||||||
nowTs: Date.now(),
|
nowTs: Date.now(),
|
||||||
_transportTimer: null,
|
_transportTimer: null,
|
||||||
|
loadingPrices: new Set(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
@@ -186,6 +198,7 @@
|
|||||||
this._transportTimer = setInterval(() => {
|
this._transportTimer = setInterval(() => {
|
||||||
this.nowTs = Date.now();
|
this.nowTs = Date.now();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
await this.loadPricesForInventory();
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
if (this._transportTimer) {
|
if (this._transportTimer) {
|
||||||
@@ -201,10 +214,45 @@
|
|||||||
...item,
|
...item,
|
||||||
sellQuantity: item.totalQuantity,
|
sellQuantity: item.totalQuantity,
|
||||||
}));
|
}));
|
||||||
|
await this.loadPricesForInventory();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading inventory:', 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) {
|
sellItem(index) {
|
||||||
const item = this.inventory[index];
|
const item = this.inventory[index];
|
||||||
const quantityToSell = item.sellQuantity || item.totalQuantity;
|
const quantityToSell = item.sellQuantity || item.totalQuantity;
|
||||||
@@ -448,5 +496,32 @@
|
|||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-spacing: 16px 0; /* horizontaler Abstand zwischen Spalten */
|
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>
|
</style>
|
||||||
|
|
||||||
Reference in New Issue
Block a user