Files
NextEdu/src/modules/parent/components/parent-attention-banner.tsx
SpecialX 1abf58c0b6 feat(parent): add attention banner, export button, and grade detail
- Add parent-attention-banner for highlighting items needing attention

- Add parent-export-button for data export capability

- Add child-grade-detail component for detailed grade viewing

- Update existing child-card, child-detail-header, child-grade-summary, child-schedule-card

- Update parent-children-data-page and data-access
2026-06-23 17:37:49 +08:00

136 lines
4.4 KiB
TypeScript

import Link from "next/link"
import { AlertTriangle, Bell, CalendarCheck, PenTool } from "lucide-react"
import { Card } from "@/shared/components/ui/card"
import { cn } from "@/shared/lib/utils"
import type { ParentDashboardData } from "@/modules/parent/types"
type AttentionItem = {
id: string
label: string
count: number
href: string
variant: "danger" | "warning" | "info"
icon: typeof AlertTriangle
}
const buildAttentionItems = (data: ParentDashboardData): AttentionItem[] => {
const items: AttentionItem[] = []
const totalOverdue = data.children.reduce((sum, c) => sum + c.homeworkSummary.overdueCount, 0)
if (totalOverdue > 0) {
// 找到第一个有逾期作业的子女,直接跳转其详情页 homework tab
const childWithOverdue = data.children.find((c) => c.homeworkSummary.overdueCount > 0)
items.push({
id: "overdue-homework",
label: "Overdue homework",
count: totalOverdue,
href: childWithOverdue
? `/parent/children/${childWithOverdue.basicInfo.id}?tab=homework`
: "/parent/dashboard",
variant: "danger",
icon: PenTool,
})
}
const totalPending = data.children.reduce((sum, c) => sum + c.homeworkSummary.pendingCount, 0)
if (totalPending > 0) {
const childWithPending = data.children.find((c) => c.homeworkSummary.pendingCount > 0)
items.push({
id: "pending-homework",
label: "Pending homework",
count: totalPending,
href: childWithPending
? `/parent/children/${childWithPending.basicInfo.id}?tab=homework`
: "/parent/dashboard",
variant: "warning",
icon: PenTool,
})
}
items.push({
id: "attendance",
label: "Attendance",
count: data.children.length,
href: "/parent/attendance",
variant: "info",
icon: CalendarCheck,
})
items.push({
id: "announcements",
label: "Announcements",
count: 0,
href: "/announcements",
variant: "info",
icon: Bell,
})
return items
}
const VARIANT_STYLES: Record<AttentionItem["variant"], string> = {
danger: "border-destructive/30 bg-destructive/5 text-destructive",
warning: "border-warning/30 bg-warning/5 text-warning-foreground",
info: "border-border bg-muted/40 text-foreground",
}
/**
* 仪表盘顶部"需要关注"横幅。
* 聚合所有子女的异常项(逾期作业、待办、考勤、公告),让家长一眼定位异常。
*/
export function ParentAttentionBanner({ data }: { data: ParentDashboardData }) {
const items = buildAttentionItems(data)
const hasUrgent = items.some((i) => i.variant === "danger" && i.count > 0)
if (items.length === 0) return null
return (
<Card
className={cn(
"p-4",
hasUrgent && "border-destructive/40 bg-destructive/5",
)}
aria-label="Items needing attention"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2">
{hasUrgent ? (
<AlertTriangle className="h-4 w-4 text-destructive" aria-hidden />
) : (
<Bell className="h-4 w-4 text-muted-foreground" aria-hidden />
)}
<span className="text-sm font-medium">
{hasUrgent ? "Needs attention" : "Nothing urgent"}
</span>
</div>
<div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap sm:items-center">
{items.map((item) => {
const Icon = item.icon
const isUrgent = item.variant === "danger" && item.count > 0
return (
<Link
key={item.id}
href={item.href}
className={cn(
"inline-flex h-9 min-w-[120px] items-center justify-between gap-2 rounded-md border px-3 text-xs font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
VARIANT_STYLES[item.variant],
isUrgent && "font-semibold",
)}
aria-label={`${item.label}: ${item.count}`}
>
<span className="flex items-center gap-1.5">
<Icon className="h-3.5 w-3.5" aria-hidden />
{item.label}
</span>
<span className="tabular-nums">{item.count}</span>
</Link>
)
})}
</div>
</div>
</Card>
)
}