Compare commits
2 Commits
61ef95a35e
...
510bef02b6
| Author | SHA1 | Date | |
|---|---|---|---|
| 510bef02b6 | |||
| 9438e17f88 |
@ -1,12 +1,14 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box, Typography, Button, CircularProgress, Alert, Chip,
|
||||
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||
TextField, Select, MenuItem, FormControl, InputLabel,
|
||||
Divider, IconButton, Tooltip,
|
||||
Divider, IconButton, Tooltip, List, ListItemButton, ListItemText,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ChevronLeft, ChevronRight, Today, EditNote,
|
||||
ChevronLeft, ChevronRight, Today, EditNote, LibraryAdd, Search, OpenInNew,
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
@ -152,9 +154,10 @@ function LessonEditDialog({ lesson, onClose, onSave }: EditDialogProps) {
|
||||
interface LessonCardProps {
|
||||
lesson: Lesson;
|
||||
onEdit: (l: Lesson) => void;
|
||||
onAssignPlan: (l: Lesson) => void;
|
||||
}
|
||||
|
||||
function LessonCard({ lesson, onEdit }: LessonCardProps) {
|
||||
function LessonCard({ lesson, onEdit, onAssignPlan }: LessonCardProps) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -174,11 +177,18 @@ function LessonCard({ lesson, onEdit }: LessonCardProps) {
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, lineHeight: 1.2 }}>
|
||||
{lesson.class_name || lesson.period_code}
|
||||
</Typography>
|
||||
<Tooltip title="Edit lesson">
|
||||
<IconButton size="small" onClick={() => onEdit(lesson)} sx={{ ml: 0.5, p: 0.25 }}>
|
||||
<EditNote fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Box sx={{ display: 'flex', gap: 0 }}>
|
||||
<Tooltip title="Assign lesson plan">
|
||||
<IconButton size="small" onClick={() => onAssignPlan(lesson)} sx={{ p: 0.25 }}>
|
||||
<LibraryAdd fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Edit lesson">
|
||||
<IconButton size="small" onClick={() => onEdit(lesson)} sx={{ p: 0.25 }}>
|
||||
<EditNote fontSize="inherit" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
{lesson.subject && (
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '0.7rem' }}>
|
||||
@ -212,6 +222,7 @@ function LessonCard({ lesson, onEdit }: LessonCardProps) {
|
||||
|
||||
const TaughtLessonsPage: React.FC = () => {
|
||||
const { accessToken } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [weekStart, setWeekStart] = useState<string>(toMonday(new Date()));
|
||||
const [days, setDays] = useState<DayEntry[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -219,6 +230,11 @@ const TaughtLessonsPage: React.FC = () => {
|
||||
const [materializing, setMaterializing] = useState(false);
|
||||
const [materializeResult, setMaterializeResult] = useState<string | null>(null);
|
||||
const [editingLesson, setEditingLesson] = useState<Lesson | null>(null);
|
||||
const [assigningLesson, setAssigningLesson] = useState<Lesson | null>(null);
|
||||
const [plans, setPlans] = useState<{ id: string; title: string; subject: string | null }[]>([]);
|
||||
const [planSearch, setPlanSearch] = useState('');
|
||||
const [assignSaving, setAssignSaving] = useState(false);
|
||||
const [assignResult, setAssignResult] = useState<string | null>(null);
|
||||
|
||||
const loadLessons = useCallback(async (ws: string) => {
|
||||
if (!accessToken) return;
|
||||
@ -281,10 +297,51 @@ const TaughtLessonsPage: React.FC = () => {
|
||||
loadLessons(weekStart);
|
||||
};
|
||||
|
||||
const openAssignDialog = async (lesson: Lesson) => {
|
||||
setAssigningLesson(lesson);
|
||||
setPlanSearch('');
|
||||
setAssignResult(null);
|
||||
if (plans.length === 0) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/lessons/plans`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) setPlans(data);
|
||||
} catch { /* silently ignore */ }
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssignPlan = async (planId: string) => {
|
||||
if (!assigningLesson) return;
|
||||
setAssignSaving(true);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/lessons/plans/${planId}/deliver`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ taught_lesson_id: assigningLesson.id }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.id) {
|
||||
setAssignResult('Plan linked ✓');
|
||||
setTimeout(() => { setAssigningLesson(null); setAssignResult(null); }, 1200);
|
||||
} else {
|
||||
setAssignResult(data.detail || 'Failed to link plan');
|
||||
}
|
||||
} catch (e: any) {
|
||||
setAssignResult(e.message);
|
||||
} finally {
|
||||
setAssignSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredPlans = plans.filter(p =>
|
||||
!planSearch || p.title.toLowerCase().includes(planSearch.toLowerCase())
|
||||
|| (p.subject || '').toLowerCase().includes(planSearch.toLowerCase())
|
||||
);
|
||||
|
||||
const totalLessons = days.reduce((sum, d) => sum + d.lessons.length, 0);
|
||||
|
||||
// Format week label
|
||||
const weekDates = days.filter(d => d.lessons.length > 0 || true).map(d => d.date);
|
||||
const weekLabel = days.length > 0
|
||||
? `${formatDate(days[0].date)} – ${formatDate(days[days.length - 1].date)}`
|
||||
: '';
|
||||
@ -369,6 +426,7 @@ const TaughtLessonsPage: React.FC = () => {
|
||||
key={lesson.id}
|
||||
lesson={lesson}
|
||||
onEdit={setEditingLesson}
|
||||
onAssignPlan={openAssignDialog}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@ -382,6 +440,71 @@ const TaughtLessonsPage: React.FC = () => {
|
||||
onClose={() => setEditingLesson(null)}
|
||||
onSave={handleSaveLesson}
|
||||
/>
|
||||
|
||||
{/* Assign Plan dialog */}
|
||||
<Dialog open={!!assigningLesson} onClose={() => setAssigningLesson(null)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ pb: 1 }}>
|
||||
<Typography variant="h6">Assign Lesson Plan</Typography>
|
||||
{assigningLesson && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{assigningLesson.class_name || assigningLesson.period_code} · {assigningLesson.date}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ pt: 1 }}>
|
||||
{assignResult ? (
|
||||
<Alert severity={assignResult.includes('✓') ? 'success' : 'error'} sx={{ mb: 1 }}>
|
||||
{assignResult}
|
||||
</Alert>
|
||||
) : null}
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Search plans…"
|
||||
value={planSearch}
|
||||
onChange={e => setPlanSearch(e.target.value)}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start"><Search fontSize="small" /></InputAdornment>,
|
||||
}}
|
||||
sx={{ mb: 1 }}
|
||||
/>
|
||||
{plans.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ py: 2, textAlign: 'center' }}>
|
||||
No lesson plans yet.{' '}
|
||||
<Button size="small" endIcon={<OpenInNew fontSize="small" />}
|
||||
onClick={() => { setAssigningLesson(null); navigate('/lesson-plans'); }}>
|
||||
Create one
|
||||
</Button>
|
||||
</Typography>
|
||||
) : (
|
||||
<List dense disablePadding sx={{ maxHeight: 300, overflow: 'auto' }}>
|
||||
{filteredPlans.map(p => (
|
||||
<ListItemButton
|
||||
key={p.id}
|
||||
onClick={() => handleAssignPlan(p.id)}
|
||||
disabled={assignSaving}
|
||||
sx={{ borderRadius: 1, mb: 0.25 }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={p.title}
|
||||
secondary={p.subject || undefined}
|
||||
primaryTypographyProps={{ variant: 'body2' }}
|
||||
secondaryTypographyProps={{ variant: 'caption' }}
|
||||
/>
|
||||
{assignSaving && <CircularProgress size={16} sx={{ ml: 1 }} />}
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setAssigningLesson(null)}>Close</Button>
|
||||
<Button size="small" endIcon={<OpenInNew fontSize="small" />}
|
||||
onClick={() => { setAssigningLesson(null); navigate('/lesson-plans'); }}>
|
||||
Manage Plans
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -19,7 +19,9 @@ import {
|
||||
GridOn as GridIcon,
|
||||
Settings as SetupIcon,
|
||||
Edit as EditIcon,
|
||||
Launch as LaunchIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigationStore } from '../../../../../../stores/navigationStore';
|
||||
import { useAuth } from '../../../../../../contexts/AuthContext';
|
||||
import { NeoGraphNode } from '../../../../../../types/navigation';
|
||||
@ -124,6 +126,7 @@ interface TreeItemProps {
|
||||
|
||||
function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
|
||||
const ctx = useContext(NavPanelContext);
|
||||
const navigate = useNavigate();
|
||||
const [expanded, setExpanded] = useState(node.is_section && node.status === 'populated');
|
||||
const [children, setChildren] = useState<TreeNode[]>(node.children || []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -132,6 +135,7 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
|
||||
const isCalendarSection = isSection && node.section_id === 'calendar';
|
||||
const isTimetableSection = isSection && node.section_id === 'timetable';
|
||||
const isSchoolSection = isSection && node.section_id === 'school';
|
||||
const isClassesSection = isSection && node.section_id === 'classes';
|
||||
const SectionIcon = node.section_id ? SECTION_ICONS[node.section_id] : null;
|
||||
const Icon = SectionIcon || NODE_ICONS[node.node_type] || HomeIcon;
|
||||
|
||||
@ -196,6 +200,8 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
|
||||
const showLegacySetup = isTimetableSection && node.status === 'empty' && !ss;
|
||||
const showTimetableEdit = isTimetableSection && node.status === 'populated'
|
||||
&& ss && ss.status !== 'no_school' && !!ss.teacher_has_timetable;
|
||||
const showTimetableView = isTimetableSection && node.status === 'populated';
|
||||
const showClassesView = isClassesSection && node.status === 'populated';
|
||||
|
||||
if (isSection) {
|
||||
return (
|
||||
@ -223,7 +229,7 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
|
||||
</IconButton>
|
||||
)
|
||||
)}
|
||||
{isEmpty && !isCalendarSection && !isTimetableSection && !isSchoolSection && (
|
||||
{isEmpty && !isCalendarSection && !isTimetableSection && !isSchoolSection && !isClassesSection && (
|
||||
<Box sx={{ width: 18, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{node.status === 'no_school'
|
||||
? <UnlinkedIcon sx={{ fontSize: 11, color: 'text.disabled' }} />
|
||||
@ -249,7 +255,7 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
|
||||
{node.label}
|
||||
</Typography>
|
||||
|
||||
{isEmpty && !isCalendarSection && !isTimetableSection && !isSchoolSection && node.status && (
|
||||
{isEmpty && !isCalendarSection && !isTimetableSection && !isSchoolSection && !isClassesSection && node.status && (
|
||||
<Tooltip title={STATUS_MESSAGES[node.status]} placement="right">
|
||||
<Typography variant="caption" sx={{ fontSize: '0.6rem', color: 'text.disabled', ml: 0.5, flexShrink: 0 }}>
|
||||
{node.status === 'no_school' ? '—' : '…'}
|
||||
@ -319,6 +325,28 @@ function TreeItem({ node, depth, onSelect, onExpand }: TreeItemProps) {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showTimetableView && (
|
||||
<Tooltip title="View as lesson list" placement="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ p: 0.25, ml: 0.5, color: 'text.secondary' }}
|
||||
onClick={e => { e.stopPropagation(); navigate('/my-lessons'); }}
|
||||
>
|
||||
<LaunchIcon sx={{ fontSize: 13 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showClassesView && (
|
||||
<Tooltip title="View my classes" placement="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{ p: 0.25, ml: 0.5, color: 'text.secondary' }}
|
||||
onClick={e => { e.stopPropagation(); navigate('/my-classes'); }}
|
||||
>
|
||||
<LaunchIcon sx={{ fontSize: 13 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Calendar mode toggle */}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user