feat(nav): timetable view toggle (By Class / By Term), fix cc-section-node crash
- Fix: skip canvas navigation for Section-type nodes (was crashing on cc-section-node) - Add By Class / By Term toggle to My Timetable section (mirrors calendar Generic/Academic) - By Class: pre-loaded SubjectClass children shown immediately on expand - By Term: lazy-loads AcademicTerms -> Weeks -> TaughtLessons when switched to term view - displayChildren respects timetableView context for the timetable section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
510bef02b6
commit
98a4212e46
@ -102,6 +102,10 @@ interface NavPanelContextValue {
|
|||||||
onSetupTimetable: () => void;
|
onSetupTimetable: () => void;
|
||||||
onOnboardSchool: () => void;
|
onOnboardSchool: () => void;
|
||||||
activeNodeId?: string;
|
activeNodeId?: string;
|
||||||
|
timetableView: 'class' | 'term';
|
||||||
|
setTimetableView: (v: 'class' | 'term') => void;
|
||||||
|
timetableTermNodes: TreeNode[];
|
||||||
|
timetableTermStatus: 'idle' | 'loading' | 'populated' | 'empty';
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavPanelContext = createContext<NavPanelContextValue>({
|
const NavPanelContext = createContext<NavPanelContextValue>({
|
||||||
@ -113,6 +117,10 @@ const NavPanelContext = createContext<NavPanelContextValue>({
|
|||||||
onSetupSchoolCalendar: () => {},
|
onSetupSchoolCalendar: () => {},
|
||||||
onSetupTimetable: () => {},
|
onSetupTimetable: () => {},
|
||||||
onOnboardSchool: () => {},
|
onOnboardSchool: () => {},
|
||||||
|
timetableView: 'class',
|
||||||
|
setTimetableView: () => {},
|
||||||
|
timetableTermNodes: [],
|
||||||
|
timetableTermStatus: 'idle',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── TreeItem ─────────────────────────────────────────────────────────────────
|
// ─── TreeItem ─────────────────────────────────────────────────────────────────
|
||||||
@ -150,7 +158,9 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
|
|||||||
|
|
||||||
const displayChildren = isCalendarSection && ctx.calendarMode === 'academic'
|
const displayChildren = isCalendarSection && ctx.calendarMode === 'academic'
|
||||||
? ctx.academicTerms
|
? ctx.academicTerms
|
||||||
: children;
|
: isTimetableSection && ctx.timetableView === 'term'
|
||||||
|
? ctx.timetableTermNodes
|
||||||
|
: children;
|
||||||
|
|
||||||
const academicEmpty = isCalendarSection
|
const academicEmpty = isCalendarSection
|
||||||
&& ctx.calendarMode === 'academic'
|
&& ctx.calendarMode === 'academic'
|
||||||
@ -349,6 +359,34 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Timetable view toggle */}
|
||||||
|
{isTimetableSection && node.status === 'populated' && (
|
||||||
|
<Box sx={{ px: 1.5, pb: 0.5 }}>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={ctx.timetableView}
|
||||||
|
exclusive
|
||||||
|
onChange={(_, v) => { if (v) ctx.setTimetableView(v); }}
|
||||||
|
size="small"
|
||||||
|
sx={{ height: 22 }}
|
||||||
|
>
|
||||||
|
<ToggleButton value="class" sx={{ px: 1, fontSize: '0.6rem', textTransform: 'none' }}>
|
||||||
|
By Class
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value="term" sx={{ px: 1, fontSize: '0.6rem', textTransform: 'none' }}>
|
||||||
|
By Term
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
{ctx.timetableView === 'term' && ctx.timetableTermStatus === 'loading' && (
|
||||||
|
<CircularProgress size={10} sx={{ mt: 0.5, ml: 0.5 }} />
|
||||||
|
)}
|
||||||
|
{ctx.timetableView === 'term' && ctx.timetableTermStatus === 'empty' && (
|
||||||
|
<Typography variant="caption" sx={{ display: 'block', color: 'text.disabled', fontSize: '0.6rem', mt: 0.5 }}>
|
||||||
|
No academic calendar linked to timetable
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Calendar mode toggle */}
|
{/* Calendar mode toggle */}
|
||||||
{isCalendarSection && (
|
{isCalendarSection && (
|
||||||
<Box sx={{ px: 1.5, pb: 0.5 }}>
|
<Box sx={{ px: 1.5, pb: 0.5 }}>
|
||||||
@ -377,7 +415,10 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(canExpand || (isCalendarSection && ctx.calendarMode === 'academic' && displayChildren.length > 0)) && (
|
{(canExpand
|
||||||
|
|| (isCalendarSection && ctx.calendarMode === 'academic' && displayChildren.length > 0)
|
||||||
|
|| (isTimetableSection && ctx.timetableView === 'term' && displayChildren.length > 0)
|
||||||
|
) && (
|
||||||
<Collapse in={expanded} timeout="auto">
|
<Collapse in={expanded} timeout="auto">
|
||||||
{displayChildren.map(child => (
|
{displayChildren.map(child => (
|
||||||
<TreeItem key={child.neo4j_node_id} node={child} depth={depth + 1}
|
<TreeItem key={child.neo4j_node_id} node={child} depth={depth + 1}
|
||||||
@ -452,6 +493,11 @@ export function CCGraphNavPanel() {
|
|||||||
const [calendarMode, setCalendarMode] = useState<CalendarMode>('generic');
|
const [calendarMode, setCalendarMode] = useState<CalendarMode>('generic');
|
||||||
const [academicCalendarStatus, setAcademicCalendarStatus] = useState<NavPanelContextValue['academicCalendarStatus']>('idle');
|
const [academicCalendarStatus, setAcademicCalendarStatus] = useState<NavPanelContextValue['academicCalendarStatus']>('idle');
|
||||||
const [academicTerms, setAcademicTerms] = useState<TreeNode[]>([]);
|
const [academicTerms, setAcademicTerms] = useState<TreeNode[]>([]);
|
||||||
|
const [timetableView, setTimetableView] = useState<'class' | 'term'>('class');
|
||||||
|
const [timetableTermNodes, setTimetableTermNodes] = useState<TreeNode[]>([]);
|
||||||
|
const [timetableTermStatus, setTimetableTermStatus] = useState<'idle' | 'loading' | 'populated' | 'empty'>('idle');
|
||||||
|
const [timetableNodeId, setTimetableNodeId] = useState<string | undefined>();
|
||||||
|
const [timetableDb, setTimetableDb] = useState<string | undefined>();
|
||||||
|
|
||||||
const [calendarWizardOpen, setCalendarWizardOpen] = useState(false);
|
const [calendarWizardOpen, setCalendarWizardOpen] = useState(false);
|
||||||
const [timetableWizardOpen, setTimetableWizardOpen] = useState(false);
|
const [timetableWizardOpen, setTimetableWizardOpen] = useState(false);
|
||||||
@ -497,10 +543,41 @@ export function CCGraphNavPanel() {
|
|||||||
if (accessToken && !tree) fetchTree();
|
if (accessToken && !tree) fetchTree();
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
if (accessToken && !schoolStatus) fetchSchoolStatus();
|
if (accessToken && !schoolStatus) fetchSchoolStatus();
|
||||||
}, [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
|
// Fetch academic calendar when switching to academic mode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (calendarMode !== 'academic' || !accessToken) return;
|
if (calendarMode !== 'academic' || !accessToken) return;
|
||||||
@ -547,7 +624,9 @@ export function CCGraphNavPanel() {
|
|||||||
}, [accessToken, apiBase]);
|
}, [accessToken, apiBase]);
|
||||||
|
|
||||||
const handleSelect = useCallback((node: TreeNode) => {
|
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]);
|
}, [navigateToNeoNode]);
|
||||||
|
|
||||||
const refreshAll = useCallback(() => {
|
const refreshAll = useCallback(() => {
|
||||||
@ -595,6 +674,10 @@ export function CCGraphNavPanel() {
|
|||||||
onSetupTimetable: () => setTimetableWizardOpen(true),
|
onSetupTimetable: () => setTimetableWizardOpen(true),
|
||||||
onOnboardSchool: () => setOnboardingWizardOpen(true),
|
onOnboardSchool: () => setOnboardingWizardOpen(true),
|
||||||
activeNodeId,
|
activeNodeId,
|
||||||
|
timetableView,
|
||||||
|
setTimetableView: (v) => { setTimetableView(v); if (v === 'class') { setTimetableTermStatus('idle'); setTimetableTermNodes([]); } },
|
||||||
|
timetableTermNodes,
|
||||||
|
timetableTermStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultSchoolInfo: SchoolInfo = {
|
const defaultSchoolInfo: SchoolInfo = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user