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 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-21 18:29:02 +00:00
parent 2e8a908f05
commit 9530125509
4 changed files with 33 additions and 20 deletions

View file

@ -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)

View file

@ -38,8 +38,8 @@ export function getWeeklyActual(weekEnding: string): Promise<WeeklyActualData> {
// ── Directors Forecast API ──────────────────────────────────────────────────
export function dfGetWorksheet(year: number, month: number): Promise<WorksheetData> {
return request<WorksheetData>(`/directors-forecast/worksheet/${year}/${month}`)
export function dfGetWorksheet(year: number, month: number, dowAlign = true): Promise<WorksheetData> {
return request<WorksheetData>(`/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<ForecastReportData> {
return request<ForecastReportData>(`/directors-forecast/report/${year}/${month}`)
export function dfGetReport(year: number, month: number, dowAlign = true): Promise<ForecastReportData> {
return request<ForecastReportData>(`/directors-forecast/report/${year}/${month}?dow_align=${dowAlign}`)
}
export function dfSaveSnapshot(year: number, month: number, data: object) {

View file

@ -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);

View file

@ -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<string, DayOverride> = {}
@ -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 <td className={v >= 0 ? 'df-pos' : 'df-neg'}>{v >= 0 ? '+' : ''}{fmtCcy(v)}</td>
}
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])
@ -497,11 +497,16 @@ export default function DirectorsForecast() {
const [year, setYear] = useState(now.getFullYear())
const [month, setMonth] = useState(now.getMonth() + 1)
const [tab, setTab] = useState<Tab>('worksheet')
const [dowAlign, setDowAlign] = useState(true)
return (
<div className="df-root">
<div className="df-header">
<MonthNav year={year} month={month} onChange={(y, m) => { setYear(y); setMonth(m) }} />
<label className="df-dow-toggle">
<input type="checkbox" checked={dowAlign} onChange={e => setDowAlign(e.target.checked)} />
DOW-aligned LY
</label>
<div className="df-tabs">
<button className={`df-tab${tab === 'worksheet' ? ' active' : ''}`} onClick={() => setTab('worksheet')}>
Worksheet
@ -513,8 +518,8 @@ export default function DirectorsForecast() {
</div>
{tab === 'worksheet'
? <WorksheetTab key={`ws-${year}-${month}`} year={year} month={month} />
: <ReportTab key={`rp-${year}-${month}`} year={year} month={month} />}
? <WorksheetTab key={`ws-${year}-${month}-${dowAlign}`} year={year} month={month} dowAlign={dowAlign} />
: <ReportTab key={`rp-${year}-${month}-${dowAlign}`} year={year} month={month} dowAlign={dowAlign} />}
</div>
)
}