Files
NextEdu/src/modules/leave-requests/components/leave-request-list.tsx
SpecialX 7567f317e1 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
2026-07-03 10:24:07 +08:00

118 lines
4.0 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.
"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
}