diff --git a/backend/src/routes/weekly-actual.js b/backend/src/routes/weekly-actual.js index d7609f03..55655c6e 100644 --- a/backend/src/routes/weekly-actual.js +++ b/backend/src/routes/weekly-actual.js @@ -88,9 +88,9 @@ export async function weeklyActualRoutes(fastify) { // 5-week window: start 4 weeks before this week's Sunday const fiveWeekStart = addDays(weekStartDate, -28) - // Month to date: 1st of month → week ending date - const monthStart = new Date(year, month - 1, 1) - const monthDays = Math.round((weekEndDate - monthStart) / 86400000) + 1 + // Full calendar month (needed for daily chart — month progress filters by date) + const monthStart = new Date(year, month - 1, 1) + const daysInMonth = new Date(year, month, 0).getDate() // 36 months of raw totals needed to compute rolling 12-month averages for // 12 display points (indices 23–35) plus their LY equivalents (indices 0–23) @@ -106,10 +106,8 @@ export async function weeklyActualRoutes(fastify) { fcFetch(`/forecast/revenue?start_date=${toDateStr(fiveWeekStart)}&days=35&type=all`), // This week rooms (for occupancy table) fcFetch(`/forecast/rooms?start_date=${toDateStr(weekStartDate)}&days=7`), - // Month to date revenue - monthDays > 0 - ? fcFetch(`/forecast/revenue?start_date=${toDateStr(monthStart)}&days=${monthDays}&type=all`) - : Promise.resolve({ data: [] }), + // Full month revenue (used for month progress totals + daily chart) + fcFetch(`/forecast/revenue?start_date=${toDateStr(monthStart)}&days=${daysInMonth}&type=all`), // 12 monthly trend revenue calls ...trendMonths.map(({ year: y, month: m }) => { const firstDay = new Date(y, m - 1, 1) @@ -118,11 +116,11 @@ export async function weeklyActualRoutes(fastify) { }), ]) - const [fiveWeekRevRaw, thisWeekRoomsRaw, monthRevRaw, ...trendRevRaw] = results + const [fiveWeekRevRaw, thisWeekRoomsRaw, fullMonthRevRaw, ...trendRevRaw] = results - const fiveWeekRevMap = buildDayMap(fiveWeekRevRaw) + const fiveWeekRevMap = buildDayMap(fiveWeekRevRaw) const thisWeekRoomsMap = buildDayMap(thisWeekRoomsRaw) - const monthRevMap = buildDayMap(monthRevRaw) + const fullMonthRevMap = buildDayMap(fullMonthRevRaw) // ── Five week summaries ──────────────────────────────────────────────────── const fiveWeeks = [0, 1, 2, 3, 4].map(w => { @@ -181,11 +179,11 @@ export async function weeklyActualRoutes(fastify) { let mBud = { rooms: 0, dry: 0, wet: 0 } let mLy = { rooms: 0, dry: 0, wet: 0 } - for (let i = 0; i < monthDays; i++) { + for (let i = 0; i < daysInMonth; i++) { const d = addDays(monthStart, i) if (d > weekEndDate) break const dateStr = toDateStr(d) - const rev = monthRevMap[dateStr] || {} + const rev = fullMonthRevMap[dateStr] || {} mNet.rooms += parseFloat(rev.accom?.otb ?? 0) mNet.dry += parseFloat(rev.dry?.otb ?? 0) @@ -224,6 +222,26 @@ export async function weeklyActualRoutes(fastify) { }, } + // ── Month daily (for cumulative progress line chart) ────────────────────── + const weekEndStr = toDateStr(weekEndDate) + const month_daily = [] + for (let i = 0; i < daysInMonth; i++) { + const d = addDays(monthStart, i) + const dateStr = toDateStr(d) + const rev = fullMonthRevMap[dateStr] || {} + month_daily.push({ + day: i + 1, + date: dateStr, + ty_rooms: parseFloat(rev.accom?.otb ?? 0), + ty_dry: parseFloat(rev.dry?.otb ?? 0), + ty_wet: parseFloat(rev.wet?.otb ?? 0), + ly_rooms: parseFloat(rev.accom?.prior_final ?? 0), + ly_dry: parseFloat(rev.dry?.prior_final ?? 0), + ly_wet: parseFloat(rev.wet?.prior_final ?? 0), + is_actual: dateStr <= weekEndStr, + }) + } + // ── Monthly trend (rolling 12-month average) ────────────────────────────── // Build raw monthly totals for all 36 months const allMonthTotals = trendMonths.map((_, i) => { @@ -279,6 +297,7 @@ export async function weeklyActualRoutes(fastify) { five_weeks: fiveWeeks, occupancy, month_progress, + month_daily, monthly_trend, monthly_split, } diff --git a/frontend/src/pages/WeeklyActual.tsx b/frontend/src/pages/WeeklyActual.tsx index b44fe1cb..cb7751c2 100644 --- a/frontend/src/pages/WeeklyActual.tsx +++ b/frontend/src/pages/WeeklyActual.tsx @@ -7,10 +7,9 @@ import { PieChart, Pie, Cell, LineChart, Line, CartesianGrid, - ReferenceLine, } from 'recharts' import { getWeeklyActual } from '../api' -import type { WeeklyActualData, WeekSummaryRow, MonthSplitEntry } from '../types' +import type { WeeklyActualData, WeekSummaryRow, MonthSplitEntry, MonthDailyEntry } from '../types' // ── Formatters ───────────────────────────────────────────────────────────── @@ -53,18 +52,16 @@ function shiftWeek(dateStr: string, weeks: number): string { // ── Chart colours ────────────────────────────────────────────────────────── const CHART_COLORS = { - rooms: '#c9a84c', - dry: '#2563eb', - wet: '#059669', - thisWeek: '#c9a84c', - budget: '#9ca3af', - lastYear: '#d1d5db', + rooms: '#c9a84c', + dry: '#2563eb', + wet: '#059669', + thisWeek: '#c9a84c', + budget: '#9ca3af', + lastYear: '#d1d5db', } const PIE_COLORS = [CHART_COLORS.rooms, CHART_COLORS.dry, CHART_COLORS.wet] -// ── Tooltip formatter ────────────────────────────────────────────────────── - // eslint-disable-next-line @typescript-eslint/no-explicit-any const fmtChartCcy = (v: any): string => { if (v == null || Array.isArray(v)) return '—' @@ -256,34 +253,49 @@ function OccupancyTable({ occupancy }: { occupancy: WeeklyActualData['occupancy' ) } -// ── Section: Monthly Dept Split (24 months) ──────────────────────────────── +// ── Chart: Month Cumulative Progress vs LY ───────────────────────────────── -function MonthSplitChart({ splits }: { splits: MonthSplitEntry[] }) { - // Mark the boundary between previous-12 and last-12 - const boundaryLabel = splits[11]?.label +function MonthProgressLineChart({ daily }: { daily: MonthDailyEntry[] }) { + let cumTyRooms = 0, cumTyDry = 0, cumTyWet = 0 + let cumLyRooms = 0, cumLyDry = 0, cumLyWet = 0 + + const chartData = daily.map(d => { + cumLyRooms += d.ly_rooms + cumLyDry += d.ly_dry + cumLyWet += d.ly_wet + if (d.is_actual) { + cumTyRooms += d.ty_rooms + cumTyDry += d.ty_dry + cumTyWet += d.ty_wet + } + return { + label: String(d.day), + ty_rooms: d.is_actual ? cumTyRooms : null, + ty_dry: d.is_actual ? cumTyDry : null, + ty_wet: d.is_actual ? cumTyWet : null, + ly_rooms: cumLyRooms, + ly_dry: cumLyDry, + ly_wet: cumLyWet, + } + }) return ( -
-

Monthly Turnover by Dept — Previous 12 & Last 12 Months

- - - - +
+

Month Cumulative by Dept — vs Last Year

+ + + + `£${(v/1000).toFixed(0)}k`} tick={{ fontSize: 10 }} width={52} /> - {boundaryLabel && ( - - )} - - - - + + + + + + +
) @@ -292,11 +304,10 @@ function MonthSplitChart({ splits }: { splits: MonthSplitEntry[] }) { // ── Section: Month Progress ──────────────────────────────────────────────── function MonthProgressSection({ - mp, trend, splits, + mp, monthDaily, }: { mp: WeeklyActualData['month_progress'] - trend: WeeklyActualData['monthly_trend'] - splits: WeeklyActualData['monthly_split'] + monthDaily: MonthDailyEntry[] }) { const deptBarData = [ { name: 'Rooms', 'Net Sales': mp.split.rooms.net, 'Budget Net': mp.split.rooms.budget_net, 'Last Year': mp.split.rooms.ly_net }, @@ -368,7 +379,7 @@ function MonthProgressSection({
- {/* Charts row */} + {/* Charts: dept bar + cumulative line */}

Month Progress — Dept vs Budget vs Last Year

@@ -386,27 +397,78 @@ function MonthProgressSection({
-
-

Rolling 12-Month Avg Turnover — Trend

- - - - - `£${(v/1000).toFixed(0)}k`} tick={{ fontSize: 10 }} width={52} /> - - - - - - -
+
- - ) } +// ── Chart: Monthly Dept Lines — 12 months TY vs prior year ──────────────── + +function MonthlySplitLineChart({ splits }: { splits: MonthSplitEntry[] }) { + // splits[0..11] = prior 12 months (LY equivalent) + // splits[12..23] = last 12 months (TY) + const chartData = splits.slice(12).map((ty, i) => ({ + label: ty.label, + ty_rooms: ty.rooms, + ty_dry: ty.dry, + ty_wet: ty.wet, + ly_rooms: splits[i].rooms, + ly_dry: splits[i].dry, + ly_wet: splits[i].wet, + })) + + return ( +
+

Monthly Turnover by Dept — Last 12 Months vs Prior Year

+ + + + + `£${(v/1000).toFixed(0)}k`} tick={{ fontSize: 10 }} width={52} /> + + + + + + + + + + +
+ ) +} + +// ── Section: Sales History ───────────────────────────────────────────────── + +function SalesHistorySection({ + trend, splits, +}: { + trend: WeeklyActualData['monthly_trend'] + splits: WeeklyActualData['monthly_split'] +}) { + return ( + <> + +
+

Rolling 12-Month Avg Turnover — Trend

+ + + + + `£${(v/1000).toFixed(0)}k`} tick={{ fontSize: 10 }} width={52} /> + + + + + + +
+ + ) +} + // ── Section: Utilities Placeholder ───────────────────────────────────────── function UtilitiesPlaceholder() { @@ -504,7 +566,13 @@ export default function WeeklyActual() {

{MONTH_NAMES[data.month_progress.month - 1]} {data.month_progress.year} — Month Progress to Date

- + + + + {/* ── Sales History ───────────────────────────────────────── */} +
+

Sales History

+
{/* ── Utilities ────────────────────────────────────────────── */} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 1fa4a502..95a4eb09 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -106,6 +106,18 @@ export interface MonthTrend { last_year_avg: number } +export interface MonthDailyEntry { + day: number + date: string + ty_rooms: number + ty_dry: number + ty_wet: number + ly_rooms: number + ly_dry: number + ly_wet: number + is_actual: boolean +} + export interface MonthSplitEntry { label: string rooms: number @@ -124,6 +136,7 @@ export interface WeeklyActualData { totals: OccTotals } month_progress: MonthProgress + month_daily: MonthDailyEntry[] monthly_trend: MonthTrend[] monthly_split: MonthSplitEntry[] }