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
This commit is contained in:
SpecialX
2026-07-03 10:26:12 +08:00
parent f3c223d914
commit e9a5264fe7
84 changed files with 6060 additions and 2530 deletions

View File

@@ -0,0 +1,163 @@
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>
)
}