diff --git a/src/utils/tldraw/ui-overrides/components/shared/navigation/CCGraphNavPanel.tsx b/src/utils/tldraw/ui-overrides/components/shared/navigation/CCGraphNavPanel.tsx index dfe5aaf..c069799 100644 --- a/src/utils/tldraw/ui-overrides/components/shared/navigation/CCGraphNavPanel.tsx +++ b/src/utils/tldraw/ui-overrides/components/shared/navigation/CCGraphNavPanel.tsx @@ -102,6 +102,10 @@ interface NavPanelContextValue { onSetupTimetable: () => void; onOnboardSchool: () => void; activeNodeId?: string; + timetableView: 'class' | 'term'; + setTimetableView: (v: 'class' | 'term') => void; + timetableTermNodes: TreeNode[]; + timetableTermStatus: 'idle' | 'loading' | 'populated' | 'empty'; } const NavPanelContext = createContext({ @@ -113,6 +117,10 @@ const NavPanelContext = createContext({ onSetupSchoolCalendar: () => {}, onSetupTimetable: () => {}, onOnboardSchool: () => {}, + timetableView: 'class', + setTimetableView: () => {}, + timetableTermNodes: [], + timetableTermStatus: 'idle', }); // ─── TreeItem ───────────────────────────────────────────────────────────────── @@ -150,7 +158,9 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) { const displayChildren = isCalendarSection && ctx.calendarMode === 'academic' ? ctx.academicTerms - : children; + : isTimetableSection && ctx.timetableView === 'term' + ? ctx.timetableTermNodes + : children; const academicEmpty = isCalendarSection && ctx.calendarMode === 'academic' @@ -349,6 +359,34 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) { )} + {/* Timetable view toggle */} + {isTimetableSection && node.status === 'populated' && ( + + { if (v) ctx.setTimetableView(v); }} + size="small" + sx={{ height: 22 }} + > + + By Class + + + By Term + + + {ctx.timetableView === 'term' && ctx.timetableTermStatus === 'loading' && ( + + )} + {ctx.timetableView === 'term' && ctx.timetableTermStatus === 'empty' && ( + + No academic calendar linked to timetable + + )} + + )} + {/* Calendar mode toggle */} {isCalendarSection && ( @@ -377,7 +415,10 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) { )} - {(canExpand || (isCalendarSection && ctx.calendarMode === 'academic' && displayChildren.length > 0)) && ( + {(canExpand + || (isCalendarSection && ctx.calendarMode === 'academic' && displayChildren.length > 0) + || (isTimetableSection && ctx.timetableView === 'term' && displayChildren.length > 0) +) && ( {displayChildren.map(child => ( ('generic'); const [academicCalendarStatus, setAcademicCalendarStatus] = useState('idle'); const [academicTerms, setAcademicTerms] = useState([]); + const [timetableView, setTimetableView] = useState<'class' | 'term'>('class'); + const [timetableTermNodes, setTimetableTermNodes] = useState([]); + const [timetableTermStatus, setTimetableTermStatus] = useState<'idle' | 'loading' | 'populated' | 'empty'>('idle'); + const [timetableNodeId, setTimetableNodeId] = useState(); + const [timetableDb, setTimetableDb] = useState(); const [calendarWizardOpen, setCalendarWizardOpen] = useState(false); const [timetableWizardOpen, setTimetableWizardOpen] = useState(false); @@ -497,10 +543,41 @@ export function CCGraphNavPanel() { if (accessToken && !tree) fetchTree(); }, [accessToken, tree, fetchTree]); + useEffect(() => { + if (!tree) return; + const ttSection = tree.children?.find(n => n.section_id === 'timetable'); + if (ttSection && ttSection.node_type === 'TeacherTimetable') { + setTimetableNodeId(ttSection.neo4j_node_id); + setTimetableDb(ttSection.neo4j_db_name); + } + }, [tree]); + useEffect(() => { if (accessToken && !schoolStatus) fetchSchoolStatus(); }, [accessToken, schoolStatus, fetchSchoolStatus]); + // Fetch timetable term nodes when switching to term view + useEffect(() => { + if (timetableView !== 'term' || !timetableNodeId || !timetableDb || !accessToken) return; + if (timetableTermStatus !== 'idle') return; + setTimetableTermStatus('loading'); + const params = new URLSearchParams({ + neo4j_node_id: timetableNodeId, + neo4j_db_name: timetableDb, + node_type: 'TeacherTimetable', + section_id: 'timetable-term', + }); + fetch(`${apiBase}/graph/node/children?${params}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }) + .then(r => r.json()) + .then(data => { + setTimetableTermNodes(data.children || []); + setTimetableTermStatus(data.children?.length > 0 ? 'populated' : 'empty'); + }) + .catch(() => setTimetableTermStatus('empty')); + }, [timetableView, timetableNodeId, timetableDb, accessToken, apiBase, timetableTermStatus]); + // Fetch academic calendar when switching to academic mode useEffect(() => { if (calendarMode !== 'academic' || !accessToken) return; @@ -547,7 +624,9 @@ export function CCGraphNavPanel() { }, [accessToken, apiBase]); const handleSelect = useCallback((node: TreeNode) => { - if (!node.is_section || node.neo4j_node_id) navigateToNeoNode(node); + // Section nodes with artificial IDs (node_type="Section") have no canvas shape util + if (node.is_section && node.node_type === "Section") return; + navigateToNeoNode(node); }, [navigateToNeoNode]); const refreshAll = useCallback(() => { @@ -595,6 +674,10 @@ export function CCGraphNavPanel() { onSetupTimetable: () => setTimetableWizardOpen(true), onOnboardSchool: () => setOnboardingWizardOpen(true), activeNodeId, + timetableView, + setTimetableView: (v) => { setTimetableView(v); if (v === 'class') { setTimetableTermStatus('idle'); setTimetableTermNodes([]); } }, + timetableTermNodes, + timetableTermStatus, }; const defaultSchoolInfo: SchoolInfo = {