import { string } from 'alga-js' import refreshTokenHelper from '../../utils/refreshTokenHelper' import errorHandlingHelper from '../../utils/errorHandlingHelper' import fetchHelper from '../../utils/fetchHelper' // Module-level cache: a location's country effectively never changes, so resolving it // once and reusing it keeps month/year/org navigation fast across requests. const countryByLocation: Record = {} const handleFunc = async (event: any, authToken: any = null) => { const token = authToken ?? await getTokenHelper(event) const query = getQuery(event) const role = getCookie(event, 'logship_role') const newUserRole = JSON.parse(role ?? '{}') const organizationId = getCookie(event, 'logship_organization_id') const orgIdParam = query?.org_id as string // Role gate: non client-administrators are locked to their current org. const canFilterOrg = !!newUserRole?.IsClientAdministrator let effectiveOrgId = '' let orgFilter = '' if (!canFilterOrg) { effectiveOrgId = String(orgIdParam || organizationId || '') if (effectiveOrgId) orgFilter = ` AND AD_Org_ID eq ${effectiveOrgId}` } else if (orgIdParam) { effectiveOrgId = String(orgIdParam) orgFilter = ` AND AD_Org_ID eq ${effectiveOrgId}` } // Selected year + month (default: current month) — scoping to a single month keeps // the fetched volume small (~1/12 of a year, less for the running month). const now = new Date() const selectedYear = query?.year ? Number(query.year) : now.getFullYear() let selectedMonth = query?.month ? Number(query.month) : now.getMonth() + 1 // 1-12 if (selectedMonth < 1 || selectedMonth > 12) selectedMonth = now.getMonth() + 1 const daysInMonth = new Date(selectedYear, selectedMonth, 0).getDate() const mm = String(selectedMonth).padStart(2, '0') const monthStart = `${selectedYear}-${mm}-01 00:00:00` const monthEnd = `${selectedYear}-${mm}-${String(daysInMonth).padStart(2, '0')} 23:59:59` // Only completed, commissioned customer shipments (C-) of fulfillment orders. // isFulfillmentOrder lives on the linked order, so it is filtered in JS after a // master $select-expand (iDempiere only allows $select inside a master expand — // a nested $expand is rejected with "Expanding a master only support the $select…"). const filter = string.urlEncode( `IsCommissioned eq true AND DocStatus eq 'CO' AND IsSOTrx eq true AND MovementType eq 'C-'` + ` AND shipping_date ge '${monthStart}' AND shipping_date le '${monthEnd}'${orgFilter}` ) // Master $select-expands only: the order's fulfillment flag, and the ship-to // bpartner-location's c_location id (the country is resolved in a second pass). const select = 'M_InOut_ID,AD_Org_ID,shipping_date,MovementDate,C_Order_ID,C_BPartner_Location_ID' const expand = 'C_Order_ID($select=IsFulfillmentOrder),C_BPartner_Location_ID($select=C_Location_ID)' // ---- Pass 1: paginate the qualifying shipments for the month ---- const pageSize = 1000 let skip = 0 const kept: { day: number; org: string; locationId: number | null }[] = [] const neededLocations = new Set() while (true) { const res: any = await fetchHelper( event, `models/m_inout?$filter=${filter}&$select=${select}&$expand=${expand}` + `&$orderby=${string.urlEncode('m_inout_id desc')}&$top=${pageSize}&$skip=${skip}`, 'GET', token, null ) const records = res?.records || [] records.forEach((rec: any) => { // Fulfillment order only (tolerant casing per iDempiere read rule). const ff = rec?.C_Order_ID?.isFulfillmentOrder ?? rec?.C_Order_ID?.IsFulfillmentOrder if (ff !== true) return // Day-of-month from the shipping date (string-parsed to avoid timezone drift). const ds = String(rec?.shipping_date || rec?.MovementDate || '') const m = ds.match(/^(\d{4})-(\d{2})-(\d{2})/) if (!m) return const day = Number(m[3]) if (day < 1 || day > 31) return const org = rec?.AD_Org_ID?.identifier || `Org-${rec?.AD_Org_ID?.id ?? '?'}` const locationId = rec?.C_BPartner_Location_ID?.C_Location_ID?.id ?? null if (locationId != null && countryByLocation[locationId] === undefined) { neededLocations.add(locationId) } kept.push({ day, org, locationId }) }) if (records.length < pageSize) break skip += pageSize if (skip > 100000) break // safety cap } // ---- Pass 2: resolve uncached location ids -> country (c_location default // C_Country_ID FK already carries the country name as its identifier) ---- const ids = Array.from(neededLocations) const batchSize = 200 for (let i = 0; i < ids.length; i += batchSize) { const batch = ids.slice(i, i + batchSize) const locFilter = string.urlEncode(`C_Location_ID in (${batch.join(',')})`) const res: any = await fetchHelper( event, `models/c_location?$filter=${locFilter}&$select=C_Location_ID,C_Country_ID&$top=${batchSize}`, 'GET', token, null ) ;(res?.records || []).forEach((loc: any) => { const lid = loc?.id ?? loc?.C_Location_ID if (lid == null) return countryByLocation[lid] = loc?.C_Country_ID?.identifier || 'Unknown' }) // Any id the batch didn't return (shouldn't happen) -> mark Unknown so we don't refetch. batch.forEach((lid) => { if (countryByLocation[lid] === undefined) countryByLocation[lid] = 'Unknown' }) } // ---- Aggregate to (day, org, country) cells — the client derives every view // (per-country chart, per-org drill-down, the breakdown list) from these. ---- const countrySet = new Set() const orgSet = new Set() // tree[day][org][country] = qty (nested to avoid delimiter collisions in names) const tree: Record>> = {} const countryTotals: Record = {} let monthTotal = 0 kept.forEach(({ day, org, locationId }) => { const country = (locationId != null && countryByLocation[locationId]) || 'Unknown' countrySet.add(country) orgSet.add(org) if (!tree[day]) tree[day] = {} if (!tree[day][org]) tree[day][org] = {} tree[day][org][country] = (tree[day][org][country] || 0) + 1 countryTotals[country] = (countryTotals[country] || 0) + 1 monthTotal += 1 }) const cells: { day: number; org: string; country: string; qty: number }[] = [] for (const dayStr of Object.keys(tree)) { const day = Number(dayStr) for (const org of Object.keys(tree[day])) { for (const country of Object.keys(tree[day][org])) { cells.push({ day, org, country, qty: tree[day][org][country] }) } } } const countries = Array.from(countrySet).sort((a, b) => a.localeCompare(b)) const orgs = Array.from(orgSet).sort((a, b) => a.localeCompare(b)) return { year: selectedYear, month: selectedMonth, daysInMonth, canFilterOrg, selectedOrgId: effectiveOrgId, countries, orgs, cells, countryTotals, monthTotal, } } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch (err: any) { try { const authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch (error: any) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) } } return data })