feat(school): add grade dashboard and insights filters
- Add grade-dashboard components directory for school-wide grade analytics - Add grade-insights-filters component for filtering grade insights - Update grades-view and data-access
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react"
|
||||
import { BarChart3, MoreHorizontal, Pencil, Plus, Trash2, Users, GraduationCap, UserCog } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { parseAsString, useQueryState } from "nuqs"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
import type { GradeListItem, SchoolListItem, StaffOption } from "../types"
|
||||
import type { GradeOverviewStats } from "../data-access"
|
||||
import { createGradeAction, deleteGradeAction, updateGradeAction } from "../actions"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
@@ -71,10 +72,12 @@ export function GradesClient({
|
||||
grades,
|
||||
schools,
|
||||
staff,
|
||||
gradeStats,
|
||||
}: {
|
||||
grades: GradeListItem[]
|
||||
schools: SchoolListItem[]
|
||||
staff: StaffOption[]
|
||||
gradeStats: GradeOverviewStats[]
|
||||
}) {
|
||||
const t = useTranslations("school")
|
||||
const router = useRouter()
|
||||
@@ -92,6 +95,13 @@ export function GradesClient({
|
||||
const [createState, setCreateState] = useState<FormState>(() => toFormState(null, defaultSchoolId))
|
||||
const [editState, setEditState] = useState<FormState>(() => toFormState(null, defaultSchoolId))
|
||||
|
||||
// 年级概览统计映射,用于卡片视图
|
||||
const statsMap = useMemo(() => {
|
||||
const m = new Map<string, GradeOverviewStats>()
|
||||
for (const s of gradeStats) m.set(s.gradeId, s)
|
||||
return m
|
||||
}, [gradeStats])
|
||||
|
||||
useEffect(() => {
|
||||
if (!createOpen) return
|
||||
if (createState.schoolId.trim().length > 0) return
|
||||
@@ -329,6 +339,122 @@ export function GradesClient({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 年级概览卡片视图:让管理员一目了然看到各年级规模 */}
|
||||
{filteredGrades.length > 0 && (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{filteredGrades.slice(0, 8).map((g) => {
|
||||
const stats = statsMap.get(g.id)
|
||||
return (
|
||||
<Card key={g.id} className="shadow-none">
|
||||
<CardContent className="space-y-3 p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-semibold">{g.name}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{g.school.name}</div>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 shrink-0" disabled={isWorking}>
|
||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
router.push(`/admin/school/grades/insights?gradeId=${encodeURIComponent(g.id)}`)
|
||||
}
|
||||
>
|
||||
<BarChart3 className="mr-2 h-3.5 w-3.5" />
|
||||
{t("grades.gradeOverview.viewInsights")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => openEdit(g)}>
|
||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
||||
{t("grades.actions.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => setDeleteItem(g)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-3.5 w-3.5" />
|
||||
{t("grades.actions.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* 统计指标 */}
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
<div className="rounded-md bg-muted/50 p-2">
|
||||
<div className="flex items-center justify-center text-muted-foreground">
|
||||
<GraduationCap className="h-3 w-3" />
|
||||
</div>
|
||||
<div className="mt-0.5 text-sm font-semibold tabular-nums">
|
||||
{stats?.classCount ?? 0}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
{t("grades.gradeOverview.classCount")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-muted/50 p-2">
|
||||
<div className="flex items-center justify-center text-muted-foreground">
|
||||
<Users className="h-3 w-3" />
|
||||
</div>
|
||||
<div className="mt-0.5 text-sm font-semibold tabular-nums">
|
||||
{stats?.studentCount ?? 0}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
{t("grades.gradeOverview.studentCount")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md bg-muted/50 p-2">
|
||||
<div className="flex items-center justify-center text-muted-foreground">
|
||||
<UserCog className="h-3 w-3" />
|
||||
</div>
|
||||
<div className="mt-0.5 text-sm font-semibold tabular-nums">
|
||||
{stats?.teacherCount ?? 0}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
{t("grades.gradeOverview.teacherCount")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 年级主任/教学主任 */}
|
||||
<div className="space-y-1 border-t pt-2 text-xs">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">{t("grades.gradeOverview.gradeHead")}</span>
|
||||
<span className="truncate font-medium">
|
||||
{g.gradeHead?.name ?? t("grades.gradeOverview.notSet")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">{t("grades.gradeOverview.teachingHead")}</span>
|
||||
<span className="truncate font-medium">
|
||||
{g.teachingHead?.name ?? t("grades.gradeOverview.notSet")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 快捷操作 */}
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
>
|
||||
<a href={`/admin/school/grades/insights?gradeId=${encodeURIComponent(g.id)}`}>
|
||||
<BarChart3 className="mr-1.5 h-3.5 w-3.5" />
|
||||
{t("grades.gradeOverview.viewInsights")}
|
||||
</a>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-1 flex-col gap-2 md:flex-row md:items-center">
|
||||
<div className="flex-1 md:max-w-sm">
|
||||
@@ -427,6 +553,7 @@ export function GradesClient({
|
||||
className="h-auto border-none shadow-none"
|
||||
/>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -482,6 +609,7 @@ export function GradesClient({
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user