Files
daily_console_web/src/api/insights.ts
2026-08-28 18:18:50 +05:30

189 lines
6.8 KiB
TypeScript

/**
* 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<OrderRow>(`${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<DeliveryRow>(`${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<OrderSummary>(`${WEB}/orders/getordersummary`, { tenantid, ...range }),
/** Per-branch order totals for one tenant. `tenantid` is required. */
locationSummary: (tenantid: number, range: DateRange = {}) =>
api.list<LocationOrderSummary>(`${WEB}/orders/getlocationsummary`, { tenantid, ...range }),
revenueSummary: (tenantid: number, range: DateRange = {}) =>
api.get<OrderSummary>(`${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<Record<string, unknown>[]>(`${WEB}/orders/gettimeseries`, {
tenantid,
granularity,
...range,
}),
deliverySummary: (tenantid: number, range: DateRange = {}) =>
api.get<DeliverySummary>(`${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<PosSalesPage>(`${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<unknown>(`${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<PosSalesSummary>(`${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<PosTerminalHealth[]> =>
api
.get<PosLocationHealth>(`${POS}/health/location`, { location_id: locationid })
.then((health) => (Array.isArray(health?.terminals) ? health.terminals : [])),
};