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>
73 lines
2.8 KiB
TypeScript
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>
|
|
);
|
|
}
|