/** * @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, getSalesSummary, 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, createProductLocations, getPosRoles, getPosStaff, createPosStaff, updatePosStaff, deactivatePosStaff, getMasterCatalog, getProductCategories, getProductSubcategories, getAllUsers, getUserById, createUser, updateUser, setUserPassword, assignRiderToOrders, changeDeliveryRider, notifyRider, NotifyRiderInput, CreateUserInput, createTenantUser, createTenantLocation, CreateTenantInput, CreateTenantLocationInput, updateTenantLocation, UpdateTenantLocationInput, deleteLocation, createProductLocation, deleteProductLocation, createStockRequest, getStockRequests, updateStockRequest, CreateProductLocationInput, DeleteProductLocationInput, StockRequestInput, UpdateStockRequestInput, getGlobalBrands, getGlobalCategories, getGlobalProducts, getGlobalProduct, } from './fiestaApi'; export const fiestaKeys = { orderSummary: (tenantid: number, fromdate: string, todate: string, locationid?: number) => ['fiesta', 'orderSummary', tenantid, fromdate, todate, locationid ?? 0] as const, salesSummary: (tenantid: number, locationid: number | undefined, fromdate: string, todate: string) => ['fiesta', 'salesSummary', tenantid, locationid, fromdate, todate] 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, stockRequests: (params: Record) => ['fiesta', 'stockRequests', params] as const, globalBrands: () => ['fiesta', 'globalBrands'] as const, globalCategories: (brand: string) => ['fiesta', 'globalCategories', brand] as const, globalProducts: (params: Record) => ['fiesta', 'globalProducts', params] 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: async () => { // The backend's /orders/getordersummary endpoint ignores locationid and returns tenant-wide // totals. To fix this data leak for store managers, we manually fetch the core statuses for // the specific location when a locationid is provided. if (locationid) { const [created, pending, processing, delivered, cancelled] = await Promise.all([ getOrders({ tenantid, locationid, status: 'created', fromdate, todate, pagesize: 500 }), getOrders({ tenantid, locationid, status: 'pending', fromdate, todate, pagesize: 500 }), getOrders({ tenantid, locationid, status: 'processing', fromdate, todate, pagesize: 500 }), getOrders({ tenantid, locationid, status: 'delivered', fromdate, todate, pagesize: 500 }), getOrders({ tenantid, locationid, status: 'cancelled', fromdate, todate, pagesize: 500 }), ]); return { total: created.length + pending.length + processing.length + delivered.length + cancelled.length, created: created.length, pending: pending.length, processing: processing.length, delivered: delivered.length, cancelled: cancelled.length, tenantid, tenantname: 'Local Store' }; } return 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 useFiestaSalesSummary(opts: { tenantid: number; locationid?: number; fromdate: string; todate: string; }) { return useQuery({ queryKey: fiestaKeys.salesSummary(opts.tenantid, opts.locationid, opts.fromdate, opts.todate), queryFn: () => getSalesSummary(opts), enabled: Boolean(opts.tenantid && opts.fromdate && opts.todate), }); } export function useFiestaLocationSummary(tenantid: number = FIESTA_TENANT_ID, fromdate?: string, todate?: string) { return useQuery({ queryKey: [...fiestaKeys.locationSummary(tenantid), fromdate, todate], queryFn: () => getLocationSummary(tenantid, fromdate, todate), 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'] }); }, }); } /** * Move a delivery to a different rider. Refreshes the deliveries board and its * KPI cards, plus the orders list, since the order's rider is shown there too. */ export function useFiestaChangeRider() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: { deliveryid: number; orderheaderid: number; userid: number }) => changeDeliveryRider(input), onSuccess: () => { qc.invalidateQueries({ queryKey: ['fiesta', 'deliveries'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'deliverySummary'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'orders'] }); }, }); } /** * Push a message to a rider's device. * * Deliberately NOT chained into the assign mutation's onSuccess: the delivery * is already committed by then, so a failed push must not read as a failed * assignment. Callers fire this after the write and report the two outcomes * separately. */ export function useFiestaNotifyRider() { return useMutation({ mutationFn: (input: NotifyRiderInput) => notifyRider(input), }); } // ── Partners / Riders ───────────────────────────────────────────────────────── /** * Riders on duty right now — see getRiders for why this is presence, not a * roster. Enabled on ANY scope: gating on tenantid alone kept the query off for * callers that legitimately scope by app-location, which is the only scope that * actually returns riders. */ export function useFiestaRiders(opts: { applocationid?: number; tenantid?: number; partnerid?: number }) { return useQuery({ queryKey: fiestaKeys.riders(opts), queryFn: () => getRiders(opts), enabled: Boolean(opts.applocationid || opts.partnerid || opts.tenantid), // Presence goes stale quickly — a rider logging off mid-shift should drop // out of the assign list rather than linger for the whole session. staleTime: 60_000, refetchInterval: 120_000, }); } 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), // A rider is required. Without this guard the query fired on every page load // with no rider selected — and `riders/getriderperiodiclogs` 404s in both the // riders/ and partners/ namespaces (no such backend route exists), so every // load spent a request on a guaranteed failure. enabled: Boolean((opts.userid || opts.riderid) && 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'] }); }, }); } /** * Move several deliveries to one rider. Each needs its orderheaderid as well as * its deliveryid — updatedelivery keys on both — so this takes delivery rows * rather than the bare id list it used to. */ export function useFiestaReassignDeliveries() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: { userid: number; deliveries: { deliveryid: number; orderheaderid: number }[] }) => reassignDeliveries(input), onSuccess: () => { qc.invalidateQueries({ queryKey: ['fiesta', 'deliveries'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'deliverySummary'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'orders'] }); }, }); } // ── Tenants / Customers ───────────────────────────────────────────────────────── export function useFiestaTenantLocations(tenantid: number = FIESTA_TENANT_ID, userid?: number) { return useQuery({ queryKey: [...fiestaKeys.tenantLocations(tenantid), userid], queryFn: () => getTenantLocations(tenantid, userid), 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, locationid: opts.locationid || 0 }), enabled: Boolean(opts.tenantid), }); } // ── 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[]) ?? [], })); } export function useFiestaStoresOrderSummary( tenantid: number, locations: Array<{ locationid: number; locationname: string }>, fromdate: string, todate: string ) { const results = useQueries({ queries: locations.map((loc) => ({ queryKey: fiestaKeys.orderSummary(tenantid, fromdate, todate, loc.locationid), queryFn: () => getOrderSummary(tenantid, fromdate, todate, loc.locationid), enabled: Boolean(tenantid && loc.locationid && fromdate && todate), })), }); const agg = { total: 0, created: 0, pending: 0, processing: 0, delivered: 0, cancelled: 0 }; let isLoading = false; results.forEach(res => { if (res.isLoading) isLoading = true; if (res.data) { agg.total += res.data.total; agg.created += res.data.created; agg.pending += res.data.pending; agg.processing += res.data.processing; agg.delivered += res.data.delivered; agg.cancelled += res.data.cancelled; } }); return { isLoading, data: agg }; } export function useFiestaStoresRevenueSummary( tenantid: number, locations: Array<{ locationid: number; locationname: string }>, fromdate: string, todate: string ) { const results = useQueries({ queries: locations.map((loc) => ({ queryKey: fiestaKeys.revenueSummary({ scope: 'stores', tenantid, fromdate, todate, locationid: loc.locationid }), queryFn: () => getRevenueSummary({ tenantid, fromdate, todate, locationid: loc.locationid }), enabled: Boolean(tenantid && loc.locationid && fromdate && todate), })), }); const agg = { grossrevenue: 0, netrevenue: 0, profit: 0, ordercount: 0, avgordervalue: 0 }; let isLoading = false; results.forEach(res => { if (res.isLoading) isLoading = true; if (res.data) { agg.grossrevenue += res.data.overallrevenue || 0; agg.netrevenue += res.data.overallrevenue || 0; agg.profit += (res.data.overallrevenue || 0) * 0.15; // Mock since API doesn't provide agg.ordercount += 0; } }); if (agg.ordercount > 0) { agg.avgordervalue = Math.round(agg.grossrevenue / agg.ordercount); } return { isLoading, data: agg }; } export function useFiestaStoresTimeSeries( tenantid: number, locations: Array<{ locationid: number; locationname: string }>, granularity: 'day' | 'month' | 'year', fromdate: string, todate: string ) { const results = useQueries({ queries: locations.map((loc) => ({ queryKey: fiestaKeys.timeSeries({ scope: 'stores', tenantid, granularity, fromdate, todate, locationid: loc.locationid }), queryFn: () => getTimeSeries({ tenantid, granularity, fromdate, todate, locationid: loc.locationid }), enabled: Boolean(tenantid && loc.locationid && fromdate && todate), })), }); const map = new Map(); let isLoading = false; results.forEach(res => { if (res.isLoading) isLoading = true; if (res.data) { res.data.forEach((d: any) => { const label = d.label || d.date || d.createdat || 'Unknown'; if (!map.has(label)) { map.set(label, { label, orders: 0, revenue: 0, cancelled: 0, skus: 0 }); } const existing = map.get(label); existing.orders += Number(d.orders || d.totalorders || d.total || 0); existing.revenue += Number(d.revenue || d.grossrevenue || d.overallrevenue || 0); existing.cancelled += Number(d.cancelled || 0); existing.skus += Number(d.activeskus || d.skus || 0); }); } }); const sortedData = Array.from(map.values()).sort((a, b) => { if (a.label === 'Unknown') return 1; if (b.label === 'Unknown') return -1; return new Date(a.label).getTime() - new Date(b.label).getTime(); }); return { isLoading, data: sortedData }; } // ── 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), }); } export function useFiestaGetStockRequests(opts: { tenantid: number; locationid?: number; status?: string; date?: string; pageno?: number; pagesize?: number; }) { return useQuery({ queryKey: fiestaKeys.stockRequests(opts), queryFn: () => getStockRequests(opts), // Omitting locationid is a deliberate tenant-wide query (the admin console // needs every outlet's requests). But passing an EMPTY one is not: the // backend treats locationid=0 as "no filter" and returns every outlet, so a // store user whose outlet hasn't resolved yet would see other stores' // requests. Wait for a real id in that case. enabled: Boolean(opts.tenantid) && (opts.locationid === undefined || Boolean(opts.locationid)), }); } export function useFiestaCreateStockRequest() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: StockRequestInput) => createStockRequest(input), onSuccess: () => { qc.invalidateQueries({ queryKey: ['fiesta', 'stockRequests'] }); }, }); } export function useFiestaUpdateStockRequest() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: UpdateStockRequestInput) => updateStockRequest(input), onSuccess: () => { qc.invalidateQueries({ queryKey: ['fiesta', 'stockRequests'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'stockStatement'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'productStocks'] }); }, }); } // ── 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'] }); }, }); } /** Update a tenant location, then refresh tenant locations list on success. */ export function useFiestaUpdateLocation() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: UpdateTenantLocationInput) => updateTenantLocation(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; const resolvedTenantId = (row?.tenantid as number) || FIESTA_TENANT_ID; // The login response didn't carry a role OR it lacks location mapping — resolve it // from the tenant user list through the query cache (deduped with useFiestaUsers). if (!result.hasRole || (row && !row.locationid && !row.applocationid)) { const params = { tenantid: resolvedTenantId, 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 = { ...(row ?? {}), ...match }; } return buildAuthUser(row, result.email); }, }); } /** * Set (or reset) a user's login password — used by the login flow's * create-password step (see PasswordSetupRequiredError in ./auth) for a * first-time tenant admin or store login. */ export function useSetPassword() { return useMutation({ mutationFn: (input: { userid: number; password: string }) => setUserPassword(input.userid, input.password), }); } export function useFiestaDeleteLocation() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: { tenantid: number; locationid: number }) => deleteLocation(input), onSuccess: (_, variables) => { qc.invalidateQueries({ queryKey: fiestaKeys.tenantLocations(variables.tenantid) }); qc.invalidateQueries({ queryKey: fiestaKeys.locationSummary(variables.tenantid) }); }, }); } export { FIESTA_TENANT_ID, FIESTA_APPLOCATION_ID, FIESTA_PRIMARY_LOCATION_ID }; // ── Global Catalogue ──────────────────────────────────────────────────────── export function useFiestaGlobalBrands() { return useQuery({ queryKey: fiestaKeys.globalBrands(), queryFn: () => getGlobalBrands(), }); } export function useFiestaGlobalCategories(brand: string) { return useQuery({ queryKey: fiestaKeys.globalCategories(brand), queryFn: () => getGlobalCategories({ brand }), enabled: Boolean(brand), }); } export function useFiestaGlobalProducts(opts: { brand: string; category?: string; keyword?: string; pageno?: number; pagesize?: number; }) { return useQuery({ queryKey: fiestaKeys.globalProducts(opts), queryFn: () => getGlobalProducts(opts), enabled: Boolean(opts.brand), }); } // ── Store Catalogue (Product Locations) ───────────────────────────────────── export function useFiestaCreateProductLocation() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: CreateProductLocationInput) => createProductLocation(input), onSuccess: (_, variables) => { qc.invalidateQueries({ queryKey: ['catalogue', 'imported'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'productLocations'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'productStocks'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'stockStatement'] }); }, onError: (error: any) => { alert(`Error adding product: ${error.message}`); }, }); } /** * Publish/reprice one product across many outlets in a single request. Used by * the admin catalogue, where a price the admin sets is meant to apply to every * store under the tenant — not just whichever outlet happened to load first. */ export function useFiestaCreateProductLocations() { const qc = useQueryClient(); return useMutation({ mutationFn: (inputs: Array<{ tenantid: number; locationid: number; productid: number; price?: number; status?: string }>) => createProductLocations(inputs), onSuccess: () => { qc.invalidateQueries({ queryKey: ['catalogue', 'imported'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'productLocations'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'productStocks'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'stockStatement'] }); }, }); } export function useFiestaDeleteProductLocation() { const qc = useQueryClient(); return useMutation({ mutationFn: (input: DeleteProductLocationInput) => deleteProductLocation(input), onSuccess: (_, variables) => { qc.invalidateQueries({ queryKey: ['catalogue', 'imported'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'productLocations'] }); qc.invalidateQueries({ queryKey: ['fiesta', 'productStocks'] }); }, }); } // ── POS staff (supervisors and cashiers) ──────────────────────────────────── // // Separate from useFiestaUsers: the backend excludes roleid 7 and 8 from // `getallusers`, so these people are invisible to that hook by design. Do not // "fix" that by merging the two lists — a till account and a console account // are different things with different creation paths. export const posStaffKeys = { roles: () => ['fiesta', 'posroles'] as const, staff: (tenantid: number, locationid: number) => ['fiesta', 'posstaff', tenantid, locationid] as const, }; /** The till roles the backend offers. Cached hard — this changes with a deploy. */ export function usePosRoles() { return useQuery({ queryKey: posStaffKeys.roles(), queryFn: getPosRoles, staleTime: 30 * 60_000, }); } export function usePosStaff(tenantid: number, locationid: number) { return useQuery({ queryKey: posStaffKeys.staff(tenantid, locationid), queryFn: () => getPosStaff({ tenantid, locationid }), enabled: Boolean(tenantid) && Boolean(locationid), }); } /** Create a till account. The resolved value carries the one-time password. */ export function useCreatePosStaff() { const qc = useQueryClient(); return useMutation({ mutationFn: createPosStaff, onSuccess: (_res, vars) => { qc.invalidateQueries({ queryKey: posStaffKeys.staff(vars.tenantid, vars.locationid) }); }, }); } export function useUpdatePosStaff() { const qc = useQueryClient(); return useMutation({ mutationFn: updatePosStaff, onSuccess: (_res, vars) => { qc.invalidateQueries({ queryKey: posStaffKeys.staff(vars.tenantid, vars.locationid) }); }, }); } export function useDeactivatePosStaff() { const qc = useQueryClient(); return useMutation({ mutationFn: deactivatePosStaff, onSuccess: (_res, vars) => { qc.invalidateQueries({ queryKey: posStaffKeys.staff(vars.tenantid, vars.locationid) }); }, }); }