/** * Order, delivery and POS reads — the numbers behind store performance. * * Note the split: online orders come from `/web/orders/*` and counter sales * from `/pos/*`, which is the ONLY part of the API carrying auth middleware. * Any figure that blends the two is eventually consistent by construction, so * screens that show one must also show its freshness. */ import { api, POS, WEB } from './client'; import type { DeliveryRow, DeliverySummary, LocationOrderSummary, OrderRow, OrderSummary, PosLocationHealth, PosTerminalHealth, PosSalesPage, PosSalesSummary, } from './types'; export interface DateRange { fromdate?: string; todate?: string; } export interface OrderQuery extends DateRange { tenantid: number; /** Omit for every branch of the tenant. */ locationid?: number; status?: string; keyword?: string; pageno?: number; pagesize?: number; } export const insightsApi = { /** * The order rows themselves. * * `/orders/tenant/getorders` rather than the bare `/orders/getorders`: the * controller routes on which ids are present, and passing a tenant with no * partner, customer or app-user reaches `GetTenantOrders`. Passing a * `locationid` as well reaches `GetTenantLocationOrders`, which is the * branch-scoped read — so one call covers both "all branches" and "one * branch" by presence alone. * * `pageno` is 1-based here. The controller floors anything <= 0 to 1, so * sending 0 silently gives page one rather than an error. */ orders: (query: OrderQuery) => api.list(`${WEB}/orders/tenant/getorders`, { tenantid: query.tenantid, locationid: query.locationid, status: query.status, keyword: query.keyword, fromdate: query.fromdate, todate: query.todate, pageno: query.pageno ?? 1, pagesize: query.pagesize ?? 50, }), /** * The delivery jobs. * * A separate read from the orders list, NOT a filter over it. The rows are a * different struct with different fields — rider name, planned vs actual * distance, rider charge vs job value, notes — and a different status ladder. * Deriving deliveries from orders, which is what this page did first, loses * every one of those. * * The controller 400s unless one of tenantid/partnerid/customerid/ * applocationid/userid/appuserid is present, so `tenantid` is required here. */ deliveries: (query: OrderQuery) => api.list(`${WEB}/deliveries/getdeliveries`, { tenantid: query.tenantid, locationid: query.locationid, status: query.status, keyword: query.keyword, fromdate: query.fromdate, todate: query.todate, pageno: query.pageno ?? 1, pagesize: query.pagesize ?? 50, }), orderSummary: (tenantid: number, range: DateRange = {}) => api.get(`${WEB}/orders/getordersummary`, { tenantid, ...range }), /** Per-branch order totals for one tenant. `tenantid` is required. */ locationSummary: (tenantid: number, range: DateRange = {}) => api.list(`${WEB}/orders/getlocationsummary`, { tenantid, ...range }), revenueSummary: (tenantid: number, range: DateRange = {}) => api.get(`${WEB}/orders/getrevenuesummary`, { tenantid, ...range }), /** * `granularity` is REQUIRED and was never sent, so this call answered 400 * every single time: "granularity query parameter is required (day, month, * year)". Nothing renders it yet, which is the only reason it went unnoticed * — the first screen to use it would have shown an error instead of a chart. * * Defaulted rather than made a required argument: a day-by-day series is what * every caller of a dated range wants, and a parameter with one sensible * answer should not be every caller's problem. */ timeSeries: ( tenantid: number, range: DateRange = {}, granularity: 'day' | 'month' | 'year' = 'day', ) => api.get[]>(`${WEB}/orders/gettimeseries`, { tenantid, granularity, ...range, }), deliverySummary: (tenantid: number, range: DateRange = {}) => api.get(`${WEB}/deliveries/deliverysummary`, { tenantid, ...range }), /** * Counter sales for ONE outlet. * * `locationid` is required and singular — there is no tenant-wide POS call, * so a multi-branch view fans out one request per branch. */ posSales: ( locationid: number, params: DateRange & { pageno?: number; pagesize?: number; /** The three server-side filters the old console's bills tab offers. */ terminalid?: string; cashiername?: string; paymentmode?: string; } = {}, ) => api.get(`${POS}/sales`, { locationid, pageno: params.pageno ?? 0, pagesize: params.pagesize ?? 50, fromdate: params.fromdate, todate: params.todate, terminalid: params.terminalid, cashiername: params.cashiername, paymentmode: params.paymentmode, }), /** * One counter bill in full, with its lines. * * `reference` accepts the till's order UUID, the invoice number, or this * backend's posorderid — a support call starts from whichever the person is * looking at, so the endpoint takes all three. */ posSaleDetail: (locationid: number, reference: string) => api.get(`${POS}/sales/detail`, { locationid, reference }), /** * Counter-sales totals for ONE outlet. * * The richest read in the API: bill count, gross, tax, discount, roundoff and * average bill, already broken down by payment mode, by day and by terminal. * Reports gets its offline half from this and nothing else. * * Kept apart from the order summaries above on purpose. These are `posorders` * rows; those are `orders` rows. The two are never added together — see * `store-admin-backend-gap.md` §3.1 for why that would double-count a branch * that both runs a till and uploads a spreadsheet. */ posSalesSummary: (locationid: number, range: DateRange = {}) => api.get(`${POS}/sales/summary`, { locationid, ...range }), /** * Till presence for one outlet — how many are online, how many bills are stranded. * * Returns the terminal list, not the wrapper. The endpoint answers * `{location_id, total, online, terminals}`; every caller wants `terminals`, * and `summariseBranch` recomputes `online` from the heartbeats anyway * because Fiesta's figure counts a stale till as present. Unwrapping here * keeps that one shape fact in the API layer instead of on every page. */ posHealth: (locationid: number): Promise => api .get(`${POS}/health/location`, { location_id: locationid }) .then((health) => (Array.isArray(health?.terminals) ? health.terminals : [])), };