Files
encoach_backend_new_v3/frontend/src/pages/student/LearningPlan.tsx
Yamen Ahmad f1c4953a63 feat(v3): restructure project + add complete frontend
- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
2026-04-10 17:26:42 +04:00

123 lines
5.5 KiB
TypeScript

import type { ReactNode } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge";
import { Loader2, Lock, CheckCircle2, PlayCircle, Circle, ArrowRight, Sparkles } from "lucide-react";
import { useLearningPlan, useGenerateLearningPlan } from "@/hooks/queries";
import { useToast } from "@/hooks/use-toast";
import type { PlanItemStatus } from "@/types";
const statusIcons: Record<PlanItemStatus, ReactNode> = {
locked: <Lock className="h-4 w-4 text-muted-foreground" />,
available: <Circle className="h-4 w-4 text-blue-500" />,
in_progress: <PlayCircle className="h-4 w-4 text-yellow-500" />,
completed: <CheckCircle2 className="h-4 w-4 text-green-500" />,
};
const statusColors: Record<PlanItemStatus, string> = {
locked: "bg-muted text-muted-foreground",
available: "bg-blue-50 text-blue-700 border-blue-200",
in_progress: "bg-yellow-50 text-yellow-700 border-yellow-200",
completed: "bg-green-50 text-green-700 border-green-200",
};
export default function LearningPlanPage() {
const { subjectId } = useParams<{ subjectId: string }>();
const navigate = useNavigate();
const { toast } = useToast();
const { data: plan, isLoading } = useLearningPlan({ subject_id: Number(subjectId) });
const generateMutation = useGenerateLearningPlan();
const handleGenerate = () => {
generateMutation.mutate(
{ subject_id: Number(subjectId) },
{
onSuccess: () => toast({ title: "Plan Generated", description: "Your personalized learning plan is ready." }),
onError: () => toast({ title: "Error", description: "Failed to generate plan", variant: "destructive" }),
}
);
};
if (isLoading) return <div className="flex items-center justify-center min-h-[400px]"><div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" /></div>;
if (!plan) {
return (
<div className="max-w-lg mx-auto text-center py-16 space-y-6">
<Sparkles className="h-16 w-16 text-primary mx-auto opacity-60" />
<h1 className="text-2xl font-bold">No Learning Plan Yet</h1>
<p className="text-muted-foreground">Complete a diagnostic assessment first, then generate your personalized learning plan.</p>
<div className="flex gap-3 justify-center">
<Button variant="outline" onClick={() => navigate(`/student/diagnostic/${subjectId}`)}>Take Diagnostic</Button>
<Button onClick={handleGenerate} disabled={generateMutation.isPending}>
{generateMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Generate Plan
</Button>
</div>
</div>
);
}
const items = plan.items.sort((a, b) => a.sequence - b.sequence);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Learning Plan: {plan.subject_name}</h1>
<p className="text-muted-foreground">{plan.ai_summary}</p>
</div>
<Badge variant={plan.status === "active" ? "default" : "secondary"}>{plan.status}</Badge>
</div>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-muted-foreground">Overall Progress</span>
<span className="text-sm font-medium">{Math.round(plan.overall_progress)}%</span>
</div>
<Progress value={plan.overall_progress} className="h-3" />
</CardContent>
</Card>
<div className="space-y-3">
{items.map((item, index) => (
<Card key={item.id} className={`border ${statusColors[item.status]}`}>
<CardContent className="py-4">
<div className="flex items-center gap-4">
<div className="flex flex-col items-center gap-1">
<div className="text-xs text-muted-foreground font-mono">{index + 1}</div>
{statusIcons[item.status]}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium">{item.topic_name}</span>
<Badge variant="outline" className="text-xs">{item.domain_name}</Badge>
</div>
<div className="flex items-center gap-4 mt-1 text-xs text-muted-foreground">
<span>~{item.estimated_hours}h estimated</span>
{item.actual_hours > 0 && <span>{item.actual_hours}h spent</span>}
<span>Mastery: {Math.round(item.mastery)}%</span>
</div>
{item.status !== "locked" && (
<Progress value={item.mastery} className="h-1 mt-2" />
)}
</div>
{(item.status === "available" || item.status === "in_progress") && (
<Button size="sm" onClick={() => navigate(`/student/topic/${item.topic_id}`)}>
{item.status === "in_progress" ? "Continue" : "Start"} <ArrowRight className="ml-1 h-3 w-3" />
</Button>
)}
{item.status === "completed" && (
<Button variant="ghost" size="sm" onClick={() => navigate(`/student/topic/${item.topic_id}`)}>Review</Button>
)}
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}