- New examRepository (R2.1 seam): the only module talking to /api/exam (Supabase JWT → Bearer, axios to API_BASE). list/get/create/archive templates. - ExamDashboardPage (/exam-marker): lists institute templates, create dialog, archive; cards link to setup (S4-9). Wrapped in ErrorBoundary (R6.4). - exam.types.ts mirrors the API contract. - Dashboard 'Exam Marker' quick action (top-level discovery, R1.3/R6.1). Note: the in-canvas TeacherNavigation is unsuitable for a nav section (it's the worker prev/next tab bar) — flagged; used the dashboard entry instead. - R1.1 removal: deleted src/pages/tldraw/CCExamMarker/ (old 3-PDF viewer) and the now-dead CCExamMarkerPanel + its wiring in CCPanel/BasePanel (examMarkerProps was only ever passed by the deleted page). - Fixed pre-existing broken AppRoutes test (missing TimetableListPage mock export). Build green (vite); AppRoutes route tests pass; typecheck clean for new files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
189 lines
7.2 KiB
TypeScript
189 lines
7.2 KiB
TypeScript
import React from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
Alert,
|
|
Box,
|
|
Button,
|
|
Chip,
|
|
Container,
|
|
Grid,
|
|
Paper,
|
|
Stack,
|
|
Typography
|
|
} from '@mui/material';
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
import WarningIcon from '@mui/icons-material/Warning';
|
|
import { useUser } from '../../contexts/UserContext';
|
|
|
|
const DashboardPage: React.FC = () => {
|
|
const navigate = useNavigate();
|
|
const { user: authUser, bootstrapData } = useAuth();
|
|
const { profile, loading } = useUser();
|
|
|
|
const displayName = profile?.display_name || authUser?.display_name || authUser?.username || 'Member';
|
|
const emailAddress = profile?.email || authUser?.email || '';
|
|
const userType = profile?.user_type || authUser?.user_type || '';
|
|
|
|
const activeInstitute = bootstrapData?.active_institute;
|
|
const activeMembership = bootstrapData?.memberships?.find(
|
|
(m) => m.institute_id === activeInstitute?.id && m.is_active
|
|
);
|
|
const schoolName = activeMembership?.institute?.name;
|
|
const onboarding = bootstrapData?.onboarding;
|
|
const calendarOk = bootstrapData?.calendar_status?.available && !bootstrapData?.calendar_status?.needs_setup;
|
|
const timetableOk = bootstrapData?.timetable_status?.available && !bootstrapData?.timetable_status?.needs_setup;
|
|
|
|
return (
|
|
<Container maxWidth="lg" sx={{ py: 6 }}>
|
|
<Stack spacing={6}>
|
|
<Box>
|
|
<Typography variant="h3" component="h1" gutterBottom>
|
|
Welcome back{displayName ? `, ${displayName}` : ''}!
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 560 }}>
|
|
This is your starting point inside ClassroomCopilot. We keep things simple here so you
|
|
can decide what to explore next.
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={12} md={6}>
|
|
<Paper elevation={2} sx={{ p: 3 }}>
|
|
<Typography variant="h6" gutterBottom>
|
|
Account overview
|
|
</Typography>
|
|
<Stack spacing={1}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Signed in as
|
|
</Typography>
|
|
<Typography variant="body1">
|
|
{emailAddress || 'No email on file'}
|
|
</Typography>
|
|
{userType && (
|
|
<Typography variant="body2" color="text.secondary">
|
|
Role: {userType}
|
|
</Typography>
|
|
)}
|
|
<Typography variant="body2" color="text.secondary">
|
|
{loading ? 'Checking profile details...' : 'Profile ready'}
|
|
</Typography>
|
|
</Stack>
|
|
</Paper>
|
|
</Grid>
|
|
|
|
<Grid item xs={12} md={6}>
|
|
<Paper elevation={2} sx={{ p: 3, height: '100%' }}>
|
|
<Typography variant="h6" gutterBottom>
|
|
Quick actions
|
|
</Typography>
|
|
<Stack spacing={2}>
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={() => navigate('/single-player')}
|
|
>
|
|
Open workspace
|
|
</Button>
|
|
<Button
|
|
variant="outlined"
|
|
onClick={() => navigate('/exam-marker')}
|
|
>
|
|
Exam Marker
|
|
</Button>
|
|
<Button
|
|
variant="outlined"
|
|
onClick={() => navigate('/calendar')}
|
|
>
|
|
View calendar
|
|
</Button>
|
|
<Button
|
|
variant="outlined"
|
|
onClick={() => navigate('/settings')}
|
|
>
|
|
Update settings
|
|
</Button>
|
|
</Stack>
|
|
</Paper>
|
|
</Grid>
|
|
</Grid>
|
|
{/* Bootstrap Status Section */}
|
|
{bootstrapData && (
|
|
<Stack spacing={3}>
|
|
<Typography variant="h5" component="h2">
|
|
Your school
|
|
</Typography>
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={12} md={6}>
|
|
<Paper elevation={2} sx={{ p: 3 }}>
|
|
<Typography variant="h6" gutterBottom>
|
|
Institute
|
|
</Typography>
|
|
<Stack spacing={1}>
|
|
<Typography variant="body1" fontWeight={600}>
|
|
{schoolName || 'No school assigned'}
|
|
</Typography>
|
|
{activeInstitute?.membership_role && (
|
|
<Chip
|
|
label={activeInstitute.membership_role.replace(/_/g, ' ')}
|
|
size="small"
|
|
color="primary"
|
|
variant="outlined"
|
|
/>
|
|
)}
|
|
<Typography variant="body2" color="text.secondary">
|
|
Status: {bootstrapData.school_status}
|
|
</Typography>
|
|
</Stack>
|
|
</Paper>
|
|
</Grid>
|
|
|
|
<Grid item xs={12} md={6}>
|
|
<Paper elevation={2} sx={{ p: 3, height: '100%' }}>
|
|
<Typography variant="h6" gutterBottom>
|
|
System status
|
|
</Typography>
|
|
<Stack spacing={1.5}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
{calendarOk ? <CheckCircleIcon color="success" fontSize="small" /> : <WarningIcon color="warning" fontSize="small" />}
|
|
<Typography variant="body2">
|
|
Calendar: {calendarOk ? 'Ready' : 'Needs setup'}
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
{timetableOk ? <CheckCircleIcon color="success" fontSize="small" /> : <WarningIcon color="warning" fontSize="small" />}
|
|
<Typography variant="body2">
|
|
Timetable: {timetableOk ? 'Ready' : 'Needs setup'}
|
|
</Typography>
|
|
</Box>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
{bootstrapData?.graph_status?.available ? <CheckCircleIcon color="success" fontSize="small" /> : <WarningIcon color="warning" fontSize="small" />}
|
|
<Typography variant="body2">
|
|
Graph projection: {bootstrapData?.graph_status?.projection_state || 'unknown'}
|
|
</Typography>
|
|
</Box>
|
|
</Stack>
|
|
</Paper>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{onboarding && onboarding.next_step !== 'complete' && (
|
|
<Alert severity="info" sx={{ mt: 1 }}>
|
|
<Typography variant="body2" fontWeight={600}>
|
|
{onboarding.message}
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ mt: 0.5 }}>
|
|
Next step: {onboarding.next_step.replace(/_/g, ' ')}
|
|
</Typography>
|
|
</Alert>
|
|
)}
|
|
</Stack>
|
|
)}
|
|
|
|
</Stack>
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export default DashboardPage;
|