import { string } from 'alga-js' import fetchHelper from './fetchHelper' import { relabelServiceToken, isRelabelOrder } from './relabelOrderHelper' /** * Order roadmap — shared data access for `orders/[id]/roadmap.get.ts` and * `orders/[id]/roadmap/push-step.post.ts`. * * The roadmap is the staff checklist on the order edit page for orders that * need extra work before they can ship: Amazon-FBA relabel orders * (C_Order.isRelabelByFulfillmentProvider → codes in C_OrderLine.printLabelCode) * and orders with BOM positions (M_Product.IsBOM / isJtlBom → m_production * linked via C_OrderLine_ID). Each step can be pushed to a mobile worker as a * ticket (R_Request linked via C_Order_ID) — see pushTask.ts. */ export const menuTypeFromEvent = (event: any): string => { const roleCookie = getCookie(event, 'logship_role') try { const role = roleCookie ? JSON.parse(roleCookie) : null return role?.FrontendMenu?.id || role?.FrontendMenu || 'Menu' } catch { return 'Menu' } } export const isBomProduct = (prod: any) => { const v = prod?.IsBOM ?? prod?.isBOM const j = prod?.isJtlBom ?? prod?.IsJtlBom return v === true || v === 'Y' || j === true || j === 'Y' } export interface RoadmapOrder { id: number documentNo: string docStatusId: string organizationId: number | null organization: string partnerId: number | null isRelabel: boolean } export const fetchRoadmapOrder = async (event: any, token: any, orderId: number | string): Promise => { const id = Number(orderId) if (!Number.isFinite(id) || id <= 0) return null // Service token: the relabel column is hidden from limited roles (column // security) — same reasoning as relabelOrderHelper.fetchRelabelOrder. const o: any = await fetchHelper(event, `models/c_order/${id}`, 'GET', relabelServiceToken(token), null) if (!o?.id) return null return { id: Number(o.id), documentNo: o.DocumentNo || '', docStatusId: o.DocStatus?.id || o.DocStatus || '', organizationId: o.AD_Org_ID?.id ?? null, organization: o.AD_Org_ID?.identifier || '', partnerId: o.C_BPartner_ID?.id ?? null, isRelabel: isRelabelOrder(o) } } export interface RoadmapLine { id: number line: number productId: number | null sku: string product: string qtyOrdered: number qtyDelivered: number isBom: boolean printLabelCode: string isActive: boolean } /** * Product lines of the order with the BOM flag of their product and the FBA * label code. One bounded list with a to-one $expand (allowed — only child * tables must not be expanded over lists). `isJtlBom` is a custom column; if * the instance doesn't have it the $select is retried without it. */ export const fetchRoadmapLines = async (event: any, token: any, orderId: number | string): Promise => { const id = Number(orderId) if (!Number.isFinite(id) || id <= 0) return [] const readToken = relabelServiceToken(token) const filter = string.urlEncode(`C_Order_ID eq ${id}`) const load = async (expandSelect: string) => { const expand = string.urlEncode(`m_product_id($select=${expandSelect})`) const res: any = await fetchHelper(event, `models/c_orderline?$filter=${filter}&$expand=${expand}&$orderby=Line&$top=500`, 'GET', readToken, null) return res?.records || [] } let records: any[] = [] try { records = await load('Name,SKU,Value,IsBOM,isJtlBom') } catch { records = await load('Name,SKU,Value,IsBOM') } return records .filter((l: any) => l?.M_Product_ID?.id) .map((l: any) => ({ id: Number(l.id), line: Number(l.Line) || 0, productId: l.M_Product_ID?.id ?? null, sku: l.M_Product_ID?.SKU || l.M_Product_ID?.Value || '', product: String(l.M_Product_ID?.Name || l.M_Product_ID?.identifier || '').replace(/^-1_/, ''), qtyOrdered: Number(l.QtyEntered ?? l.QtyOrdered ?? 0) || 0, qtyDelivered: Number(l.QtyDelivered ?? 0) || 0, isBom: isBomProduct(l.M_Product_ID), printLabelCode: String(l?.printLabelCode ?? l?.PrintLabelCode ?? '').trim(), isActive: l?.IsActive !== false })) } export interface RoadmapTicket { id: number documentNo: string summary: string created: string statusId: number | null status: string isClosed: boolean isComplete: boolean salesRepId: number | null salesRep: string requestTypeId: number | null requestType: string productionId: number | null productId: number | null isPushedTask: boolean pushedTaskAt: string | null taskStatus: string } /** * Tickets linked to the order (R_Request.C_Order_ID), newest first, with the * push-task state. No $select on purpose: the custom columns * (IsPushedTask/PushedTaskAt) may not exist on every instance and REST simply * omits columns that are NULL/absent. Open/closed joins r_status.IsClosed * (same as orders/[id]/requests.get.ts). */ export const fetchRoadmapTickets = async (event: any, token: any, orderId: number | string): Promise => { const id = Number(orderId) if (!Number.isFinite(id) || id <= 0) return [] const res: any = await fetchHelper(event, `models/r_request?$filter=${string.urlEncode('C_Order_ID eq ' + id)}&$orderby=${string.urlEncode('Created desc')}&$top=200`, 'GET', token, null) const statusClosed: Record = {} try { const stRes: any = await fetchHelper(event, 'models/r_status?$top=200', 'GET', token, null) for (const st of (stRes?.records || [])) statusClosed[st.id] = st.IsClosed === true } catch {} // non-fatal — isClosed then relies on Processed / IsComplete alone return (res?.records || []).map((r: any): RoadmapTicket => { const statusId = r.R_Status_ID?.id ?? null const isComplete = r.IsComplete === true || r.isComplete === true || r.IsComplete === 'Y' return { id: Number(r.id), documentNo: r.DocumentNo || '', summary: r.Summary || '', created: r.Created || '', statusId, status: r.R_Status_ID?.identifier || '', isClosed: (statusId != null && statusClosed[statusId] === true) || r.Processed === true || isComplete, isComplete, salesRepId: r.SalesRep_ID?.id ?? null, salesRep: r.SalesRep_ID?.identifier || '', requestTypeId: r.R_RequestType_ID?.id ?? null, requestType: r.R_RequestType_ID?.identifier || '', productionId: r.M_Production_ID?.id ?? null, productId: r.M_Product_ID?.id ?? null, isPushedTask: !!(r.IsPushedTask ?? r.isPushedTask), pushedTaskAt: r.PushedTaskAt ?? r.pushedTaskAt ?? null, taskStatus: String(r.TaskStatus?.id ?? r.TaskStatus ?? '') } }) } // A ticket the roadmap may still reuse for a push: not closed / completed. export const isOpenTicket = (t: RoadmapTicket | null | undefined) => !!t && !t.isClosed && !t.isComplete