feat: relocate orders and deliveries to store console & polish store cover images

This commit is contained in:
Suriya
2026-06-03 18:20:43 +05:30
commit 6eaeb5c4a7
32 changed files with 13430 additions and 0 deletions

420
src/services/api.ts Normal file
View File

@@ -0,0 +1,420 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Workolik Hasura REST client — the `/api/rest/*` surface documented at
* https://developer.nearledaily.com (Users, Orders, Tenants, Products,
* Apps & Locations, Partners). The "Mobile" section is intentionally omitted.
*
* All requests go through the Vite dev proxy at `/hasura/*`, which rewrites to
* `https://api.workolik.com/api/rest/*` and injects the `x-hasura-admin-secret`
* header server-side (see vite.config.ts). The secret never reaches the browser.
*
* Components should not call these directly — use the TanStack Query hooks in
* `./queries`, which add caching, dedup, and loading/error state.
*/
const HASURA_BASE = '/hasura';
/** Tenant whose live data the dashboard displays. */
export const DEFAULT_TENANT_ID = 1087;
/** Order-module config the tenant's order summary is computed against. */
export const DEFAULT_CONFIG_ID = 1;
type QueryParams = Record<string, string | number | undefined | null>;
async function hasuraGet<T = unknown>(endpoint: string, params: QueryParams = {}): Promise<T> {
const qs = new URLSearchParams();
Object.entries(params).forEach(([k, v]) => {
if (v !== undefined && v !== null && v !== '') qs.append(k, String(v));
});
const query = qs.toString();
const res = await fetch(`${HASURA_BASE}/${endpoint}${query ? `?${query}` : ''}`, {
headers: { Accept: 'application/json' },
});
if (!res.ok) {
throw new Error(`Hasura ${endpoint} failed: ${res.status} ${res.statusText}`);
}
return res.json() as Promise<T>;
}
/**
* Hasura REST responses come back in a few shapes depending on the route:
* - a bare array: [ {...}, {...} ]
* - an envelope with `details`: { details: [...] } or { details: {...} }
* - an envelope keyed by the table: { tenants: [...] }, { orders: [...] }
* This normalizes any of those to an array of rows.
*/
export function toRows<T = Record<string, unknown>>(json: unknown): T[] {
if (Array.isArray(json)) return json as T[];
if (json && typeof json === 'object') {
const obj = json as Record<string, unknown>;
if (Array.isArray(obj.details)) return obj.details as T[];
if (obj.details && typeof obj.details === 'object') return [obj.details as T];
if (Array.isArray(obj.data)) return obj.data as T[];
// Envelope keyed by the route/table name (Hasura's default for REST endpoints).
const firstArray = Object.values(obj).find((v) => Array.isArray(v));
if (Array.isArray(firstArray)) return firstArray as T[];
}
return [];
}
function firstRow<T = Record<string, unknown>>(json: unknown): T | null {
const rows = toRows<T>(json);
return rows.length ? rows[0] : null;
}
function n(v: unknown): number {
const num = typeof v === 'number' ? v : Number(v);
return Number.isFinite(num) ? num : 0;
}
type Row = Record<string, unknown>;
/** ISO datetime helpers for endpoints that expect full timestamps. */
export const startOfDay = (d: string) => (d.includes('T') ? d : `${d}T00:00:00`);
export const endOfDay = (d: string) => (d.includes('T') ? d : `${d}T23:59:59`);
// ════════════════════════════════════════════════════════════════════════════
// USERS
// ════════════════════════════════════════════════════════════════════════════
/** /getusers?roleid=&tenantid=&limit=&offset= — list users in the system. */
export async function getUsers(opts: {
tenantid: number;
roleid?: number;
limit?: number;
offset?: number;
}): Promise<Row[]> {
return toRows(
await hasuraGet('getusers', {
tenantid: opts.tenantid,
roleid: opts.roleid,
limit: opts.limit ?? 10,
offset: opts.offset ?? 0,
}),
);
}
/** /getapproles?configid= — all application roles for a config. */
export async function getAppRoles(configid: number = 15): Promise<Row[]> {
return toRows(await hasuraGet('getapproles', { configid }));
}
// ════════════════════════════════════════════════════════════════════════════
// ORDERS
// ════════════════════════════════════════════════════════════════════════════
/**
* /getorders?start=&end=&status=&limit=&offset= — detailed orders in a window.
* NOTE: this is a global feed (no tenant filter accepted) and `status` is required.
*/
export async function getOrders(opts: {
start: string; // ISO with time
end: string;
status: string; // e.g. 'delivered' | 'created' | 'cancelled'
limit?: number;
offset?: number;
}): Promise<Row[]> {
return toRows(
await hasuraGet('getorders', {
start: startOfDay(opts.start),
end: endOfDay(opts.end),
status: opts.status,
limit: opts.limit ?? 10,
offset: opts.offset ?? 0,
}),
);
}
export interface OrderSummary {
total: number;
created: number;
pending: number;
processing: number;
delivered: number;
cancelled: number;
tenantid?: number;
tenantname?: string;
}
/** /getordersummary?tenantid=&configid=&fromdate=&todate= — aggregated order counts. */
export async function getOrderSummary(
tenantid: number,
fromdate: string,
todate: string,
configid: number = DEFAULT_CONFIG_ID,
): Promise<OrderSummary | null> {
// Workolik returns counts as { total: { aggregate: { count: N } }, ... } plus a
// `tenants` array. (The fiesta variant returns flat numbers.) Read both shapes.
const json = await hasuraGet<Row>('getordersummary/', { tenantid, fromdate, todate, configid });
if (!json || typeof json !== 'object') return null;
const count = (key: string): number => {
const node = (json as Row)[key];
if (node && typeof node === 'object' && 'aggregate' in (node as Row)) {
const agg = (node as { aggregate?: { count?: unknown } }).aggregate;
return n(agg?.count);
}
return n(node);
};
const tenants = (json as { tenants?: Row[] }).tenants;
const tenantRow = Array.isArray(tenants) && tenants.length ? tenants[0] : null;
return {
total: count('total'),
created: count('created'),
pending: count('pending'),
processing: count('processing'),
delivered: count('delivered'),
cancelled: count('cancelled'),
tenantid: tenantRow?.tenantid != null ? n(tenantRow.tenantid) : undefined,
tenantname: typeof tenantRow?.tenantname === 'string' ? (tenantRow.tenantname as string) : undefined,
};
}
// ════════════════════════════════════════════════════════════════════════════
// TENANTS
// ════════════════════════════════════════════════════════════════════════════
/** /gettenantinfo?tenantid= — info about a tenant. */
export async function getTenantInfo(tenantid: number): Promise<Row | null> {
return firstRow(await hasuraGet('gettenantinfo', { tenantid }));
}
/** /gettenantlocations?tenantid= — physical locations linked to a tenant. */
export async function getTenantLocations(tenantid: number): Promise<Row[]> {
return toRows(await hasuraGet('gettenantlocations', { tenantid }));
}
/** /getcustomersbytenant?tenantid=&limit=&offset= — customers for a tenant. */
export async function getCustomersByTenant(opts: {
tenantid: number;
limit?: number;
offset?: number;
}): Promise<Row[]> {
return toRows(
await hasuraGet('getcustomersbytenant', {
tenantid: opts.tenantid,
limit: opts.limit ?? 10,
offset: opts.offset ?? 0,
}),
);
}
/** /gettenantcustomers?tenantid=&locationid=&limit=&offset= — customers under a tenant+location. */
export async function getTenantCustomers(opts: {
tenantid: number;
locationid: number;
limit?: number;
offset?: number;
}): Promise<Row[]> {
return toRows(
await hasuraGet('gettenantcustomers', {
tenantid: opts.tenantid,
locationid: opts.locationid,
limit: opts.limit ?? 10,
offset: opts.offset ?? 0,
}),
);
}
/** /gettenantdeliveries?tenantid=&status=&fromdate=&todate=&keyword=&limit=&offset= */
export async function getTenantDeliveries(opts: {
tenantid: number;
status?: string;
fromdate: string; // ISO with time
todate: string;
keyword?: string;
limit?: number;
offset?: number;
}): Promise<Row[]> {
return toRows(
await hasuraGet('gettenantdeliveries', {
tenantid: opts.tenantid,
status: opts.status,
fromdate: startOfDay(opts.fromdate),
todate: endOfDay(opts.todate),
keyword: opts.keyword ?? '%',
limit: opts.limit ?? 10,
offset: opts.offset ?? 0,
}),
);
}
// ════════════════════════════════════════════════════════════════════════════
// PRODUCTS
// ════════════════════════════════════════════════════════════════════════════
/** /getproductcategories?moduleid= — root product categories for a module. */
export async function getProductCategories(moduleid: number): Promise<Row[]> {
return toRows(await hasuraGet('getproductcategories', { moduleid }));
}
/** /getsubcategory?moduleid=&categoryid= — a subcategory under a root category. */
export async function getSubcategory(moduleid: number, categoryid: number): Promise<Row[]> {
return toRows(await hasuraGet('getsubcategory', { moduleid, categoryid }));
}
/** /getproductsubcategories?categoryid= — all subcategories under a root category. */
export async function getProductSubcategories(categoryid: number): Promise<Row[]> {
return toRows(await hasuraGet('getproductsubcategories', { categoryid }));
}
/** /getproductvariants?tenantid=&subcategoryid= — product variants for a tenant. */
export async function getProductVariants(tenantid: number, subcategoryid: number): Promise<Row[]> {
return toRows(await hasuraGet('getproductvariants', { tenantid, subcategoryid }));
}
/** /getstockstatement?tenantid=&locationid=&subcategoryid=&keyword=&limit=&offset= */
export async function getStockStatement(opts: {
tenantid: number;
locationid: number;
subcategoryid?: number;
keyword?: string;
limit?: number;
offset?: number;
}): Promise<Row[]> {
return toRows(
await hasuraGet('getstockstatement', {
tenantid: opts.tenantid,
locationid: opts.locationid,
subcategoryid: opts.subcategoryid,
keyword: opts.keyword ?? '%',
limit: opts.limit ?? 10,
offset: opts.offset ?? 0,
}),
);
}
/** /getproductscount?tenantid=&categoryid=&subcategoryid= — available/out-of-stock counts. */
export async function getProductsCount(opts: {
tenantid: number;
categoryid: number;
subcategoryid?: number;
}): Promise<Row | null> {
return firstRow(
await hasuraGet('getproductscount', {
tenantid: opts.tenantid,
categoryid: opts.categoryid,
subcategoryid: opts.subcategoryid,
}),
);
}
// ════════════════════════════════════════════════════════════════════════════
// APPS & LOCATIONS
// ════════════════════════════════════════════════════════════════════════════
/** /getapplocations — all currently active locations globally. */
export async function getAppLocations(): Promise<Row[]> {
return toRows(await hasuraGet('getapplocations'));
}
/** /getapplocationconfig — global configuration for application locations. */
export async function getAppLocationConfig(): Promise<Row[]> {
return toRows(await hasuraGet('getapplocationconfig'));
}
/** /getapptypes?tag= — application types grouped by a tag (e.g. 'partner', 'DELIVERY'). */
export async function getAppTypes(tag: string): Promise<Row[]> {
return toRows(await hasuraGet('getapptypes', { tag }));
}
// ════════════════════════════════════════════════════════════════════════════
// PARTNERS
// ════════════════════════════════════════════════════════════════════════════
/** /getpartners?applocationid=&partnerid=&limit=&offset= — active partners. */
export async function getPartners(opts: {
applocationid: number;
partnerid?: number;
limit?: number;
offset?: number;
}): Promise<Row[]> {
return toRows(
await hasuraGet('getpartners', {
applocationid: opts.applocationid,
partnerid: opts.partnerid ?? 0,
limit: opts.limit ?? 10,
offset: opts.offset ?? 0,
}),
);
}
/** /getridershifts?applocationid= — historic + active rider shift records. */
export async function getRiderShifts(applocationid: number): Promise<Row[]> {
return toRows(await hasuraGet('getridershifts', { applocationid }));
}
// ════════════════════════════════════════════════════════════════════════════
// INVOICE (used by the dashboard; not part of a public docs section)
// ════════════════════════════════════════════════════════════════════════════
export interface InvoiceInsight {
revenue: number;
profit: number;
raw: Row;
}
/** /getinvoiceinsight?tenantid= — revenue / financial roll-up for a tenant. */
export async function getInvoiceInsight(tenantid: number): Promise<InvoiceInsight | null> {
const row = firstRow<Row>(await hasuraGet('getinvoiceinsight', { tenantid }));
if (!row) return null;
const pick = (...keys: string[]) => {
for (const k of keys) {
if (row[k] != null && row[k] !== '') return n(row[k]);
}
return 0;
};
return {
revenue: pick('revenue', 'totalrevenue', 'grossrevenue', 'sales', 'totalsales', 'totalamount'),
profit: pick('profit', 'netprofit', 'margin', 'totalprofit'),
raw: row,
};
}
// ════════════════════════════════════════════════════════════════════════════
// DASHBOARD AGGREGATE
// ════════════════════════════════════════════════════════════════════════════
export interface DashboardKpis {
totalOrders: number;
delivered: number;
// null when the tenant has no invoice/revenue records (so the card can show "—"
// instead of a misleading ₹0).
todaysRevenue: number | null;
monthlyProfit: number | null;
tenantName?: string;
}
const ymd = (d: Date) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
/**
* Pulls the four dashboard KPIs from live data for the given tenant:
* - Total Orders ← month-to-date order summary `.total`
* - Delivered ← month-to-date order summary `.delivered`
* - Today's Revenue ← invoice insight revenue (null if the tenant has none)
* - Monthly Profit ← invoice insight profit (null if the tenant has none)
*/
export async function getDashboardKpis(tenantid: number = DEFAULT_TENANT_ID): Promise<DashboardKpis> {
const today = new Date();
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const [monthSummary, insight] = await Promise.all([
getOrderSummary(tenantid, ymd(monthStart), ymd(today)),
getInvoiceInsight(tenantid).catch(() => null),
]);
return {
totalOrders: monthSummary?.total ?? 0,
delivered: monthSummary?.delivered ?? 0,
todaysRevenue: insight ? insight.revenue : null,
monthlyProfit: insight ? insight.profit : null,
tenantName: monthSummary?.tenantname,
};
}

462
src/services/fiestaApi.ts Normal file
View File

@@ -0,0 +1,462 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Fiesta REST client — the merchant-facing `live/api/v1/web/*` surface served by
* https://fiesta.nearle.app (documented at developer.nearledaily.com under the
* REST tab). This is the operational backend: order/delivery/location summaries,
* the deliveries board, riders, stock statements, and customers.
*
* Requests go through the Vite dev proxy at `/fiesta/*`, which forwards to
* `https://fiesta.nearle.app/*` (see vite.config.ts). Fiesta is CORS-enabled and
* needs no auth header for these read endpoints.
*
* This sits alongside `./api` (the Hasura/workolik REST surface the dashboard
* uses). Components should call the TanStack hooks in `./fiestaQueries`, not
* these functions directly.
*/
const FIESTA_BASE = '/fiesta/live/api/v1/web';
/** Tenant / location scope shared by the merchant console (Ragul Stores, Coimbatore). */
export const FIESTA_TENANT_ID = 1087;
export const FIESTA_APPLOCATION_ID = 1;
/** Primary outlet for this tenant — the one carrying live orders/stock. */
export const FIESTA_PRIMARY_LOCATION_ID = 1097;
export type Row = Record<string, unknown>;
type QueryParams = Record<string, string | number | undefined | null>;
async function fiestaGet<T = unknown>(endpoint: string, params: QueryParams = {}): Promise<T> {
const qs = new URLSearchParams();
Object.entries(params).forEach(([k, v]) => {
// Fiesta requires some params to be present-but-empty (e.g. keyword=), so we
// keep empty strings and only drop undefined/null.
if (v !== undefined && v !== null) qs.append(k, String(v));
});
const query = qs.toString();
const res = await fetch(`${FIESTA_BASE}/${endpoint}${query ? `?${query}` : ''}`, {
headers: { Accept: 'application/json' },
});
if (!res.ok) {
throw new Error(`Fiesta ${endpoint} failed: ${res.status} ${res.statusText}`);
}
return res.json() as Promise<T>;
}
async function fiestaSend<T = unknown>(
endpoint: string,
method: 'POST' | 'PUT' | 'DELETE',
body?: unknown,
): Promise<T> {
const res = await fetch(`${FIESTA_BASE}/${endpoint}`, {
method,
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const json = (await res.json().catch(() => null)) as
| { message?: string; status?: boolean }
| null;
if (!res.ok || (json && json.status === false)) {
throw new Error(json?.message || `Fiesta ${endpoint} failed: ${res.status} ${res.statusText}`);
}
return json as T;
}
/**
* Fiesta envelopes responses as `{ code, details, message, status }`. `details`
* is usually an array of rows, sometimes a single object, sometimes null.
*/
export function toRows<T = Row>(json: unknown): T[] {
if (Array.isArray(json)) return json as T[];
if (json && typeof json === 'object') {
const d = (json as { details?: unknown }).details;
if (Array.isArray(d)) return d as T[];
if (d && typeof d === 'object') return [d as T];
}
return [];
}
export function firstRow<T = Row>(json: unknown): T | null {
const d = (json as { details?: unknown })?.details;
if (Array.isArray(d)) return (d.length ? (d[0] as T) : null);
if (d && typeof d === 'object') return d as T;
return null;
}
export function num(v: unknown): number {
const n = typeof v === 'number' ? v : Number(v);
return Number.isFinite(n) ? n : 0;
}
export const str = (v: unknown): string => (v == null ? '' : String(v));
/** Fiesta date params want a bare `YYYY-MM-DD`. */
export const ymd = (d: Date) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
// ════════════════════════════════════════════════════════════════════════════
// ORDERS
// ════════════════════════════════════════════════════════════════════════════
export interface FiestaOrderSummary {
total: number;
created: number;
pending: number;
processing: number;
delivered: number;
cancelled: number;
tenantid?: number;
tenantname?: string;
}
/** /orders/getordersummary?tenantid=&fromdate=&todate= — flat order counts. */
export async function getOrderSummary(
tenantid: number,
fromdate: string,
todate: string,
): Promise<FiestaOrderSummary | null> {
const row = firstRow<Row>(await fiestaGet('orders/getordersummary', { tenantid, fromdate, todate }));
if (!row) return null;
return {
total: num(row.total),
created: num(row.created),
pending: num(row.pending),
processing: num(row.processing),
delivered: num(row.delivered),
cancelled: num(row.cancelled),
tenantid: row.tenantid != null ? num(row.tenantid) : undefined,
tenantname: typeof row.tenantname === 'string' ? row.tenantname : undefined,
};
}
export interface FiestaLocationSummary {
locationid: number;
locationname: string;
total: number;
created: number;
pending: number;
processing: number;
delivered: number;
cancelled: number;
}
/** /orders/getlocationsummary?tenantid= — per-outlet order rollup. */
export async function getLocationSummary(tenantid: number): Promise<FiestaLocationSummary[]> {
return toRows<Row>(await fiestaGet('orders/getlocationsummary', { tenantid })).map((r) => ({
locationid: num(r.locationid),
locationname: str(r.locationname),
total: num(r.total),
created: num(r.created),
pending: num(r.pending),
processing: num(r.processing),
delivered: num(r.delivered),
cancelled: num(r.cancelled),
}));
}
/** /orders/getorderinsight?tenantid= — per-location monthly order counts. */
export async function getOrderInsight(tenantid: number): Promise<Row[]> {
return toRows(await fiestaGet('orders/getorderinsight', { tenantid }));
}
/** /orders/getorders?tenantid=&status=&fromdate=&todate=&pageno=&pagesize= — orders board. */
export async function getOrders(opts: {
tenantid: number;
status: string;
fromdate: string;
todate: string;
pageno?: number;
pagesize?: number;
}): Promise<Row[]> {
return toRows(
await fiestaGet('orders/getorders', {
tenantid: opts.tenantid,
status: opts.status,
fromdate: opts.fromdate,
todate: opts.todate,
pageno: opts.pageno ?? 1,
pagesize: opts.pagesize ?? 20,
}),
);
}
// ════════════════════════════════════════════════════════════════════════════
// DELIVERIES
// ════════════════════════════════════════════════════════════════════════════
export interface FiestaDeliverySummary {
total: number;
created: number;
pending: number;
accepted: number;
arrived: number;
picked: number;
active: number;
delivered: number;
cancelled: number;
}
/** /deliveries/deliverysummary?tenantid=&applocationid=&fromdate=&todate= — dispatch counts. */
export async function getDeliverySummary(opts: {
tenantid: number;
applocationid?: number;
fromdate: string;
todate: string;
}): Promise<FiestaDeliverySummary | null> {
const row = firstRow<Row>(
await fiestaGet('deliveries/deliverysummary', {
tenantid: opts.tenantid,
applocationid: opts.applocationid ?? FIESTA_APPLOCATION_ID,
fromdate: opts.fromdate,
todate: opts.todate,
}),
);
if (!row) return null;
return {
total: num(row.total),
created: num(row.created),
pending: num(row.pending),
accepted: num(row.accepted),
arrived: num(row.arrived),
picked: num(row.picked),
active: num(row.active),
delivered: num(row.delivered),
cancelled: num(row.cancelled),
};
}
/** /deliveries/getdeliveries?tenantid=&fromdate=&todate= — the master deliveries board. */
export async function getDeliveries(opts: {
tenantid: number;
fromdate: string;
todate: string;
}): Promise<Row[]> {
return toRows(
await fiestaGet('deliveries/getdeliveries', {
tenantid: opts.tenantid,
fromdate: opts.fromdate,
todate: opts.todate,
}),
);
}
/** /deliveries/getdeliveryinsight?tenantid= — daily delivery insight. */
export async function getDeliveryInsight(tenantid: number): Promise<Row[]> {
return toRows(await fiestaGet('deliveries/getdeliveryinsight', { tenantid }));
}
// ════════════════════════════════════════════════════════════════════════════
// PARTNERS / RIDERS
// ════════════════════════════════════════════════════════════════════════════
/** /partners/getriders?applocationid=&tenantid= — active rider fleet. */
export async function getRiders(opts: {
applocationid?: number;
tenantid: number;
}): Promise<Row[]> {
return toRows(
await fiestaGet('partners/getriders', {
applocationid: opts.applocationid ?? FIESTA_APPLOCATION_ID,
tenantid: opts.tenantid,
}),
);
}
/** /partners/getridershifts?applocationid= — rider shift records. */
export async function getRiderShifts(applocationid: number = FIESTA_APPLOCATION_ID): Promise<Row[]> {
return toRows(await fiestaGet('partners/getridershifts', { applocationid }));
}
// ════════════════════════════════════════════════════════════════════════════
// TENANTS / CUSTOMERS
// ════════════════════════════════════════════════════════════════════════════
/** /tenants/gettenantlocations?tenantid= — outlet locations for a tenant. */
export async function getTenantLocations(tenantid: number): Promise<Row[]> {
return toRows(await fiestaGet('tenants/gettenantlocations', { tenantid }));
}
/** /tenants/getalltenants?applocationid=&status=&pageno=&pagesize= — active tenants. */
export async function getAllTenants(opts: {
applocationid?: number;
status?: string;
pageno?: number;
pagesize?: number;
} = {}): Promise<Row[]> {
return toRows(
await fiestaGet('tenants/getalltenants', {
applocationid: opts.applocationid ?? FIESTA_APPLOCATION_ID,
status: opts.status ?? 'Active',
pageno: opts.pageno ?? 1,
pagesize: opts.pagesize ?? 20,
}),
);
}
/** /customers/gettenantcustomers?tenantid=&locationid=&pageno=&pagesize=&keyword= */
export async function getTenantCustomers(opts: {
tenantid: number;
locationid: number;
keyword?: string;
pageno?: number;
pagesize?: number;
}): Promise<Row[]> {
return toRows(
await fiestaGet('customers/gettenantcustomers', {
tenantid: opts.tenantid,
locationid: opts.locationid,
keyword: opts.keyword ?? '',
pageno: opts.pageno ?? 1,
pagesize: opts.pagesize ?? 20,
}),
);
}
// ════════════════════════════════════════════════════════════════════════════
// PRODUCTS / STOCK
// ════════════════════════════════════════════════════════════════════════════
/** /products/getstockstatement?tenantid=&locationid=&subcategoryid=&keyword=&pageno=&pagesize= */
export async function getStockStatement(opts: {
tenantid: number;
locationid: number;
subcategoryid?: number;
keyword?: string;
pageno?: number;
pagesize?: number;
}): Promise<Row[]> {
return toRows(
await fiestaGet('products/getstockstatement', {
tenantid: opts.tenantid,
locationid: opts.locationid,
subcategoryid: opts.subcategoryid,
keyword: opts.keyword ?? '',
pageno: opts.pageno ?? 1,
pagesize: opts.pagesize ?? 50,
}),
);
}
/** /products/getproductscount?tenantid=&categoryid=&subcategoryid=&approve= */
export async function getProductsCount(opts: {
tenantid: number;
categoryid: number;
subcategoryid?: number;
}): Promise<Row | null> {
return firstRow(
await fiestaGet('products/getproductscount', {
tenantid: opts.tenantid,
categoryid: opts.categoryid,
subcategoryid: opts.subcategoryid,
approve: 1,
}),
);
}
// ════════════════════════════════════════════════════════════════════════════
// USERS
// ════════════════════════════════════════════════════════════════════════════
/** Best-effort role label from the numeric roleid (roles aren't fully resolvable for every config). */
export function roleName(roleid: number): string {
const map: Record<number, string> = {
0: 'Unassigned',
1: 'Owner',
2: 'Manager',
3: 'Admin',
4: 'Staff',
5: 'Rider',
6: 'Cashier',
};
return map[roleid] || `Role ${roleid}`;
}
/** /users/getallusers?roleid=&tenantid=&pageno=&pagesize=&keyword= — staff/users under a tenant. */
export async function getAllUsers(opts: {
tenantid: number;
roleid?: number;
keyword?: string;
pageno?: number;
pagesize?: number;
}): Promise<Row[]> {
return toRows(
await fiestaGet('users/getallusers', {
tenantid: opts.tenantid,
roleid: opts.roleid,
keyword: opts.keyword ?? '',
pageno: opts.pageno ?? 1,
pagesize: opts.pagesize ?? 50,
}),
);
}
/** /users/getusers?userid= — a single user profile. */
export async function getUserById(userid: number): Promise<Row | null> {
return firstRow(await fiestaGet('users/getusers', { userid }));
}
export interface CreateUserInput {
firstname: string;
lastname?: string;
email: string;
contactno: string;
password: string;
roleid: number;
dialcode?: string;
pin?: number;
address?: string;
suburb?: string;
city?: string;
state?: string;
postcode?: string;
tenantid: number;
locationid?: number;
applocationid?: number;
status?: string;
}
/** POST /users/create — register a new web staff user. */
export async function createUser(input: CreateUserInput): Promise<Row> {
return fiestaSend<Row>('users/create', 'POST', {
authname: input.email,
firstname: input.firstname,
lastname: input.lastname ?? '',
password: input.password,
email: input.email,
dialcode: input.dialcode ?? '+91',
contactno: input.contactno,
roleid: input.roleid,
pin: input.pin ?? 0,
address: input.address ?? '',
suburb: input.suburb ?? '',
city: input.city ?? '',
state: input.state ?? '',
postcode: input.postcode ?? '',
tenantid: input.tenantid,
locationid: input.locationid ?? 0,
applocationid: input.applocationid ?? FIESTA_APPLOCATION_ID,
status: input.status ?? 'active',
});
}
export interface UpdateUserInput {
userid: number;
firstname?: string;
lastname?: string;
email?: string;
contactno?: string;
address?: string;
suburb?: string;
city?: string;
state?: string;
postcode?: string;
status?: string;
}
/** PUT /users/update — update an existing web staff user. */
export async function updateUser(input: UpdateUserInput): Promise<Row> {
return fiestaSend<Row>('users/update', 'PUT', input);
}

View File

@@ -0,0 +1,124 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Adapters that turn raw Fiesta REST rows into the display shapes the existing
* views already render (ProductMatrixItem, InventoryItem, CustomerOrder). Keeping
* the mapping here lets several views (Inventory, Operations, Reports, Orders)
* share one source of truth and keeps the components focused on presentation.
*/
import { ProductMatrixItem, InventoryItem, CustomerOrder } from '../types';
import { num, str, Row } from './fiestaApi';
/** Best-effort category label from the numeric category id Fiesta returns. */
export function categoryName(categoryid: number): string {
const map: Record<number, string> = {
1: 'Food & Dining',
2: 'Grocery & Daily',
3: 'Pharmacy',
4: 'Retail',
};
return map[categoryid] || `Category ${categoryid}`;
}
const PLACEHOLDER_IMG =
'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200';
/** Derive a healthy/low/critical status from a closing balance. */
function stockStatus(closing: number): InventoryItem['status'] {
if (closing <= 0) return 'Critical';
if (closing < 25) return 'Critical';
if (closing < 120) return 'Low Stock';
return 'Optimal';
}
/** stock-statement row -> catalog card. */
export function stockRowToProduct(row: Row): ProductMatrixItem {
const closing = num(row.closing);
const cost = num(row.productcost);
const retail = num(row.retailprice) || cost;
const sold = Math.max(0, num(row.debit));
const status = stockStatus(closing);
return {
id: str(row.productid) || str(row.productname),
name: str(row.productname) || 'Unnamed product',
sku: `SKU-${str(row.productid)}`,
unitsSold: sold,
revenue: Math.round(sold * retail),
stockStatus: status === 'Optimal' ? 'Healthy' : status,
trend: num(row.credit) > num(row.debit) ? 'up' : num(row.debit) > 0 ? 'down' : 'flat',
image: str(row.productimage) || PLACEHOLDER_IMG,
category: categoryName(num(row.categoryid)),
exposure: `${str(row.productunit) || 'unit'} · ${str(row.unitvalue) || '1'}`,
verified: num(row.retailprice) > 0,
};
}
/** stock-statement row -> hub balance ledger entry. */
export function stockRowToInventory(row: Row, locationName: string): InventoryItem {
const closing = num(row.closing);
const opening = num(row.opening);
return {
sku: `SKU-${str(row.productid)}`,
name: str(row.productname) || 'Unnamed product',
warehouse: locationName || `Location ${str(row.locationid)}`,
stockLevel: closing,
maxCapacity: Math.max(opening, closing, 100),
status: stockStatus(closing),
region: categoryName(num(row.categoryid)),
};
}
const ORDER_STATUS_MAP: Record<string, CustomerOrder['status']> = {
delivered: 'DELIVERED',
picked: 'OUT_FOR_DELIVERY',
active: 'OUT_FOR_DELIVERY',
arrived: 'OUT_FOR_DELIVERY',
accepted: 'CONFIRMED',
assigned: 'CONFIRMED',
ready: 'CONFIRMED',
created: 'PROCESSING',
pending: 'PROCESSING',
processing: 'PROCESSING',
};
/** Map a Fiesta delivery/order status string onto the view's status union. */
export function mapOrderStatus(raw: string): CustomerOrder['status'] {
return ORDER_STATUS_MAP[str(raw).toLowerCase()] || 'PROCESSING';
}
/** Format a Fiesta timestamp (ISO or "YYYY-MM-DD HH:mm:ss") into a short time label. */
export function shortTime(raw: unknown): string {
const s = str(raw);
if (!s) return '—';
const m = s.match(/(\d{1,2}):(\d{2})/);
if (m) return `${m[1]}:${m[2]}`;
return s.slice(0, 16).replace('T', ' ');
}
/** deliveries-board row -> dispatch order card. */
export function deliveryRowToOrder(row: Row): CustomerOrder {
const amount = num(row.deliveryamt) || num(row.orderamount);
const rider = str(row.ridername) || str(row.deliveryusername) || str(row.username);
return {
id: str(row.orderid) || `DLV-${str(row.deliveryid)}`,
customerName: str(row.deliverycustomer) || str(row.customername) || 'Customer',
phone: str(row.deliverycontactno) || str(row.contactno) || '—',
address:
str(row.deliveryaddress) ||
str(row.Pickupaddress) ||
str(row.pickupaddress) ||
'Address unavailable',
items: [],
amount,
time: shortTime(row.assigntime || row.deliverydate),
status: mapOrderStatus(str(row.orderstatus)),
assignedRider: rider || 'Pending Assignment',
hub: str(row.pickupcustomer) || str(row.pickuplocation) || `Location ${str(row.locationid)}`,
itemCount: num(row.itemcount),
locationid: num(row.locationid)
};
}

View File

@@ -0,0 +1,248 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
/**
* TanStack Query hooks wrapping the Fiesta REST client in `./fiestaApi`.
*
* Components call these (never fetch directly) to get caching, dedup, and
* loading/error state. These power the operational pages (Inventory, Orders &
* Deliveries, Operations, Reports, Stores/Logistics/Staffing); the Dashboard
* continues to use the Hasura hooks in `./queries`.
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
FIESTA_TENANT_ID,
FIESTA_APPLOCATION_ID,
FIESTA_PRIMARY_LOCATION_ID,
getOrderSummary,
getLocationSummary,
getOrderInsight,
getOrders,
getDeliverySummary,
getDeliveries,
getDeliveryInsight,
getRiders,
getRiderShifts,
getTenantLocations,
getAllTenants,
getTenantCustomers,
getStockStatement,
getProductsCount,
getAllUsers,
getUserById,
createUser,
updateUser,
CreateUserInput,
} from './fiestaApi';
export const fiestaKeys = {
orderSummary: (tenantid: number, fromdate: string, todate: string) =>
['fiesta', 'orderSummary', tenantid, fromdate, todate] as const,
locationSummary: (tenantid: number) => ['fiesta', 'locationSummary', tenantid] as const,
orderInsight: (tenantid: number) => ['fiesta', 'orderInsight', tenantid] as const,
orders: (params: Record<string, unknown>) => ['fiesta', 'orders', params] as const,
deliverySummary: (params: Record<string, unknown>) => ['fiesta', 'deliverySummary', params] as const,
deliveries: (params: Record<string, unknown>) => ['fiesta', 'deliveries', params] as const,
deliveryInsight: (tenantid: number) => ['fiesta', 'deliveryInsight', tenantid] as const,
riders: (params: Record<string, unknown>) => ['fiesta', 'riders', params] as const,
riderShifts: (applocationid: number) => ['fiesta', 'riderShifts', applocationid] as const,
tenantLocations: (tenantid: number) => ['fiesta', 'tenantLocations', tenantid] as const,
allTenants: (params: Record<string, unknown>) => ['fiesta', 'allTenants', params] as const,
tenantCustomers: (params: Record<string, unknown>) => ['fiesta', 'tenantCustomers', params] as const,
stockStatement: (params: Record<string, unknown>) => ['fiesta', 'stockStatement', params] as const,
productsCount: (params: Record<string, unknown>) => ['fiesta', 'productsCount', params] as const,
users: (params: Record<string, unknown>) => ['fiesta', 'users', params] as const,
user: (userid: number) => ['fiesta', 'user', userid] as const,
};
// ── Orders ──────────────────────────────────────────────────────────────────
export function useFiestaOrderSummary(tenantid: number = FIESTA_TENANT_ID, fromdate: string, todate: string) {
return useQuery({
queryKey: fiestaKeys.orderSummary(tenantid, fromdate, todate),
queryFn: () => getOrderSummary(tenantid, fromdate, todate),
enabled: Boolean(tenantid && fromdate && todate),
});
}
export function useFiestaLocationSummary(tenantid: number = FIESTA_TENANT_ID) {
return useQuery({
queryKey: fiestaKeys.locationSummary(tenantid),
queryFn: () => getLocationSummary(tenantid),
enabled: Boolean(tenantid),
});
}
export function useFiestaOrderInsight(tenantid: number = FIESTA_TENANT_ID) {
return useQuery({
queryKey: fiestaKeys.orderInsight(tenantid),
queryFn: () => getOrderInsight(tenantid),
enabled: Boolean(tenantid),
});
}
export function useFiestaOrders(opts: {
tenantid: number;
status: string;
fromdate: string;
todate: string;
pageno?: number;
pagesize?: number;
}) {
return useQuery({
queryKey: fiestaKeys.orders(opts),
queryFn: () => getOrders(opts),
enabled: Boolean(opts.tenantid && opts.status && opts.fromdate && opts.todate),
});
}
// ── Deliveries ────────────────────────────────────────────────────────────────
export function useFiestaDeliverySummary(opts: {
tenantid: number;
applocationid?: number;
fromdate: string;
todate: string;
}) {
return useQuery({
queryKey: fiestaKeys.deliverySummary(opts),
queryFn: () => getDeliverySummary(opts),
enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate),
});
}
export function useFiestaDeliveries(opts: { tenantid: number; fromdate: string; todate: string }) {
return useQuery({
queryKey: fiestaKeys.deliveries(opts),
queryFn: () => getDeliveries(opts),
enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate),
});
}
export function useFiestaDeliveryInsight(tenantid: number = FIESTA_TENANT_ID) {
return useQuery({
queryKey: fiestaKeys.deliveryInsight(tenantid),
queryFn: () => getDeliveryInsight(tenantid),
enabled: Boolean(tenantid),
});
}
// ── Partners / Riders ─────────────────────────────────────────────────────────
export function useFiestaRiders(opts: { applocationid?: number; tenantid: number }) {
return useQuery({
queryKey: fiestaKeys.riders(opts),
queryFn: () => getRiders(opts),
enabled: Boolean(opts.tenantid),
});
}
export function useFiestaRiderShifts(applocationid: number = FIESTA_APPLOCATION_ID) {
return useQuery({
queryKey: fiestaKeys.riderShifts(applocationid),
queryFn: () => getRiderShifts(applocationid),
enabled: Boolean(applocationid),
});
}
// ── Tenants / Customers ─────────────────────────────────────────────────────────
export function useFiestaTenantLocations(tenantid: number = FIESTA_TENANT_ID) {
return useQuery({
queryKey: fiestaKeys.tenantLocations(tenantid),
queryFn: () => getTenantLocations(tenantid),
enabled: Boolean(tenantid),
});
}
export function useFiestaAllTenants(opts: {
applocationid?: number;
status?: string;
pageno?: number;
pagesize?: number;
} = {}) {
return useQuery({
queryKey: fiestaKeys.allTenants(opts),
queryFn: () => getAllTenants(opts),
});
}
export function useFiestaTenantCustomers(opts: {
tenantid: number;
locationid: number;
keyword?: string;
pageno?: number;
pagesize?: number;
}) {
return useQuery({
queryKey: fiestaKeys.tenantCustomers(opts),
queryFn: () => getTenantCustomers(opts),
enabled: Boolean(opts.tenantid && opts.locationid),
});
}
// ── Products / Stock ─────────────────────────────────────────────────────────
export function useFiestaStockStatement(opts: {
tenantid: number;
locationid: number;
subcategoryid?: number;
keyword?: string;
pageno?: number;
pagesize?: number;
}) {
return useQuery({
queryKey: fiestaKeys.stockStatement(opts),
queryFn: () => getStockStatement(opts),
enabled: Boolean(opts.tenantid && opts.locationid),
});
}
export function useFiestaProductsCount(opts: { tenantid: number; categoryid: number; subcategoryid?: number }) {
return useQuery({
queryKey: fiestaKeys.productsCount(opts),
queryFn: () => getProductsCount(opts),
enabled: Boolean(opts.tenantid && opts.categoryid),
});
}
// ── Users ─────────────────────────────────────────────────────────────────────
export function useFiestaUsers(opts: {
tenantid: number;
roleid?: number;
keyword?: string;
pageno?: number;
pagesize?: number;
}) {
return useQuery({
queryKey: fiestaKeys.users(opts),
queryFn: () => getAllUsers(opts),
enabled: Boolean(opts.tenantid),
});
}
export function useFiestaUser(userid: number) {
return useQuery({
queryKey: fiestaKeys.user(userid),
queryFn: () => getUserById(userid),
enabled: Boolean(userid),
});
}
/** Create a user, then refresh every users list on success. */
export function useFiestaCreateUser() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: CreateUserInput) => createUser(input),
onSuccess: () => qc.invalidateQueries({ queryKey: ['fiesta', 'users'] }),
});
}
/** Update a user, then refresh every users list on success. */
export function useFiestaUpdateUser() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: Parameters<typeof updateUser>[0]) => updateUser(input),
onSuccess: () => qc.invalidateQueries({ queryKey: ['fiesta', 'users'] }),
});
}
export { FIESTA_TENANT_ID, FIESTA_APPLOCATION_ID, FIESTA_PRIMARY_LOCATION_ID };

283
src/services/queries.ts Normal file
View File

@@ -0,0 +1,283 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
/**
* TanStack Query hooks that wrap the raw API functions in `./api`.
*
* Components call these hooks (never `fetch`/the api functions directly) so they
* get caching, dedup, loading/error state, and refetching for free. Covers the
* non-mobile `/api/rest/*` catalog from developer.nearledaily.com.
*/
import { useQuery } from '@tanstack/react-query';
import {
DEFAULT_TENANT_ID,
DEFAULT_CONFIG_ID,
getDashboardKpis,
// Users
getUsers,
getAppRoles,
// Orders
getOrders,
getOrderSummary,
// Tenants
getTenantInfo,
getTenantLocations,
getCustomersByTenant,
getTenantCustomers,
getTenantDeliveries,
// Products
getProductCategories,
getSubcategory,
getProductSubcategories,
getProductVariants,
getStockStatement,
getProductsCount,
// Apps & Locations
getAppLocations,
getAppLocationConfig,
getAppTypes,
// Partners
getPartners,
getRiderShifts,
// Invoice
getInvoiceInsight,
} from './api';
/** Centralized, stable query keys — keep all cache keys discoverable in one place. */
export const queryKeys = {
dashboardKpis: (tenantid: number) => ['dashboardKpis', tenantid] as const,
// Users
users: (params: Record<string, unknown>) => ['users', params] as const,
appRoles: (configid: number) => ['appRoles', configid] as const,
// Orders
orders: (params: Record<string, unknown>) => ['orders', params] as const,
orderSummary: (tenantid: number, fromdate: string, todate: string, configid: number) =>
['orderSummary', tenantid, fromdate, todate, configid] as const,
// Tenants
tenantInfo: (tenantid: number) => ['tenantInfo', tenantid] as const,
tenantLocations: (tenantid: number) => ['tenantLocations', tenantid] as const,
customersByTenant: (params: Record<string, unknown>) => ['customersByTenant', params] as const,
tenantCustomers: (params: Record<string, unknown>) => ['tenantCustomers', params] as const,
tenantDeliveries: (params: Record<string, unknown>) => ['tenantDeliveries', params] as const,
// Products
productCategories: (moduleid: number) => ['productCategories', moduleid] as const,
subcategory: (moduleid: number, categoryid: number) => ['subcategory', moduleid, categoryid] as const,
productSubcategories: (categoryid: number) => ['productSubcategories', categoryid] as const,
productVariants: (tenantid: number, subcategoryid: number) =>
['productVariants', tenantid, subcategoryid] as const,
stockStatement: (params: Record<string, unknown>) => ['stockStatement', params] as const,
productsCount: (params: Record<string, unknown>) => ['productsCount', params] as const,
// Apps & Locations
appLocations: () => ['appLocations'] as const,
appLocationConfig: () => ['appLocationConfig'] as const,
appTypes: (tag: string) => ['appTypes', tag] as const,
// Partners
partners: (params: Record<string, unknown>) => ['partners', params] as const,
riderShifts: (applocationid: number) => ['riderShifts', applocationid] as const,
// Invoice
invoiceInsight: (tenantid: number) => ['invoiceInsight', tenantid] as const,
};
// ── Dashboard ─────────────────────────────────────────────────────────────────
export function useDashboardKpis(tenantid: number = DEFAULT_TENANT_ID) {
return useQuery({
queryKey: queryKeys.dashboardKpis(tenantid),
queryFn: () => getDashboardKpis(tenantid),
});
}
// ── Users ───────────────────────────────────────────────────────────────────
export function useUsers(opts: { tenantid: number; roleid?: number; limit?: number; offset?: number }) {
return useQuery({
queryKey: queryKeys.users(opts),
queryFn: () => getUsers(opts),
enabled: Boolean(opts.tenantid),
});
}
export function useAppRoles(configid = 15) {
return useQuery({ queryKey: queryKeys.appRoles(configid), queryFn: () => getAppRoles(configid) });
}
// ── Orders ────────────────────────────────────────────────────────────────────
export function useOrders(opts: {
start: string;
end: string;
status: string;
limit?: number;
offset?: number;
}) {
return useQuery({
queryKey: queryKeys.orders(opts),
queryFn: () => getOrders(opts),
enabled: Boolean(opts.start && opts.end && opts.status),
});
}
export function useOrderSummary(
tenantid: number,
fromdate: string,
todate: string,
configid: number = DEFAULT_CONFIG_ID,
) {
return useQuery({
queryKey: queryKeys.orderSummary(tenantid, fromdate, todate, configid),
queryFn: () => getOrderSummary(tenantid, fromdate, todate, configid),
enabled: Boolean(tenantid && fromdate && todate),
});
}
// ── Tenants ───────────────────────────────────────────────────────────────────
export function useTenantInfo(tenantid: number = DEFAULT_TENANT_ID) {
return useQuery({ queryKey: queryKeys.tenantInfo(tenantid), queryFn: () => getTenantInfo(tenantid) });
}
export function useTenantLocations(tenantid: number = DEFAULT_TENANT_ID) {
return useQuery({
queryKey: queryKeys.tenantLocations(tenantid),
queryFn: () => getTenantLocations(tenantid),
});
}
export function useCustomersByTenant(opts: { tenantid: number; limit?: number; offset?: number }) {
return useQuery({
queryKey: queryKeys.customersByTenant(opts),
queryFn: () => getCustomersByTenant(opts),
enabled: Boolean(opts.tenantid),
});
}
export function useTenantCustomers(opts: {
tenantid: number;
locationid: number;
limit?: number;
offset?: number;
}) {
return useQuery({
queryKey: queryKeys.tenantCustomers(opts),
queryFn: () => getTenantCustomers(opts),
enabled: Boolean(opts.tenantid && opts.locationid),
});
}
export function useTenantDeliveries(opts: {
tenantid: number;
status?: string;
fromdate: string;
todate: string;
keyword?: string;
limit?: number;
offset?: number;
}) {
return useQuery({
queryKey: queryKeys.tenantDeliveries(opts),
queryFn: () => getTenantDeliveries(opts),
enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate),
});
}
// ── Products ──────────────────────────────────────────────────────────────────
export function useProductCategories(moduleid: number) {
return useQuery({
queryKey: queryKeys.productCategories(moduleid),
queryFn: () => getProductCategories(moduleid),
enabled: Boolean(moduleid),
});
}
export function useSubcategory(moduleid: number, categoryid: number) {
return useQuery({
queryKey: queryKeys.subcategory(moduleid, categoryid),
queryFn: () => getSubcategory(moduleid, categoryid),
enabled: Boolean(moduleid && categoryid),
});
}
export function useProductSubcategories(categoryid: number) {
return useQuery({
queryKey: queryKeys.productSubcategories(categoryid),
queryFn: () => getProductSubcategories(categoryid),
enabled: Boolean(categoryid),
});
}
export function useProductVariants(tenantid: number, subcategoryid: number) {
return useQuery({
queryKey: queryKeys.productVariants(tenantid, subcategoryid),
queryFn: () => getProductVariants(tenantid, subcategoryid),
enabled: Boolean(tenantid && subcategoryid),
});
}
export function useStockStatement(opts: {
tenantid: number;
locationid: number;
subcategoryid?: number;
keyword?: string;
limit?: number;
offset?: number;
}) {
return useQuery({
queryKey: queryKeys.stockStatement(opts),
queryFn: () => getStockStatement(opts),
enabled: Boolean(opts.tenantid && opts.locationid),
});
}
export function useProductsCount(opts: { tenantid: number; categoryid: number; subcategoryid?: number }) {
return useQuery({
queryKey: queryKeys.productsCount(opts),
queryFn: () => getProductsCount(opts),
enabled: Boolean(opts.tenantid && opts.categoryid),
});
}
// ── Apps & Locations ──────────────────────────────────────────────────────────
export function useAppLocations() {
return useQuery({ queryKey: queryKeys.appLocations(), queryFn: () => getAppLocations() });
}
export function useAppLocationConfig() {
return useQuery({ queryKey: queryKeys.appLocationConfig(), queryFn: () => getAppLocationConfig() });
}
export function useAppTypes(tag: string) {
return useQuery({
queryKey: queryKeys.appTypes(tag),
queryFn: () => getAppTypes(tag),
enabled: Boolean(tag),
});
}
// ── Partners ──────────────────────────────────────────────────────────────────
export function usePartners(opts: {
applocationid: number;
partnerid?: number;
limit?: number;
offset?: number;
}) {
return useQuery({
queryKey: queryKeys.partners(opts),
queryFn: () => getPartners(opts),
enabled: Boolean(opts.applocationid),
});
}
export function useRiderShifts(applocationid: number) {
return useQuery({
queryKey: queryKeys.riderShifts(applocationid),
queryFn: () => getRiderShifts(applocationid),
enabled: Boolean(applocationid),
});
}
// ── Invoice ───────────────────────────────────────────────────────────────────
export function useInvoiceInsight(tenantid: number = DEFAULT_TENANT_ID) {
return useQuery({
queryKey: queryKeys.invoiceInsight(tenantid),
queryFn: () => getInvoiceInsight(tenantid),
});
}