From 9530125509b6d340a8fd2981d236d97e7ec7c8e0 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Tue, 21 Jul 2026 18:29:02 +0000 Subject: [PATCH] Add DOW-aligned LY toggle to directors forecast worksheet and report Checkbox in the header switches between 364-day same-weekday comparison and same calendar-date last year. Default remains DOW-aligned. Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/directors-forecast.js | 13 +++++++----- frontend/src/api.ts | 8 +++---- frontend/src/index.css | 5 +++++ frontend/src/pages/DirectorsForecast.tsx | 27 ++++++++++++++---------- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/backend/src/routes/directors-forecast.js b/backend/src/routes/directors-forecast.js index d7670f15..819d3cce 100644 --- a/backend/src/routes/directors-forecast.js +++ b/backend/src/routes/directors-forecast.js @@ -41,12 +41,13 @@ async function getSessionAndOverrides(year, month) { return { session, overrideMap } } -async function fetchMonthFromApi(year, month, lastDayNum) { +async function fetchMonthFromApi(year, month, lastDayNum, dowAlign = true) { const startDate = `${year}-${String(month).padStart(2, '0')}-01` const days = lastDayNum + const da = `&dow_align=${dowAlign}` const [revData, roomsData] = await Promise.all([ - fcFetch(`/forecast/revenue?start_date=${startDate}&days=${days}&type=all`), - fcFetch(`/forecast/rooms?start_date=${startDate}&days=${days}`), + fcFetch(`/forecast/revenue?start_date=${startDate}&days=${days}&type=all${da}`), + fcFetch(`/forecast/rooms?start_date=${startDate}&days=${days}${da}`), ]) const revMap = {} for (const d of revData.data) revMap[d.date] = d @@ -65,6 +66,7 @@ export async function directorsForecastRoutes(fastify) { if (isNaN(year) || isNaN(month) || month < 1 || month > 12) { return reply.status(400).send({ error: 'Invalid year/month' }) } + const dowAlign = request.query.dow_align !== 'false' const lastDayNum = new Date(year, month, 0).getDate() const today = new Date().toISOString().split('T')[0] @@ -72,7 +74,7 @@ export async function directorsForecastRoutes(fastify) { const [{ session, overrideMap }, { revMap, roomsMap }] = await Promise.all([ getSessionAndOverrides(year, month), - fetchMonthFromApi(year, month, lastDayNum), + fetchMonthFromApi(year, month, lastDayNum, dowAlign), ]) const rate = parseFloat(session.pickup_avg_rate || sessionRate) @@ -186,10 +188,11 @@ export async function directorsForecastRoutes(fastify) { const year = parseInt(request.params.year) const month = parseInt(request.params.month) const lastDayNum = new Date(year, month, 0).getDate() + const dowAlign = request.query.dow_align !== 'false' const [{ session, overrideMap }, { revMap, roomsMap }] = await Promise.all([ getSessionAndOverrides(year, month), - fetchMonthFromApi(year, month, lastDayNum), + fetchMonthFromApi(year, month, lastDayNum, dowAlign), ]) const sessionRate = parseFloat(session.pickup_avg_rate || 135) diff --git a/frontend/src/api.ts b/frontend/src/api.ts index ed03d0eb..01393c94 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -38,8 +38,8 @@ export function getWeeklyActual(weekEnding: string): Promise { // ── Directors Forecast API ────────────────────────────────────────────────── -export function dfGetWorksheet(year: number, month: number): Promise { - return request(`/directors-forecast/worksheet/${year}/${month}`) +export function dfGetWorksheet(year: number, month: number, dowAlign = true): Promise { + return request(`/directors-forecast/worksheet/${year}/${month}?dow_align=${dowAlign}`) } export function dfSaveWorksheet(year: number, month: number, data: { pickup_avg_rate: number; overrides: object[] }): Promise<{ ok: boolean }> { @@ -48,8 +48,8 @@ export function dfSaveWorksheet(year: number, month: number, data: { pickup_avg_ }) } -export function dfGetReport(year: number, month: number): Promise { - return request(`/directors-forecast/report/${year}/${month}`) +export function dfGetReport(year: number, month: number, dowAlign = true): Promise { + return request(`/directors-forecast/report/${year}/${month}?dow_align=${dowAlign}`) } export function dfSaveSnapshot(year: number, month: number, data: object) { diff --git a/frontend/src/index.css b/frontend/src/index.css index c6493c58..de776f6a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -449,6 +449,11 @@ html, body, #root { } .df-nav-btn:hover { background: var(--body-bg); } .df-month-label { font-weight: 600; font-size: 14px; min-width: 140px; text-align: center; } +.df-dow-toggle { + display: flex; align-items: center; gap: 6px; margin-left: auto; + font-size: 12px; color: var(--text-muted); cursor: pointer; user-select: none; +} +.df-dow-toggle input[type="checkbox"] { cursor: pointer; accent-color: var(--gold); } .df-tabs { display: flex; gap: 4px; } .df-tab { padding: 5px 14px; border-radius: 4px; border: 1px solid var(--border); diff --git a/frontend/src/pages/DirectorsForecast.tsx b/frontend/src/pages/DirectorsForecast.tsx index 965aa6fd..46fe8f70 100644 --- a/frontend/src/pages/DirectorsForecast.tsx +++ b/frontend/src/pages/DirectorsForecast.tsx @@ -42,7 +42,7 @@ type DayOverride = { // ── Worksheet tab ───────────────────────────────────────────────────────────── -function WorksheetTab({ year, month }: { year: number; month: number }) { +function WorksheetTab({ year, month, dowAlign }: { year: number; month: number; dowAlign: boolean }) { const { user } = useAuth() const canEdit = can(user, 'edit') @@ -57,7 +57,7 @@ function WorksheetTab({ year, month }: { year: number; month: number }) { const load = useCallback(async () => { setLoading(true); setError(null) try { - const d: WorksheetData = await dfGetWorksheet(year, month) + const d: WorksheetData = await dfGetWorksheet(year, month, dowAlign) setData(d) setRate(String(d.session.pickup_avg_rate ?? 135)) const ovrs: Record = {} @@ -73,7 +73,7 @@ function WorksheetTab({ year, month }: { year: number; month: number }) { setDirty(false) } catch { setError('Failed to load worksheet data.') } finally { setLoading(false) } - }, [year, month]) + }, [year, month, dowAlign]) useEffect(() => { load() }, [load]) @@ -287,7 +287,7 @@ function VarCell({ a, b }: { a: number; b: number }) { return = 0 ? 'df-pos' : 'df-neg'}>{v >= 0 ? '+' : ''}{fmtCcy(v)} } -function ReportTab({ year, month }: { year: number; month: number }) { +function ReportTab({ year, month, dowAlign }: { year: number; month: number; dowAlign: boolean }) { const { user } = useAuth() const canEdit = can(user, 'edit') @@ -299,10 +299,10 @@ function ReportTab({ year, month }: { year: number; month: number }) { const load = useCallback(async () => { setLoading(true); setError(null) - try { setData(await dfGetReport(year, month)) } + try { setData(await dfGetReport(year, month, dowAlign)) } catch { setError('Failed to load report data.') } finally { setLoading(false) } - }, [year, month]) + }, [year, month, dowAlign]) useEffect(() => { load() }, [load]) @@ -494,14 +494,19 @@ type Tab = 'worksheet' | 'report' export default function DirectorsForecast() { useFrameViewport('desktop') const now = new Date() - const [year, setYear] = useState(now.getFullYear()) - const [month, setMonth] = useState(now.getMonth() + 1) - const [tab, setTab] = useState('worksheet') + const [year, setYear] = useState(now.getFullYear()) + const [month, setMonth] = useState(now.getMonth() + 1) + const [tab, setTab] = useState('worksheet') + const [dowAlign, setDowAlign] = useState(true) return (
{ setYear(y); setMonth(m) }} /> +
{tab === 'worksheet' - ? - : } + ? + : }
) }