diff --git a/backend/controllers/falukantController.js b/backend/controllers/falukantController.js index bf84b07..89b3f61 100644 --- a/backend/controllers/falukantController.js +++ b/backend/controllers/falukantController.js @@ -214,6 +214,16 @@ class FalukantController { } return this.service.getProductPricesInCities(userId, productId, currentPrice, currentRegionId); }); + this.getProductPricesInCitiesBatch = this._wrapWithUser((userId, req) => { + const body = req.body || {}; + const items = Array.isArray(body.items) ? body.items : []; + const currentRegionId = body.currentRegionId != null ? parseInt(body.currentRegionId, 10) : null; + const valid = items.map(i => ({ + productId: parseInt(i.productId, 10), + currentPrice: parseFloat(i.currentPrice) + })).filter(i => !Number.isNaN(i.productId) && !Number.isNaN(i.currentPrice)); + return this.service.getProductPricesInCitiesBatch(userId, valid, Number.isNaN(currentRegionId) ? null : currentRegionId); + }); this.renovate = this._wrapWithUser((userId, req) => this.service.renovate(userId, req.body.element)); this.renovateAll = this._wrapWithUser((userId) => this.service.renovateAll(userId)); diff --git a/backend/routers/falukantRouter.js b/backend/routers/falukantRouter.js index 618e20f..2272d05 100644 --- a/backend/routers/falukantRouter.js +++ b/backend/routers/falukantRouter.js @@ -82,6 +82,7 @@ router.get('/cities', falukantController.getRegions); router.get('/products/price-in-region', falukantController.getProductPriceInRegion); router.get('/products/prices-in-region', falukantController.getAllProductPricesInRegion); router.get('/products/prices-in-cities', falukantController.getProductPricesInCities); +router.post('/products/prices-in-cities-batch', falukantController.getProductPricesInCitiesBatch); router.get('/branches/:branchId/taxes', falukantController.getBranchTaxes); router.get('/vehicles/types', falukantController.getVehicleTypes); router.post('/vehicles', falukantController.buyVehicles); diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 326265c..5765b93 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -4528,6 +4528,103 @@ class FalukantService extends BaseService { return results; } + /** + * Batch-Variante: Preise für mehrere Produkte in einem Request. + * @param {string} hashedUserId + * @param {Array<{ productId: number, currentPrice: number }>} items + * @param {number|null} currentRegionId + * @returns {Promise>>} + */ + async getProductPricesInCitiesBatch(hashedUserId, items, currentRegionId = null) { + if (!items || items.length === 0) return {}; + const productIds = [...new Set(items.map(i => i.productId))]; + const priceByProduct = new Map(items.map(i => [i.productId, i.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}`); + } + + const [products, knowledges, cities, townWorths] = await Promise.all([ + ProductType.findAll({ where: { id: { [Op.in]: productIds } }, attributes: ['id', 'sellCost'] }), + Knowledge.findAll({ + where: { characterId: character.id, productId: { [Op.in]: productIds } }, + attributes: ['productId', 'knowledge'] + }), + 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 + } + ] + }), + TownProductWorth.findAll({ + where: { productId: { [Op.in]: productIds } }, + attributes: ['productId', 'regionId', 'worthPercent'] + }) + ]); + + const knowledgeByProduct = new Map(knowledges.map(k => [k.productId, k.knowledge || 0])); + const worthByProductRegion = new Map(); + for (const tw of townWorths) { + const key = `${tw.productId}-${tw.regionId}`; + worthByProductRegion.set(key, tw.worthPercent); + } + const productById = new Map(products.map(p => [p.id, p])); + + const PRICE_TOLERANCE = 0.01; + const out = {}; + + for (const product of products) { + const knowledgeFactor = knowledgeByProduct.get(product.id) || 0; + const currentPrice = priceByProduct.get(product.id) ?? product.sellCost ?? 0; + let currentRegionalPrice = currentPrice; + if (currentRegionId) { + const wp = worthByProductRegion.get(`${product.id}-${currentRegionId}`) ?? 50; + currentRegionalPrice = calcRegionalSellPriceSync(product, knowledgeFactor, wp) ?? currentPrice; + } + + const results = []; + for (const city of cities) { + if (currentRegionId && city.id === currentRegionId) continue; + const worthPercent = worthByProductRegion.get(`${product.id}-${city.id}`) ?? 50; + const priceInCity = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent); + if (priceInCity == null) continue; + if (priceInCity <= currentRegionalPrice - PRICE_TOLERANCE) continue; + + let branchType = null; + if (city.branches && city.branches.length > 0) { + const branchTypes = city.branches.map(b => b.branchType?.labelTr).filter(Boolean); + if (branchTypes.includes('store') || branchTypes.includes('fullstack')) branchType = 'store'; + else if (branchTypes.includes('production')) branchType = 'production'; + } + results.push({ + regionId: city.id, + regionName: city.name, + price: priceInCity, + branchType + }); + } + results.sort((a, b) => b.price - a.price); + out[product.id] = results; + } + + return out; + } + async renovate(hashedUserId, element) { const user = await getFalukantUserOrFail(hashedUserId); const house = await UserHouse.findOne({ diff --git a/frontend/src/components/falukant/RevenueSection.vue b/frontend/src/components/falukant/RevenueSection.vue index b2ddcbc..3c91fc6 100644 --- a/frontend/src/components/falukant/RevenueSection.vue +++ b/frontend/src/components/falukant/RevenueSection.vue @@ -114,36 +114,28 @@ } }, async loadPricesForAllProducts() { - if (this.currentRegionId === null || this.currentRegionId === undefined) { + if (this.currentRegionId === null || this.currentRegionId === undefined || !this.products.length) { return; } - const requests = this.products.map(async (product) => { - if (this.loadingPrices.has(product.id)) return; - 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, - currentRegionId: this.currentRegionId - } - }); - 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); + 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] : []; } - }); - await Promise.all(requests); + } 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] || []; diff --git a/frontend/src/components/falukant/SaleSection.vue b/frontend/src/components/falukant/SaleSection.vue index 9376ea8..27aead0 100644 --- a/frontend/src/components/falukant/SaleSection.vue +++ b/frontend/src/components/falukant/SaleSection.vue @@ -293,27 +293,26 @@ } }, async loadPricesForInventory() { - const requests = this.inventory.map(async (item) => { - const itemKey = `${item.region.id}-${item.product.id}-${item.quality}`; - if (this.loadingPrices.has(itemKey)) return; - this.loadingPrices.add(itemKey); - try { - const currentPrice = item.product.sellCost || 0; - const { data } = await apiClient.get('/api/falukant/products/prices-in-cities', { - params: { - productId: item.product.id, - currentPrice: currentPrice - } - }); - item.betterPrices = data || []; - } catch (error) { - console.error(`Error loading prices for item ${itemKey}:`, error); - item.betterPrices = []; - } finally { - this.loadingPrices.delete(itemKey); + if (this.inventory.length === 0) return; + const currentRegionId = this.inventory[0]?.region?.id ?? null; + const items = this.inventory.map(item => ({ + productId: item.product.id, + currentPrice: item.product.sellCost || 0 + })); + try { + const { data } = await apiClient.post('/api/falukant/products/prices-in-cities-batch', { + currentRegionId, + items + }); + for (const item of this.inventory) { + item.betterPrices = data && data[item.product.id] ? data[item.product.id] : []; } - }); - await Promise.all(requests); + } catch (error) { + console.error('Error loading prices for inventory:', error); + for (const item of this.inventory) { + item.betterPrices = []; + } + } }, getCityPriceClass(branchType) { if (branchType === 'store') return 'city-price-green';