/** * @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, useQueries } from '@tanstack/react-query'; import type { Row } from './fiestaApi'; import { loginRequest, matchTenantUser, buildAuthUser, type AuthUser } from './auth'; import { FIESTA_TENANT_ID, FIESTA_APPLOCATION_ID, FIESTA_PRIMARY_LOCATION_ID, getOrderSummary, getRevenueSummary, getTimeSeries, getLocationSummary, getOrderInsight, getOrders, getDeliverySummary, getDeliveries, getDeliveryInsight, getDeliveryReport, getFleetSummary, getOrderDetails, getCustomerOrders, getRiders, getRiderShifts, getRiderPeriodicLogs, getRiderLogs, getBatchEfficiency, updateDelivery, reassignDeliveries, getTenantLocations, getAllTenants, getTenantCustomers, getStockStatement, getProductsCount, getProductStocks, getProductLocations, getMasterCatalog, getProductCategories, getProductSubcategories, getAllUsers, getUserById, createUser, updateUser, assignRiderToOrders, CreateUserInput, createTenantUser, createTenantLocation, CreateTenantInput, CreateTenantLocationInput, } from './fiestaApi'; export const fiestaKeys = { orderSummary: (tenantid: number, fromdate: string, todate: string, locationid?: number) => ['fiesta', 'orderSummary', tenantid, fromdate, todate, locationid ?? 0] as const, revenueSummary: (params: Record) => ['fiesta', 'revenueSummary', params] as const, timeSeries: (params: Record) => ['fiesta', 'timeSeries', params] as const, locationSummary: (tenantid: number) => ['fiesta', 'locationSummary', tenantid] as const, orderInsight: (tenantid: number) => ['fiesta', 'orderInsight', tenantid] as const, orders: (params: Record) => ['fiesta', 'orders', params] as const, deliverySummary: (params: Record) => ['fiesta', 'deliverySummary', params] as const, deliveries: (params: Record) => ['fiesta', 'deliveries', params] as const, deliveryInsight: (tenantid: number) => ['fiesta', 'deliveryInsight', tenantid] as const, riders: (params: Record) => ['fiesta', 'riders', params] as const, riderShifts: (applocationid: number) => ['fiesta', 'riderShifts', applocationid] as const, riderPeriodicLogs: (params: Record) => ['fiesta', 'riderPeriodicLogs', params] as const, riderLogs: (params: Record) => ['fiesta', 'riderLogs', params] as const, batchEfficiency: (params: Record) => ['fiesta', 'batchEfficiency', params] as const, // v2: bumped when test-row filtering was added to getTenantLocations so any // warm cache holding the old unfiltered (duplicated/junk) rows is bypassed. tenantLocations: (tenantid: number) => ['fiesta', 'tenantLocations', 'v2', tenantid] as const, allTenants: (params: Record) => ['fiesta', 'allTenants', params] as const, tenantCustomers: (params: Record) => ['fiesta', 'tenantCustomers', params] as const, stockStatement: (params: Record) => ['fiesta', 'stockStatement', params] as const, productsCount: (params: Record) => ['fiesta', 'productsCount', params] as const, productStocks: (params: Record) => ['fiesta', 'productStocks', params] as const, productLocations: (params: Record) => ['fiesta', 'productLocations', params] as const, masterCatalog: (params: Record) => ['fiesta', 'masterCatalog', params] as const, productCategories: () => ['fiesta', 'productCategories'] as const, productSubcategories: (params: Record) => ['fiesta', 'productSubcategories', params] as const, orderDetails: (orderheaderid: number | string) => ['fiesta', 'orderDetails', orderheaderid] as const, customerOrders: (params: Record) => ['fiesta', 'customerOrders', params] as const, deliveryReport: (params: Record) => ['fiesta', 'deliveryReport', params] as const, fleetSummary: (params: Record) => ['fiesta', 'fleetSummary', params] as const, users: (params: Record) => ['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, locationid?: number) { return useQuery({ queryKey: fiestaKeys.orderSummary(tenantid, fromdate, todate, locationid), queryFn: () => getOrderSummary(tenantid, fromdate, todate, locationid), enabled: Boolean(tenantid && fromdate && todate), }); } export function useFiestaRevenueSummary(opts: { tenantid: number; fromdate: string; todate: string; locationid?: number; }) { return useQuery({ queryKey: fiestaKeys.revenueSummary(opts as Record), queryFn: () => getRevenueSummary(opts), enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate), }); } export function useFiestaTimeSeries(opts: { tenantid: number; granularity: 'day' | 'month' | 'year'; fromdate: string; todate: string; locationid?: number; }) { return useQuery({ queryKey: fiestaKeys.timeSeries(opts as Record), queryFn: () => getTimeSeries(opts), enabled: Boolean(opts.tenantid && opts.fromdate && opts.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; locationid?: number; applocationid?: number; keyword?: string; pageno?: number; pagesize?: number; }) { return useQuery({ queryKey: fiestaKeys.orders(opts), queryFn: () => getOrders(opts), enabled: Boolean(opts.tenantid && opts.status && opts.fromdate && opts.todate), }); } /** * Fetches orders across all statuses for a given date range by firing one * request per status in parallel and merging the results. This is needed * because the /orders/getorders API requires an explicit status param and * returns an empty array when status is blank or 'all'. */ export function useFiestaAllOrders(opts: { tenantid: number; fromdate: string; todate: string; locationid?: number; applocationid?: number; keyword?: string; }) { return useQuery({ queryKey: ['fiesta', 'allOrders', opts], queryFn: async () => { // Include all known statuses from ORDER_STATUS_MAP to ensure we don't miss orders const statuses = [ 'created', 'pending', 'processing', 'delivered', 'cancelled', 'accepted', 'assigned', 'ready', 'picked', 'active', 'arrived' ]; // Fetch sequentially to avoid rate-limiting or proxy dropping parallel requests const results: Row[][] = []; for (const status of statuses) { try { const res = await getOrders({ tenantid: opts.tenantid, status, fromdate: opts.fromdate, todate: opts.todate, locationid: opts.locationid, applocationid: opts.applocationid, keyword: opts.keyword, pagesize: 500, }); results.push(res); } catch (e) { results.push([]); } } // Merge and deduplicate by orderid/orderheaderid const merged: Row[] = []; const seen = new Set(); for (const list of results) { for (const row of list) { const id = String(row.orderid || row.orderheaderid || Math.random()); if (!seen.has(id)) { seen.add(id); merged.push(row); } } } return merged; }, enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate), }); } // ── Deliveries ──────────────────────────────────────────────────────────────── export function useFiestaDeliverySummary(opts: { tenantid: number; applocationid?: number; locationid?: 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; status?: string; locationid?: number; applocationid?: number; keyword?: string; pageno?: number; pagesize?: number; }) { 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), }); } /** * Bulk-assign one rider to many orders (the Orders board's multi-select assign). * Fires one updatedelivery per row in parallel, tolerates partial failure, and * refreshes the orders + deliveries lists on completion. */ export function useFiestaAssignRider() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: { userid: number; orders: Row[] }) => assignRiderToOrders(input.userid, input.orders), onSuccess: () => { qc.invalidateQueries({ queryKey: ['fiesta', 'orders'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'orderSummary'] }); // Refresh the Deliveries board AND its KPI summary so a freshly-assigned // order shows up on the deliveries page immediately (table + count cards). qc.invalidateQueries({ queryKey: ['fiesta', 'deliveries'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'deliverySummary'] }); }, }); } // ── Partners / Riders ───────────────────────────────────────────────────────── export function useFiestaRiders(opts: { applocationid?: number; tenantid: number; partnerid?: 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), }); } // ── Dispatch / Telemetry ───────────────────────────────────────────────────── export function useFiestaRiderPeriodicLogs(opts: { userid?: number; riderid?: number; fromdate: string; todate: string; tenantid?: number; applocationid?: number; }) { return useQuery({ queryKey: fiestaKeys.riderPeriodicLogs(opts), queryFn: () => getRiderPeriodicLogs(opts), enabled: Boolean(opts.fromdate && opts.todate), }); } export function useFiestaRiderLogs(opts: { userid?: number; riderid?: number; fromdate: string; todate: string; tenantid?: number; applocationid?: number; pageno?: number; pagesize?: number; }) { return useQuery({ queryKey: fiestaKeys.riderLogs(opts), queryFn: () => getRiderLogs(opts), enabled: Boolean(opts.fromdate && opts.todate), }); } export function useFiestaBatchEfficiency(opts: { partnerid?: number; tenantid: number; fromdate: string; todate: string; }) { return useQuery({ queryKey: fiestaKeys.batchEfficiency(opts), queryFn: () => getBatchEfficiency(opts), enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate), }); } export function useFiestaUpdateDelivery() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: { deliveryid: number; updates: Row }) => updateDelivery(input.deliveryid, input.updates), onSuccess: () => { qc.invalidateQueries({ queryKey: ['fiesta', 'deliveries'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'deliverySummary'] }); }, }); } export function useFiestaReassignDeliveries() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: { userid: number; deliveryids: number[] }) => reassignDeliveries(input), onSuccess: () => { qc.invalidateQueries({ queryKey: ['fiesta', 'deliveries'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'deliverySummary'] }); }, }); } // ── 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), }); } export interface StoreStock { locationid: number; locationname: string; isLoading: boolean; isError: boolean; rows: Row[]; } /** * Live stock statement for every outlet under the tenant — powers the admin's * "all stores' stock" view. One query per location (deduped/cached by React * Query); the returned array stays aligned with the `locations` input. */ export function useFiestaStoresStock( tenantid: number, locations: Array<{ locationid: number; locationname: string }>, ): StoreStock[] { const results = useQueries({ queries: locations.map((loc) => ({ queryKey: fiestaKeys.stockStatement({ scope: 'stores', tenantid, locationid: loc.locationid }), queryFn: () => getStockStatement({ tenantid, locationid: loc.locationid, pagesize: 200 }), enabled: Boolean(tenantid && loc.locationid), })), }); return locations.map((loc, i) => ({ locationid: loc.locationid, locationname: loc.locationname, isLoading: results[i]?.isLoading ?? true, isError: results[i]?.isError ?? false, rows: (results[i]?.data as Row[]) ?? [], })); } // ── Order details / customer history ─────────────────────────────────────────── export function useFiestaOrderDetails(orderheaderid: number | string | null | undefined) { return useQuery({ queryKey: fiestaKeys.orderDetails(orderheaderid ?? ''), queryFn: () => getOrderDetails(orderheaderid as number | string), enabled: Boolean(orderheaderid), }); } export function useFiestaCustomerOrders(opts: { customerid: number | string | null | undefined; status?: string; pageno?: number; pagesize?: number; }) { return useQuery({ queryKey: fiestaKeys.customerOrders(opts as Record), queryFn: () => getCustomerOrders({ customerid: opts.customerid as number | string, status: opts.status, pageno: opts.pageno, pagesize: opts.pagesize, }), enabled: Boolean(opts.customerid), }); } // ── Deliveries report / fleet ─────────────────────────────────────────────────── export function useFiestaDeliveryReport(opts: { tenantid: number; applocationid?: number; partnerid?: number; userid?: number; fromdate: string; todate: string; }) { return useQuery({ queryKey: fiestaKeys.deliveryReport(opts), queryFn: () => getDeliveryReport(opts), enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate), }); } export function useFiestaFleetSummary(opts: { tenantid: number; applocationid?: number; partnerid?: number; fromdate: string; todate: string; }) { return useQuery({ queryKey: fiestaKeys.fleetSummary(opts), queryFn: () => getFleetSummary(opts), enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate), }); } // ── Products: live stocks / catalog / categories ──────────────────────────────── export function useFiestaProductStocks(opts: { tenantid: number; locationid: number }) { return useQuery({ queryKey: fiestaKeys.productStocks(opts), queryFn: () => getProductStocks(opts), enabled: Boolean(opts.tenantid && opts.locationid), }); } export function useFiestaProductLocations(opts: { tenantid: number; locationid: number; subcategoryid?: number; pageno?: number; pagesize?: number; }) { return useQuery({ queryKey: fiestaKeys.productLocations(opts), queryFn: () => getProductLocations(opts), enabled: Boolean(opts.tenantid && opts.locationid), }); } export function useFiestaMasterCatalog(opts: { tenantid: number; locationid?: number; subcategoryid?: number; keyword?: string; pageno?: number; pagesize?: number; }) { return useQuery({ queryKey: fiestaKeys.masterCatalog(opts), queryFn: () => getMasterCatalog(opts), enabled: Boolean(opts.tenantid), }); } export function useFiestaProductCategories() { return useQuery({ queryKey: fiestaKeys.productCategories(), queryFn: () => getProductCategories(), }); } export function useFiestaProductSubcategories(opts: { categoryid: number; tenantid?: number }) { return useQuery({ queryKey: fiestaKeys.productSubcategories(opts), queryFn: () => getProductSubcategories(opts), enabled: Boolean(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[0]) => updateUser(input), onSuccess: () => qc.invalidateQueries({ queryKey: ['fiesta', 'users'] }), }); } /** Create a new tenant and admin user, then refresh tenants list on success. */ export function useFiestaCreateTenant() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: CreateTenantInput) => createTenantUser(input), onSuccess: () => qc.invalidateQueries({ queryKey: ['fiesta', 'allTenants'] }), }); } /** Create a new tenant location, then refresh tenant locations list on success. */ export function useFiestaCreateLocation() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: CreateTenantLocationInput) => createTenantLocation(input), onSuccess: () => { qc.invalidateQueries({ queryKey: ['fiesta', 'tenantLocations'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'locationSummary'] }); }, }); } // ── Auth ────────────────────────────────────────────────────────────────────── /** * Verify login credentials against the Fiesta web-login endpoint. A mutation * (not a query) since it's a POST with side effects; the form drives it via * `mutate`/`mutateAsync` and reads `isPending`/`error` for loading + error UI. * * Both network calls go through React Query: the login POST is the mutation, * and the role-resolution fallback (when the login response omits the role) is * fetched via the query client — so it shares the Users-panel cache. */ export function useLogin() { const qc = useQueryClient(); return useMutation({ mutationFn: async ({ email, password }) => { const result = await loginRequest(email, password); let row = result.row; // The login response didn't carry a role — resolve it from the tenant user // list through the query cache (deduped with useFiestaUsers). if (!result.hasRole) { const params = { tenantid: FIESTA_TENANT_ID, keyword: result.email, pagesize: 50 }; const users = await qc.fetchQuery({ queryKey: fiestaKeys.users(params), queryFn: () => getAllUsers(params), }); const match = matchTenantUser(users, result.email); if (match) row = { ...match, ...(row ?? {}) }; } return buildAuthUser(row, result.email); }, }); } export { FIESTA_TENANT_ID, FIESTA_APPLOCATION_ID, FIESTA_PRIMARY_LOCATION_ID };