feat(modules): add leave-requests, invitation-codes, and standards modules

- Add leave-requests module for staff and student leave request management

- Add invitation-codes module for class invitation code generation and redemption

- Add standards module for curriculum standards management
This commit is contained in:
SpecialX
2026-07-03 10:24:07 +08:00
parent ac1de9e433
commit 7567f317e1
19 changed files with 3192 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
"use client"
import { useTranslations } from "next-intl"
import { CalendarDays, CheckCircle2, XCircle, Clock, Ban } from "lucide-react"
import { Badge } from "@/shared/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { EmptyState } from "@/shared/components/ui/empty-state"
import type { LeaveRequestListItem, LeaveStatus, LeaveType } from "../types"
type IconKey = "clock" | "check" | "x" | "ban"
const STATUS_STYLES: Record<LeaveStatus, { variant: "default" | "secondary" | "destructive" | "outline"; iconKey: IconKey }> = {
pending: { variant: "secondary", iconKey: "clock" },
approved: { variant: "default", iconKey: "check" },
rejected: { variant: "destructive", iconKey: "x" },
cancelled: { variant: "outline", iconKey: "ban" },
}
const STATUS_ICONS: Record<IconKey, typeof Clock> = {
clock: Clock,
check: CheckCircle2,
x: XCircle,
ban: Ban,
}
/**
* 请假申请列表(只读展示)。
*
* 适用于家长/学生查看自己的请假记录,教师查看本班请假(审批操作走 LeaveReviewDialog
*/
export function LeaveRequestList({
items,
emptyTitle,
emptyDescription,
}: {
items: LeaveRequestListItem[]
emptyTitle: string
emptyDescription: string
}) {
const t = useTranslations("leave")
if (items.length === 0) {
return (
<EmptyState
icon={CalendarDays}
title={emptyTitle}
description={emptyDescription}
className="border-none shadow-none"
/>
)
}
return (
<div className="space-y-3">
{items.map((item) => {
const meta = STATUS_STYLES[item.status]
const Icon = STATUS_ICONS[meta.iconKey]
return (
<Card key={item.id}>
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<CardTitle className="text-base flex items-center gap-2">
<Icon className="h-4 w-4 text-muted-foreground" aria-hidden />
<span>{item.studentName}</span>
<span className="text-muted-foreground font-normal">·</span>
<span className="text-muted-foreground font-normal">{item.className}</span>
</CardTitle>
<p className="text-sm text-muted-foreground">
{t("types." + item.leaveType)} · {item.startDate} ~ {item.endDate}
</p>
</div>
<Badge variant={meta.variant}>
{t("status." + item.status)}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-2">
<div className="text-sm">
<span className="text-muted-foreground">{t("list.reason")}</span>
<span>{item.reason}</span>
</div>
{item.reviewerName && item.reviewedAt && (
<div className="text-sm text-muted-foreground">
{t("list.reviewedBy", { name: item.reviewerName, date: item.reviewedAt.slice(0, 10) })}
</div>
)}
{item.reviewComment && (
<div className="rounded-md bg-muted/50 p-2 text-sm">
<span className="text-muted-foreground">{t("list.reviewComment")}</span>
<span>{item.reviewComment}</span>
</div>
)}
{item.attendanceSynced && (
<div className="text-xs text-emerald-600 dark:text-emerald-400">
{t("list.attendanceSynced")}
</div>
)}
</CardContent>
</Card>
)
})}
</div>
)
}
/** 请假类型中文标签(复用工具函数,避免重复定义)。 */
export function leaveTypeLabel(type: LeaveType): string {
const labels: Record<LeaveType, string> = {
sick: "病假",
personal: "事假",
family: "家庭事务",
other: "其他",
}
return labels[type] ?? type
}