463 lines
16 KiB
TypeScript
463 lines
16 KiB
TypeScript
/**
|
|
* @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);
|
|
}
|