// MultiSelect dropdown — used for branches, categories, products const MultiSelect = ({ label, options, selected, onChange, placeholder = "All", disabled = false }) => { const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(''); const ref = React.useRef(null); React.useEffect(() => { const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); const toggle = (opt) => { if (selected.includes(opt)) onChange(selected.filter((x) => x !== opt));else onChange([...selected, opt]); }; const filtered = options.filter((o) => o.toLowerCase().includes(search.toLowerCase())); const allSelected = selected.length === options.length; const noneSelected = selected.length === 0; let summary; if (noneSelected) summary = placeholder;else if (allSelected) summary = `All ${label.toLowerCase()}`;else if (selected.length === 1) summary = selected[0];else summary = `${selected.length} selected`; return (
{open &&
{options.length > 6 &&
setSearch(e.target.value)} placeholder={`Search ${label.toLowerCase()}…`} className="w-full px-2.5 py-1.5 text-sm border border-slate-200 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500" />
}
{filtered.length === 0 &&
No matches
} {filtered.map((opt) => )}
}
); }; // DatePopover — custom calendar so we can enforce strict "commit only when a // real date is clicked." Native fires `change` on month/ // year nudges and wheel-spins, which the rep complained about. Here, month/ // year navigation only mutates the popover's internal `view` state; the only // path that calls onChange is the day-cell click. const DatePopover = ({ value, onChange, ariaLabel }) => { const [open, setOpen] = React.useState(false); const [view, setView] = React.useState(() => { const d = value ? new Date(value) : new Date(); return { y: d.getFullYear(), m: d.getMonth() }; }); const ref = React.useRef(null); React.useEffect(() => { if (!open) return; const onDocDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; const onEsc = (e) => { if (e.key === 'Escape') setOpen(false); }; document.addEventListener('mousedown', onDocDown); document.addEventListener('keydown', onEsc); return () => { document.removeEventListener('mousedown', onDocDown); document.removeEventListener('keydown', onEsc); }; }, [open]); const openPopover = () => { // Reset the visible month to whatever the committed value points at, so // re-opening after browsing months doesn't strand the user on a stale view. const d = value ? new Date(value) : new Date(); setView({ y: d.getFullYear(), m: d.getMonth() }); setOpen(true); }; const pad = (n) => String(n).padStart(2, '0'); const fmtIso = (y, m, d) => `${y}-${pad(m + 1)}-${pad(d)}`; const display = value ? new Date(value).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) : 'Pick date'; const firstDow = new Date(view.y, view.m, 1).getDay(); const daysInMonth = new Date(view.y, view.m + 1, 0).getDate(); const cells = []; for (let i = 0; i < firstDow; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(d); while (cells.length % 7 !== 0) cells.push(null); const prev = () => setView(v => v.m === 0 ? { y: v.y - 1, m: 11 } : { y: v.y, m: v.m - 1 }); const next = () => setView(v => v.m === 11 ? { y: v.y + 1, m: 0 } : { y: v.y, m: v.m + 1 }); // The ONLY path that commits upward. const selectDay = (d) => { onChange(fmtIso(view.y, view.m, d)); setOpen(false); }; const baseYear = new Date().getFullYear(); const yearOptions = []; for (let y = baseYear - 10; y <= baseYear + 5; y++) yearOptions.push(y); if (!yearOptions.includes(view.y)) { yearOptions.push(view.y); yearOptions.sort((a, b) => a - b); } const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; const dowNames = ['S','M','T','W','T','F','S']; const selectedDay = value && (() => { const d = new Date(value); return (d.getFullYear() === view.y && d.getMonth() === view.m) ? d.getDate() : null; })(); return (
{open && (
{dowNames.map((n, i) => (
{n}
))}
{cells.map((d, i) => { if (d == null) return
; const isSelected = d === selectedDay; return ( ); })}
)}
); }; // Date range — two custom popovers, one per bound. The popover commits only // when a day cell is clicked; outside-click / Esc / month-year nav never fire. const DateRange = ({ start, end, onChange }) => { return (
onChange({ start: v, end })} /> onChange({ start, end: v })} />
); }; // Series toggles — checkboxes const SeriesToggles = ({ series, onChange }) => { const items = [ { key: 'forecast', label: 'Forecast', color: '#3b82f6' }, { key: 'actual', label: 'Actual Sales', color: '#16a34a' }, { key: 'inventory', label: 'Inventory', color: '#f59e0b' }, { key: 'requisitions', label: 'Requisitions', color: '#7c3aed' }]; return (
{items.map((it) => )}
); }; // Confidence-band controls. Schema-driven: receives the list of upper/lower // levels actually present in the `results` table (discovered server-side via // INFORMATION_SCHEMA) and renders one checkbox per level on each side. // // `selection` is an array of keys like ["upper_80", "lower_60"]. The chart // pairs same-level upper+lower into a single band with one color; otherwise // each selection gets its own color (see chart.jsx). const ConfidenceControls = ({ bandLevels, selection, onChange, disabled }) => { const upperLevels = (bandLevels && bandLevels.upper) || []; const lowerLevels = (bandLevels && bandLevels.lower) || []; const empty = upperLevels.length === 0 && lowerLevels.length === 0; const sel = new Set(selection || []); const toggle = (key, on) => { const next = new Set(sel); if (on) next.add(key); else next.delete(key); onChange([...next]); }; return (
{empty ? ( No band columns in results table ) : (
{upperLevels.length > 0 && (
Upper {upperLevels.map((lvl) => { const key = `upper_${lvl}`; return ( ); })}
)} {lowerLevels.length > 0 && (
Lower {lowerLevels.map((lvl) => { const key = `lower_${lvl}`; return ( ); })}
)}
)}
); }; window.MultiSelect = MultiSelect; window.DateRange = DateRange; window.SeriesToggles = SeriesToggles; window.ConfidenceControls = ConfidenceControls;