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
164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
import { useTranslations } from "next-intl"
|
||
import {
|
||
CalendarCheck,
|
||
CheckCircle2,
|
||
Clock,
|
||
TrendingUp,
|
||
Users,
|
||
XCircle,
|
||
Inbox,
|
||
} from "lucide-react"
|
||
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||
import { Badge } from "@/shared/components/ui/badge"
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from "@/shared/components/ui/table"
|
||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||
import { StatItem } from "@/shared/components/ui/stat-item"
|
||
import {
|
||
ATTENDANCE_STATUS_BADGE_VARIANTS,
|
||
ATTENDANCE_STATUS_LABEL_KEYS,
|
||
} from "@/shared/constants/attendance-status"
|
||
|
||
import type { ParentStudentAttendanceSummary } from "@/modules/parent/types"
|
||
|
||
/**
|
||
* 家长视角的单个子女考勤详情组件(统计卡片 + 最近记录表)。
|
||
*
|
||
* 解耦说明(P1-2 修复):
|
||
* - 此组件替代原先从 `@/modules/attendance/components/student-attendance-view` 直接导入的 `StudentAttendanceView`。
|
||
* - parent 模块不再依赖 attendance 模块的 UI 组件,仅依赖自身类型与 shared 常量。
|
||
* - 数据通过 props 注入(由 parent page 通过 `AttendanceReadService` 接口获取后传入)。
|
||
*/
|
||
export function ParentStudentAttendanceDetail({
|
||
summary,
|
||
}: {
|
||
summary: ParentStudentAttendanceSummary
|
||
}) {
|
||
const t = useTranslations("attendance")
|
||
const { stats, recentRecords } = summary
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||
<Card>
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||
{t("list.columns.student")}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-2xl font-bold">{summary.studentName}</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||
{t("stats.totalRecords")}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-2xl font-bold">{stats.total}</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{stats.total === 0 ? (
|
||
<EmptyState
|
||
title={t("stats.noData")}
|
||
description={t("stats.noDataDescription")}
|
||
icon={CalendarCheck}
|
||
className="border-none shadow-none"
|
||
/>
|
||
) : (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>{t("title.teacherStats")}</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||
<StatItem
|
||
label={t("stats.totalRecords")}
|
||
value={stats.total}
|
||
icon={<Users className="h-4 w-4" />}
|
||
/>
|
||
<StatItem
|
||
label={t("stats.present")}
|
||
value={stats.present}
|
||
icon={<CheckCircle2 className="h-4 w-4" />}
|
||
/>
|
||
<StatItem
|
||
label={t("stats.absent")}
|
||
value={stats.absent}
|
||
icon={<XCircle className="h-4 w-4" />}
|
||
/>
|
||
<StatItem
|
||
label={t("stats.late")}
|
||
value={stats.late}
|
||
icon={<Clock className="h-4 w-4" />}
|
||
/>
|
||
<StatItem
|
||
label={t("stats.attendanceRate")}
|
||
value={`${stats.presentRate.toFixed(1)}%`}
|
||
icon={<TrendingUp className="h-4 w-4" />}
|
||
/>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{recentRecords.length === 0 ? (
|
||
<EmptyState
|
||
title={t("list.empty")}
|
||
description={t("list.emptyDescription")}
|
||
icon={Inbox}
|
||
className="border-none shadow-none"
|
||
/>
|
||
) : (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>{t("stats.recentRecords")}</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="rounded-md border">
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>{t("list.columns.date")}</TableHead>
|
||
<TableHead>{t("list.columns.status")}</TableHead>
|
||
<TableHead>{t("list.columns.remark")}</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{recentRecords.map((r) => (
|
||
<TableRow key={r.id}>
|
||
<TableCell className="font-medium">{r.date}</TableCell>
|
||
<TableCell>
|
||
<Badge
|
||
variant={ATTENDANCE_STATUS_BADGE_VARIANTS[r.status]}
|
||
className="capitalize"
|
||
>
|
||
{t(ATTENDANCE_STATUS_LABEL_KEYS[r.status])}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell className="text-muted-foreground">
|
||
{r.remark ?? "-"}
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|