/** * @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; type QueryParams = Record; async function fiestaGet(endpoint: string, params: QueryParams = {}): Promise { 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; } async function fiestaSend( endpoint: string, method: 'POST' | 'PUT' | 'DELETE', body?: unknown, ): Promise { 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(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(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 { const row = firstRow(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 { return toRows(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 { 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 { 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 { const row = firstRow( 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 { 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 { 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 { 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 { return toRows(await fiestaGet('partners/getridershifts', { applocationid })); } // ════════════════════════════════════════════════════════════════════════════ // TENANTS / CUSTOMERS // ════════════════════════════════════════════════════════════════════════════ /** /tenants/gettenantlocations?tenantid= — outlet locations for a tenant. */ export async function getTenantLocations(tenantid: number): Promise { 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 { 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 { 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 { 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 { 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 = { 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 { 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 { 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 { return fiestaSend('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 { return fiestaSend('users/update', 'PUT', input); }