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

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);
}