import React, { useEffect, useState, useCallback, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Box, Typography, Button, CircularProgress, Alert, Chip, TextField, IconButton, Tooltip, Divider, Paper, Select, MenuItem, FormControl, InputLabel, Switch, FormControlLabel, Dialog, DialogTitle, DialogContent, DialogActions, } from '@mui/material'; import { ArrowBack, Add, Delete, AutoAwesome, DragIndicator, Save, LibraryBooks, LinkOff, } from '@mui/icons-material'; import { useAuth } from '../../contexts/AuthContext'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api'; // ─── Types ──────────────────────────────────────────────────────────────────── interface Objective { id: string; text: string; bloom_level?: string; order: number; } interface Activity { id: string; section: string; title: string; description: string; duration_minutes?: number; order: number; } interface LessonPlan { id: string; title: string; subject: string | null; year_group: string | null; duration_minutes: number | null; context_notes: string | null; objectives: Objective[]; activities: Activity[]; is_public: boolean; creator_id: string; } // ─── Constants ──────────────────────────────────────────────────────────────── const BLOOM_LEVELS = ['remember', 'understand', 'apply', 'analyse', 'evaluate', 'create']; const SECTION_SUGGESTIONS = ['Starter', 'Main', 'Plenary', 'Extension', 'Assessment', 'Discussion']; const BLOOM_COLORS: Record = { remember: '#90caf9', understand: '#a5d6a7', apply: '#fff176', analyse: '#ffcc80', evaluate: '#ef9a9a', create: '#ce93d8', }; // ─── AI Suggest button ──────────────────────────────────────────────────────── function SuggestButton({ onClick, loading }: { onClick: () => void; loading: boolean }) { return ( {loading ? : } ); } // ─── Main page ──────────────────────────────────────────────────────────────── const LessonPlanDetailPage: React.FC = () => { const { planId } = useParams<{ planId: string }>(); const navigate = useNavigate(); const { accessToken } = useAuth(); const [plan, setPlan] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [suggestingField, setSuggestingField] = useState(null); // Local editable state (mirrors plan) const [title, setTitle] = useState(''); const [subject, setSubject] = useState(''); const [yearGroup, setYearGroup] = useState(''); const [duration, setDuration] = useState(''); const [contextNotes, setContextNotes] = useState(''); const [isPublic, setIsPublic] = useState(false); const [objectives, setObjectives] = useState([]); const [activities, setActivities] = useState([]); const autoSaveTimer = useRef | null>(null); const load = useCallback(async () => { if (!accessToken || !planId) return; setLoading(true); setError(null); try { const res = await fetch(`${API_BASE}/lessons/plans/${planId}`, { headers: { Authorization: `Bearer ${accessToken}` }, }); const data = await res.json(); if (data.id) { setPlan(data); setTitle(data.title || ''); setSubject(data.subject || ''); setYearGroup(data.year_group || ''); setDuration(data.duration_minutes?.toString() || ''); setContextNotes(data.context_notes || ''); setIsPublic(data.is_public || false); setObjectives(data.objectives || []); setActivities(data.activities || []); } else { setError(data.detail || 'Plan not found'); } } catch (e: any) { setError(e.message); } finally { setLoading(false); } }, [accessToken, planId]); useEffect(() => { load(); }, [load]); // ── Save ────────────────────────────────────────────────────────────────── const save = useCallback(async ( overrides?: Partial<{ title: string; subject: string; year_group: string; duration_minutes: number | null; context_notes: string; is_public: boolean; objectives: Objective[]; activities: Activity[] }> ) => { if (!accessToken || !planId) return; setSaving(true); setSaved(false); try { await fetch(`${API_BASE}/lessons/plans/${planId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ title, subject: subject || null, year_group: yearGroup || null, duration_minutes: duration ? parseInt(duration, 10) : null, context_notes: contextNotes || null, is_public: isPublic, objectives, activities, ...overrides, }), }); setSaved(true); setTimeout(() => setSaved(false), 2000); } catch (e: any) { setError(e.message); } finally { setSaving(false); } }, [accessToken, planId, title, subject, yearGroup, duration, contextNotes, isPublic, objectives, activities]); const scheduleAutoSave = useCallback(() => { if (autoSaveTimer.current) clearTimeout(autoSaveTimer.current); autoSaveTimer.current = setTimeout(() => save(), 1500); }, [save]); // ── Objectives ──────────────────────────────────────────────────────────── const addObjective = () => { const next: Objective = { id: crypto.randomUUID(), text: '', bloom_level: undefined, order: objectives.length }; setObjectives(prev => [...prev, next]); }; const updateObjective = (id: string, patch: Partial) => { setObjectives(prev => prev.map(o => o.id === id ? { ...o, ...patch } : o)); scheduleAutoSave(); }; const removeObjective = (id: string) => { setObjectives(prev => prev.filter(o => o.id !== id).map((o, i) => ({ ...o, order: i }))); scheduleAutoSave(); }; // ── Activities ──────────────────────────────────────────────────────────── const addActivity = () => { const next: Activity = { id: crypto.randomUUID(), section: 'Main', title: '', description: '', order: activities.length, }; setActivities(prev => [...prev, next]); }; const updateActivity = (id: string, patch: Partial) => { setActivities(prev => prev.map(a => a.id === id ? { ...a, ...patch } : a)); scheduleAutoSave(); }; const removeActivity = (id: string) => { setActivities(prev => prev.filter(a => a.id !== id).map((a, i) => ({ ...a, order: i }))); scheduleAutoSave(); }; // ── AI Suggest ──────────────────────────────────────────────────────────── const suggest = async (field: string, targetId?: string) => { if (!accessToken || !planId) return; const key = targetId ? `${field}_${targetId}` : field; setSuggestingField(key); try { const body: Record = { field, context: contextNotes, objective_texts: objectives.map(o => o.text).filter(Boolean), }; if (field === 'activity_description' && targetId) { const act = activities.find(a => a.id === targetId); if (act) body.activity_section = act.section; } const res = await fetch(`${API_BASE}/lessons/plans/${planId}/suggest`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json(); if (data.suggestion) { if (field === 'objectives') { addObjective(); setObjectives(prev => { const updated = [...prev]; updated[updated.length - 1] = { ...updated[updated.length - 1], text: data.suggestion }; return updated; }); } else if (field === 'activity_description' && targetId) { updateActivity(targetId, { description: data.suggestion }); } else if (field === 'title') { setTitle(data.suggestion); scheduleAutoSave(); } } } catch (e: any) { setError('AI suggest failed: ' + e.message); } finally { setSuggestingField(null); } }; // ── Render ──────────────────────────────────────────────────────────────── if (loading) { return ( ); } if (error && !plan) { return ( {error} ); } return ( {/* Header */} {saved && Saved} {error && ( setError(null)} sx={{ mb: 2 }}>{error} )} {/* Title row */} { setTitle(e.target.value); scheduleAutoSave(); }} variant="standard" fullWidth inputProps={{ style: { fontSize: '1.5rem', fontWeight: 700 } }} placeholder="Lesson title…" /> suggest('title')} loading={suggestingField === 'title'} /> {/* Metadata row */} { setSubject(e.target.value); scheduleAutoSave(); }} size="small" sx={{ width: 200 }} /> { setYearGroup(e.target.value); scheduleAutoSave(); }} size="small" sx={{ width: 120 }} /> { setDuration(e.target.value); scheduleAutoSave(); }} size="small" type="number" inputProps={{ min: 1, max: 300 }} sx={{ width: 140 }} /> { setIsPublic(e.target.checked); scheduleAutoSave(); }} /> } label={Public} sx={{ ml: 0.5 }} /> {/* Context notes */} Context / notes { setContextNotes(e.target.value); scheduleAutoSave(); }} multiline minRows={2} fullWidth size="small" placeholder="Class context, prior knowledge, SEN considerations, links to curriculum…" /> {/* Objectives */} Learning objectives suggest('objectives')} loading={suggestingField === 'objectives'} /> {objectives.length === 0 ? ( No objectives yet — add one or use ✨ to suggest. ) : ( {objectives.map((obj, idx) => ( updateObjective(obj.id, { text: e.target.value })} fullWidth size="small" multiline placeholder={`Objective ${idx + 1}…`} variant="standard" /> removeObjective(obj.id)} sx={{ flexShrink: 0, p: 0.25 }}> ))} )} {/* Activities */} Activities {activities.length === 0 ? ( No activities yet — add one to structure your lesson. ) : ( {activities.map((act, idx) => ( {/* Activity header */} {/* Section selector */} updateActivity(act.id, { section: e.target.value })} size="small" label="Section" sx={{ width: 140, flexShrink: 0 }} select={false} inputProps={{ list: `section-suggestions-${act.id}` }} placeholder="Starter / Main…" /> {SECTION_SUGGESTIONS.map(s => updateActivity(act.id, { title: e.target.value })} size="small" label="Activity title" fullWidth placeholder={`Activity ${idx + 1}…`} /> updateActivity(act.id, { duration_minutes: e.target.value ? parseInt(e.target.value, 10) : undefined })} size="small" label="Min" type="number" inputProps={{ min: 1 }} sx={{ width: 72, flexShrink: 0 }} /> removeActivity(act.id)} sx={{ flexShrink: 0, p: 0.25 }}> {/* Description */} updateActivity(act.id, { description: e.target.value })} size="small" multiline minRows={2} fullWidth placeholder="Describe what the teacher and pupils do…" /> suggest('activity_description', act.id)} loading={suggestingField === `activity_description_${act.id}`} /> ))} )} ); }; export default LessonPlanDetailPage;