import { string } from 'alga-js' import refreshTokenHelper from "../../../../utils/refreshTokenHelper" import forceLogoutHelper from "../../../../utils/forceLogoutHelper" import errorHandlingHelper from "../../../../utils/errorHandlingHelper" import getReturnLocatorIds from "../../../../utils/returnLocatorHelper" import { fetchProductsWithStorage } from '../../../../utils/productStorageHelper' const idempiereFilterName = (key: string) => { const map: Record = { name: 'Name', value: 'Value', sku: 'SKU', mpn: 'mpn', upc: 'UPC', weight: 'Weight', width: 'Width', length: 'Length', height: 'Height', volume: 'Volume', isBOM: 'IsBOM', organization: 'AD_Org_ID', qtyonhand: 'QtyOnHand', taricCode: 'taricCode' } return map[key] || '' } const filterHandler = (filterModel: any) => { let newFilter = '' for (const key of Object.keys(filterModel)) { const filterName = idempiereFilterName(key) if (!filterName) continue if (newFilter !== '') { newFilter += ' AND ' } const item = filterModel[key] if (item.filterType == 'text') { newFilter += textFilterHandler(filterName, item) } else if (item.filterType == 'number') { newFilter += numberFilterHandler(filterName, item) } } if (newFilter !== '') { newFilter = ` AND (${newFilter})` } return newFilter } const numberFilterHandler = (key: string, item: any) => { let newNumberFilter = '' if (item.type == 'equals') { newNumberFilter = `${key} eq ${item.filter}` } else if (item.type == 'notEqual') { newNumberFilter = `${key} neq ${item.filter}` } else if (item.type == 'greaterThan') { newNumberFilter = `${key} gt ${item.filter}` } else if (item.type == 'greaterThanOrEqual') { newNumberFilter = `${key} ge ${item.filter}` } else if (item.type == 'lessThan') { newNumberFilter = `${key} lt ${item.filter}` } else if (item.type == 'lessThanOrEqual') { newNumberFilter = `${key} le ${item.filter}` } else if (item.type == 'inRange') { newNumberFilter = `${key} ge ${item.filter} AND ${key} le ${item.filterTo}` } return newNumberFilter } const textFilterHandler = (key: string, item: any) => { let newTextFilter = '' if (item.type == 'equals') { newTextFilter = `${key} eq ${item.filter}` } else if (item.type == 'notEqual') { newTextFilter = `${key} neq ${item.filter}` } else if (item.type == 'contains') { newTextFilter = `contains(${key},${item.filter.replaceAll('%', '')})` } else if (item.type == 'notContains') { newTextFilter = `not contains(${key},${item.filter.replaceAll('%', '')})` } else if (item.type == 'startsWith') { newTextFilter = `startswith(${key},'${item.filter}')` } else if (item.type == 'endsWith') { newTextFilter = `endswith(${key},'${item.filter}')` } return newTextFilter } const sortHandler = (sortModel: any[]) => { if (!sortModel || sortModel.length === 0) { return 'M_Product_ID desc' } const parts = sortModel.map((s: any) => { const col = idempiereFilterName(s.colId) if (!col) return '' return `${col} ${s.sort}` }).filter(Boolean) return parts.length > 0 ? parts.join(',') : 'M_Product_ID desc' } const storageCount = (storages: any[], key: string) => { return storages.map(i => i[key]).reduce((acc: number, prev: any) => Number(acc) + Number(prev), 0) } const splitStorageByLocator = (storages: any[], returnLocatorIds: Set) => { let onHand = 0 let returnOnHand = 0 for (const s of storages) { const qty = Number(s?.QtyOnHand ?? 0) const locId = s?.M_Locator_ID?.id ?? s?.M_Locator_ID if (locId !== undefined && locId !== null && returnLocatorIds.has(Number(locId))) { returnOnHand += qty } else { onHand += qty } } return { onHand, returnOnHand } } const handleFunc = async (event: any, authToken: any = null) => { let data: any = { rows: [], lastRow: 0 } const config = useRuntimeConfig() const token = authToken ?? await getTokenHelper(event) const body = await readBody(event) const startRow = body.startRow const endRow = body.endRow const pageSize = (endRow - startRow) + 1 const orderBy = sortHandler(body.sortModel) // NB: isJtlBom/taricCode are custom columns with lowercase-first ColumnName — PascalCase // ('IsJtlBom'/'TaricCode') is rejected by iDempiere's $select and would break the whole grid. const select = string.urlEncode('M_Product_ID,M_Product_UU,Name,Value,SKU,mpn,UPC,Weight,Width,Length,Height,Volume,IsBOM,isJtlBom,AD_Org_ID,Strapi_Product_documentId,CompletedAllMarketplaceData,CustomFulfillmentPrice,isCustomFulfillmentPrice,CUST_FulfillmentProductPricingRules_ID,labelprinter,taricCode,C_Country_ID') // Build org filter from toolbar switch. // Include org 0 ('*') so shared/client-level product masters aren't filtered out // when a concrete org is selected (mirrors materials/products/search.get.ts). let orgFilter = '' if (body.orgId) { orgFilter = ` AND (AD_Org_ID eq ${body.orgId} OR AD_Org_ID eq 0)` } // By default hide bill-of-materials articles (standard BOM + JTL-BOM). The // products-list toolbar can opt back in via `includeBom`. NB: iDempiere's OData // `ne`/`not(...)` are unsupported here, so we use `eq false` (which also drops // the handful of rows whose flag is null — acceptable, the toggle reveals them). let bomFilter = '' if (!body.includeBom) { bomFilter = ' AND IsBOM eq false AND isJtlBom eq false' } const [res, returnLocatorIds]: any = await Promise.all([ // C_Country_ID is expanded because the flag needs the country's CountryCode — // the plain FK object only carries id + identifier. fetchProductsWithStorage(event, token, `models/m_product?$select=${select}&$expand=C_Country_ID&$filter=${string.urlEncode('(isActive eq true OR isActive eq false)' + orgFilter + bomFilter + filterHandler(body.filterModel))}&$orderby=${string.urlEncode(orderBy)}&$top=${pageSize}&$skip=${startRow}`), getReturnLocatorIds(event, token, body?.orgId || null) ]) if (res) { const allRecords = res['records']?.map((item: any) => { const { onHand, returnOnHand } = splitStorageByLocator(item?.M_Storage ?? [], returnLocatorIds) return { id: item.id, uid: item.uid, name: item.Name, value: item.Value, sku: item.SKU, mpn: item.mpn, upc: item.UPC, weight: item.Weight, width: item.width || item.Width || 0, length: item.length || item.Length || 0, height: item.height || item.Height || 0, volume: item.volume || item.Volume || 0, isBOM: item.IsBOM, isJtlBom: item.IsJtlBom ?? item.isJtlBom ?? false, organization: item.AD_Org_ID?.identifier || '', organizationId: item.AD_Org_ID?.id || '', strapiProductId: item.Strapi_Product_documentId, completedAllMarketplaceData: item.CompletedAllMarketplaceData, qtyonhand: onHand, returnQtyOnHand: returnOnHand, customFulfillmentPrice: item.CustomFulfillmentPrice || 0, isCustomFulfillmentPrice: item.isCustomFulfillmentPrice ?? false, fulfillmentProductPricingRules: item.CUST_FulfillmentProductPricingRules_ID?.identifier || '', fulfillmentProductPricingRulesId: item.CUST_FulfillmentProductPricingRules_ID?.id || '', labelPrinter: item.labelprinter?.identifier || '', labelPrinterId: item.labelprinter?.id || '', taricCode: item.taricCode ?? '', // expanded FK: full c_country record (identifier is null when expanded) originCountry: item.C_Country_ID?.Name ?? item.C_Country_ID?.identifier ?? '', originCountryCode: item.C_Country_ID?.CountryCode || '', originCountryId: item.C_Country_ID?.id || '' } }) || [] data.rows = allRecords data.lastRow = res['row-count'] || 0 } return data } export default defineEventHandler(async (event) => { let data: any = {} try { console.log('[Products AG Grid] handleFunc attempt 1') data = await handleFunc(event) console.log('[Products AG Grid] Success - rows:', data?.rows?.length, 'lastRow:', data?.lastRow) } catch (err: any) { console.log('[Products AG Grid] First attempt failed:', err?.statusCode || err?.message || err) try { let authToken: any = await refreshTokenHelper(event) console.log('[Products AG Grid] Got refresh token, retrying handleFunc') data = await handleFunc(event, authToken) console.log('[Products AG Grid] Retry success - rows:', data?.rows?.length, 'lastRow:', data?.lastRow) } catch (error: any) { console.log('[Products AG Grid] Retry also failed:', error?.statusCode || error?.message || error) data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) } } return data })