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(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('custom'); const [chosen, setChosen] = useState({}); const value = useMemo(() => { 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 {children}; } /** * 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 ; }