// Per-product aggregate of tracked stock for the products-v2 grid overlay: // { [productId]: { serialCount, chargeCount, trackedQty, nextMhd, expiredQty, expiringQty } }. // One bounded m_storageonhand sweep (ASI > 0, stock > 0) per org + chunked ASI // lookups; cached 60 s per org. `?orgId=` optional (default: session org). import getTokenHelper from "../../utils/getTokenHelper" import fetchHelper from "../../utils/fetchHelper" import withAuthRetry from "../../utils/withAuthRetry" import { fetchAsis, daysUntil } from "../../utils/attributeTracking" const enc = encodeURIComponent const TTL_MS = 60_000 const TOP = 5000 const g = globalThis as any const cache: Map = g.__logshipTrackingOverview ?? (g.__logshipTrackingOverview = new Map()) const handleFunc = async (event: any, authToken: any = null) => { const token = authToken ?? await getTokenHelper(event) const query = getQuery(event) const orgId = Number(query.orgId || getCookie(event, 'logship_organization_id')) || 0 const key = `${getCookie(event, 'logship_client_id')}:${getCookie(event, 'logship_role_id')}:${orgId}` const hit = cache.get(key) if (hit && Date.now() - hit.at < TTL_MS && !query.refresh) return hit.data const filter = [orgId ? `AD_Org_ID eq ${orgId}` : '', 'QtyOnHand gt 0', 'M_AttributeSetInstance_ID gt 0'].filter(Boolean).join(' AND ') const res: any = await fetchHelper(event, `models/m_storageonhand?$filter=${enc(filter)}&$orderby=${enc('M_Product_ID asc')}&$top=${TOP}`, 'GET', token, null) const rows: any[] = res?.records ?? [] const asis = await fetchAsis(event, token, rows.map((r) => Number(r?.M_AttributeSetInstance_ID?.id ?? r?.M_AttributeSetInstance_ID) || 0)) const products: Record = {} for (const r of rows) { const pid = Number(r?.M_Product_ID?.id ?? r?.M_Product_ID) || 0 const asi = asis.get(Number(r?.M_AttributeSetInstance_ID?.id ?? r?.M_AttributeSetInstance_ID) || 0) if (!pid || !asi) continue const qty = Number(r?.QtyOnHand ?? 0) const p = products[pid] ??= { serialCount: 0, chargeCount: 0, chargeAsis: new Set(), trackedQty: 0, nextMhd: '', expiredQty: 0, expiringQty: 0 } p.trackedQty += qty if (asi.serNo) p.serialCount += 1 else if (asi.lot || asi.mhd) p.chargeAsis.add(asi.id) if (asi.mhd) { const d = daysUntil(asi.mhd) if (d !== null && d < 0) p.expiredQty += qty else if (d !== null && d <= 30) p.expiringQty += qty if (!p.nextMhd || asi.mhd < p.nextMhd) p.nextMhd = asi.mhd } } for (const p of Object.values(products)) { p.chargeCount = p.chargeAsis.size; delete p.chargeAsis } const data = { status: 200, orgId, products, truncated: rows.length >= TOP, rowCount: rows.length } cache.set(key, { at: Date.now(), data }) return data } export default defineEventHandler(async (event) => withAuthRetry(event, handleFunc))