Initial commit
This commit is contained in:
95
src/features/store-admin/BranchScope.tsx
Normal file
95
src/features/store-admin/BranchScope.tsx
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user