pos implemented

This commit is contained in:
2026-08-10 18:14:52 +05:30
parent 14e4529613
commit 31d5535b72
14 changed files with 1373 additions and 301 deletions

View File

@@ -23,7 +23,20 @@
* Components call the hooks in `./posQueries`, not these functions.
*/
const POS_BASE = import.meta.env.VITE_FIESTA_POS_URL || 'https://fiesta.nearle.app/live/api/v1/pos';
/**
* The console reads counter sales from `/v1/web/pos`, not `/v1/pos`.
*
* `/v1/pos` is the terminal group and sits behind `PosAuth`, which pins a
* request to the outlet inside a till's token. A browser has no such token and
* cannot get one — `/pos/login` refuses any account that is not a till account.
* So the day `POS_AUTH_REQUIRED=true` is set, every read on that group would
* 401 and this page would go blank.
*
* `/v1/web/pos` serves the same five handlers outside the guard. Identical
* responses — they read their scope from query parameters and never touch the
* session — so nothing else in this client changes.
*/
const POS_BASE = import.meta.env.VITE_FIESTA_POS_URL || 'https://fiesta.nearle.app/live/api/v1/web/pos';
type QueryParams = Record<string, string | number | undefined | null>;
@@ -136,6 +149,48 @@ export interface PosSalesFilter {
pagesize?: number;
}
/**
* The terminal group, used only until the console base exists on a server.
*
* TRANSITIONAL. The frontend and the backend deploy separately and the backend
* is currently behind, so shipping this build first would point every read at a
* route that answers 404 and blank the page. Rather than make the two deploys
* ordered — a constraint nobody would remember in six months — the base is
* resolved once at runtime: try the console group, and if the server is old
* enough not to have it, fall back to the terminal group for this session.
*
* Safe to delete once every environment is past the deploy that added
* `/v1/web/pos`. It is a no-op there: the first probe succeeds and the fallback
* is never reached.
*/
const POS_BASE_LEGACY = POS_BASE.replace('/v1/web/pos', '/v1/pos').replace('/v1/mob/pos', '/v1/pos');
let resolvedBase: string | null = POS_BASE === POS_BASE_LEGACY ? POS_BASE : null;
let baseProbe: Promise<string> | null = null;
/** Resolves the base once per session; concurrent callers share one probe. */
async function posBase(): Promise<string> {
if (resolvedBase) return resolvedBase;
if (!baseProbe) {
baseProbe = (async () => {
try {
// `sales/summary` needs no data to exist — a missing route 404s, a
// present one answers 200 even for an outlet with no bills.
const res = await fetch(`${POS_BASE}/sales/summary?locationid=0`, {
headers: { Accept: 'application/json' },
});
resolvedBase = res.status === 404 ? POS_BASE_LEGACY : POS_BASE;
} catch {
// A network failure says nothing about which routes exist; assume the
// current one and let the real request surface the error.
resolvedBase = POS_BASE;
}
return resolvedBase;
})();
}
return baseProbe;
}
// ── Transport ────────────────────────────────────────────────────────────────
async function posGet<T = unknown>(endpoint: string, params: QueryParams = {}): Promise<T> {
@@ -144,7 +199,8 @@ async function posGet<T = unknown>(endpoint: string, params: QueryParams = {}):
if (v !== undefined && v !== null && v !== '') qs.append(k, String(v));
});
const query = qs.toString();
const res = await fetch(`${POS_BASE}/${endpoint}${query ? `?${query}` : ''}`, {
const base = await posBase();
const res = await fetch(`${base}/${endpoint}${query ? `?${query}` : ''}`, {
headers: { Accept: 'application/json' },
});
if (!res.ok) {