/** * POST /api/materials/warehouse-layouts/meta/stock-by-locators — what is stored in these locators (for the * 3D rendering's article thumbnails, loaded only for racks near the camera). * Body { locatorIds: number[], warehouseId?, allWarehouses? } (≤ 200). With `allWarehouses` the stock of every * locator with the same Value in any warehouse is merged onto the requested id (one physical hall mirrored per org). One M_StorageOnHand query per 60 ids (OR chain, QtyOnHand > 0), * product thumbnails via the cached /api/mobile/product-thumb/:id resolver (service token, 10 min cache). * Response { byLocator: { [locatorId]: [{ productId, value, name, qty, thumb }] } }, cached 60 s per id set. */ import { string } from 'alga-js' import { withIdempiere } from '../../../../utils/withIdempiere' import fetchHelper from '../../../../utils/fetchHelper' import pLimit from '../../../../utils/pLimit' import { loadLocatorsByValues, loadWarehouseLocators } from '../../../../utils/warehouseLayout' const cache = new Map() const TTL = 60 * 1000 export default withIdempiere(async (event, token) => { const body = await readBody(event) const ids = [...new Set((Array.isArray(body?.locatorIds) ? body.locatorIds : []).map((n: any) => Number(n)).filter((n: number) => n > 0))].sort((a, b) => a - b).slice(0, 200) if (!ids.length) return { status: 200, byLocator: {} } const allWarehouses = body?.allWarehouses === undefined || body?.allWarehouses === null ? !!(useRuntimeConfig() as any).public?.warehouseLayoutShared : !!body.allWarehouses const key = (allWarehouses ? 'all:' : '') + ids.join(',') const hit = cache.get(key) if (hit && Date.now() - hit.at < TTL) return { status: 200, byLocator: hit.data, cached: true } // mirror map: any-warehouse locator id → requested (master) id, by Value const alias = new Map() let queryIds = ids if (allWarehouses && body?.warehouseId) { const master = await loadWarehouseLocators(event, token, Number(body.warehouseId)) const byId = new Map(master.records.map(r => [r.id, r])) const valueToMaster = new Map() const originals: string[] = [] for (const id of ids) { const v = byId.get(id)?.value; if (v) { valueToMaster.set(String(v).trim().toLowerCase(), id); originals.push(String(v).trim()) } } // iDempiere compares Value case-sensitively → query with the original spelling const mirrors = await loadLocatorsByValues(event, token, originals) for (const m of mirrors) { const mid = valueToMaster.get(String(m.value).trim().toLowerCase()); if (mid) alias.set(m.id, mid) } queryIds = [...new Set([...ids, ...alias.keys()])] } const byLocator: Record = {} const productIds = new Set() for (let i = 0; i < queryIds.length; i += 60) { const chunk = queryIds.slice(i, i + 60) const filter = string.urlEncode('(' + chunk.map(id => `M_Locator_ID eq ${id}`).join(' OR ') + ') AND QtyOnHand gt 0') const res: any = await fetchHelper(event, `models/m_storageonhand?$filter=${filter}&$expand=${string.urlEncode('M_Product_ID($select=Value,Name,ImageURL)')}&$top=5000`, 'GET', token, null) for (const r of res?.records || []) { const rawLid = Number(r.M_Locator_ID?.id ?? r.M_Locator_ID) const lid = alias.get(rawLid) ?? rawLid const p = r.M_Product_ID || {} const qty = Number(r.QtyOnHand) || 0 if (!lid || !p.id || qty <= 0) continue const list = byLocator[lid] || (byLocator[lid] = []) const ex = list.find(x => x.productId === Number(p.id)) if (ex) ex.qty += qty else { list.push({ productId: Number(p.id), value: String(p.Value || ''), name: String(p.Name || ''), qty, imageUrl: p.ImageURL || null, thumb: null }); productIds.add(Number(p.id)) } } } // thumbnails (cached resolver, service token) — bounded concurrency const limit = pLimit(5) const thumbs = new Map() await Promise.all([...productIds].map(pid => limit(async () => { try { const r: any = await $fetch(`/api/mobile/product-thumb/${pid}`); thumbs.set(pid, r?.url || null) } catch { thumbs.set(pid, null) } }))) for (const list of Object.values(byLocator)) for (const a of list) { a.thumb = thumbs.get(a.productId) || a.imageUrl || null; list.sort((x, y) => y.qty - x.qty) } if (cache.size > 500) cache.delete(cache.keys().next().value as string) cache.set(key, { at: Date.now(), data: byLocator }) return { status: 200, byLocator } })