Files
NextEdu/src/modules/parent/components/parent-attendance-rate-card.tsx
SpecialX e9a5264fe7 feat(parent,auth,onboarding,files,notifications,adaptive-practice,ai): add module updates
parent:

- Add parent-student-attendance-detail component

auth:

- Add actions, data-access, schema, services, types

onboarding:

- Add parent-children-form and hooks directory

files:

- Add actions, schema, hooks directory

notifications:

- Add schema and schema test

adaptive-practice:

- Add answer-input, answer-result, practice-result-view, practice-starter-with-nav

- Add question-card, question-content, lib and services directories

ai:

- Add context/create-ai-client-service, hooks/use-drag-position, hooks/use-position-persistence
2026-07-03 10:26:12 +08:00

131 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { CalendarCheck, CalendarX, Clock, TrendingUp } from "lucide-react"
import { getTranslations } from "next-intl/server"
import { Card } from "@/shared/components/ui/card"
import { cn } from "@/shared/lib/utils"
import type { ParentStudentAttendanceSummary } from "@/modules/parent/types"
type AggregateStats = {
totalStudents: number
avgPresentRate: number
totalAbsent: number
totalLate: number
}
/**
* 聚合多个子女的考勤统计(纯函数,便于测试)。
*/
export function aggregateStats(
summaries: ParentStudentAttendanceSummary[],
): AggregateStats {
if (summaries.length === 0) {
return { totalStudents: 0, avgPresentRate: 0, totalAbsent: 0, totalLate: 0 }
}
const totalStudents = summaries.length
const sumRate = summaries.reduce(
(sum, s) => sum + (s.stats.total > 0 ? s.stats.presentRate : 0),
0,
)
const avgPresentRate = sumRate / totalStudents
const totalAbsent = summaries.reduce((sum, s) => sum + s.stats.absent, 0)
const totalLate = summaries.reduce((sum, s) => sum + s.stats.late, 0)
return { totalStudents, avgPresentRate, totalLate, totalAbsent }
}
/**
* 根据出勤率返回语气色调(纯函数,便于测试)。
*/
export function rateTone(rate: number): "good" | "warn" | "bad" {
if (rate >= 95) return "good"
if (rate >= 90) return "warn"
return "bad"
}
const TONE_STYLES: Record<"good" | "warn" | "bad", string> = {
good: "text-emerald-600",
warn: "text-amber-600",
bad: "text-destructive",
}
/**
* 家长考勤页顶部的出勤率汇总卡片RSC
* 聚合所有子女的出勤率、缺勤、迟到总数,让家长一眼掌握整体情况。
*
* P2-8 修复:从 client component 降级为 RSC使用 `getTranslations`。
*/
export async function ParentAttendanceRateCard({
summaries,
}: {
summaries: ParentStudentAttendanceSummary[]
}) {
const t = await getTranslations("attendance")
const stats = aggregateStats(summaries)
if (stats.totalStudents === 0) return null
const tone = rateTone(stats.avgPresentRate)
const rateLabel =
stats.avgPresentRate >= 95
? t("parent.rateExcellent")
: stats.avgPresentRate >= 90
? t("parent.rateNeedsAttention")
: t("parent.rateBelowStandard")
return (
<Card className="p-4" aria-label={t("parent.rateCardTitle")}>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<TrendingUp className="h-3 w-3" aria-hidden />
{t("stats.attendanceRate")}
</div>
<div className={cn("text-2xl font-bold tabular-nums", TONE_STYLES[tone])}>
{stats.avgPresentRate.toFixed(1)}%
</div>
<div className="text-xs text-muted-foreground">{rateLabel}</div>
</div>
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<CalendarCheck className="h-3 w-3" aria-hidden />
{t("parent.children")}
</div>
<div className="text-2xl font-bold tabular-nums">{stats.totalStudents}</div>
<div className="text-xs text-muted-foreground">{t("parent.linked")}</div>
</div>
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<CalendarX className="h-3 w-3" aria-hidden />
{t("stats.absent")}
</div>
<div
className={cn(
"text-2xl font-bold tabular-nums",
stats.totalAbsent > 0 && "text-destructive",
)}
>
{stats.totalAbsent}
</div>
<div className="text-xs text-muted-foreground">{t("parent.thisPeriod")}</div>
</div>
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" aria-hidden />
{t("stats.late")}
</div>
<div
className={cn(
"text-2xl font-bold tabular-nums",
stats.totalLate > 0 && "text-amber-600",
)}
>
{stats.totalLate}
</div>
<div className="text-xs text-muted-foreground">{t("parent.thisPeriod")}</div>
</div>
</div>
</Card>
)
}