Files
daily_console_web/src/components/shell/DateScope.tsx
2026-09-18 16:57:24 +05:30

132 lines
5.5 KiB
TypeScript

import { createContext, useContext, useMemo, useState, type ReactNode } from 'react';
import type { DateRange } from '@/api/insights';
import {
DateRangePicker,
presetRange,
type RangePreset,
} from '@/features/store-admin/DateRangePicker';
/**
* The console's date filter, held once for the whole workspace.
*
* It sits in the top bar beside the profile, not on the page, and every page
* reads it from here — the same relationship `BranchScope` already has to the
* branch picker beside the logo. The two scopes now work the same way: the two
* questions every page is asked, "which shop" and "when", are answered once in
* the chrome rather than re-answered on each screen.
*
* ── What this changes about the pages ───────────────────────────────────────
*
* The range survives navigation. Setting March on Sales and clicking through to
* Reports shows March, which is what somebody looking into a month actually
* wants and is the whole reason for lifting it. It also means the live boards —
* Console, Counters, Terminals — no longer force themselves back to today; they
* follow the shared range like everything else. That is the trade the move
* makes, and it is the right one, but it IS a change: those three used to be
* pinned to the day whatever else you had chosen.
*/
export interface DateScopeValue {
preset: RangePreset;
/**
* What the PAGES read — always a real window.
*
* When nobody has picked anything this is the default week, not an empty
* object. Left empty, every read on the console became unbounded and each
* part of a page bounded it differently: the Console's chart drew however
* many days the last 500 orders happened to span (nine, in practice) while
* the KPI tiles beside it totalled all of them, and the counter figures came
* from a POS summary with no window at all. One page, three answers.
*/
range: DateRange;
/**
* What the PICKER shows — empty until somebody chooses.
*
* This is the half that keeps the filter unselected on arrival. The two are
* separate on purpose: "no filter has been set" is a fact about the control,
* and "which week are we looking at" is a fact about the data, and collapsing
* them into one value is what forced a choice between a filter nobody set and
* a page with no window.
*/
chosen: DateRange;
set: (preset: RangePreset, range: DateRange) => void;
/** Back to no filter at all — what an empty state offers as a way out. */
clear: () => void;
/** True when the USER has narrowed the page, not merely that a window exists. */
isFiltered: boolean;
}
/**
* The window the console shows when nobody has picked one.
*
* A WEEK — while the picker still shows nothing selected. Those are two
* separate statements and both are deliberate.
*
* The filter opens unselected because the console used to narrow every page
* before anyone asked: somebody signing in to see how trade is going was shown
* a slice with no sign that it was one, and the range then followed them across
* the whole console.
*
* But unselected must not mean UNBOUNDED. With no dates at all the reads were
* capped only by `pagesize`, and each part of a page then bounded itself
* differently — the Console's chart drew however many days the last 500 orders
* happened to span (nine), the KPI tiles beside it totalled all of those
* orders, and the counter figures came from a POS summary with no window
* whatsoever. One page, three different answers to "when".
*
* A default window is not a filter. Nothing is hidden from the reader, the
* control is empty, and one choice replaces it.
*/
const DEFAULT_WINDOW: RangePreset = 'week';
const DateScopeContext = createContext<DateScopeValue | null>(null);
export function DateScopeProvider({ children }: { children: ReactNode }) {
/* What the user picked. Empty until they do — this is what the picker shows. */
const [preset, setPreset] = useState<RangePreset>('custom');
const [chosen, setChosen] = useState<DateRange>({});
const value = useMemo<DateScopeValue>(() => {
const hasChoice = Boolean(chosen.fromdate || chosen.todate);
return {
preset,
chosen,
/* The window the pages actually read: what was picked, or the default
week. Resolved here, once, so every read on a page shares it — the
orders, the POS summaries and the chart cannot end up describing
different spans. */
range: hasChoice ? chosen : presetRange(DEFAULT_WINDOW),
set: (nextPreset, nextRange) => {
setPreset(nextPreset);
setChosen(nextRange);
},
clear: () => {
setPreset('custom');
setChosen({});
},
isFiltered: hasChoice,
};
}, [preset, chosen]);
return <DateScopeContext.Provider value={value}>{children}</DateScopeContext.Provider>;
}
/**
* The shared range.
*
* Throws outside the provider rather than inventing a local range: a page that
* silently filtered on its own dates while the bar showed something else would
* be the exact confusion this exists to remove.
*/
export function useDateScope(): DateScopeValue {
const value = useContext(DateScopeContext);
if (!value) throw new Error('useDateScope must be used inside a DateScopeProvider');
return value;
}
/** The control itself. Rendered once, in the top bar. */
export function DateScopePicker() {
const dates = useDateScope();
return <DateRangePicker range={dates.chosen} onChange={dates.set} />;
}