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:
parent
2e8a908f05
commit
9530125509
4 changed files with 33 additions and 20 deletions
|
|
@ -41,12 +41,13 @@ async function getSessionAndOverrides(year, month) {
|
||||||
return { session, overrideMap }
|
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 startDate = `${year}-${String(month).padStart(2, '0')}-01`
|
||||||
const days = lastDayNum
|
const days = lastDayNum
|
||||||
|
const da = `&dow_align=${dowAlign}`
|
||||||
const [revData, roomsData] = await Promise.all([
|
const [revData, roomsData] = await Promise.all([
|
||||||
fcFetch(`/forecast/revenue?start_date=${startDate}&days=${days}&type=all`),
|
fcFetch(`/forecast/revenue?start_date=${startDate}&days=${days}&type=all${da}`),
|
||||||
fcFetch(`/forecast/rooms?start_date=${startDate}&days=${days}`),
|
fcFetch(`/forecast/rooms?start_date=${startDate}&days=${days}${da}`),
|
||||||
])
|
])
|
||||||
const revMap = {}
|
const revMap = {}
|
||||||
for (const d of revData.data) revMap[d.date] = d
|
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) {
|
if (isNaN(year) || isNaN(month) || month < 1 || month > 12) {
|
||||||
return reply.status(400).send({ error: 'Invalid year/month' })
|
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 lastDayNum = new Date(year, month, 0).getDate()
|
||||||
const today = new Date().toISOString().split('T')[0]
|
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([
|
const [{ session, overrideMap }, { revMap, roomsMap }] = await Promise.all([
|
||||||
getSessionAndOverrides(year, month),
|
getSessionAndOverrides(year, month),
|
||||||
fetchMonthFromApi(year, month, lastDayNum),
|
fetchMonthFromApi(year, month, lastDayNum, dowAlign),
|
||||||
])
|
])
|
||||||
|
|
||||||
const rate = parseFloat(session.pickup_avg_rate || sessionRate)
|
const rate = parseFloat(session.pickup_avg_rate || sessionRate)
|
||||||
|
|
@ -186,10 +188,11 @@ export async function directorsForecastRoutes(fastify) {
|
||||||
const year = parseInt(request.params.year)
|
const year = parseInt(request.params.year)
|
||||||
const month = parseInt(request.params.month)
|
const month = parseInt(request.params.month)
|
||||||
const lastDayNum = new Date(year, month, 0).getDate()
|
const lastDayNum = new Date(year, month, 0).getDate()
|
||||||
|
const dowAlign = request.query.dow_align !== 'false'
|
||||||
|
|
||||||
const [{ session, overrideMap }, { revMap, roomsMap }] = await Promise.all([
|
const [{ session, overrideMap }, { revMap, roomsMap }] = await Promise.all([
|
||||||
getSessionAndOverrides(year, month),
|
getSessionAndOverrides(year, month),
|
||||||
fetchMonthFromApi(year, month, lastDayNum),
|
fetchMonthFromApi(year, month, lastDayNum, dowAlign),
|
||||||
])
|
])
|
||||||
|
|
||||||
const sessionRate = parseFloat(session.pickup_avg_rate || 135)
|
const sessionRate = parseFloat(session.pickup_avg_rate || 135)
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@ export function getWeeklyActual(weekEnding: string): Promise<WeeklyActualData> {
|
||||||
|
|
||||||
// ── Directors Forecast API ──────────────────────────────────────────────────
|
// ── Directors Forecast API ──────────────────────────────────────────────────
|
||||||
|
|
||||||
export function dfGetWorksheet(year: number, month: number): Promise<WorksheetData> {
|
export function dfGetWorksheet(year: number, month: number, dowAlign = true): Promise<WorksheetData> {
|
||||||
return request<WorksheetData>(`/directors-forecast/worksheet/${year}/${month}`)
|
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 }> {
|
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> {
|
export function dfGetReport(year: number, month: number, dowAlign = true): Promise<ForecastReportData> {
|
||||||
return request<ForecastReportData>(`/directors-forecast/report/${year}/${month}`)
|
return request<ForecastReportData>(`/directors-forecast/report/${year}/${month}?dow_align=${dowAlign}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dfSaveSnapshot(year: number, month: number, data: object) {
|
export function dfSaveSnapshot(year: number, month: number, data: object) {
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,11 @@ html, body, #root {
|
||||||
}
|
}
|
||||||
.df-nav-btn:hover { background: var(--body-bg); }
|
.df-nav-btn:hover { background: var(--body-bg); }
|
||||||
.df-month-label { font-weight: 600; font-size: 14px; min-width: 140px; text-align: center; }
|
.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-tabs { display: flex; gap: 4px; }
|
||||||
.df-tab {
|
.df-tab {
|
||||||
padding: 5px 14px; border-radius: 4px; border: 1px solid var(--border);
|
padding: 5px 14px; border-radius: 4px; border: 1px solid var(--border);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ type DayOverride = {
|
||||||
|
|
||||||
// ── Worksheet tab ─────────────────────────────────────────────────────────────
|
// ── 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 { user } = useAuth()
|
||||||
const canEdit = can(user, 'edit')
|
const canEdit = can(user, 'edit')
|
||||||
|
|
||||||
|
|
@ -57,7 +57,7 @@ function WorksheetTab({ year, month }: { year: number; month: number }) {
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true); setError(null)
|
setLoading(true); setError(null)
|
||||||
try {
|
try {
|
||||||
const d: WorksheetData = await dfGetWorksheet(year, month)
|
const d: WorksheetData = await dfGetWorksheet(year, month, dowAlign)
|
||||||
setData(d)
|
setData(d)
|
||||||
setRate(String(d.session.pickup_avg_rate ?? 135))
|
setRate(String(d.session.pickup_avg_rate ?? 135))
|
||||||
const ovrs: Record<string, DayOverride> = {}
|
const ovrs: Record<string, DayOverride> = {}
|
||||||
|
|
@ -73,7 +73,7 @@ function WorksheetTab({ year, month }: { year: number; month: number }) {
|
||||||
setDirty(false)
|
setDirty(false)
|
||||||
} catch { setError('Failed to load worksheet data.') }
|
} catch { setError('Failed to load worksheet data.') }
|
||||||
finally { setLoading(false) }
|
finally { setLoading(false) }
|
||||||
}, [year, month])
|
}, [year, month, dowAlign])
|
||||||
|
|
||||||
useEffect(() => { load() }, [load])
|
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>
|
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 { user } = useAuth()
|
||||||
const canEdit = can(user, 'edit')
|
const canEdit = can(user, 'edit')
|
||||||
|
|
||||||
|
|
@ -299,10 +299,10 @@ function ReportTab({ year, month }: { year: number; month: number }) {
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true); setError(null)
|
setLoading(true); setError(null)
|
||||||
try { setData(await dfGetReport(year, month)) }
|
try { setData(await dfGetReport(year, month, dowAlign)) }
|
||||||
catch { setError('Failed to load report data.') }
|
catch { setError('Failed to load report data.') }
|
||||||
finally { setLoading(false) }
|
finally { setLoading(false) }
|
||||||
}, [year, month])
|
}, [year, month, dowAlign])
|
||||||
|
|
||||||
useEffect(() => { load() }, [load])
|
useEffect(() => { load() }, [load])
|
||||||
|
|
||||||
|
|
@ -497,11 +497,16 @@ export default function DirectorsForecast() {
|
||||||
const [year, setYear] = useState(now.getFullYear())
|
const [year, setYear] = useState(now.getFullYear())
|
||||||
const [month, setMonth] = useState(now.getMonth() + 1)
|
const [month, setMonth] = useState(now.getMonth() + 1)
|
||||||
const [tab, setTab] = useState<Tab>('worksheet')
|
const [tab, setTab] = useState<Tab>('worksheet')
|
||||||
|
const [dowAlign, setDowAlign] = useState(true)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="df-root">
|
<div className="df-root">
|
||||||
<div className="df-header">
|
<div className="df-header">
|
||||||
<MonthNav year={year} month={month} onChange={(y, m) => { setYear(y); setMonth(m) }} />
|
<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">
|
<div className="df-tabs">
|
||||||
<button className={`df-tab${tab === 'worksheet' ? ' active' : ''}`} onClick={() => setTab('worksheet')}>
|
<button className={`df-tab${tab === 'worksheet' ? ' active' : ''}`} onClick={() => setTab('worksheet')}>
|
||||||
Worksheet
|
Worksheet
|
||||||
|
|
@ -513,8 +518,8 @@ export default function DirectorsForecast() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tab === 'worksheet'
|
{tab === 'worksheet'
|
||||||
? <WorksheetTab key={`ws-${year}-${month}`} year={year} month={month} />
|
? <WorksheetTab key={`ws-${year}-${month}-${dowAlign}`} year={year} month={month} dowAlign={dowAlign} />
|
||||||
: <ReportTab key={`rp-${year}-${month}`} year={year} month={month} />}
|
: <ReportTab key={`rp-${year}-${month}-${dowAlign}`} year={year} month={month} dowAlign={dowAlign} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue