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:
117
src/modules/leave-requests/components/leave-request-list.tsx
Normal file
117
src/modules/leave-requests/components/leave-request-list.tsx
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user