From d169d24934703f858cdcb2fbe5de5595fc319eb6 Mon Sep 17 00:00:00 2001 From: abhishek Date: Fri, 25 Sep 2026 15:21:02 +0530 Subject: [PATCH] ui fix --- src/api/deliveries.ts | 1 + src/api/insights.ts | 27 ++++ src/api/people.ts | 18 +++ src/api/types.ts | 46 ++++++ .../store-admin/OrderDetailDrawer.tsx | 142 ++++++++++++++++-- src/features/store-admin/PeopleDrawers.tsx | 74 ++++++++- src/features/store-admin/RiderDrawer.tsx | 39 +++-- src/features/store-admin/orderItems.test.ts | 90 +++++++++++ src/features/store-admin/orderItems.ts | 77 ++++++++++ .../store-user/pages/StoreStaffPage.tsx | 11 +- src/queries/hooks.ts | 16 ++ src/queries/keys.ts | 3 + 12 files changed, 516 insertions(+), 28 deletions(-) create mode 100644 src/features/store-admin/orderItems.test.ts create mode 100644 src/features/store-admin/orderItems.ts diff --git a/src/api/deliveries.ts b/src/api/deliveries.ts index db5421d..141d763 100644 --- a/src/api/deliveries.ts +++ b/src/api/deliveries.ts @@ -319,6 +319,7 @@ export interface RiderShift { */ export interface NewRiderShift { applocationid: number; + shiftname?: string; starttime: string; endtime: string; basefare?: number; diff --git a/src/api/insights.ts b/src/api/insights.ts index 06f0453..5f0e770 100644 --- a/src/api/insights.ts +++ b/src/api/insights.ts @@ -12,6 +12,7 @@ import type { DeliveryRow, DeliverySummary, LocationOrderSummary, + OrderItem, OrderRow, OrderSummary, PosLocationHealth, @@ -163,6 +164,32 @@ export const insightsApi = { ...range, }), + /** + * What is actually IN an order. + * + * The only read that carries line items. Every list endpoint returns an + * order's totals and never its contents, which is why the detail sheet could + * say an order was worth ₹840 and not what the ₹840 bought. + * + * The envelope rather than `api.list`, because the authoritative total lives + * outside `details`: `OrderDetail.Orderamount` is `json:"-"` on the server, so + * `pricedetails.orderamount` is the only place it appears. Summing the lines + * would be recomputing a figure Fiesta has already worked out, and the two + * would disagree the first time a discount rounded differently. + */ + orderItems: async (orderheaderid: number) => { + const envelope = await api.envelope(`${WEB}/orders/getorderdetails`, { + params: { orderheaderid }, + }); + return { + // `details: null` for an order with no lines is as common here as `[]`; + // see the note on `api.list`. + items: envelope.details ?? [], + amount: envelope.pricedetails?.orderamount ?? 0, + tax: envelope.pricedetails?.totaltaxamount ?? 0, + }; + }, + deliverySummary: (tenantid: number, range: DateRange = {}) => api.get(`${WEB}/deliveries/deliverysummary`, { tenantid, ...range }), diff --git a/src/api/people.ts b/src/api/people.ts index bb61d67..10cc24c 100644 --- a/src/api/people.ts +++ b/src/api/people.ts @@ -192,6 +192,24 @@ export const posUsersApi = { locationid, }) .then((page) => (Array.isArray(page?.shifts) ? page.shifts : [])), + + /** + * Open a shift window at one branch. + * + * The endpoint has existed since till staff were built; nothing in the console + * called it. So `getstaffshifts` answered `{"shifts": []}` at every branch — + * the comment on StoreStaffPage says exactly that — and the picker on this + * drawer offered "Any shift" and nothing else, for everyone, permanently. + * + * `weekdays` is a seven-character mask starting Monday; empty means every day. + * The server rejects anything that is not seven 0/1 characters, so it is sent + * as the mask rather than as a list the console would have to encode twice. + */ + createShift: ( + tenantid: number, + locationid: number, + shift: { name: string; start_time: string; end_time: string; weekdays?: string }, + ) => api.post(`${WEB}/tenants/createstaffshift`, { tenantid, locationid, ...shift }), }; /** diff --git a/src/api/types.ts b/src/api/types.ts index 001a46d..b196527 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -44,6 +44,52 @@ export interface FiestaEnvelope { token?: string; /** Unix seconds. The tab closing normally ends the session well before this. */ tokenexpiresat?: number; + /** + * Order totals, from `orders/getorderdetails` only. + * + * Beside `details` rather than inside it because the line items are a list + * and this is one figure about the order as a whole. `OrderDetail.Orderamount` + * is tagged `json:"-"` on the server, so the authoritative total exists HERE + * and nowhere else in the response — summing the lines is an approximation of + * a number Fiesta has already worked out. + */ + pricedetails?: { orderamount?: number; totaltaxamount?: number }; +} + +/* ──────────────────────────────────────────────────────────────────────────── + Order line items — models/order.go:OrderDetail + ──────────────────────────────────────────────────────────────────────────── */ + +/** + * One product on an order. + * + * Returned by `orders/getorderdetails`, which is the ONLY read that carries + * them — the list endpoints give an order's totals and never what is in it. + * That is why every screen showing an order has, until now, been able to say + * what it cost but not what it was. + */ +export interface OrderItem { + orderdetailid: number; + orderheaderid: number; + productid: number; + productname: string; + productdescription?: string; + /** What was ordered. A float because some products sell by weight. */ + orderqty: number; + /** What the shop could actually supply — less than `orderqty` is a short fill. */ + supplyqty?: number; + price: number; + unitname?: string; + taxamount?: number; + discountamount?: number; + /** The line total as Fiesta computed it: quantity, tax and discount applied. */ + productsumprice?: number; + itemstatus?: string; + /** + * Always empty today. The column is `gorm:"-"` on the server, so it is + * serialised and never populated — do not build a thumbnail on it. + */ + productimage?: string; } /* ──────────────────────────────────────────────────────────────────────────── diff --git a/src/features/store-admin/OrderDetailDrawer.tsx b/src/features/store-admin/OrderDetailDrawer.tsx index 403711e..b9097ca 100644 --- a/src/features/store-admin/OrderDetailDrawer.tsx +++ b/src/features/store-admin/OrderDetailDrawer.tsx @@ -1,11 +1,13 @@ import type { ReactNode } from 'react'; import { ArrowDown, Bike, Check, MapPin, Phone, X } from 'lucide-react'; -import type { DeliveryRow, OrderRow } from '@/api/types'; +import type { DeliveryRow, OrderItem, OrderRow } from '@/api/types'; import { orderStage, type Stage } from './orderProgress'; import { useDeliveryMoves } from './DeliveryProgress'; import { Drawer } from './Drawer'; import { Badge, DrawerButton, DrawerCard, Metric, Metrics, Note, Row, Section } from './drawerKit'; import { branchLabel, moneyExact } from './format'; +import { lineTotal, qtyLabel, summarise } from './orderItems'; +import { useOrderItems } from '@/queries/hooks'; import { DELIVERY_STATUS, ORDER_STATUS, @@ -135,6 +137,30 @@ function Sheet({ const value = job ? (job.deliveryamt ?? 0) : orderValue(row); const cancelled = (status ?? '').toLowerCase().includes('cancel'); + /* + * What is in the order, read once here and used twice. + * + * The three figures at the top of this sheet — value, cash to collect, items + * — came only from the row the list handed over, and the list does not carry + * them on every order: order 1151-1 opened with "Value —", "Cash to collect + * —", "Items —" while the section at the bottom of the same sheet correctly + * reported one item. Two blocks describing the same order, disagreeing, + * because one of them had asked Fiesta and the other had not. + * + * So the detail read fills the gaps. The row wins when it has a figure — it + * is the number the table behind this sheet is showing, and the two must not + * differ — and this answers only where the row was silent. + * + * One fetch: the same query key, passed down rather than called again. + */ + const detail = useOrderItems(row.orderheaderid); + const items = detail.data?.items ?? []; + const detailAmount = detail.data?.amount ?? 0; + // Units, not lines, because this metric sits beside a money figure and reads + // as "how much is in it". The Items section below names both. + const itemCount = + (job ? job.itemcount : orderQuantity(row)) || Math.round(summarise(items).units); + return ( - 0 ? moneyExact(value) : '—'} /> + {/* The row's figure where it has one, Fiesta's where it does not. An + order the list reported as worth nothing is usually an order the + list was not told about, not a free one. */} + 0 ? moneyExact(value) : detailAmount > 0 ? moneyExact(detailAmount) : '—'} + /> {job ? ( )} - + {/* Counted from the lines when the row carries no count. `itemcount` + and `quantity` are both routinely absent on the list read, which is + what made this a dash on an order that plainly had something in it. */} + 0 ? String(itemCount) : '—'} isSmall />
@@ -262,10 +293,7 @@ function Sheet({
) : null} - - Line items are not in the list read — Fiesta returns them from the order-detail endpoint, - which this sheet does not call yet. - +
); } @@ -447,3 +475,95 @@ const km = (value: string | undefined): string => { const n = Number(value); return Number.isFinite(n) && n > 0 ? `${n.toFixed(1)} km` : '—'; }; +/* ── What is actually in the order ───────────────────────────────────────── */ + +/** + * The products on the order, line by line. + * + * This sheet has always been able to say what an order was WORTH and never what + * it was. The note that used to sit here said as much: Fiesta returns the + * contents from `orders/getorderdetails` and nothing called it. So an operator + * chasing a stalled delivery could see ₹840 and had to open another system to + * find out whether that was rice or ice cream — which decides whether it can + * wait an hour. + * + * Handed its data rather than fetching its own. The drawer above needs the same + * read to fill the three figures in its header, and two components asking + * separately is how the header came to say "Items —" on an order this section + * was, at that moment, listing. + */ +function OrderItems({ + items, + isLoading, + isError, +}: { + items: readonly OrderItem[]; + isLoading: boolean; + isError: boolean; +}) { + if (isLoading) { + return ( +
+ Reading what is in this order… +
+ ); + } + + if (isError) { + // Not a blank section. The rest of the sheet is sound and this one read + // failed; saying so beats an empty heading that reads like an empty order. + return ( +
+ Could not read the items on this order. +
+ ); + } + + if (items.length === 0) { + return ( +
+ + + +
+ ); + } + + const { lines, units, short } = summarise(items); + + return ( +
+ {short > 0 ? ( + + {short === 1 ? 'One line was' : `${short} lines were`} short-supplied — the shop could not + give the full quantity ordered. + + ) : null} + + + {items.map((item) => ( + + {/* Quantity first: a picker reads this column, not the price. */} + + {qtyLabel(item.orderqty, item.unitname)} + + {moneyExact(lineTotal(item))} + + } + /> + ))} + +
+ ); +} diff --git a/src/features/store-admin/PeopleDrawers.tsx b/src/features/store-admin/PeopleDrawers.tsx index e646551..e84cc14 100644 --- a/src/features/store-admin/PeopleDrawers.tsx +++ b/src/features/store-admin/PeopleDrawers.tsx @@ -1,6 +1,6 @@ import { useState, type FormEvent } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { Copy, MapPin } from 'lucide-react'; +import { Clock, Copy, MapPin } from 'lucide-react'; import { errorMessage } from '@/api/client'; import { normaliseMobile, posUsersApi, staffApi, weekdayLabel } from '@/api/people'; import type { PosRole, PosUser, StaffInfo, StaffShift, TenantLocation } from '@/api/types'; @@ -333,6 +333,40 @@ export function TillDrawer({ /** The credential the backend hands back on create. Shown once, never re-read. */ const [issued, setIssued] = useState(null); + /* + * Opening a shift window, from the form that needs one. + * + * `createstaffshift` has existed on the server since till staff were built and + * nothing in the console ever called it — so `getstaffshifts` answered + * `{"shifts": []}` at every branch, and this drawer's picker offered "Any + * shift" and nothing else, for every cashier, permanently. The same gap the + * riders had: a read wired up, a write that only the database could reach. + * + * Here rather than on a settings screen because this is where it is missed. + */ + const [newShift, setNewShift] = useState({ name: '', start_time: '09:00', end_time: '17:00' }); + const setShiftField = (field: keyof typeof newShift) => (value: string) => + setNewShift((prev) => ({ ...prev, [field]: value })); + + const addShift = useMutation({ + mutationFn: () => + posUsersApi.createShift(tenantid, locationid, { + name: newShift.name.trim(), + start_time: newShift.start_time.trim(), + end_time: newShift.end_time.trim(), + }), + onSuccess: async (shift) => { + setProblem(null); + await queryClient.invalidateQueries({ queryKey: queryKeys.people.shifts(tenantid, locationid) }); + // Selected straight away — somebody who just typed these hours meant to + // put this person on them. + if (shift?.staff_shift_id) setShiftId(String(shift.staff_shift_id)); + }, + onError: (cause) => setProblem(errorMessage(cause)), + }); + + const hasNoShifts = shifts.length === 0; + const save = useMutation({ mutationFn: async () => { const status = isActive ? 'Active' : 'Inactive'; @@ -517,6 +551,44 @@ export function TillDrawer({ ]} /> + + {/* The way out of the dead end. Only when the branch genuinely has none + — a shift creator on a screen that already has shifts is how you end + up with four overlapping windows nobody can tell apart. */} + {hasNoShifts ? ( + <> + + No shift windows exist at {branchName || 'this branch'} yet, so there is nothing to + put anyone on. Name one and give its hours — every till account here can then use it. + + + + + + + + + + + + + } + isDisabled={ + addShift.isPending || + newShift.name.trim() === '' || + newShift.start_time.trim() === '' || + newShift.end_time.trim() === '' + } + onClick={() => addShift.mutate()} + /> + + ) : null}
diff --git a/src/features/store-admin/RiderDrawer.tsx b/src/features/store-admin/RiderDrawer.tsx index 0d74aaa..df8c431 100644 --- a/src/features/store-admin/RiderDrawer.tsx +++ b/src/features/store-admin/RiderDrawer.tsx @@ -125,7 +125,7 @@ export function RiderDrawer({ row, owner, branch, onClose }: RiderDrawerProps) { * wall is hit. A person hiring their first rider in a new city should not * have to find out that shifts are a separate concept somewhere else. */ - const [newShift, setNewShift] = useState({ starttime: '09:00', endtime: '17:00' }); + const [newShift, setNewShift] = useState({ shiftname: '', starttime: '09:00', endtime: '17:00' }); const setShiftField = (key: keyof typeof newShift) => (value: string) => setNewShift((prev) => ({ ...prev, [key]: value })); @@ -133,6 +133,7 @@ export function RiderDrawer({ row, owner, branch, onClose }: RiderDrawerProps) { mutationFn: () => ridersApi.createShift({ applocationid, + shiftname: newShift.shiftname.trim(), starttime: newShift.starttime.trim(), endtime: newShift.endtime.trim(), }), @@ -308,11 +309,18 @@ export function RiderDrawer({ row, owner, branch, onClose }: RiderDrawerProps) { none — a shift creator on a screen that already has shifts is a way to end up with four overlapping windows nobody can tell apart. */} {hasNoShifts && applocationid > 0 ? ( - + <> No shift windows exist in this region yet, and a rider cannot be hired without one. Set the hours they work — every rider in this region can then be put on it. + + + @@ -326,12 +334,13 @@ export function RiderDrawer({ row, owner, branch, onClose }: RiderDrawerProps) { icon={} isDisabled={ addShift.isPending || + newShift.shiftname.trim() === '' || newShift.starttime.trim() === '' || newShift.endtime.trim() === '' } onClick={() => addShift.mutate()} /> - + ) : null} {/* A merchant's rider works out of one of that merchant's branches. @@ -388,17 +397,21 @@ export function RiderDrawer({ row, owner, branch, onClose }: RiderDrawerProps) {
{!isNew ? ( -
- - + + set('status')('Active')} /> - + set('status')('InActive')} + /> +
) : null} diff --git a/src/features/store-admin/orderItems.test.ts b/src/features/store-admin/orderItems.test.ts new file mode 100644 index 0000000..fb8c21f --- /dev/null +++ b/src/features/store-admin/orderItems.test.ts @@ -0,0 +1,90 @@ +import { strict as assert } from 'node:assert'; +import { test } from 'node:test'; +import { lineTotal, qtyLabel, summarise } from './orderItems'; +import type { OrderItem } from '@/api/types'; + +/* +What an order contains, as opposed to what it cost. + +Every list read in this product returns an order's totals and none of them +return its contents, so the detail sheet could say ₹840 and not whether that was +rice or ice cream — which is what decides whether a stalled delivery can wait an +hour. These cover the arithmetic behind the new Items section. +*/ + +const item = (over: Partial = {}): OrderItem => ({ + orderdetailid: 1, + orderheaderid: 900, + productid: 10, + productname: 'Sona Masoori rice 25kg', + orderqty: 1, + price: 100, + ...over, +}); + +test('lines and units are different numbers, and both are reported', () => { + // "How many products" has two honest answers. Showing one and labelling it as + // the other is how a picker packs the wrong trolley. + const summary = summarise([ + item({ orderdetailid: 1, orderqty: 2 }), + item({ orderdetailid: 2, orderqty: 10 }), + ]); + + assert.equal(summary.lines, 2); + assert.equal(summary.units, 12); +}); + +test('an empty order summarises to zero rather than throwing', () => { + assert.deepEqual(summarise([]), { lines: 0, units: 0, short: 0 }); +}); + +test('a missing supplyqty is not a short fill', () => { + // The old console read the absent field as 0 and reported every order short. + // Absent means "not sent", which is not the same as "could not supply". + assert.equal(summarise([item({ orderqty: 5 })]).short, 0); + assert.equal(summarise([item({ orderqty: 5, supplyqty: 5 })]).short, 0); + assert.equal(summarise([item({ orderqty: 5, supplyqty: 2 })]).short, 1); +}); + +test('quantity reads the way a person writes it', () => { + // A column of "2.00" reads like a price. Weights keep their decimals. + assert.equal(qtyLabel(2), '2'); + assert.equal(qtyLabel(1.5), '1.5'); + assert.equal(qtyLabel(1.5, 'kg'), '1.5 kg'); + assert.equal(qtyLabel(2, 'pcs'), '2 pcs'); +}); + +test('a quantity that is not a number shows a dash, not NaN', () => { + assert.equal(qtyLabel(Number.NaN), '—'); + assert.equal(qtyLabel(Number.POSITIVE_INFINITY), '—'); +}); + +test("a line's total is Fiesta's own figure where there is one", () => { + // `productsumprice` has tax and discount applied. Recomputing price × qty + // would disagree with the order total the first time a discount rounded + // differently, and the sheet would contradict itself. + assert.equal(lineTotal(item({ price: 100, orderqty: 3, productsumprice: 318.6 })), 318.6); +}); + +test('a line with no computed total falls back rather than showing blank', () => { + // Rows predating that column. Price × quantity is the best available answer. + assert.equal(lineTotal(item({ price: 100, orderqty: 3 })), 300); + assert.equal(lineTotal(item({ price: 100, orderqty: 3, productsumprice: 0 })), 300); +}); + +test('a line with nothing to go on is zero, not NaN', () => { + // A NaN renders as "₹NaN" on the row, which reads as a broken order rather + // than a thin one. + const total = lineTotal({ orderdetailid: 2, orderheaderid: 900, productid: 1, productname: 'x' } as OrderItem); + assert.equal(total, 0); +}); + +test('a weighed order sums to a fractional unit count', () => { + // 1.5kg of rice plus 2 packets is 3.5 units across 2 lines. Rounding this to + // an integer would report 3 or 4 units, neither of which was ordered. + const summary = summarise([item({ orderdetailid: 1, orderqty: 1.5 }), item({ orderdetailid: 2, orderqty: 2 })]); + + assert.equal(summary.lines, 2); + assert.equal(summary.units, 3.5); + assert.equal(qtyLabel(summary.units), '3.5'); +}); diff --git a/src/features/store-admin/orderItems.ts b/src/features/store-admin/orderItems.ts new file mode 100644 index 0000000..b74cebb --- /dev/null +++ b/src/features/store-admin/orderItems.ts @@ -0,0 +1,77 @@ +import type { OrderItem } from '@/api/types'; + +/** + * What an order's contents add up to. + * + * Kept out of the drawer so it can be tested without rendering one, and because + * three screens now ask the same question — the Sales order sheet, the Dispatch + * stop sheet, and the customer sheet on the store workspace. + * + * ── Quantity is not a count of rows ───────────────────────────────────────── + * + * "How many products" has two honest answers and they are different numbers: + * four LINES on the order, twelve UNITS across them. Showing one and labelling + * it as the other is how a picker packs the wrong trolley, so both are returned + * and the drawer names each. + * + * `orderqty` is a float because some products sell by weight — 1.5kg of rice is + * one line and one and a half units. Summing it gives a number that must be + * rendered with `qtyLabel`, never with `toFixed(0)`. + */ +export interface ItemSummary { + /** Distinct products on the order. */ + lines: number; + /** Units across every line, which is not the same number. */ + units: number; + /** Lines the shop could not fully supply. */ + short: number; +} + +export function summarise(items: readonly OrderItem[]): ItemSummary { + let units = 0; + let short = 0; + + for (const item of items) { + units += item.orderqty || 0; + // `supplyqty` is what the shop could actually give. Undefined means the + // field was not sent, which is not the same as a short fill and must not be + // counted as one — the old console read it as 0 and reported every order + // short. + if (typeof item.supplyqty === 'number' && item.supplyqty < (item.orderqty || 0)) { + short += 1; + } + } + + return { lines: items.length, units, short }; +} + +/** + * A quantity as a person writes it. + * + * Whole numbers have no decimal tail — "2", not "2.00" — because most orders + * are whole units and a column of `2.00` reads like a price. Weights keep up to + * two decimals and lose the trailing zeros, so 1.50 shows as 1.5. + */ +export function qtyLabel(qty: number, unit?: string): string { + if (!Number.isFinite(qty)) return '—'; + + const rounded = Math.round(qty * 100) / 100; + const text = Number.isInteger(rounded) ? String(rounded) : String(rounded); + return unit ? `${text} ${unit}` : text; +} + +/** + * What one line cost. + * + * `productsumprice` is Fiesta's own figure with tax and discount applied, and it + * is preferred wherever it is present — recomputing it here would disagree with + * the order total the moment a discount rounded differently. The fallback is + * for rows that predate that column, where price × quantity is the best + * available answer rather than a blank. + */ +export function lineTotal(item: OrderItem): number { + if (typeof item.productsumprice === 'number' && item.productsumprice > 0) { + return item.productsumprice; + } + return (item.price || 0) * (item.orderqty || 0); +} diff --git a/src/features/store-user/pages/StoreStaffPage.tsx b/src/features/store-user/pages/StoreStaffPage.tsx index 131b812..05e6998 100644 --- a/src/features/store-user/pages/StoreStaffPage.tsx +++ b/src/features/store-user/pages/StoreStaffPage.tsx @@ -39,10 +39,15 @@ import { usePaged } from '@/components/usePaged'; * shift_name / shift_start / shift_end 0% * * The old table spent a sixth of its width on a Shift column. Those fields - * exist on the model with `omitempty` and are filled for nobody — - * `getstaffshifts` answers `{"shifts": []}` at every branch — so the column + * exist on the model with `omitempty` and were filled for nobody — + * `getstaffshifts` answered `{"shifts": []}` at every branch — so the column * read "Any" on every row of every shop. A shift now rides under the name, - * where it appears if one is ever set and takes no space while none is. + * where it appears if one is set and takes no space while none is. + * + * The empty answer was not a data problem: `createstaffshift` had existed since + * till staff were built and nothing in the console called it, so no branch + * could ever have a shift. The till drawer now offers to open one when a branch + * has none, which is why this column can finally show something. * * `contactno` got the same treatment for the same reason at 50%. * diff --git a/src/queries/hooks.ts b/src/queries/hooks.ts index c0c1d42..7c91e88 100644 --- a/src/queries/hooks.ts +++ b/src/queries/hooks.ts @@ -757,3 +757,19 @@ export function useRiderLive(userids: readonly number[], isEnabled = true) { 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, + }); +} diff --git a/src/queries/keys.ts b/src/queries/keys.ts index b9845da..b07cb27 100644 --- a/src/queries/keys.ts +++ b/src/queries/keys.ts @@ -90,6 +90,9 @@ export const queryKeys = { /** The directory — everyone, working today or not. */ riderRoster: (tenantid: number) => [...queryKeys.insights.all, 'rider-roster', tenantid] as const, + /** What is in one order. Keyed by the order, never by the list it came from. */ + orderItems: (orderheaderid: number) => + [...queryKeys.insights.all, 'order-items', orderheaderid] as const, riderShifts: (applocationid: number) => [...queryKeys.insights.all, 'rider-shifts', applocationid] as const, partners: (applocationid: number) =>