Initial commit

This commit is contained in:
2026-08-24 20:35:18 +05:30
commit 1dc582ba07
85 changed files with 21081 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
import { createContext, useContext, useMemo, type ReactNode } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/AuthContext';
import { useTenantLocations } from '@/queries/hooks';
import type { TenantLocation } from '@/api/types';
/** `null` means All branches. */
export type BranchSelection = number | null;
export interface BranchScopeValue {
/** Every branch this tenant runs, in a stable order. */
branches: TenantLocation[];
isLoading: boolean;
/** The tenant these branches belong to — from the session, never the URL. */
tenantid: number;
/** `null` = All branches. */
selected: BranchSelection;
select: (next: BranchSelection) => void;
/**
* The branches a page should actually read.
*
* All branches → every one of them; a specific branch → just that one. Pages
* fan out over this rather than branching on `selected` themselves, which is
* what keeps "All" and "one" the same code path.
*/
scoped: TenantLocation[];
/** The selected branch, or undefined under All. */
current: TenantLocation | undefined;
}
const BranchScopeContext = createContext<BranchScopeValue | null>(null);
/** The URL param. In the URL so a link to a page carries its branch with it. */
const PARAM = 'branch';
/**
* Which branch the Store Admin is looking at.
*
* Held in the URL rather than in component state for two reasons. A link to
* "Inventory, Peelamedu" has to survive being pasted into a chat, and a reload
* during a shift must not silently drop the operator back to All branches while
* they are reading a number that only makes sense for one shop.
*
* The tenant, by contrast, comes from the session and is deliberately NOT in
* the URL. Fiesta has no web auth, so tenant scoping is enforced by this client
* alone — putting the tenant id in an editable address bar would turn the one
* boundary we control into a text field.
*/
export function BranchScopeProvider({ children }: { children: ReactNode }) {
const { user } = useAuth();
const [params, setParams] = useSearchParams();
const tenantid = user?.tenantid ?? 0;
const { data, isLoading } = useTenantLocations(tenantid || undefined);
const branches = useMemo(
() => [...(data ?? [])].sort((a, b) => a.locationid - b.locationid),
[data],
);
const raw = params.get(PARAM);
const parsed = raw === null || raw === 'all' ? null : Number(raw);
// An id in the URL that this tenant does not own falls back to All rather
// than showing an empty page — the id is user-editable, so it is untrusted.
const selected =
parsed !== null && Number.isFinite(parsed) && branches.some((b) => b.locationid === parsed)
? parsed
: null;
const value = useMemo<BranchScopeValue>(() => {
const current = selected === null ? undefined : branches.find((b) => b.locationid === selected);
return {
branches,
isLoading,
tenantid,
selected,
current,
scoped: current ? [current] : branches,
select: (next) => {
const nextParams = new URLSearchParams(params);
if (next === null) nextParams.delete(PARAM);
else nextParams.set(PARAM, String(next));
setParams(nextParams, { replace: true });
},
};
}, [branches, isLoading, tenantid, selected, params, setParams]);
return <BranchScopeContext.Provider value={value}>{children}</BranchScopeContext.Provider>;
}
export function useBranchScope(): BranchScopeValue {
const value = useContext(BranchScopeContext);
if (!value) throw new Error('useBranchScope must be used inside a BranchScopeProvider');
return value;
}