udpates on the ui changesand api integration
This commit is contained in:
233
src/services/auth.ts
Normal file
233
src/services/auth.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* @license
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Login / authentication client.
|
||||
*
|
||||
* The backend login endpoint is the ONE thing that still needs confirming — see
|
||||
* the CONFIG block below. Everything else (request shape, response parsing,
|
||||
* role mapping, error handling) is wired and ready. Once the four CONFIG values
|
||||
* match the real API, the login form verifies credentials and gates entry.
|
||||
*
|
||||
* Requests are routed through the existing Vite dev proxy so no secret/CORS
|
||||
* concern reaches the browser (see vite.config.ts: `/fiesta/*` → fiesta.nearle.app,
|
||||
* `/hasura/*` → api.workolik.com). If the login route lives on a different host,
|
||||
* add a proxy entry there and point LOGIN_ENDPOINT at it.
|
||||
*/
|
||||
|
||||
import { firstRow, num, str, type Row } from './fiestaApi';
|
||||
|
||||
// ── Backend login config ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fiesta application login. Routed through the Vite `/fiesta` proxy →
|
||||
* https://fiesta.nearle.app/live/api/v1/web/users/applogin.
|
||||
* Observed shape:
|
||||
* request: { authname: <email>, password: <password>, configid: 1, userfcmtoken: null }
|
||||
* failure: { code: 409, message: "Invalid Email", status: false }
|
||||
* success: status !== false (Fiesta envelope, optionally with `details`)
|
||||
*/
|
||||
const LOGIN_ENDPOINT = '/fiesta/live/api/v1/web/users/applogin';
|
||||
|
||||
/** Request body field names the endpoint expects for the credentials. */
|
||||
const REQUEST_FIELDS = {
|
||||
email: 'authname',
|
||||
password: 'password',
|
||||
} as const;
|
||||
|
||||
/** Extra fields the applogin endpoint expects alongside the credentials. */
|
||||
const EXTRA_FIELDS = {
|
||||
configid: 1,
|
||||
userfcmtoken: null,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Field names read from the user record (login response `details`, else the
|
||||
* tenant user list). Verified against the live `users/getallusers` response.
|
||||
*/
|
||||
const RESPONSE_FIELDS = {
|
||||
roleid: 'roleid',
|
||||
firstname: 'firstname',
|
||||
fullname: 'fullname',
|
||||
email: 'email',
|
||||
contactno: 'contactno',
|
||||
userid: 'userid',
|
||||
// Store binding: a non-admin user is allocated to an app-location via
|
||||
// applocationid; `applocation` is its human-readable name (e.g. "Coimbatore").
|
||||
// locationid/locationname are captured when present (often 0/absent on the
|
||||
// user record — the outlet is resolved from the tenant locations list).
|
||||
applocationid: 'applocationid',
|
||||
applocation: 'applocation',
|
||||
locationid: 'locationid',
|
||||
locationname: 'locationname',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* roleids that land on the ADMIN dashboard; everyone else lands on the user page.
|
||||
* From fiestaApi.roleName(): 1 = Owner, 2 = Manager, 3 = Admin, 4 = Staff,
|
||||
* 5 = Rider, 6 = Cashier.
|
||||
*/
|
||||
const ADMIN_ROLE_IDS = new Set<number>([1, 3]);
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type LoginRole = 'admin' | 'user';
|
||||
|
||||
export interface AuthUser {
|
||||
role: LoginRole;
|
||||
name: string;
|
||||
email: string;
|
||||
userid?: number;
|
||||
roleid?: number;
|
||||
/** Phone number on the user record. */
|
||||
contactno?: string;
|
||||
/** The app-location this user is allocated to. */
|
||||
applocationid?: number;
|
||||
/** App-location / zone name on the user record (e.g. "Coimbatore"). */
|
||||
applocation?: string;
|
||||
/** Outlet/location id, when the record carries it directly (often 0). */
|
||||
locationid?: number;
|
||||
/** Outlet display name, when the record carries it directly. */
|
||||
locationname?: string;
|
||||
}
|
||||
|
||||
/** Map a numeric roleid to the workspace the user is allowed into. */
|
||||
export function roleFromRoleId(roleid: number): LoginRole {
|
||||
return ADMIN_ROLE_IDS.has(roleid) ? 'admin' : 'user';
|
||||
}
|
||||
|
||||
/** Build a friendly display name from the response, falling back to the email. */
|
||||
function displayName(row: Record<string, unknown>, email: string): string {
|
||||
const full = str(row[RESPONSE_FIELDS.fullname]).trim();
|
||||
if (full) return full;
|
||||
const first = str(row[RESPONSE_FIELDS.firstname]).trim();
|
||||
if (first) return first;
|
||||
// Fallback: derive from the email's local part.
|
||||
return (
|
||||
email
|
||||
.split('@')[0]
|
||||
.split(/[._-]+/)
|
||||
.filter(Boolean)
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ') || 'Account'
|
||||
);
|
||||
}
|
||||
|
||||
/** The verified-login response: the user record (if any) + the resolved email. */
|
||||
export interface LoginResult {
|
||||
/** User record from the login response `details`, or null if it carried none. */
|
||||
row: Row | null;
|
||||
/** Whether `row` already includes the role (so no tenant-list lookup is needed). */
|
||||
hasRole: boolean;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST the credentials to the Fiesta web-login endpoint. Resolves with the raw
|
||||
* user record on success; throws an Error with a user-facing message on invalid
|
||||
* credentials or any failure. Role resolution is left to the caller so the
|
||||
* (optional) tenant-user lookup can go through React Query — see `useLogin`.
|
||||
*/
|
||||
export async function loginRequest(email: string, password: string): Promise<LoginResult> {
|
||||
const trimmedEmail = email.trim();
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(LOGIN_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
[REQUEST_FIELDS.email]: trimmedEmail,
|
||||
[REQUEST_FIELDS.password]: password,
|
||||
...EXTRA_FIELDS,
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
throw new Error('Unable to reach the login service. Check your connection and try again.');
|
||||
}
|
||||
|
||||
// Parse the JSON body (may be absent on some error responses).
|
||||
const json = (await res.json().catch(() => null)) as
|
||||
| { code?: number; status?: boolean; message?: string; details?: unknown }
|
||||
| null;
|
||||
|
||||
// Failure: HTTP error, or the Fiesta `status: false` envelope (e.g. wrong
|
||||
// email/password → { code: 409, message: "Invalid Email", status: false }).
|
||||
if (!res.ok || (json && json.status === false)) {
|
||||
throw new Error(json?.message?.trim() || 'Incorrect email or password.');
|
||||
}
|
||||
|
||||
const row = firstRow<Row>(json);
|
||||
const resolvedEmail = (row && str(row[RESPONSE_FIELDS.email]).trim()) || trimmedEmail;
|
||||
return { row, hasRole: Boolean(row && row[RESPONSE_FIELDS.roleid] != null), email: resolvedEmail };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the email/authname exists and is registered by sending email and configid: 1.
|
||||
* Returns true if the email exists, false if it is invalid.
|
||||
*/
|
||||
export async function checkEmailRequest(email: string): Promise<boolean> {
|
||||
const trimmedEmail = email.trim();
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(LOGIN_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
[REQUEST_FIELDS.email]: trimmedEmail,
|
||||
...EXTRA_FIELDS,
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
throw new Error('Unable to reach the login service. Check your connection and try again.');
|
||||
}
|
||||
|
||||
const json = (await res.json().catch(() => null)) as
|
||||
| { code?: number; status?: boolean; message?: string }
|
||||
| null;
|
||||
|
||||
// A 409 Invalid Email code means the email does not exist.
|
||||
if (json && json.status === false && (json.code === 409 || json.message?.trim().toLowerCase() === 'invalid email')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Any other result (such as 403 Unauthorized email) means the email is registered.
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/** Find a user in the tenant user list by email or authname (case-insensitive). */
|
||||
export function matchTenantUser(users: Row[], email: string): Row | null {
|
||||
const target = email.toLowerCase();
|
||||
return (
|
||||
users.find(
|
||||
(u) =>
|
||||
str(u[RESPONSE_FIELDS.email]).toLowerCase() === target ||
|
||||
str(u.authname).toLowerCase() === target,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/** Assemble the final AuthUser (role + identity) from a resolved user record. */
|
||||
export function buildAuthUser(row: Row | null, email: string): AuthUser {
|
||||
const roleid = row ? num(row[RESPONSE_FIELDS.roleid]) : 0;
|
||||
const applocation = row ? str(row[RESPONSE_FIELDS.applocation]).trim() : '';
|
||||
const locationname = row ? str(row[RESPONSE_FIELDS.locationname]).trim() : '';
|
||||
const contactno = row ? str(row[RESPONSE_FIELDS.contactno]).trim() : '';
|
||||
return {
|
||||
role: roleFromRoleId(roleid),
|
||||
name: displayName(row ?? {}, email),
|
||||
email,
|
||||
userid: row && row[RESPONSE_FIELDS.userid] != null ? num(row[RESPONSE_FIELDS.userid]) : undefined,
|
||||
roleid,
|
||||
contactno: contactno || undefined,
|
||||
applocationid:
|
||||
row && row[RESPONSE_FIELDS.applocationid] != null ? num(row[RESPONSE_FIELDS.applocationid]) : undefined,
|
||||
applocation: applocation || undefined,
|
||||
locationid: row && row[RESPONSE_FIELDS.locationid] != null ? num(row[RESPONSE_FIELDS.locationid]) : undefined,
|
||||
locationname: locationname || undefined,
|
||||
};
|
||||
}
|
||||
@@ -183,6 +183,28 @@ export async function getOrders(opts: {
|
||||
);
|
||||
}
|
||||
|
||||
/** /orders/getorderdetails?orderheaderid= — line items for a single order. */
|
||||
export async function getOrderDetails(orderheaderid: number | string): Promise<Row[]> {
|
||||
return toRows(await fiestaGet('orders/getorderdetails', { orderheaderid }));
|
||||
}
|
||||
|
||||
/** /orders/getorders?customerid=&status=&pageno=&pagesize= — one customer's order history. */
|
||||
export async function getCustomerOrders(opts: {
|
||||
customerid: number | string;
|
||||
status?: string;
|
||||
pageno?: number;
|
||||
pagesize?: number;
|
||||
}): Promise<Row[]> {
|
||||
return toRows(
|
||||
await fiestaGet('orders/getorders', {
|
||||
customerid: opts.customerid,
|
||||
status: opts.status ?? '',
|
||||
pageno: opts.pageno ?? 1,
|
||||
pagesize: opts.pagesize ?? 20,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// DELIVERIES
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -248,6 +270,48 @@ export async function getDeliveryInsight(tenantid: number): Promise<Row[]> {
|
||||
return toRows(await fiestaGet('deliveries/getdeliveryinsight', { tenantid }));
|
||||
}
|
||||
|
||||
/** /deliveries/getdeliveryreport?tenantid=&applocationid=&partnerid=&userid=&fromdate=&todate= —
|
||||
* deliveries financial report summary (per the endpoint sheet). */
|
||||
export async function getDeliveryReport(opts: {
|
||||
tenantid: number;
|
||||
applocationid?: number;
|
||||
partnerid?: number;
|
||||
userid?: number;
|
||||
fromdate: string;
|
||||
todate: string;
|
||||
}): Promise<Row[]> {
|
||||
return toRows(
|
||||
await fiestaGet('deliveries/getdeliveryreport', {
|
||||
tenantid: opts.tenantid,
|
||||
applocationid: opts.applocationid ?? FIESTA_APPLOCATION_ID,
|
||||
partnerid: opts.partnerid,
|
||||
userid: opts.userid,
|
||||
fromdate: opts.fromdate,
|
||||
todate: opts.todate,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** /partners/getfleetsummary?applocationid=&partnerid=&tenantid=&fromdate=&todate= —
|
||||
* fleet rider summary metrics (per the endpoint sheet). */
|
||||
export async function getFleetSummary(opts: {
|
||||
applocationid?: number;
|
||||
partnerid?: number;
|
||||
tenantid: number;
|
||||
fromdate: string;
|
||||
todate: string;
|
||||
}): Promise<Row[]> {
|
||||
return toRows(
|
||||
await fiestaGet('partners/getfleetsummary', {
|
||||
applocationid: opts.applocationid ?? FIESTA_APPLOCATION_ID,
|
||||
partnerid: opts.partnerid,
|
||||
tenantid: opts.tenantid,
|
||||
fromdate: opts.fromdate,
|
||||
todate: opts.todate,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// PARTNERS / RIDERS
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
@@ -356,6 +420,79 @@ export async function getProductsCount(opts: {
|
||||
);
|
||||
}
|
||||
|
||||
/** /products/getproductstocks?tenantid=&locationid= — live stock levels for an outlet. */
|
||||
export async function getProductStocks(opts: {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
}): Promise<Row[]> {
|
||||
return toRows(
|
||||
await fiestaGet('products/getproductstocks', {
|
||||
tenantid: opts.tenantid,
|
||||
locationid: opts.locationid,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** /products/getproductlocations?tenantid=&locationid=&subcategoryid=&pageno=&pagesize= —
|
||||
* geofenced per-outlet inventory. */
|
||||
export async function getProductLocations(opts: {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
subcategoryid?: number;
|
||||
pageno?: number;
|
||||
pagesize?: number;
|
||||
}): Promise<Row[]> {
|
||||
return toRows(
|
||||
await fiestaGet('products/getproductlocations', {
|
||||
tenantid: opts.tenantid,
|
||||
locationid: opts.locationid,
|
||||
subcategoryid: opts.subcategoryid,
|
||||
pageno: opts.pageno ?? 1,
|
||||
pagesize: opts.pagesize ?? 50,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** /products/getproducts?tenantid=&locationid=&subcategoryid=&keyword=&pageno=&pagesize= —
|
||||
* master catalog listings (global assortment). */
|
||||
export async function getMasterCatalog(opts: {
|
||||
tenantid: number;
|
||||
locationid?: number;
|
||||
subcategoryid?: number;
|
||||
keyword?: string;
|
||||
pageno?: number;
|
||||
pagesize?: number;
|
||||
}): Promise<Row[]> {
|
||||
return toRows(
|
||||
await fiestaGet('products/getproducts', {
|
||||
tenantid: opts.tenantid,
|
||||
locationid: opts.locationid,
|
||||
subcategoryid: opts.subcategoryid,
|
||||
keyword: opts.keyword ?? '',
|
||||
pageno: opts.pageno ?? 1,
|
||||
pagesize: opts.pagesize ?? 50,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** /products/getproductcategories — global product categories. */
|
||||
export async function getProductCategories(): Promise<Row[]> {
|
||||
return toRows(await fiestaGet('products/getproductcategories', {}));
|
||||
}
|
||||
|
||||
/** /products/getproductsubcategories?categoryid=&tenantid= — subcategories under a category. */
|
||||
export async function getProductSubcategories(opts: {
|
||||
categoryid: number;
|
||||
tenantid?: number;
|
||||
}): Promise<Row[]> {
|
||||
return toRows(
|
||||
await fiestaGet('products/getproductsubcategories', {
|
||||
categoryid: opts.categoryid,
|
||||
tenantid: opts.tenantid,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// USERS
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
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,
|
||||
@@ -25,6 +26,10 @@ import {
|
||||
getDeliverySummary,
|
||||
getDeliveries,
|
||||
getDeliveryInsight,
|
||||
getDeliveryReport,
|
||||
getFleetSummary,
|
||||
getOrderDetails,
|
||||
getCustomerOrders,
|
||||
getRiders,
|
||||
getRiderShifts,
|
||||
getTenantLocations,
|
||||
@@ -32,6 +37,11 @@ import {
|
||||
getTenantCustomers,
|
||||
getStockStatement,
|
||||
getProductsCount,
|
||||
getProductStocks,
|
||||
getProductLocations,
|
||||
getMasterCatalog,
|
||||
getProductCategories,
|
||||
getProductSubcategories,
|
||||
getAllUsers,
|
||||
getUserById,
|
||||
createUser,
|
||||
@@ -55,6 +65,15 @@ export const fiestaKeys = {
|
||||
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,
|
||||
};
|
||||
@@ -238,6 +257,117 @@ export function useFiestaStoresStock(
|
||||
}));
|
||||
}
|
||||
|
||||
// ── 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;
|
||||
@@ -279,4 +409,38 @@ export function useFiestaUpdateUser() {
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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 };
|
||||
|
||||
Reference in New Issue
Block a user