Files
daily_merchant_web/src/services/fiestaQueries.ts
2026-06-15 19:17:13 +05:30

678 lines
23 KiB
TypeScript

/**
* @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<string, unknown>) => ['fiesta', 'revenueSummary', params] as const,
timeSeries: (params: Record<string, unknown>) => ['fiesta', 'timeSeries', params] as const,
locationSummary: (tenantid: number) => ['fiesta', 'locationSummary', tenantid] as const,
orderInsight: (tenantid: number) => ['fiesta', 'orderInsight', tenantid] as const,
orders: (params: Record<string, unknown>) => ['fiesta', 'orders', params] as const,
deliverySummary: (params: Record<string, unknown>) => ['fiesta', 'deliverySummary', params] as const,
deliveries: (params: Record<string, unknown>) => ['fiesta', 'deliveries', params] as const,
deliveryInsight: (tenantid: number) => ['fiesta', 'deliveryInsight', tenantid] as const,
riders: (params: Record<string, unknown>) => ['fiesta', 'riders', params] as const,
riderShifts: (applocationid: number) => ['fiesta', 'riderShifts', applocationid] as const,
riderPeriodicLogs: (params: Record<string, unknown>) => ['fiesta', 'riderPeriodicLogs', params] as const,
riderLogs: (params: Record<string, unknown>) => ['fiesta', 'riderLogs', params] as const,
batchEfficiency: (params: Record<string, unknown>) => ['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<string, unknown>) => ['fiesta', 'allTenants', params] as const,
tenantCustomers: (params: Record<string, unknown>) => ['fiesta', 'tenantCustomers', params] as const,
stockStatement: (params: Record<string, unknown>) => ['fiesta', 'stockStatement', params] as const,
productsCount: (params: Record<string, unknown>) => ['fiesta', 'productsCount', params] as const,
productStocks: (params: Record<string, unknown>) => ['fiesta', 'productStocks', params] as const,
productLocations: (params: Record<string, unknown>) => ['fiesta', 'productLocations', params] as const,
masterCatalog: (params: Record<string, unknown>) => ['fiesta', 'masterCatalog', params] as const,
productCategories: () => ['fiesta', 'productCategories'] as const,
productSubcategories: (params: Record<string, unknown>) => ['fiesta', 'productSubcategories', params] as const,
orderDetails: (orderheaderid: number | string) => ['fiesta', 'orderDetails', orderheaderid] as const,
customerOrders: (params: Record<string, unknown>) => ['fiesta', 'customerOrders', params] as const,
deliveryReport: (params: Record<string, unknown>) => ['fiesta', 'deliveryReport', params] as const,
fleetSummary: (params: Record<string, unknown>) => ['fiesta', 'fleetSummary', params] as const,
users: (params: Record<string, unknown>) => ['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<string, unknown>),
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<string, unknown>),
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<string>();
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<string, unknown>),
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<typeof updateUser>[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<AuthUser, Error, { email: string; password: string }>({
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 };