feat: relocate orders and deliveries to store console & polish store cover images
This commit is contained in:
420
src/services/api.ts
Normal file
420
src/services/api.ts
Normal 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
462
src/services/fiestaApi.ts
Normal 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);
|
||||
}
|
||||
124
src/services/fiestaMappers.ts
Normal file
124
src/services/fiestaMappers.ts
Normal 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)
|
||||
};
|
||||
}
|
||||
248
src/services/fiestaQueries.ts
Normal file
248
src/services/fiestaQueries.ts
Normal 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
283
src/services/queries.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user