feat(phase-b): school onboarding wizard + GAIS school search UI

- SchoolOnboardingWizard.tsx: new 3-step wizard (GAIS search → confirm →
  calendar setup) triggered from the nav panel school section when no school
  is linked. Calls GET /school/search for live school lookup and
  POST /school/register to create the institute record.
- CCGraphNavPanel.tsx: add showOnboard button for no_school state,
  onOnboardSchool context action, wire up SchoolOnboardingWizard state.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-26 01:56:37 +01:00
co-authored by Claude Sonnet 4.6
parent b0b2a7f2c3
commit 9d78f06c97
2 changed files with 370 additions and 1 deletions
@@ -26,6 +26,7 @@ import { NeoGraphNode } from '../../../../../../types/navigation';
import { logger } from '../../../../../../debugConfig';
import { SchoolCalendarWizard, SchoolInfo } from './SchoolCalendarWizard';
import { TeacherTimetableWizard, PeriodTemplate } from './TeacherTimetableWizard';
import { SchoolOnboardingWizard } from './SchoolOnboardingWizard';
type NodeStatus = 'populated' | 'empty' | 'no_school' | 'not_initialized';
type CalendarMode = 'generic' | 'academic';
@@ -97,6 +98,7 @@ interface NavPanelContextValue {
schoolStatus: SchoolStatus | null;
onSetupSchoolCalendar: () => void;
onSetupTimetable: () => void;
onOnboardSchool: () => void;
activeNodeId?: string;
}
@@ -108,6 +110,7 @@ const NavPanelContext = createContext<NavPanelContextValue>({
schoolStatus: null,
onSetupSchoolCalendar: () => {},
onSetupTimetable: () => {},
onOnboardSchool: () => {},
});
// ─── TreeItem ─────────────────────────────────────────────────────────────────
@@ -181,6 +184,8 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
const showCalendarPending = isSchoolSection
&& ss && ss.status !== 'no_school'
&& !ss.school_has_calendar && ss.user_role !== 'school_admin';
// School section: onboarding prompt when no school linked
const showOnboard = isSchoolSection && (!ss || ss.status === 'no_school');
// Timetable section: teacher timetable setup (requires school calendar first)
const showTimetableSetup = isTimetableSection && node.status === 'empty'
&& ss && ss.status !== 'no_school'
@@ -249,7 +254,19 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
</Tooltip>
)}
{/* Timetable section — role-aware action */}
{/* School section — onboarding when no school */}
{showOnboard && (
<Tooltip title="Find and join your school" placement="right">
<IconButton
size="small"
sx={{ p: 0.25, ml: 0.5, color: 'primary.main' }}
onClick={e => { e.stopPropagation(); ctx.onOnboardSchool(); }}
>
<SetupIcon sx={{ fontSize: 13 }} />
</IconButton>
</Tooltip>
)}
{/* Calendar section — role-aware action */}
{showCalendarSetup && (
<Tooltip title="Set up school calendar" placement="right">
<IconButton
@@ -407,6 +424,7 @@ export function CCGraphNavPanel() {
const [calendarWizardOpen, setCalendarWizardOpen] = useState(false);
const [timetableWizardOpen, setTimetableWizardOpen] = useState(false);
const [onboardingWizardOpen, setOnboardingWizardOpen] = useState(false);
const apiBase = import.meta.env.VITE_API_BASE as string;
const activeNodeId = context.node?.type !== 'workspace' ? context.node?.id : undefined;
@@ -544,6 +562,7 @@ export function CCGraphNavPanel() {
schoolStatus,
onSetupSchoolCalendar: () => setCalendarWizardOpen(true),
onSetupTimetable: () => setTimetableWizardOpen(true),
onOnboardSchool: () => setOnboardingWizardOpen(true),
activeNodeId,
};
@@ -581,6 +600,18 @@ export function CCGraphNavPanel() {
periodsTemplate={schoolStatus?.periods_template || []}
timetableId={schoolStatus?.timetable_id || null}
/>
<SchoolOnboardingWizard
open={onboardingWizardOpen}
onClose={() => setOnboardingWizardOpen(false)}
onComplete={() => {
setOnboardingWizardOpen(false);
// Reload tree + school status after successful onboarding
setTree(null);
setSchoolStatus(null);
}}
apiBase={apiBase}
/>
</NavPanelContext.Provider>
);
}