feat(classes): optimize teacher dashboard ui and implement grade management
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { BookOpen, Calendar, ChevronRight, Clock, Users } from "lucide-react"
|
||||
|
||||
import { getClassHomeworkInsights, getClassSchedule, getClassStudents } from "@/modules/classes/data-access"
|
||||
import { ScheduleView } from "@/modules/classes/components/schedule-view"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table"
|
||||
import { formatDate } from "@/shared/lib/utils"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/components/ui/avatar"
|
||||
import { cn, formatDate } from "@/shared/lib/utils"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
@@ -23,6 +25,15 @@ const formatNumber = (v: number | null, digits = 1) => {
|
||||
return v.toFixed(digits)
|
||||
}
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.slice(0, 2)
|
||||
}
|
||||
|
||||
export default async function ClassDetailPage({
|
||||
params,
|
||||
searchParams,
|
||||
@@ -63,253 +74,304 @@ export default async function ClassDetailPage({
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div className="flex flex-col min-h-full space-y-8 p-8">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/teacher/classes/my">Back</Link>
|
||||
</Button>
|
||||
<Badge variant="secondary">{insights.class.grade}</Badge>
|
||||
<Badge variant="outline">{insights.studentCounts.total} students</Badge>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 text-muted-foreground mb-2">
|
||||
<Link href="/teacher/classes/my" className="hover:text-foreground transition-colors">
|
||||
My Classes
|
||||
</Link>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<span className="text-foreground font-medium">{insights.class.name}</span>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">{insights.class.name}</h2>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{insights.class.room ? `Room: ${insights.class.room}` : "Room: Not set"}
|
||||
{insights.class.homeroom ? ` · Homeroom: ${insights.class.homeroom}` : null}
|
||||
<h2 className="text-3xl font-bold tracking-tight">{insights.class.name}</h2>
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<Badge variant="secondary" className="rounded-sm font-normal">
|
||||
{insights.class.grade}
|
||||
</Badge>
|
||||
{insights.class.homeroom && (
|
||||
<>
|
||||
<span className="w-1 h-1 rounded-full bg-border" />
|
||||
<span>Homeroom: {insights.class.homeroom}</span>
|
||||
</>
|
||||
)}
|
||||
{insights.class.room && (
|
||||
<>
|
||||
<span className="w-1 h-1 rounded-full bg-border" />
|
||||
<span>Room: {insights.class.room}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/teacher/classes/students?classId=${encodeURIComponent(insights.class.id)}`}>Students</Link>
|
||||
<Link href={`/teacher/classes/students?classId=${encodeURIComponent(insights.class.id)}`}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Students
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/teacher/classes/schedule?classId=${encodeURIComponent(insights.class.id)}`}>Schedule</Link>
|
||||
<Link href={`/teacher/classes/schedule?classId=${encodeURIComponent(insights.class.id)}`}>
|
||||
<Calendar className="mr-2 h-4 w-4" />
|
||||
Schedule
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/teacher/classes/insights?classId=${encodeURIComponent(insights.class.id)}`}>Insights</Link>
|
||||
<Button asChild>
|
||||
<Link href={`/teacher/homework/assignments/create?classId=${encodeURIComponent(insights.class.id)}`}>
|
||||
Create Homework
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
{/* Stats Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Students</CardTitle>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Students</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{insights.studentCounts.total}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Active {insights.studentCounts.active} · Inactive {insights.studentCounts.inactive}
|
||||
{insights.studentCounts.active} active · {insights.studentCounts.inactive} inactive
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Schedule items</CardTitle>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Schedule Items</CardTitle>
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{schedule.length}</div>
|
||||
<div className="text-xs text-muted-foreground">Weekly timetable entries</div>
|
||||
<div className="text-xs text-muted-foreground">Weekly sessions</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Assignments</CardTitle>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Active Assignments</CardTitle>
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{insights.assignments.length}</div>
|
||||
<div className="text-xs text-muted-foreground">{latest ? `Latest ${formatDate(latest.createdAt)}` : "No homework yet"}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Overall avg</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{formatNumber(insights.overallScores.avg, 1)}</div>
|
||||
<div className="text-2xl font-bold">
|
||||
{insights.assignments.filter((a) => a.isActive).length}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Median {formatNumber(insights.overallScores.median, 1)} · Count {insights.overallScores.count}
|
||||
{insights.assignments.filter((a) => a.isOverdue).length} overdue
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Class Average</CardTitle>
|
||||
<BookOpen className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{formatNumber(insights.overallScores.avg, 1)}%</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Based on {insights.overallScores.count} graded submissions
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{latest ? (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-base">Latest homework</CardTitle>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">{latest.title}</span>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{latest.status}
|
||||
</Badge>
|
||||
<span>·</span>
|
||||
<span>{formatDate(latest.createdAt)}</span>
|
||||
{latest.dueAt ? (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span>Due {formatDate(latest.dueAt)}</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={`/teacher/homework/assignments/${latest.assignmentId}`}>Open</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={`/teacher/homework/assignments/${latest.assignmentId}/submissions`}>Submissions</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 md:grid-cols-5">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Targeted</div>
|
||||
<div className="text-lg font-semibold">{latest.targetCount}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Submitted</div>
|
||||
<div className="text-lg font-semibold">{latest.submittedCount}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Graded</div>
|
||||
<div className="text-lg font-semibold">{latest.gradedCount}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Average</div>
|
||||
<div className="text-lg font-semibold">{formatNumber(latest.scoreStats.avg, 1)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Median</div>
|
||||
<div className="text-lg font-semibold">{formatNumber(latest.scoreStats.median, 1)}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-base">Students (preview)</CardTitle>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={`/teacher/classes/students?classId=${encodeURIComponent(insights.class.id)}`}>View all</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{students.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">No students enrolled.</div>
|
||||
) : (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{students.slice(0, 8).map((s) => (
|
||||
<TableRow key={s.id}>
|
||||
<TableCell className="font-medium">{s.name}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{s.email}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{s.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-base">Schedule</CardTitle>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={`/teacher/classes/schedule?classId=${encodeURIComponent(insights.class.id)}`}>View all</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScheduleView schedule={schedule} classes={scheduleBuilderClasses} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<CardTitle className="text-base">Homework history</CardTitle>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button asChild size="sm" variant={hwFilter === "all" ? "secondary" : "outline"}>
|
||||
<Link href={`/teacher/classes/my/${encodeURIComponent(insights.class.id)}`}>All</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant={hwFilter === "active" ? "secondary" : "outline"}>
|
||||
<Link href={`/teacher/classes/my/${encodeURIComponent(insights.class.id)}?hw=active`}>Active</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant={hwFilter === "overdue" ? "secondary" : "outline"}>
|
||||
<Link href={`/teacher/classes/my/${encodeURIComponent(insights.class.id)}?hw=overdue`}>Overdue</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={`/teacher/homework/assignments?classId=${encodeURIComponent(insights.class.id)}`}>Open list</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm">
|
||||
<Link href={`/teacher/homework/assignments/create?classId=${encodeURIComponent(insights.class.id)}`}>New homework</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!hasAssignments ? (
|
||||
<div className="text-sm text-muted-foreground">No homework assignments yet.</div>
|
||||
) : (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Assignment</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Due</TableHead>
|
||||
<TableHead className="text-right">Targeted</TableHead>
|
||||
<TableHead className="text-right">Submitted</TableHead>
|
||||
<TableHead className="text-right">Graded</TableHead>
|
||||
<TableHead className="text-right">Avg</TableHead>
|
||||
<TableHead className="text-right">Median</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredAssignments.map((a) => (
|
||||
<TableRow key={a.assignmentId}>
|
||||
<TableCell className="font-medium">
|
||||
<Link href={`/teacher/homework/assignments/${a.assignmentId}`} className="hover:underline">
|
||||
{a.title}
|
||||
</Link>
|
||||
<div className="text-xs text-muted-foreground">Created {formatDate(a.createdAt)}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{a.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{a.dueAt ? formatDate(a.dueAt) : "-"}</TableCell>
|
||||
<TableCell className="text-right">{a.targetCount}</TableCell>
|
||||
<TableCell className="text-right">{a.submittedCount}</TableCell>
|
||||
<TableCell className="text-right">{a.gradedCount}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(a.scoreStats.avg, 1)}</TableCell>
|
||||
<TableCell className="text-right">{formatNumber(a.scoreStats.median, 1)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="grid gap-6 lg:grid-cols-7">
|
||||
{/* Main Content Area */}
|
||||
<div className="lg:col-span-4 space-y-6">
|
||||
{/* Latest Homework */}
|
||||
{latest && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<CardTitle>Latest Homework</CardTitle>
|
||||
<CardDescription>Most recent assignment activity</CardDescription>
|
||||
</div>
|
||||
<Badge variant={latest.isActive ? "default" : "secondary"}>
|
||||
{latest.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex flex-col gap-4 rounded-lg border p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
href={`/teacher/homework/assignments/${latest.assignmentId}`}
|
||||
className="font-semibold hover:underline"
|
||||
>
|
||||
{latest.title}
|
||||
</Link>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>Due {latest.dueAt ? formatDate(latest.dueAt) : "No due date"}</span>
|
||||
<span>·</span>
|
||||
<span>{latest.submittedCount}/{latest.targetCount} Submitted</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={`/teacher/homework/assignments/${latest.assignmentId}/submissions`}>
|
||||
Grade
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 border-t pt-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold">{latest.gradedCount}</div>
|
||||
<div className="text-xs text-muted-foreground">Graded</div>
|
||||
</div>
|
||||
<div className="text-center border-l border-r">
|
||||
<div className="text-2xl font-bold">{formatNumber(latest.scoreStats.avg, 1)}</div>
|
||||
<div className="text-xs text-muted-foreground">Average</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold">{formatNumber(latest.scoreStats.median, 1)}</div>
|
||||
<div className="text-xs text-muted-foreground">Median</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Students Preview */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<CardTitle>Students</CardTitle>
|
||||
<CardDescription>Recently active students</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href={`/teacher/classes/students?classId=${encodeURIComponent(insights.class.id)}`}>
|
||||
View All
|
||||
<ChevronRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{students.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No students enrolled yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{students.slice(0, 5).map((s) => (
|
||||
<div key={s.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-9 w-9">
|
||||
<AvatarImage src={s.image || undefined} />
|
||||
<AvatarFallback>{getInitials(s.name)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium text-sm">{s.name}</div>
|
||||
<div className="text-xs text-muted-foreground">{s.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={s.status === "active" ? "outline" : "secondary"} className="text-xs font-normal">
|
||||
{s.status}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar Area */}
|
||||
<div className="lg:col-span-3 space-y-6">
|
||||
{/* Schedule Widget */}
|
||||
<Card className="h-fit">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Schedule</CardTitle>
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<Link href={`/teacher/classes/schedule?classId=${encodeURIComponent(insights.class.id)}`}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScheduleView schedule={schedule} classes={scheduleBuilderClasses} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Homework History */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>History</CardTitle>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={hwFilter === "all" ? "secondary" : "ghost"}
|
||||
asChild
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Link href={`/teacher/classes/my/${encodeURIComponent(insights.class.id)}`}>All</Link>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={hwFilter === "active" ? "secondary" : "ghost"}
|
||||
asChild
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Link href={`/teacher/classes/my/${encodeURIComponent(insights.class.id)}?hw=active`}>Active</Link>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={hwFilter === "overdue" ? "secondary" : "ghost"}
|
||||
asChild
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Link href={`/teacher/classes/my/${encodeURIComponent(insights.class.id)}?hw=overdue`}>Overdue</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="divide-y">
|
||||
{filteredAssignments.slice(0, 5).map((a) => (
|
||||
<div key={a.assignmentId} className="p-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-start justify-between gap-4 mb-2">
|
||||
<Link
|
||||
href={`/teacher/homework/assignments/${a.assignmentId}`}
|
||||
className="text-sm font-medium hover:underline line-clamp-1"
|
||||
>
|
||||
{a.title}
|
||||
</Link>
|
||||
<Badge variant={a.isActive ? "default" : "secondary"} className="shrink-0 text-[10px] h-5">
|
||||
{a.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>Due {a.dueAt ? formatDate(a.dueAt) : "-"}</span>
|
||||
<div className="flex gap-3">
|
||||
<span>{a.submittedCount} submitted</span>
|
||||
<span>{formatNumber(a.scoreStats.avg, 0)}% avg</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{filteredAssignments.length === 0 && (
|
||||
<div className="p-8 text-center text-sm text-muted-foreground">
|
||||
No assignments found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{filteredAssignments.length > 5 && (
|
||||
<div className="p-2 border-t text-center">
|
||||
<Button variant="ghost" size="sm" className="w-full text-muted-foreground" asChild>
|
||||
<Link href={`/teacher/homework/assignments?classId=${encodeURIComponent(insights.class.id)}`}>
|
||||
View All Assignments
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,16 +13,6 @@ export default function MyClassesPage() {
|
||||
|
||||
async function MyClassesPageImpl() {
|
||||
const classes = await getTeacherClasses()
|
||||
const session = await auth()
|
||||
const role = String(session?.user?.role ?? "")
|
||||
const userId = String(session?.user?.id ?? "").trim()
|
||||
|
||||
const canCreateClass = await (async () => {
|
||||
if (role === "admin") return true
|
||||
if (!userId) return false
|
||||
const [row] = await db.select({ id: grades.id }).from(grades).where(eq(grades.gradeHeadId, userId)).limit(1)
|
||||
return Boolean(row)
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
@@ -35,7 +25,7 @@ async function MyClassesPageImpl() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MyClassesGrid classes={classes} canCreateClass={canCreateClass} />
|
||||
<MyClassesGrid classes={classes} canCreateClass={false} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,13 +21,15 @@ async function StudentsResults({ searchParams }: { searchParams: Promise<SearchP
|
||||
|
||||
const q = getParam(params, "q") || undefined
|
||||
const classId = getParam(params, "classId")
|
||||
const status = getParam(params, "status")
|
||||
|
||||
const filteredStudents = await getClassStudents({
|
||||
q,
|
||||
classId: classId && classId !== "all" ? classId : undefined,
|
||||
status: status && status !== "all" ? status : undefined,
|
||||
})
|
||||
|
||||
const hasFilters = Boolean(q || (classId && classId !== "all"))
|
||||
const hasFilters = Boolean(q || (classId && classId !== "all") || (status && status !== "all"))
|
||||
|
||||
if (filteredStudents.length === 0) {
|
||||
return (
|
||||
|
||||
@@ -12,9 +12,8 @@ export default async function BuildExamPage({ params }: { params: Promise<{ id:
|
||||
const exam = await getExamById(id)
|
||||
if (!exam) return notFound()
|
||||
|
||||
// Fetch all available questions (for selection pool)
|
||||
// In a real app, this might be paginated or filtered by exam subject/grade
|
||||
const { data: questionsData } = await getQuestions({ pageSize: 100 })
|
||||
// Fetch initial questions for the bank (pagination handled by client)
|
||||
const { data: questionsData } = await getQuestions({ pageSize: 20 })
|
||||
|
||||
const initialSelected = (exam.questions || []).map(q => ({
|
||||
id: q.id,
|
||||
@@ -103,13 +102,7 @@ export default async function BuildExamPage({ params }: { params: Promise<{ id:
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">Build Exam</h2>
|
||||
<p className="text-muted-foreground">Add questions and adjust scores.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full flex-col space-y-4 p-4">
|
||||
<ExamAssembly
|
||||
examId={exam.id}
|
||||
title={exam.title}
|
||||
|
||||
@@ -131,13 +131,6 @@ export default async function AllExamsPage({
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div className="flex flex-col justify-between space-y-4 md:flex-row md:items-center md:space-y-0">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">All Exams</h2>
|
||||
<p className="text-muted-foreground">View and manage all your exams.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<Suspense fallback={<div className="h-10 w-full animate-pulse rounded-md bg-muted" />}>
|
||||
<ExamFilters />
|
||||
|
||||
@@ -1,14 +1,37 @@
|
||||
import { ExamForm } from "@/modules/exams/components/exam-form"
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/shared/components/ui/breadcrumb"
|
||||
|
||||
export default function CreateExamPage() {
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col space-y-8 p-8 max-w-[1200px] mx-auto">
|
||||
<div className="space-y-4">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/teacher/exams/all">Exams</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Create</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">Create Exam</h2>
|
||||
<p className="text-muted-foreground">Design a new exam for your students.</p>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Create Exam</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Set up a new exam draft and choose your assembly method.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ExamForm />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
import { getTeacherIdForMutations } from "@/modules/classes/data-access"
|
||||
import { getGradeHomeworkInsights } from "@/modules/classes/data-access"
|
||||
import { getGradesForStaff } from "@/modules/school/data-access"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table"
|
||||
import { BarChart3 } from "lucide-react"
|
||||
import { formatDate } from "@/shared/lib/utils"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
type SearchParams = { [key: string]: string | string[] | undefined }
|
||||
|
||||
const getParam = (params: SearchParams, key: string) => {
|
||||
const v = params[key]
|
||||
if (typeof v === "string") return v
|
||||
if (Array.isArray(v)) return v[0]
|
||||
return undefined
|
||||
}
|
||||
|
||||
const fmt = (v: number | null, digits = 1) => (typeof v === "number" && Number.isFinite(v) ? v.toFixed(digits) : "-")
|
||||
|
||||
export default async function TeacherGradeInsightsPage({ searchParams }: { searchParams: Promise<SearchParams> }) {
|
||||
const params = await searchParams
|
||||
const gradeId = getParam(params, "gradeId")
|
||||
|
||||
const teacherId = await getTeacherIdForMutations()
|
||||
const grades = await getGradesForStaff(teacherId)
|
||||
const allowedIds = new Set(grades.map((g) => g.id))
|
||||
const selected = gradeId && gradeId !== "all" && allowedIds.has(gradeId) ? gradeId : ""
|
||||
|
||||
const insights = selected ? await getGradeHomeworkInsights({ gradeId: selected, limit: 50 }) : null
|
||||
|
||||
if (grades.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-2xl font-bold tracking-tight">Grade Insights</h2>
|
||||
<p className="text-muted-foreground">View grade-level homework statistics for grades you lead.</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title="No grades assigned"
|
||||
description="You are not assigned as a grade head or teaching head for any grade."
|
||||
className="h-[360px] bg-card"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-2xl font-bold tracking-tight">Grade Insights</h2>
|
||||
<p className="text-muted-foreground">Homework statistics aggregated across all classes in a grade.</p>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="text-base">Filters</CardTitle>
|
||||
<Badge variant="secondary" className="tabular-nums">
|
||||
{grades.length}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form action="/teacher/grades/insights" method="get" className="flex flex-col gap-3 md:flex-row md:items-center">
|
||||
<label className="text-sm font-medium">Grade</label>
|
||||
<select
|
||||
name="gradeId"
|
||||
defaultValue={selected || "all"}
|
||||
className="h-10 w-full rounded-md border bg-background px-3 text-sm md:w-[360px]"
|
||||
>
|
||||
<option value="all">Select a grade</option>
|
||||
{grades.map((g) => (
|
||||
<option key={g.id} value={g.id}>
|
||||
{g.school.name} / {g.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button type="submit" className="md:ml-2">
|
||||
Apply
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{!selected ? (
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title="Select a grade to view insights"
|
||||
description="Pick a grade to see latest homework and historical score statistics."
|
||||
className="h-[360px] bg-card"
|
||||
/>
|
||||
) : !insights ? (
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title="Grade not found"
|
||||
description="This grade may not exist or has no accessible data."
|
||||
className="h-[360px] bg-card"
|
||||
/>
|
||||
) : insights.assignments.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title="No homework data for this grade"
|
||||
description="No homework assignments were targeted to students in this grade yet."
|
||||
className="h-[360px] bg-card"
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Classes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold tabular-nums">{insights.classCount}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{insights.grade.school.name} / {insights.grade.name}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Students</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold tabular-nums">{insights.studentCounts.total}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Active {insights.studentCounts.active} • Inactive {insights.studentCounts.inactive}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Overall Avg</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold tabular-nums">{fmt(insights.overallScores.avg)}</div>
|
||||
<div className="text-xs text-muted-foreground">Across graded homework</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">Latest Avg</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold tabular-nums">{fmt(insights.latest?.scoreStats.avg ?? null)}</div>
|
||||
<div className="text-xs text-muted-foreground">{insights.latest ? insights.latest.title : "-"}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="text-base">Homework timeline</CardTitle>
|
||||
<Badge variant="secondary" className="tabular-nums">
|
||||
{insights.assignments.length}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead>Assignment</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead className="text-right">Targeted</TableHead>
|
||||
<TableHead className="text-right">Submitted</TableHead>
|
||||
<TableHead className="text-right">Graded</TableHead>
|
||||
<TableHead className="text-right">Avg</TableHead>
|
||||
<TableHead className="text-right">Median</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{insights.assignments.map((a) => (
|
||||
<TableRow key={a.assignmentId}>
|
||||
<TableCell className="font-medium">{a.title}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary" className="capitalize">
|
||||
{a.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{formatDate(a.createdAt)}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{a.targetCount}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{a.submittedCount}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{a.gradedCount}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{fmt(a.scoreStats.avg)}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{fmt(a.scoreStats.median)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="text-base">Class ranking</CardTitle>
|
||||
<Badge variant="secondary" className="tabular-nums">
|
||||
{insights.classes.length}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead>Class</TableHead>
|
||||
<TableHead className="text-right">Students</TableHead>
|
||||
<TableHead className="text-right">Latest Avg</TableHead>
|
||||
<TableHead className="text-right">Prev Avg</TableHead>
|
||||
<TableHead className="text-right">Δ</TableHead>
|
||||
<TableHead className="text-right">Overall Avg</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{insights.classes.map((c) => (
|
||||
<TableRow key={c.class.id}>
|
||||
<TableCell className="font-medium">
|
||||
{c.class.name}
|
||||
{c.class.homeroom ? <span className="text-muted-foreground"> • {c.class.homeroom}</span> : null}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{c.studentCounts.total}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{fmt(c.latestAvg)}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{fmt(c.prevAvg)}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{fmt(c.deltaAvg)}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{fmt(c.overallScores.avg)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { getHomeworkAssignmentAnalytics } from "@/modules/homework/data-access"
|
||||
import { HomeworkAssignmentExamContentCard } from "@/modules/homework/components/homework-assignment-exam-content-card"
|
||||
import { HomeworkAssignmentQuestionErrorDetailsCard } from "@/modules/homework/components/homework-assignment-question-error-details-card"
|
||||
import { HomeworkAssignmentQuestionErrorOverviewCard } from "@/modules/homework/components/homework-assignment-question-error-overview-card"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
import { formatDate } from "@/shared/lib/utils"
|
||||
import { ChevronLeft, Users, Calendar, BarChart3, CheckCircle2 } from "lucide-react"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
@@ -20,82 +20,82 @@ export default async function HomeworkAssignmentDetailPage({ params }: { params:
|
||||
const { assignment, questions, gradedSampleCount } = analytics
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-center">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-2xl font-bold tracking-tight">{assignment.title}</h2>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{assignment.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1">{assignment.description || "—"}</p>
|
||||
<div className="mt-2 text-sm text-muted-foreground">
|
||||
<span>Source Exam: {assignment.sourceExamTitle}</span>
|
||||
<span className="mx-2">•</span>
|
||||
<span>Created: {formatDate(assignment.createdAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/teacher/homework/assignments">Back</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link href={`/teacher/homework/assignments/${assignment.id}/submissions`}>View Submissions</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Targets</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{assignment.targetCount}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Submissions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{assignment.submissionCount}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">Due</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-sm">
|
||||
<div className="font-medium">{assignment.dueAt ? formatDate(assignment.dueAt) : "—"}</div>
|
||||
<div className="text-muted-foreground">
|
||||
Late:{" "}
|
||||
{assignment.allowLate
|
||||
? assignment.lateDueAt
|
||||
? formatDate(assignment.lateDueAt)
|
||||
: "Allowed"
|
||||
: "Not allowed"}
|
||||
</div>
|
||||
<div className="flex flex-col min-h-full">
|
||||
{/* Header */}
|
||||
<div className="border-b bg-background px-8 py-5">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-1">
|
||||
<Link href="/teacher/homework/assignments" className="flex items-center hover:text-foreground transition-colors">
|
||||
<ChevronLeft className="h-4 w-4 mr-1" />
|
||||
Assignments
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span>Details</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-foreground">{assignment.title}</h1>
|
||||
<Badge variant={assignment.status === "published" ? "default" : "secondary"} className="capitalize">
|
||||
{assignment.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm max-w-2xl">{assignment.description || "No description provided."}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 mt-2 md:mt-0">
|
||||
<Button asChild variant="outline" className="shadow-sm">
|
||||
<Link href={`/teacher/homework/assignments/${assignment.id}/submissions`}>
|
||||
<Users className="h-4 w-4 mr-2" />
|
||||
View Submissions
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Stats Row */}
|
||||
<div className="flex flex-wrap gap-x-8 gap-y-2 mt-6 text-sm">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Calendar className="h-4 w-4" />
|
||||
<span>Due: <span className="font-medium text-foreground">{assignment.dueAt ? formatDate(assignment.dueAt) : "No due date"}</span></span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Users className="h-4 w-4" />
|
||||
<span>Targets: <span className="font-medium text-foreground">{assignment.targetCount}</span></span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
<span>Submissions: <span className="font-medium text-foreground">{assignment.submissionCount}</span></span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
<span>Graded: <span className="font-medium text-foreground">{gradedSampleCount}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<HomeworkAssignmentQuestionErrorOverviewCard questions={questions} gradedSampleCount={gradedSampleCount} />
|
||||
<HomeworkAssignmentQuestionErrorDetailsCard questions={questions} gradedSampleCount={gradedSampleCount} />
|
||||
</div>
|
||||
<div className="flex-1 p-8 space-y-8 bg-muted/5">
|
||||
{/* Analytics Section */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold tracking-tight">Performance Analytics</h2>
|
||||
</div>
|
||||
<div className="grid gap-6 md:grid-cols-1">
|
||||
<HomeworkAssignmentQuestionErrorOverviewCard questions={questions} gradedSampleCount={gradedSampleCount} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<HomeworkAssignmentExamContentCard
|
||||
structure={assignment.structure}
|
||||
questions={questions}
|
||||
gradedSampleCount={gradedSampleCount}
|
||||
/>
|
||||
{/* Content Section */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold tracking-tight">Assignment Content</h2>
|
||||
</div>
|
||||
<HomeworkAssignmentExamContentCard
|
||||
structure={assignment.structure}
|
||||
questions={questions}
|
||||
gradedSampleCount={gradedSampleCount}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ export default async function HomeworkSubmissionGradingPage({ params }: { params
|
||||
status={submission.status}
|
||||
totalScore={submission.totalScore}
|
||||
answers={submission.answers}
|
||||
prevSubmissionId={submission.prevSubmissionId}
|
||||
nextSubmissionId={submission.nextSubmissionId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ async function TextbooksResults({ searchParams }: { searchParams: Promise<Search
|
||||
title={hasFilters ? "No textbooks match your filters" : "No textbooks yet"}
|
||||
description={hasFilters ? "Try clearing filters or adjusting keywords." : "Create your first textbook to start organizing chapters."}
|
||||
action={hasFilters ? { label: "Clear filters", href: "/teacher/textbooks" } : undefined}
|
||||
className="bg-card"
|
||||
className="min-h-[400px] border-muted-foreground/10"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -50,7 +50,7 @@ async function TextbooksResults({ searchParams }: { searchParams: Promise<Search
|
||||
export default async function TextbooksPage({ searchParams }: { searchParams: Promise<SearchParams> }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 p-8">
|
||||
{/* Page Header */}
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user