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
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
import { AlertTriangle, Phone } 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 Warning = {
|
||
studentId: string
|
||
studentName: string
|
||
message: string
|
||
severity: "high" | "medium"
|
||
}
|
||
|
||
/** 翻译函数类型(与 useTranslations / getTranslations 返回值兼容) */
|
||
type Translator = (
|
||
key: string,
|
||
values?: Record<string, string | number | Date>,
|
||
) => string
|
||
|
||
/**
|
||
* 构建考勤异常预警列表(纯函数,便于测试)。
|
||
*/
|
||
export function buildWarnings(
|
||
summaries: ParentStudentAttendanceSummary[],
|
||
t: Translator,
|
||
): Warning[] {
|
||
const warnings: Warning[] = []
|
||
|
||
for (const s of summaries) {
|
||
const { stats, studentName, studentId } = s
|
||
if (stats.absent >= 3) {
|
||
warnings.push({
|
||
studentId,
|
||
studentName,
|
||
message: t("parent.absentHighSeverity", { count: stats.absent }),
|
||
severity: "high",
|
||
})
|
||
} else if (stats.absent >= 1) {
|
||
warnings.push({
|
||
studentId,
|
||
studentName,
|
||
message: t("parent.absentWarning", { count: stats.absent }),
|
||
severity: "medium",
|
||
})
|
||
}
|
||
|
||
if (stats.late >= 3) {
|
||
warnings.push({
|
||
studentId,
|
||
studentName,
|
||
message: t("parent.lateWarning", { count: stats.late }),
|
||
severity: "medium",
|
||
})
|
||
}
|
||
|
||
if (stats.presentRate < 90 && stats.total > 0) {
|
||
warnings.push({
|
||
studentId,
|
||
studentName,
|
||
message: t("parent.lowRateWarning", { rate: stats.presentRate.toFixed(1) }),
|
||
severity: "high",
|
||
})
|
||
}
|
||
}
|
||
|
||
return warnings
|
||
}
|
||
|
||
/**
|
||
* 家长视角的考勤异常预警横幅(RSC)。
|
||
* 聚合所有子女的考勤异常(缺勤、迟到、低出勤率),提醒家长及时关注。
|
||
*
|
||
* P2-8 修复:从 client component 降级为 RSC,使用 `getTranslations`。
|
||
*/
|
||
export async function ParentAttendanceWarning({
|
||
summaries,
|
||
}: {
|
||
summaries: ParentStudentAttendanceSummary[]
|
||
}) {
|
||
const t = await getTranslations("attendance")
|
||
const warnings = buildWarnings(summaries, t)
|
||
if (warnings.length === 0) return null
|
||
|
||
return (
|
||
<Card
|
||
className={cn(
|
||
"border-destructive/30 bg-destructive/5 p-4",
|
||
warnings.some((w) => w.severity === "high") && "border-destructive/50",
|
||
)}
|
||
aria-label={t("parent.warningTitle")}
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
<AlertTriangle
|
||
className="h-5 w-5 text-destructive shrink-0 mt-0.5"
|
||
aria-hidden
|
||
/>
|
||
<div className="flex-1 space-y-2">
|
||
<div className="text-sm font-semibold text-destructive">
|
||
{t("parent.warningTitle")}
|
||
</div>
|
||
<ul className="space-y-1.5 text-sm">
|
||
{warnings.map((w, idx) => (
|
||
<li key={`${w.studentId}-${idx}`} className="flex items-start gap-2">
|
||
<span className="font-medium shrink-0">{w.studentName}:</span>
|
||
<span className="text-muted-foreground">{w.message}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
<div className="flex items-center gap-2 pt-1 text-xs text-muted-foreground">
|
||
<Phone className="h-3 w-3" aria-hidden />
|
||
<span>{t("parent.contactHomeroom")}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)
|
||
}
|