loreal-utilisation-dept/frontend/src/pages/Tutorial.tsx
DJP cd1c99d5e0 feat: KPI tiles, active/soft booking split, hour-breakdown drill-down, period toggle, forecast line, sync button
Major parity push against the original SPA's bundle-level feature set
(non-architectural items — separate Forecast / ProjectType / TimeLog
views and AI Chat remain TBD).

Backend (40/40 tests, +7):

- merge.py splits booked hours by booking status: active vs soft.
  Active set: Active, Active Booked, Fully Booked, Partially Booked.
  Soft set: Soft Booking, Soft Booked, Soft-Booked. Unknown statuses
  default to active so they're not silently dropped. Existing
  `bookedHours` field is preserved as the sum for back-compat.
- compute_totals(): rolls KPIs across the filtered summary —
  totalBooked, activeBooked, softBooked, totalLogged, totalBillable,
  totalLeave, totalProjects (distinct projectName/projectNumber),
  activePeople (distinct employees with logged>0), allocated,
  allocatedNetOfLeave.
- breakdown_by_project(): drills into a single period+employee (or
  whole-period) and returns per-project logged + booked hours.
- New /api/utilisation/breakdown endpoint. /api/utilisation/summary
  response gains `totals` and accepts `period=week|month`.
- New schemas: UtilisationTotals, BreakdownResponse, plus
  activeBookedHours / softBookedHours on UtilisationSummaryRow.

Frontend (typecheck/lint/build clean):

- KpiTiles component shows Total Booked / Logged / Billable, Total
  projects, Active People (logged), Active bookings, Avg/person/week
  or /month, Allocated (net of leave), Period covered.
- PeriodToggle (Per day / Per week / Per month). Day is rendered
  disabled with an explanatory tooltip — backend only accepts week/
  month.
- HourBreakdown drill-down panel: per-project logged + booked rows,
  shown when a chart bar is clicked; "Upload a time log to see logged
  breakdown" empty-state when no upload yet.
- MonthlyUtilisation: ComposedChart with stacked Active/Soft booked
  bars + forecast Line overlay driven by the existing showForecast
  toggle. onPeriodClick wired into HourBreakdown.
- WeeklyUtilisation, BookingVsActual: same Active/Soft stack treatment.
- Resourcing now passes the timelog hash through to summary so
  loggedHours actually populates there too (was 0 before).
- Sync Airtable button on both Department and Resourcing — force-
  refreshes bookings cache, re-derives summary.
- Tutorial steps re-mapped to the original SPA's chapter titles:
  "Reading the Utilisation Chart", "Hours & Utilisation", "Drill-In",
  "Forecast Line & Filters", "Spotting Resource Issues", "Sync
  Airtable Bookings". Tutorial page heading is now "Interactive
  Walkthrough" with the original copy.
- Defensive coercion in Bookings table totalHoursBooked rendering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:06:23 -04:00

73 lines
2.8 KiB
TypeScript

import { lazy, Suspense, useState } from 'react';
import { Play } from 'lucide-react';
import { allSteps, type TutorialSection } from '../components/tutorial/steps';
const TutorialOverlay = lazy(() => import('../components/tutorial/TutorialOverlay'));
const SECTIONS: { key: TutorialSection; label: string; blurb: string }[] = [
{
key: 'department',
label: 'Department',
blurb: 'Upload your timelog, filter by department and see monthly utilisation, booking-vs-actual and billability.',
},
{
key: 'resourcing',
label: 'Resourcing',
blurb: 'Weekly utilisation, per-person project load, and FTE-vs-Freelancer side-by-side.',
},
{
key: 'bookings',
label: 'Bookings',
blurb: 'Virtualised booking table with cache info and an Airtable sync button.',
},
];
export default function Tutorial() {
const [activeSection, setActiveSection] = useState<TutorialSection | null>(null);
return (
<div className="space-y-4">
<section className="card">
<h1 className="text-lg font-semibold text-slate-900">Tutorial Interactive Walkthrough</h1>
<p className="mt-1 text-sm text-slate-600">
A walkthrough of every tab. Use the chapter buttons to replay any section the overlay only highlights
elements that are currently visible, so navigate to the matching tab first, then click <em>Replay</em>.
</p>
<p className="mt-2 text-xs text-slate-500">
Note: the original Video Walkthrough has been retired; the interactive tour below covers the same ground.
Jump to any step using the in-overlay arrow controls.
</p>
</section>
<div className="grid gap-4 md:grid-cols-3">
{SECTIONS.map((s) => (
<div key={s.key} className="card flex flex-col">
<h2 className="text-base font-semibold text-slate-800">{s.label}</h2>
<p className="mt-1 flex-1 text-sm text-slate-600">{s.blurb}</p>
<button
type="button"
onClick={() => setActiveSection(s.key)}
className="btn-primary mt-3 self-start"
data-tutorial-id={`replay-${s.key}`}
>
<Play className="h-4 w-4" aria-hidden /> Replay
</button>
<ul className="mt-3 list-disc space-y-1 pl-5 text-xs text-slate-500">
{allSteps[s.key].map((step) => (
<li key={`${s.key}-${step.selector}-${step.title}`}>
<strong className="text-slate-700">{step.title}:</strong> {step.description}
</li>
))}
</ul>
</div>
))}
</div>
{activeSection && (
<Suspense fallback={null}>
<TutorialOverlay section={activeSection} onClose={() => setActiveSection(null)} />
</Suspense>
)}
</div>
);
}