Files
daily_console_web/src/queries/hooks.ts
2026-09-25 15:21:02 +05:30

776 lines
29 KiB
TypeScript

/**
* TanStack Query hooks.
*
* The 30-second cadence is the CONSOLE's refetch interval, not a terminal push
* rate: the tills push over MQTT as bills happen, and this is how often the
* screen goes back and asks. It is applied per-query rather than globally so
* that a provisioning form is not re-polling a tenant list it will never see
* change mid-edit.
*/
import { useQueries, useQuery, type UseQueryOptions } from '@tanstack/react-query';
import { catalogueApi, type CatalogueQuery } from '@/api/catalogue';
import { deliveriesApi, ridersApi } from '@/api/deliveries';
import { insightsApi, type DateRange, type OrderQuery } from '@/api/insights';
import { productsApi } from '@/api/products';
import { posUsersApi, staffApi } from '@/api/people';
import { stockApi, type StockRequestQuery } from '@/api/stock';
import { tenantsApi, utilsApi, type TenantListQuery } from '@/api/tenants';
import { partnersApi } from '@/api/deliveries';
import { customersApi, type CustomerQuery } from '@/api/customers';
import { telemetryApi, type RiderSnapshot } from '@/api/telemetry';
import { uploadsApi } from '@/api/uploads';
import { APP_BROWSE_CATEGORY } from '@/features/catalogue/tenantCategories';
import { dedupePartners } from '@/features/nearle-admin/partnerList';
import { queryKeys } from './keys';
import { LIVE_REFETCH_MS } from './interval';
/** How often live-operations screens go back to the backend. */
export { LIVE_REFETCH_MS };
/**
* Shared options for anything that changes during a trading day.
*
* `refetchIntervalInBackground: false` pauses polling while the tab is hidden
* and resumes with an immediate refetch when it comes back — a console left
* open on a second monitor should not spend the afternoon talking to the API.
* `staleTime` sits below the interval so focus and mount refetches aren't
* skipped as already-fresh.
*/
const live = {
refetchInterval: LIVE_REFETCH_MS,
refetchIntervalInBackground: false,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
staleTime: 15_000,
} satisfies Partial<UseQueryOptions>;
/** Reference data that only changes when someone changes it. */
const stable = {
staleTime: 5 * 60_000,
refetchOnWindowFocus: false,
} satisfies Partial<UseQueryOptions>;
/* ── Tenants ─────────────────────────────────────────────────────────────── */
export function useTenants(query: TenantListQuery = {}) {
return useQuery({
queryKey: queryKeys.tenants.list({ ...query }),
queryFn: () => tenantsApi.listAll(query),
...stable,
});
}
/**
* The approval queue.
*
* Its own hook rather than a filter over `useTenants`, because it is a
* different endpoint: `getalltenants` cannot see an unapproved tenant at all.
*/
export function useTenantsByApproval(status: 'pending' | 'Active' | 'InActive', keyword?: string) {
return useQuery({
queryKey: queryKeys.tenants.approval(status, keyword ?? ''),
queryFn: () => tenantsApi.byApproval(status, keyword),
...stable,
});
}
/** The business-category master, for onboarding. */
export function useAppCategories() {
return useQuery({
queryKey: queryKeys.tenants.appCategories(),
queryFn: () => utilsApi.appCategories(),
...stable,
});
}
export function useTenantLocations(tenantid: number | undefined) {
return useQuery({
queryKey: queryKeys.tenants.locations(tenantid ?? 0),
queryFn: () => tenantsApi.locations(tenantid as number),
// Never fire without the scoping id — the backend would 400, and a 400 in
// the cache reads to the user as a broken page rather than a missing input.
enabled: typeof tenantid === 'number' && tenantid > 0,
...stable,
});
}
/* ── Global catalogue ────────────────────────────────────────────────────── */
export function useCatalogueProducts(query: CatalogueQuery) {
return useQuery({
queryKey: queryKeys.catalogue.products(query as Record<string, unknown>),
queryFn: () => catalogueApi.products(query),
...stable,
});
}
/**
* The categories inside one brand's table.
*
* Brand-scoped because the backend reads them from that brand's own table
* (`catalogueController.go:38-45` refuses without one) — there is no
* catalogue-wide category list to ask for. That is why the rail nests
* categories under a brand rather than listing them at the top level.
*/
export function useCatalogueCategories(brand: string | undefined) {
return useQuery({
queryKey: queryKeys.catalogue.categories(brand ?? ''),
queryFn: () => catalogueApi.categories(brand as string),
enabled: Boolean(brand),
...stable,
});
}
export function useCatalogueBrands() {
return useQuery({
queryKey: queryKeys.catalogue.brands(),
queryFn: () => catalogueApi.brands(),
...stable,
});
}
export function useImportedRefs(tenantid: number | undefined) {
return useQuery({
queryKey: queryKeys.catalogue.importedRefs(tenantid ?? 0),
queryFn: () => catalogueApi.importedRefs(tenantid as number),
enabled: typeof tenantid === 'number' && tenantid > 0,
...stable,
});
}
/* ── Products ────────────────────────────────────────────────────────────── */
/**
* Every delivery partner across every region.
*
* `getpartners` takes one region at a time and refuses 0, so the whole list is
* the regions fanned out and joined up. Three regions today — Coimbatore,
* Madurai, Nagercoil — so this is three requests, not a page of them.
*
* Joined up, not concatenated: the backend scopes on `partnerlocations`, so a
* partner covering two cities is returned by both of those requests and is
* still one partner. See `dedupePartners`.
*/
export function useAllPartners() {
const regions = useAppRegions();
const ids = (regions.data ?? []).map((region) => region.applocationid);
const results = useQueries({
queries: ids.map((applocationid) => ({
queryKey: queryKeys.partners.inRegion(applocationid),
queryFn: () => partnersApi.list(applocationid),
...stable,
})),
});
// Deduped, because the per-region answers legitimately overlap now that the
// backend scopes on `partnerlocations` — see `dedupePartners`.
return {
data: dedupePartners(results.map((result) => result.data)),
isLoading: regions.isLoading || results.some((result) => result.isLoading),
};
}
/** The delivery regions. `0` asks for all of them — see `utilsApi.appLocations`. */
export function useAppRegions() {
return useQuery({
queryKey: queryKeys.regions.all,
queryFn: () => utilsApi.appLocations(0),
...stable,
});
}
/**
* One delivery partner's riders.
*
* The roster, not `getriders`: the second wants a clock-in stamped today, so a
* rider added five minutes ago is absent from it — which reads as a failed
* save. The directory shows everybody and reports duty as a state.
*/
export function usePartnerRiders(partnerid: number | undefined) {
return useQuery({
queryKey: [...queryKeys.partners.all, 'riders', partnerid ?? 0] as const,
queryFn: () => ridersApi.partnerRoster(partnerid as number),
enabled: typeof partnerid === 'number' && partnerid > 0,
...stable,
});
}
/**
* How many riders each partner has, keyed by partnerid.
*
* Fanned out because `getriderroster` takes one partner at a time. Five
* partners today, so five requests — and the count is what makes the Riders
* button worth pressing, since "Riders" alone asks you to open a drawer to find
* out whether there are any.
*/
export function usePartnerRiderCounts(partnerids: readonly number[]) {
const ids = [...new Set(partnerids)].filter((id) => id > 0);
const results = useQueries({
queries: ids.map((partnerid) => ({
queryKey: [...queryKeys.partners.all, 'riders', partnerid] as const,
queryFn: () => ridersApi.partnerRoster(partnerid),
...stable,
})),
});
const counts = new Map<number, number>();
results.forEach((result, index) => {
const id = ids[index];
if (id !== undefined && result.data) counts.set(id, result.data.length);
});
return counts;
}
/**
* Every GPS ping a partner's riders sent over a window.
*
* ── Why the window is small by default ────────────────────────────────────
*
* The response is one row per ping and the riders ping constantly: August 2026
* returned 320,132 rows for eight riders. A month is several megabytes of JSON
* to fetch, parse and smooth, so the fleet view asks for a day or two and says
* which day it is showing.
*
* Cached longer than `stable` allows because it is history: yesterday's pings
* do not change, and a refetch on every focus would re-download the day.
*/
export function usePartnerRiderLogs(
partnerid: number | undefined,
range: { fromdate: string; todate: string },
) {
return useQuery({
queryKey: queryKeys.partners.logs(partnerid ?? 0, range.fromdate, range.todate),
queryFn: () =>
partnersApi.riderLogs({
partnerid: partnerid as number,
fromdate: range.fromdate,
todate: range.todate,
}),
enabled:
typeof partnerid === 'number' && partnerid > 0 && Boolean(range.fromdate && range.todate),
...stable,
staleTime: 5 * 60_000,
});
}
/** The regions one partner covers. */
export function usePartnerLocations(partnerid: number | undefined) {
return useQuery({
queryKey: queryKeys.partners.locations(partnerid ?? 0),
queryFn: () => partnersApi.locations(partnerid as number),
enabled: typeof partnerid === 'number' && partnerid > 0,
...stable,
});
}
/**
* The ten aisles the customer app groups products into.
*
* `productsubcategories` for category 2 — platform rows, the same ten for
* every tenant, so the tenantid only satisfies the endpoint's signature. This
* is the list `getproductsbysubcategory` buckets against, which makes it the
* one the console has to name products from too; see `appAisle.ts`.
*/
export function useAppAisles(tenantid: number | undefined) {
return useQuery({
queryKey: queryKeys.products.subCategories(tenantid ?? 0, APP_BROWSE_CATEGORY),
queryFn: () => productsApi.subCategories(tenantid as number, APP_BROWSE_CATEGORY),
enabled: typeof tenantid === 'number' && tenantid > 0,
...stable,
});
}
/**
* One global-catalogue row, for the product drawer.
*
* Not retried: the common failure is that a re-scrape retired the source row,
* and asking three more times does not bring it back.
*/
export function useCatalogueProduct(brand: string | undefined, sku: string | undefined) {
return useQuery({
queryKey: queryKeys.catalogue.product(brand ?? '', sku ?? ''),
queryFn: () => catalogueApi.product(brand as string, sku as string),
enabled: Boolean(brand) && Boolean(sku),
retry: false,
...stable,
});
}
/**
* A tenant's products, at one branch or across all of them.
*
* `locationid` of 0 or undefined means EVERY branch, and the backend answers it
* — one row per product, stock summed across outlets. It did not always: the
* read required a single branch, so "All branches" listed the FIRST one and a
* merchant with five outlets saw four products at RS Puram instead of the six
* they stock. Picking one branch showed more than picking all of them.
*
* So the tenant alone is enough to run this query now. Gating on `locationid`
* is what turned a missing backend feature into a silently wrong list.
*/
export function useLocationProducts(
tenantid: number | undefined,
locationid: number | undefined,
page = 0,
options: {
/**
* Treat a missing branch as "every branch" rather than as "not ready yet".
*
* Off by default, and that default is the safe one. A Store user is pinned
* to a single outlet and their branch resolves asynchronously — so during
* that window an opt-out default would answer tenant-wide and show them
* every other branch's stock as if it were their own.
*
* The admin catalogue opts in, because there "All branches" is a real
* selection a merchant made, not a value that has not arrived.
*/
allBranches?: boolean;
} = {},
) {
const wantsAll = options.allBranches === true;
return useQuery({
queryKey: queryKeys.products.byLocation(tenantid ?? 0, locationid ?? 0, page),
queryFn: () =>
productsApi.locationProducts({
tenantid: tenantid as number,
// 0 is meaningful to the backend here, not a missing value.
locationid: locationid ?? 0,
pageno: page,
}),
enabled: Boolean(tenantid) && (wantsAll || Boolean(locationid)),
...live,
});
}
/* ── Performance ─────────────────────────────────────────────────────────── */
/*
No `useLocationSummary`.
It had four callers — Reports, both Consoles and the platform's store detail —
and every one of them read `revenue` and `totalorders` off its rows. Neither
field is in the response; it carries counts under different names and no money
at all. So every "App revenue" on this console read ₹0 against shops with real
takings, and nothing went red because both fields were optional on a type with
an index signature.
It also ignores `fromdate`/`todate`, so even the counts it does return are
all-time whatever the date picker says.
Per-branch figures now come from the order rows via `branchOrderStats`, which
has both the money and a working date filter. `insightsApi.locationSummary`
stays in the API map, correctly typed, if the counts are ever wanted.
*/
export function useOrderSummary(tenantid: number | undefined) {
return useQuery({
queryKey: queryKeys.insights.orders(tenantid ?? 0),
queryFn: () => insightsApi.orderSummary(tenantid as number),
enabled: typeof tenantid === 'number' && tenantid > 0,
...live,
});
}
/* ── Store Admin: per-branch fan-out ─────────────────────────────────────── */
/**
* Till health for several branches at once.
*
* `useQueries` rather than a loop of `useQuery`, because the number of branches
* is data — a merchant can commission a fourth outlet while this screen is open,
* and a hook count that changes between renders is a crash, not a re-render.
*
* One request per branch is not a choice: `/pos/health/location` takes a single
* `location_id` and there is no tenant-wide POS endpoint anywhere in Fiesta.
*/
export function usePosHealthByBranch(locationIds: readonly number[]) {
return useQueries({
queries: locationIds.map((locationid) => ({
queryKey: queryKeys.insights.posHealth(locationid),
queryFn: () => insightsApi.posHealth(locationid),
...live,
})),
});
}
/** Counter-sales totals for several branches at once. Same fan-out, same reason. */
export function usePosSalesByBranch(locationIds: readonly number[], range: DateRange = {}) {
return useQueries({
queries: locationIds.map((locationid) => ({
queryKey: queryKeys.insights.posSalesSummary(locationid, { ...range }),
queryFn: () => insightsApi.posSalesSummary(locationid, range),
...live,
})),
});
}
/* ── Store Admin: stock ──────────────────────────────────────────────────── */
export function useStockRequests(query: StockRequestQuery | undefined) {
return useQuery({
queryKey: queryKeys.stock.requests((query ?? {}) as Record<string, unknown>),
queryFn: () => stockApi.requests(query as StockRequestQuery),
enabled: Boolean(query?.tenantid),
...live,
});
}
export function useStockStatement(
tenantid: number | undefined,
locationid: number | undefined,
params: { keyword?: string; pagesize?: number } = {},
) {
return useQuery({
queryKey: queryKeys.stock.statement(tenantid ?? 0, locationid ?? 0, params),
queryFn: () =>
stockApi.statement({
tenantid: tenantid as number,
locationid: locationid as number,
...params,
}),
enabled: Boolean(tenantid) && Boolean(locationid),
...live,
});
}
/**
* The stock statement for EVERY branch in scope, one request each.
*
* `getstockstatement` takes a single `locationid` and there is no tenant-wide
* stock read, so a multi-branch view fans out — the same constraint, and the same
* shape of answer, as `usePosSalesByBranch`.
*
* The Reports inventory tab read `scoped[0]` before this: whatever branch
* happened to sort first stood in for the whole tenant, on a page where every
* other figure covers all of them. A reader comparing inventory value against
* this period's revenue was comparing one shop against the chain.
*/
export function useStockStatementByBranch(
tenantid: number | undefined,
locationIds: readonly number[],
params: { keyword?: string; pagesize?: number } = {},
) {
return useQueries({
queries: locationIds.map((locationid) => ({
queryKey: queryKeys.stock.statement(tenantid ?? 0, locationid, params),
queryFn: () =>
stockApi.statement({ tenantid: tenantid as number, locationid, ...params }),
enabled: Boolean(tenantid),
...live,
})),
});
}
/* ── Store Admin: order and bill rows ────────────────────────────────────── */
/**
* The order rows.
*
* One call for every branch or for one, decided by whether `locationid` is
* present — the backend routes on that, so there is nothing to fan out here.
*/
export function useOrders(query: OrderQuery | undefined) {
return useQuery({
queryKey: queryKeys.insights.orderList((query ?? {}) as unknown as Record<string, unknown>),
queryFn: () => insightsApi.orders(query as OrderQuery),
/* Either scope runs it. `tenantid: 0` is what a partner-scoped read sends
for the unused field (see `scopeQuery`), and gating on the tenant alone
meant the platform board fetched nothing at all — the map came up empty
against a partner whose API rows numbered 376. */
enabled: Boolean(query?.tenantid || query?.partnerid),
...live,
});
}
/**
* Every order in the window, paged until the rows run out.
*
* For the screens that TOTAL orders rather than list them. `useOrders` returns
* one page and is right for a table with a pager under it; a sum built from one
* page is simply wrong past that page, and wrong without saying so.
*
* Returns the flag as well as the rows: at the page bound the figures ARE partial
* and the screen has to say so. A caller that ignores `truncated` has recreated
* the bug this exists to fix.
*/
export function useAllOrders(query: OrderQuery | undefined, pagesize = 500) {
return useQuery({
queryKey: queryKeys.insights.orderList({
...((query ?? {}) as unknown as Record<string, unknown>),
all: true,
pagesize,
}),
queryFn: () => insightsApi.ordersAll(query as OrderQuery, { pagesize }),
enabled: Boolean(query?.tenantid || query?.partnerid),
...live,
});
}
/**
* Counter bills, per branch.
*
* Fanned out because `/pos/sales` takes a single `locationid` and there is no
* tenant-wide POS read — the same constraint as the health and summary calls.
*/
export function usePosBillsByBranch(
locationIds: readonly number[],
params: DateRange & { pagesize?: number } = {},
) {
return useQueries({
queries: locationIds.map((locationid) => ({
queryKey: queryKeys.insights.posBills(locationid, { ...params }),
queryFn: () => insightsApi.posSales(locationid, params),
...live,
})),
});
}
/** The delivery jobs — its own read, its own row shape, its own status ladder. */
export function useDeliveries(query: OrderQuery | undefined) {
return useQuery({
queryKey: queryKeys.insights.deliveryList((query ?? {}) as unknown as Record<string, unknown>),
queryFn: () => insightsApi.deliveries(query as OrderQuery),
/* Either scope runs it. `tenantid: 0` is what a partner-scoped read sends
for the unused field (see `scopeQuery`), and gating on the tenant alone
meant the platform board fetched nothing at all — the map came up empty
against a partner whose API rows numbered 376. */
enabled: Boolean(query?.tenantid || query?.partnerid),
...live,
});
}
/**
* Riders on duty in one delivery region.
*
* `live` rather than `stable`, unlike the other reference lists: this one is
* not reference data. A rider clocking on or off changes it during the shift,
* and a picker offering somebody who went home twenty minutes ago sends the
* order nowhere.
*
* Scoped by `applocationid`. Passing a tenant instead returns an empty list
* with a 200 — see `deliveriesApi.riders`.
*/
export function useRiders(scope: {
/** The merchant's own riders. */
tenantid?: number | undefined;
/** A delivery partner's riders. */
partnerid?: number | undefined;
/** Only used when neither of the above is given. */
applocationid?: number | undefined;
}) {
const { tenantid, partnerid, applocationid } = scope;
return useQuery({
// The scope is part of the key, or one fleet's riders would be served to
// the other after a toggle.
queryKey: [...queryKeys.insights.all, 'riders', tenantid ?? 0, partnerid ?? 0, applocationid ?? 0] as const,
queryFn: () => deliveriesApi.riders({ tenantid, partnerid, applocationid }),
enabled: Boolean(tenantid || partnerid || applocationid),
...live,
});
}
/* ── People ──────────────────────────────────────────────────────────────── */
/** The back-office directory. Role names arrive resolved; never map ids here. */
/**
* The branch's customers.
*
* Page size is always sent — see `customersApi`. Left off, the backend returns
* an empty list rather than an error, which reads as "no customers".
*/
export function useCustomers(query: CustomerQuery | undefined) {
return useQuery({
queryKey: queryKeys.customers.list({ ...query }),
queryFn: () => customersApi.list(query as CustomerQuery),
enabled: Boolean(query?.tenantid),
...live,
});
}
export function useStaff(tenantid: number | undefined) {
return useQuery({
queryKey: queryKeys.people.staff(tenantid ?? 0),
queryFn: () => staffApi.list(tenantid as number),
enabled: Boolean(tenantid),
...stable,
});
}
/**
* Till accounts, per branch.
*
* Fanned out because `getposusers` takes a single `locationid` — the same
* constraint as every other POS read.
*/
/**
* Till accounts, per branch.
*
* `includeInactive` is opt-in and part of the cache key, so a page that wants
* to keep a switched-off cashier on screen gets them without changing what
* every other caller sees — see `posUsersApi.list` for why the default stayed
* as it was.
*/
export function usePosUsersByBranch(
tenantid: number | undefined,
locationIds: readonly number[],
includeInactive = false,
) {
return useQueries({
queries: locationIds.map((locationid) => ({
queryKey: [...queryKeys.people.posUsers(tenantid ?? 0, locationid), includeInactive] as const,
queryFn: () => posUsersApi.list(tenantid as number, locationid, includeInactive),
enabled: Boolean(tenantid),
...stable,
})),
});
}
/** The role picker's source. Cached hard — it changes when the product changes. */
export function usePosRoles() {
return useQuery({
queryKey: queryKeys.people.posRoles(),
queryFn: () => posUsersApi.roles(),
...stable,
});
}
export function useStaffShifts(tenantid: number | undefined, locationid: number | undefined) {
return useQuery({
queryKey: queryKeys.people.shifts(tenantid ?? 0, locationid ?? 0),
queryFn: () => posUsersApi.shifts(tenantid as number, locationid as number),
enabled: Boolean(tenantid) && Boolean(locationid),
...stable,
});
}
/**
* The signed-in merchant's own business record.
*
* `gettenantinfo`, not `listAll`. The latter is `getalltenants` — paginated
* across 262 merchants — so a shop on page two was simply not found, and a
* profile screen built on it would show nothing with no visible reason why.
*/
export function useOwnTenant(tenantid: number | undefined) {
return useQuery({
queryKey: queryKeys.tenants.byId(tenantid ?? 0),
queryFn: () => tenantsApi.byId(tenantid as number),
enabled: Boolean(tenantid),
...stable,
});
}
/** Upload receipts for a shop — used by the Uploads page and the setup checklist. */
export function useUploads(tenantid: number | undefined, locationid?: number) {
return useQuery({
queryKey: queryKeys.uploads.list(tenantid ?? 0, locationid ?? 0),
queryFn: () => uploadsApi.list({ tenantid, ...(locationid ? { locationid } : {}) }),
enabled: Boolean(tenantid),
...stable,
});
}
/**
* The rider directory — everyone, whether or not they are working today.
*
* `stable`, not `live`, and that is the difference from `useRiders`. This is a
* staff list: it changes when somebody is hired or edited, not through the day
* as shifts start and end. The on-duty read is the one that has to keep asking.
*/
export function useRiderRoster(tenantid: number | undefined) {
return useQuery({
queryKey: queryKeys.insights.riderRoster(tenantid ?? 0),
queryFn: () => ridersApi.roster(tenantid as number),
enabled: Boolean(tenantid),
...stable,
});
}
/** Shifts a rider can be put on. Scoped by region; the param is required. */
export function useRiderShifts(applocationid: number | undefined) {
return useQuery({
queryKey: queryKeys.insights.riderShifts(applocationid ?? 0),
queryFn: () => ridersApi.shifts(applocationid as number),
enabled: Boolean(applocationid),
...stable,
});
}
/** Delivery partners in a region. */
export function usePartners(applocationid: number | undefined) {
return useQuery({
queryKey: queryKeys.insights.partners(applocationid ?? 0),
queryFn: () => ridersApi.partners(applocationid as number),
enabled: Boolean(applocationid),
...stable,
});
}
/**
* Live positions for a set of riders.
*
* ── Fanned out, because there is no fleet form ────────────────────────────
*
* `getriderperiodiclogs` answers for one rider at a time. A merchant's round is
* a handful of riders, so a handful of requests every fifteen seconds is
* cheap — and the fan-out means one rider's failure does not blank the others,
* which a single fleet call would.
*
* ── Fifteen seconds, matching the source ──────────────────────────────────
*
* The rider app reports every twenty seconds or so, so polling faster only
* re-reads the same fix. Fifteen keeps the board a beat ahead of the data
* without asking for anything that does not exist yet.
*
* Paused when the tab is hidden: nobody is watching a map they cannot see, and
* a background tab polling a third-party service all afternoon is rude.
*/
export function useRiderLive(userids: readonly number[], isEnabled = true) {
const ids = [...new Set(userids)].filter((id) => id > 0);
const results = useQueries({
queries: ids.map((userid) => ({
queryKey: queryKeys.live.rider(userid),
queryFn: ({ signal }: { signal: AbortSignal }) => telemetryApi.rider(userid, signal),
enabled: isEnabled,
refetchInterval: isEnabled ? 15_000 : false,
refetchIntervalInBackground: false,
// A stale fix is worth drawing while the next one is fetched; blanking
// the map every fifteen seconds would make it unreadable.
staleTime: 10_000,
retry: 1,
})),
});
const byRider = new Map<number, RiderSnapshot | null>();
results.forEach((result, index) => {
const id = ids[index];
if (id !== undefined) byRider.set(id, result.data ?? null);
});
return {
data: byRider,
isLoading: results.some((result) => result.isLoading),
/** True while any rider is being re-read — for a quiet "updating" hint. */
isFetching: results.some((result) => result.isFetching),
};
}
/**
* What is in one order — the line items, and the total Fiesta computed.
*
* Enabled only when a drawer is actually open on an order, because this is a
* request per order and the list behind it can hold hundreds. A delivery row
* carries the same `orderheaderid`, so the same hook serves both kinds of sheet.
*/
export function useOrderItems(orderheaderid: number | undefined) {
return useQuery({
queryKey: queryKeys.insights.orderItems(orderheaderid ?? 0),
queryFn: () => insightsApi.orderItems(orderheaderid as number),
enabled: Boolean(orderheaderid),
...stable,
});
}