Restructure weekly actuals charts — Sales History section + month cumulative

Backend:
- Always fetch full calendar month (not truncated at week ending)
- Add month_daily[] to response (per-day TY/LY by dept, is_actual flag)

Frontend:
- New 'Sales History' section with:
  - Monthly dept line chart: 12-month x-axis, TY vs prior year per dept
    (Rooms/Dry/Wet solid, LY dashed — same colour per dept)
  - Rolling 12-month average trend (moved from Month Progress)
- Month Progress section gains cumulative line chart:
  - X-axis = day of month, 6 lines (3 TY solid + 3 LY dashed)
  - TY lines stop at week ending; LY spans full month

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-20 14:03:51 +00:00
parent 7f0d1057c1
commit ffa13d6e57
3 changed files with 165 additions and 65 deletions

View file

@ -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 (
<div className="wa-chart-card" style={{ marginTop: 0 }}>
<h4>Monthly Turnover by Dept Previous 12 &amp; Last 12 Months</h4>
<ResponsiveContainer width="100%" height={240}>
<BarChart data={splits} margin={{ top: 8, right: 8, left: 8, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" vertical={false} />
<XAxis dataKey="label" tick={{ fontSize: 8 }} interval={1} />
<div className="wa-chart-card">
<h4>Month Cumulative by Dept vs Last Year</h4>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={chartData} margin={{ top: 8, right: 8, left: 8, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey="label" tick={{ fontSize: 9 }} />
<YAxis tickFormatter={v => `£${(v/1000).toFixed(0)}k`} tick={{ fontSize: 10 }} width={52} />
<Tooltip formatter={fmtChartCcy} />
<Legend iconSize={10} wrapperStyle={{ fontSize: 11 }} />
{boundaryLabel && (
<ReferenceLine
x={boundaryLabel}
stroke="#6b7280"
strokeDasharray="4 3"
label={{ value: '← prev 12 | last 12 →', position: 'top', fontSize: 9, fill: '#6b7280' }}
/>
)}
<Bar dataKey="rooms" stackId="a" fill={CHART_COLORS.rooms} name="Rooms" />
<Bar dataKey="dry" stackId="a" fill={CHART_COLORS.dry} name="Dry" />
<Bar dataKey="wet" stackId="a" fill={CHART_COLORS.wet} name="Wet" radius={[2,2,0,0]} />
</BarChart>
<Line type="monotone" dataKey="ty_rooms" stroke={CHART_COLORS.rooms} strokeWidth={2.5} dot={false} name="Rooms TY" />
<Line type="monotone" dataKey="ty_dry" stroke={CHART_COLORS.dry} strokeWidth={2.5} dot={false} name="Dry TY" />
<Line type="monotone" dataKey="ty_wet" stroke={CHART_COLORS.wet} strokeWidth={2.5} dot={false} name="Wet TY" />
<Line type="monotone" dataKey="ly_rooms" stroke={CHART_COLORS.rooms} strokeWidth={1.5} dot={false} name="Rooms LY" strokeDasharray="4 3" strokeOpacity={0.5} />
<Line type="monotone" dataKey="ly_dry" stroke={CHART_COLORS.dry} strokeWidth={1.5} dot={false} name="Dry LY" strokeDasharray="4 3" strokeOpacity={0.5} />
<Line type="monotone" dataKey="ly_wet" stroke={CHART_COLORS.wet} strokeWidth={1.5} dot={false} name="Wet LY" strokeDasharray="4 3" strokeOpacity={0.5} />
</LineChart>
</ResponsiveContainer>
</div>
)
@ -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({
</table>
</div>
{/* Charts row */}
{/* Charts: dept bar + cumulative line */}
<div className="wa-charts-row" style={{ marginTop: 16 }}>
<div className="wa-chart-card">
<h4>Month Progress Dept vs Budget vs Last Year</h4>
@ -386,27 +397,78 @@ function MonthProgressSection({
</ResponsiveContainer>
</div>
<div className="wa-chart-card">
<h4>Rolling 12-Month Avg Turnover Trend</h4>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={trend} margin={{ top: 8, right: 8, left: 8, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey="label" tick={{ fontSize: 9 }} interval={1} />
<YAxis tickFormatter={v => `£${(v/1000).toFixed(0)}k`} tick={{ fontSize: 10 }} width={52} />
<Tooltip formatter={fmtChartCcy} />
<Legend iconSize={10} wrapperStyle={{ fontSize: 11 }} />
<Line type="monotone" dataKey="this_year_avg" stroke={CHART_COLORS.thisWeek} strokeWidth={2.5} dot={false} name="This Year" />
<Line type="monotone" dataKey="last_year_avg" stroke={CHART_COLORS.budget} strokeWidth={2} dot={false} name="Last Year" strokeDasharray="5 3" />
</LineChart>
</ResponsiveContainer>
</div>
<MonthProgressLineChart daily={monthDaily} />
</div>
<MonthSplitChart splits={splits} />
</div>
)
}
// ── 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 (
<div className="wa-chart-card">
<h4>Monthly Turnover by Dept Last 12 Months vs Prior Year</h4>
<ResponsiveContainer width="100%" height={260}>
<LineChart data={chartData} margin={{ top: 8, right: 8, left: 8, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey="label" tick={{ fontSize: 9 }} />
<YAxis tickFormatter={v => `£${(v/1000).toFixed(0)}k`} tick={{ fontSize: 10 }} width={52} />
<Tooltip formatter={fmtChartCcy} />
<Legend iconSize={10} wrapperStyle={{ fontSize: 11 }} />
<Line type="monotone" dataKey="ty_rooms" stroke={CHART_COLORS.rooms} strokeWidth={2.5} dot={false} name="Rooms" />
<Line type="monotone" dataKey="ty_dry" stroke={CHART_COLORS.dry} strokeWidth={2.5} dot={false} name="Dry" />
<Line type="monotone" dataKey="ty_wet" stroke={CHART_COLORS.wet} strokeWidth={2.5} dot={false} name="Wet" />
<Line type="monotone" dataKey="ly_rooms" stroke={CHART_COLORS.rooms} strokeWidth={1.5} dot={false} name="Rooms LY" strokeDasharray="4 3" strokeOpacity={0.5} />
<Line type="monotone" dataKey="ly_dry" stroke={CHART_COLORS.dry} strokeWidth={1.5} dot={false} name="Dry LY" strokeDasharray="4 3" strokeOpacity={0.5} />
<Line type="monotone" dataKey="ly_wet" stroke={CHART_COLORS.wet} strokeWidth={1.5} dot={false} name="Wet LY" strokeDasharray="4 3" strokeOpacity={0.5} />
</LineChart>
</ResponsiveContainer>
</div>
)
}
// ── Section: Sales History ─────────────────────────────────────────────────
function SalesHistorySection({
trend, splits,
}: {
trend: WeeklyActualData['monthly_trend']
splits: WeeklyActualData['monthly_split']
}) {
return (
<>
<MonthlySplitLineChart splits={splits} />
<div className="wa-chart-card" style={{ marginTop: 16 }}>
<h4>Rolling 12-Month Avg Turnover Trend</h4>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={trend} margin={{ top: 8, right: 8, left: 8, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey="label" tick={{ fontSize: 9 }} interval={1} />
<YAxis tickFormatter={v => `£${(v/1000).toFixed(0)}k`} tick={{ fontSize: 10 }} width={52} />
<Tooltip formatter={fmtChartCcy} />
<Legend iconSize={10} wrapperStyle={{ fontSize: 11 }} />
<Line type="monotone" dataKey="this_year_avg" stroke={CHART_COLORS.thisWeek} strokeWidth={2.5} dot={false} name="This Year" />
<Line type="monotone" dataKey="last_year_avg" stroke={CHART_COLORS.budget} strokeWidth={2} dot={false} name="Last Year" strokeDasharray="5 3" />
</LineChart>
</ResponsiveContainer>
</div>
</>
)
}
// ── Section: Utilities Placeholder ─────────────────────────────────────────
function UtilitiesPlaceholder() {
@ -504,7 +566,13 @@ export default function WeeklyActual() {
<h2 className="wa-section-title">
{MONTH_NAMES[data.month_progress.month - 1]} {data.month_progress.year} Month Progress to Date
</h2>
<MonthProgressSection mp={data.month_progress} trend={data.monthly_trend} splits={data.monthly_split} />
<MonthProgressSection mp={data.month_progress} monthDaily={data.month_daily} />
</section>
{/* ── Sales History ───────────────────────────────────────── */}
<section className="wa-section">
<h2 className="wa-section-title">Sales History</h2>
<SalesHistorySection trend={data.monthly_trend} splits={data.monthly_split} />
</section>
{/* ── Utilities ────────────────────────────────────────────── */}