import { string } from 'alga-js' import refreshTokenHelper from "../../../utils/refreshTokenHelper" import errorHandlingHelper from "../../../utils/errorHandlingHelper" const idempiereFilterName = (key: string) => { const map: Record = { documentNo: 'DocumentNo', externalorderId: 'ExternalOrderId', partnerLocationName: 'ship_name', partnerLocationName2: 'ship_name2', locationZipCode: 'ship_postal', locationStreet: 'ship_address1', locationCity: 'ship_city', shipperName: 'shipper_name', grandTotal: 'GrandTotal', totalQty: 'TotalQty', fulfillmentFeeTotal: 'fulfillment_fee_total', trackingNo: 'TrackingNo', created: 'Created', id: 'id', } return map[key] || '' } const sortFieldName = (key: string) => { const map: Record = { documentNo: 'DocumentNo', organization: 'AD_Org_ID', externalorderId: 'ExternalOrderId', partnerLocationName: 'ship_name', partnerLocationName2: 'ship_name2', locationZipCode: 'ship_postal', locationStreet: 'ship_address1', locationCity: 'ship_city', shipperName: 'shipper_name', grandTotal: 'GrandTotal', fulfillmentFeeTotal: 'fulfillment_fee_total', trackingNo: 'TrackingNo', created: 'Created', id: 'id', totalQty: 'TotalQty', shippingDate: 'shipping_date', shipmentStatus: 'DocStatus', } return map[key] || '' } const filterHandler = (filterModel: any) => { if (!filterModel || Object.keys(filterModel).length === 0) return '' let parts: string[] = [] for (const key of Object.keys(filterModel)) { const mapped = idempiereFilterName(key) if (!mapped) continue const item = filterModel[key] if (item.filterType === 'text') { const text = textFilterHandler(mapped, item) if (text) parts.push(text) } else if (item.filterType === 'number') { const num = numberFilterHandler(mapped, item) if (num) parts.push(num) } else if (item.filterType === 'date') { parts.push(`${mapped} eq ${item.dateFrom}`) } } if (parts.length === 0) return '' return ' AND (' + parts.join(' AND ') + ')' } const numberFilterHandler = (key: string, item: any) => { if (item.type === 'equals') return `${key} eq ${item.filter}` if (item.type === 'notEqual') return `${key} neq ${item.filter}` if (item.type === 'greaterThan') return `${key} gt ${item.filter}` if (item.type === 'greaterThanOrEqual') return `${key} ge ${item.filter}` if (item.type === 'lessThan') return `${key} lt ${item.filter}` if (item.type === 'lessThanOrEqual') return `${key} le ${item.filter}` if (item.type === 'inRange') return `${key} ge ${item.filter} AND ${key} le ${item.filterTo}` return '' } const textFilterHandler = (key: string, item: any) => { const val = item.filter?.toString() || '' const isFkField = key.endsWith('_ID') if (isFkField) { // FK fields: contains() matches against identifier text, upper() not supported const cleanVal = val.replaceAll('%', '') if (item.type === 'equals') return `${key} eq '${cleanVal}'` if (item.type === 'notEqual') return `${key} neq '${cleanVal}'` if (item.type === 'contains') return `contains(${key},'${cleanVal}')` if (item.type === 'notContains') return `not contains(${key},'${cleanVal}')` if (item.type === 'startsWith') return `startswith(${key},'${cleanVal}')` if (item.type === 'endsWith') return `endswith(${key},'${cleanVal}')` return '' } // Case-insensitive via tolower() (OData standard) const lowerKey = `tolower(${key})` const lowerVal = val.toLowerCase().replaceAll('%', '') if (item.type === 'equals') return `${lowerKey} eq '${lowerVal}'` if (item.type === 'notEqual') return `${lowerKey} neq '${lowerVal}'` if (item.type === 'contains') return `contains(${lowerKey},'${lowerVal}')` if (item.type === 'notContains') return `not contains(${lowerKey},'${lowerVal}')` if (item.type === 'startsWith') return `startswith(${lowerKey},'${lowerVal}')` if (item.type === 'endsWith') return `endswith(${lowerKey},'${lowerVal}')` return '' } const buildSortClause = (sortModel: any[]) => { if (!sortModel || sortModel.length === 0) return 'Created desc' const parts = sortModel .map(s => { const col = sortFieldName(s.colId) if (!col) return '' return `${col} ${s.sort}` }) .filter(Boolean) return parts.length > 0 ? parts.join(',') : 'Created desc' } const calculateAge = (fromDate: string, toDate: string) => { if (!fromDate || !toDate) return '' const diffSeconds = Math.abs((new Date(toDate).getTime() - new Date(fromDate).getTime()) / 1000) if (diffSeconds < 3600) return Math.round(diffSeconds / 60) + ' minutes' if (diffSeconds < 86400) return Math.round(diffSeconds / 3600) + ' hours' return Math.round(diffSeconds / 86400) + ' days' } const mapRow = (item: any) => { // Build minout array from shipments_summary const minout: any[] = [] if (item.shipments_summary) { item.shipments_summary.split(' | ').forEach((s: string) => { const [inoutId, docNo] = s.split(':') if (inoutId) { minout.push({ id: parseInt(inoutId), DocumentNo: docNo, TrackingNo: item.TrackingNo || '', IsCommissioned: item.IsCommissioned, IsCommissionedConfirmed: item.IsCommissionedConfirmed, DocStatus: item.shipment_docstatus?.id || '', shipping_date: item.shipping_date, }) } }) } // Build orderSource object const orderSource = { ...(item.C_OrderSource_ID || {}), Name: item.order_source_name || item.C_OrderSource_ID?.identifier || '', Description: item.order_source_description || '', Marketplace: { identifier: item.marketplace_name || '' }, } return { marketplaceKey: item.marketplace_name || '', id: item.id, isActive: item.IsActive, documentNo: item.DocumentNo, description: item.Description || '', dateOrdered: item.DateOrdered, created: item.Created, grandTotal: item.GrandTotal, trackingNo: item.TrackingNo || '', isSOTrx: item.IsSOTrx, docStatus: item.DocStatus?.identifier || '', docStatusId: item.DocStatus?.id || '', docType: item.C_DocType_ID?.identifier || '', docTypeId: item.C_DocType_ID?.id || '', docTypeTarget: item.C_DocTypeTarget_ID?.identifier || '', docTypeTargetId: item.C_DocTypeTarget_ID?.id || '', currency: item.C_Currency_ID?.identifier || '', currencyId: item.C_Currency_ID?.id || '', priorityRule: item.PriorityRule?.identifier || '', priorityRuleId: item.PriorityRule?.id || '', warehouse: item.M_Warehouse_ID?.identifier || '', warehouseId: item.M_Warehouse_ID?.id || '', partner: item.C_BPartner_ID?.identifier || '', partnerId: item.C_BPartner_ID?.id || '', partnerLocation: item.C_BPartner_Location_ID || {}, partnerLocationId: item.C_BPartner_Location_ID?.id || '', partnerLocationName: item.ship_name || '', partnerLocationName2: item.ship_name2 || '', billPartner: item.Bill_BPartner_ID?.identifier || '', billPartnerId: item.Bill_BPartner_ID?.id || '', billPartnerLocation: item.Bill_Location_ID || {}, billPartnerLocationId: item.Bill_Location_ID?.id || '', organization: item.AD_Org_ID?.identifier || '', organizationId: item.AD_Org_ID?.id || '', client: item.AD_Client_ID?.identifier || '', clientId: item.AD_Client_ID?.id || '', orderSource: orderSource, orderSourceId: item.C_OrderSource_ID?.id || '', orderSourceName: item.order_source_name || item.C_OrderSource_ID?.identifier || '', externalorderId: item.ExternalOrderId || '', shopware6_order_id: item.shopware6_order_id || '', shopify_order_id: item.shopify_order_id || '', amazon_order_id: item.amazon_order_id || '', plentyone_order_id: item.plentyone_order_id || '', jtl_order_id: item.jtl_order_id || '', country: item.ship_country || '', countryCode: item.ship_countrycode?.id || '', countryId: '', locationStreet: item.ship_address1 || '', locationStreet2: item.ship_address2 || '', locationZipCode: item.ship_postal || '', locationCity: item.ship_city || '', name2: item.ship_name2 || '', location: { id: item.ship_location_id?.id || item.ship_location_id || null }, minout: minout, shipperId: item.shipper_id || '', shipper: item.shipper_name || '', shipperName: item.shipper_name || '', shipperCfgIdentifier: item.shipper_cfg || '', isAttachmentRequired: item.is_attachment_required === true, isPrintDINA4: item.is_print_dina4 === true, paketTypeName: item.paket_type_name || '', shipmentWeight: item.shipment_weight || null, shippingDate: item.shipping_date || '', totalQty: item.TotalQty || 0, orderLines: [{ QtyEntered: item.TotalQty || 0 }], fulfillmentFeeTotal: item.fulfillment_fee_total || 0, feeProcessed: item.fee_processed === true || item.fee_processed === 'Y', feeEditable: (item.fulfillment_fee_total || 0) > 0 && !(item.fee_processed === true || item.fee_processed === 'Y'), daysSinceCreated: item.Created ? Math.floor((Date.now() - new Date(item.Created).getTime()) / 86400000) : 0, createdToOrderedAge: calculateAge(item.Created, item.DateOrdered), hasShipment: item.has_shipment === 'Y', hasInvoice: item.has_invoice === 'Y', hasDescription: item.has_description === 'Y', billingDiffersFromShipping: item.billing_differs_from_shipping === 'Y', } } const handleFunc = async (event: any, authToken: any = null) => { let data: any = { rows: [], lastRow: 0 } const token = authToken ?? await getTokenHelper(event) const body = await readBody(event) const startRow = body.startRow || 0 const endRow = body.endRow || 100 const pageSize = endRow - startRow // Base filter let baseFilter = 'IsSOTrx eq true' // Custom switch filters if (body.orgId) { baseFilter += ` AND AD_Org_ID eq ${body.orgId}` } if (body.openOnly === true) { baseFilter += ` AND has_shipment eq 'N' AND (DocStatus eq 'CO' or DocStatus eq 'DR' or DocStatus eq 'IP')` } if (body.openShipments === true) { baseFilter += ` AND has_shipment eq 'Y' AND IsCommissioned eq false AND (DocStatus eq 'CO' or DocStatus eq 'DR' or DocStatus eq 'IP')` } if (body.shipped === true) { baseFilter += ` AND has_shipment eq 'Y' AND TrackingNo gt '' AND (DocStatus eq 'CO' or DocStatus eq 'IP')` } if (body.noFees === true) { baseFilter += ` AND fulfillment_fee_total eq 0` } // Shipper filter if (body.shipperId) { baseFilter += ` AND shipper_id eq ${body.shipperId}` } // Marketplace filter (TODO: fix filtering) // if (body.marketplaceName) { // baseFilter += ` AND marketplace_name eq '${body.marketplaceName}'` // } // Date filter — use shipping_date if available, fallback to Created if (body.dateFrom || body.dateTo) { let shippingCond = 'shipping_date neq null' let createdCond = 'shipping_date eq null' if (body.dateFrom) { shippingCond += ` AND shipping_date ge ${body.dateFrom}` createdCond += ` AND Created ge ${body.dateFrom}` } if (body.dateTo) { const nextDay = new Date(body.dateTo + 'T00:00:00') nextDay.setDate(nextDay.getDate() + 1) const nextDayStr = `${nextDay.getFullYear()}-${String(nextDay.getMonth() + 1).padStart(2, '0')}-${String(nextDay.getDate()).padStart(2, '0')}` shippingCond += ` AND shipping_date lt ${nextDayStr}` createdCond += ` AND Created lt ${nextDayStr}` } baseFilter += ` AND ((${shippingCond}) OR (${createdCond}))` } // AG Grid column filters const agFilter = filterHandler(body.filterModel || {}) const fullFilter = baseFilter + agFilter const sortClause = buildSortClause(body.sortModel) console.log('[summary] fullFilter:', fullFilter) const [res, sumRes]: any[] = await Promise.all([ event.context.fetch( `models/v_orders_summary?$filter=${string.urlEncode(fullFilter)}&$orderby=${string.urlEncode(sortClause)}&$top=${pageSize}&$skip=${startRow}`, 'GET', token, null ), event.context.fetch( `models/v_orders_summary?$filter=${string.urlEncode(fullFilter)}&$select=fulfillment_fee_total,GrandTotal&$top=999999`, 'GET', token, null ), ]) if (res) { data.rows = (res.records || []).map((item: any) => mapRow(item)) data.lastRow = res['row-count'] || 0 } if (sumRes?.records) { data.totalFees = sumRes.records.reduce((sum: number, r: any) => sum + (r.fulfillment_fee_total || 0), 0) data.totalGrandTotal = sumRes.records.reduce((sum: number, r: any) => sum + (r.GrandTotal || 0), 0) } return data } 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 })