Fitur lengkap: - Auth JWT + RBAC (Admin/Teacher) + halaman login - Shell 3 kolom (left nav, topbar, right rail dashboard) - CRUD siswa + follow up kanban + status calon/student/ex - Dashboard: siswa aktif, kehadiran, bar sumber lead, perlu follow up - Pembayaran prorata per pertemuan (cash/transfer/qris) - Kelas, enrollment, generate sesi, kehadiran bulk - Jurnal aktivitas + upload multi-foto - Raport + placement test (tampil di detail siswa) - Pengaturan & seed data contoh Stack: Next.js 16, TypeScript, Tailwind v4, Prisma (SQLite), Recharts, Zod
505 lines
23 KiB
TypeScript
505 lines
23 KiB
TypeScript
import Link from "next/link";
|
||
import { notFound } from "next/navigation";
|
||
import {
|
||
CalendarCheck,
|
||
FileText,
|
||
GraduationCap,
|
||
LayoutList,
|
||
Mail,
|
||
MessageCircle,
|
||
NotebookPen,
|
||
Pencil,
|
||
Phone,
|
||
Target,
|
||
UserRound,
|
||
Wallet,
|
||
} from "lucide-react";
|
||
import { prisma } from "@/lib/prisma";
|
||
import { requireUser } from "@/lib/rbac";
|
||
import { formatDateID, formatIDR, formatRelativeID } from "@/lib/utils";
|
||
import { getPricePerMeeting, gradeFromScore } from "@/lib/prorata";
|
||
import { LEAD_SOURCE_META } from "@/lib/constants";
|
||
import { Card, CardBody, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Avatar } from "@/components/ui/avatar";
|
||
import { EmptyState } from "@/components/ui/empty-state";
|
||
import { archiveStudentAction } from "../actions";
|
||
import { LevelBadge, MethodChip, PaymentStatusBadge, SourceChip, StatusBadge } from "@/components/students/badges";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
export const metadata = { title: "Detail Siswa — Kodeva" };
|
||
|
||
const TABS = [
|
||
{ key: "ringkasan", label: "Ringkasan", icon: LayoutList },
|
||
{ key: "kelas", label: "Kelas & Kehadiran", icon: CalendarCheck },
|
||
{ key: "pembayaran", label: "Pembayaran", icon: Wallet },
|
||
{ key: "placement", label: "Placement Test", icon: Target },
|
||
{ key: "raport", label: "Raport", icon: FileText },
|
||
{ key: "jurnal", label: "Jurnal", icon: NotebookPen },
|
||
];
|
||
|
||
function waLink(phone: string) {
|
||
return `https://wa.me/${phone.replace(/[^0-9]/g, "").replace(/^0/, "62")}`;
|
||
}
|
||
|
||
export default async function StudentDetailPage({
|
||
params,
|
||
searchParams,
|
||
}: {
|
||
params: Promise<{ id: string }>;
|
||
searchParams: Promise<{ tab?: string }>;
|
||
}) {
|
||
const user = await requireUser();
|
||
const { id } = await params;
|
||
const sp = await searchParams;
|
||
const tab = TABS.some((t) => t.key === sp.tab) ? sp.tab! : "ringkasan";
|
||
|
||
const student = await prisma.student.findUnique({
|
||
where: { id },
|
||
include: {
|
||
enrollments: { include: { class: { include: { teacher: true } } } },
|
||
payments: { include: { class: true }, orderBy: { paidAt: "desc" } },
|
||
attendances: { include: { session: true }, orderBy: { session: { date: "desc" } } },
|
||
placementTests: { include: { scores: true, examiner: true }, orderBy: { testDate: "desc" } },
|
||
reportCards: { include: { scores: true, teacher: true, class: true }, orderBy: { createdAt: "desc" } },
|
||
journalStudents: {
|
||
include: { journal: { include: { class: true, photos: true } } },
|
||
orderBy: { journal: { activityDate: "desc" } },
|
||
},
|
||
followUps: { include: { activities: true, pic: true }, orderBy: { updatedAt: "desc" }, take: 1 },
|
||
},
|
||
});
|
||
if (!student) notFound();
|
||
|
||
// Referensi kelas untuk rekomendasi placement
|
||
const classes = await prisma.class.findMany({ select: { id: true, name: true }, orderBy: { name: "asc" } });
|
||
|
||
// Prorata per kelas
|
||
const prorataByClass = new Map<string, { paidMeetings: number; usedMeetings: number; ppm: number; className: string }>();
|
||
for (const en of student.enrollments) {
|
||
const paid = student.payments
|
||
.filter((p) => p.classId === en.classId && p.status !== "REFUNDED")
|
||
.reduce((a, p) => a + p.meetingsPaid, 0);
|
||
const used = student.attendances.filter(
|
||
(a) => a.session.classId === en.classId && (a.status === "PRESENT" || a.status === "LATE"),
|
||
).length;
|
||
prorataByClass.set(en.classId, {
|
||
paidMeetings: paid,
|
||
usedMeetings: used,
|
||
ppm: getPricePerMeeting(en.class),
|
||
className: en.class.name,
|
||
});
|
||
}
|
||
|
||
const latestPlacement = student.placementTests[0];
|
||
|
||
const attendanceCount = student.attendances.length;
|
||
const presentCount = student.attendances.filter((a) => a.status === "PRESENT" || a.status === "LATE").length;
|
||
const attendanceRate = attendanceCount ? Math.round((presentCount / attendanceCount) * 100) : 0;
|
||
|
||
const sourceMeta = LEAD_SOURCE_META[student.leadSource];
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header profil */}
|
||
<div className="animate-fade-up overflow-hidden rounded-[24px] border border-ink-100 bg-surface shadow-[var(--shadow-sm)]">
|
||
<div className="grad-primary h-24" />
|
||
<div className="px-5 pb-5 sm:px-7">
|
||
<div className="-mt-10 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||
<div className="flex items-end gap-4">
|
||
<Avatar src={student.photoUrl} name={student.fullName} size={84} ring className="border-4 border-surface" />
|
||
<div className="pb-0.5">
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<h1 className="text-[22px] font-extrabold text-ink-900">{student.fullName}</h1>
|
||
<StatusBadge status={student.status} />
|
||
{latestPlacement ? <LevelBadge level={latestPlacement.levelResult} /> : null}
|
||
</div>
|
||
<div className="mt-1 flex flex-wrap items-center gap-x-4 gap-y-1 text-[12.5px] text-ink-500">
|
||
<span className="flex items-center gap-1.5">
|
||
<Phone className="size-3.5" /> {student.phone}
|
||
</span>
|
||
{student.email ? (
|
||
<span className="flex items-center gap-1.5">
|
||
<Mail className="size-3.5" /> {student.email}
|
||
</span>
|
||
) : null}
|
||
<span className="flex items-center gap-1.5">
|
||
<UserRound className="size-3.5" /> Ortu: {student.parentName ?? "-"}
|
||
</span>
|
||
<SourceChip source={student.leadSource} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<a href={waLink(student.phone)} target="_blank" rel="noreferrer">
|
||
<Button variant="secondary">
|
||
<MessageCircle className="size-4" /> WhatsApp
|
||
</Button>
|
||
</a>
|
||
<a href={`tel:${student.phone}`}>
|
||
<Button variant="outline">
|
||
<Phone className="size-4" /> Telepon
|
||
</Button>
|
||
</a>
|
||
<Link href={`/students/${student.id}/edit`}>
|
||
<Button variant="outline">
|
||
<Pencil className="size-4" /> Edit
|
||
</Button>
|
||
</Link>
|
||
{user.role === "ADMIN" ? (
|
||
<form action={archiveStudentAction}>
|
||
<input type="hidden" name="id" value={student.id} />
|
||
<Button variant="ghost" className="text-danger-500 hover:bg-danger-100">
|
||
Arsipkan
|
||
</Button>
|
||
</form>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stat mini */}
|
||
<div className="mt-5 grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||
{[
|
||
{ label: "Kehadiran", value: `${attendanceRate}%`, icon: CalendarCheck, color: "#12B886" },
|
||
{ label: "Pertemuan hadir", value: `${presentCount}/${attendanceCount}`, icon: GraduationCap, color: "#7C5CFC" },
|
||
{ label: "Kelas aktif", value: student.enrollments.filter((e) => e.status === "ACTIVE").length, icon: LayoutList, color: "#2AA9FF" },
|
||
{ label: "Total pembayaran", value: formatIDR(student.payments.filter((p) => p.status === "PAID").reduce((a, p) => a + p.amount, 0)), icon: Wallet, color: "#FFB020" },
|
||
].map((s) => (
|
||
<div key={s.label} className="flex items-center gap-3 rounded-[14px] border border-ink-100 bg-canvas px-3 py-2.5">
|
||
<span className="flex size-9 items-center justify-center rounded-[10px]" style={{ backgroundColor: `${s.color}1a`, color: s.color }}>
|
||
<s.icon className="size-[18px]" />
|
||
</span>
|
||
<div className="min-w-0">
|
||
<p className="truncate font-num text-[15px] font-bold text-ink-900">{s.value}</p>
|
||
<p className="truncate text-[11px] text-ink-500">{s.label}</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tab nav */}
|
||
<div className="flex gap-1 overflow-x-auto rounded-[16px] border border-ink-100 bg-surface p-1.5">
|
||
{TABS.map((t) => {
|
||
const Icon = t.icon;
|
||
const active = tab === t.key;
|
||
return (
|
||
<Link
|
||
key={t.key}
|
||
href={`/students/${student.id}?tab=${t.key}`}
|
||
className={cn(
|
||
"flex shrink-0 items-center gap-2 rounded-[12px] px-3.5 py-2 text-[13px] font-medium transition-colors",
|
||
active ? "grad-primary text-white" : "text-ink-700 hover:bg-brand-50 hover:text-brand-700",
|
||
)}
|
||
>
|
||
<Icon className="size-4" />
|
||
{t.label}
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* ── TAB: RINGKASAN ─────────────────────────────── */}
|
||
{tab === "ringkasan" ? (
|
||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||
<Card className="lg:col-span-1">
|
||
<CardHeader>
|
||
<CardTitle>Informasi</CardTitle>
|
||
</CardHeader>
|
||
<CardBody className="space-y-3 text-[13px]">
|
||
<Info label="No. telepon" value={student.phone} />
|
||
<Info label="Email" value={student.email ?? "-"} />
|
||
<Info label="Orang tua" value={student.parentName ?? "-"} />
|
||
<Info label="Telepon orang tua" value={student.parentPhone ?? "-"} />
|
||
<Info label="Alamat" value={student.address ?? "-"} />
|
||
<Info label="Tahu dari" value={sourceMeta?.label ?? student.leadSource} />
|
||
<Info label="Bergabung" value={formatDateID(student.joinedAt ?? student.createdAt)} />
|
||
{student.notes ? (
|
||
<div className="rounded-[12px] bg-amber-100 px-3 py-2.5">
|
||
<p className="text-[11px] font-semibold uppercase text-amber-500">Catatan</p>
|
||
<p className="mt-0.5 text-ink-700">{student.notes}</p>
|
||
</div>
|
||
) : null}
|
||
</CardBody>
|
||
</Card>
|
||
|
||
<Card className="lg:col-span-2">
|
||
<CardHeader>
|
||
<CardTitle>Kelas & Kuota Sesi</CardTitle>
|
||
<Link href={`/students/${student.id}?tab=kelas`} className="text-[13px] font-semibold text-brand-600 hover:underline">
|
||
Detail
|
||
</Link>
|
||
</CardHeader>
|
||
<CardBody className="space-y-3 pt-0">
|
||
{student.enrollments.length === 0 ? (
|
||
<EmptyState title="Belum terdaftar di kelas mana pun" description="Tambahkan kelas lewat tombol Edit." />
|
||
) : (
|
||
student.enrollments.map((en) => {
|
||
const p = prorataByClass.get(en.classId)!;
|
||
const remaining = Math.max(0, p.paidMeetings - p.usedMeetings);
|
||
const progress = p.paidMeetings ? Math.min(100, Math.round((p.usedMeetings / p.paidMeetings) * 100)) : 0;
|
||
return (
|
||
<div key={en.id} className="rounded-[16px] border border-ink-100 p-4">
|
||
<div className="flex items-center justify-between gap-2">
|
||
<div className="flex items-center gap-3">
|
||
<span className="size-9 rounded-[10px]" style={{ backgroundColor: `${en.class.color ?? "#7C5CFC"}1a` }} />
|
||
<div>
|
||
<p className="text-[14px] font-semibold text-ink-900">{en.class.name}</p>
|
||
<p className="text-[12px] text-ink-500">{en.class.schedule ?? "-"}</p>
|
||
</div>
|
||
</div>
|
||
<StatusBadge status={en.status === "ACTIVE" ? "STUDENT" : "EX_STUDENT"} />
|
||
</div>
|
||
<div className="mt-3">
|
||
<div className="mb-1 flex justify-between text-[12px] text-ink-500">
|
||
<span>
|
||
Terpakai <b className="text-ink-900">{p.usedMeetings}</b> / <b className="text-ink-900">{p.paidMeetings}</b> sesi
|
||
</span>
|
||
<span>Sisa <b className="text-ink-900">{remaining}</b> sesi</span>
|
||
</div>
|
||
<div className="h-2 overflow-hidden rounded-full bg-ink-100">
|
||
<div className="h-full rounded-full" style={{ width: `${progress}%`, backgroundColor: en.class.color ?? "#7C5CFC" }} />
|
||
</div>
|
||
<p className="mt-2 text-[12px] text-ink-500">
|
||
Nilai sisa: <span className="font-semibold text-ink-900">{formatIDR(remaining * p.ppm)}</span>
|
||
{" "}· {formatIDR(p.ppm)}/pertemuan
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
})
|
||
)}
|
||
</CardBody>
|
||
</Card>
|
||
</div>
|
||
) : null}
|
||
|
||
{/* ── TAB: KELAS & KEHADIRAN ─────────────────────── */}
|
||
{tab === "kelas" ? (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>Riwayat Kehadiran</CardTitle>
|
||
<span className="text-[12px] text-ink-500">{attendanceCount} catatan</span>
|
||
</CardHeader>
|
||
<CardBody className="pt-0">
|
||
{student.attendances.length === 0 ? (
|
||
<EmptyState icon={<CalendarCheck className="size-6" />} title="Belum ada catatan kehadiran" />
|
||
) : (
|
||
<div className="divide-y divide-ink-100">
|
||
{student.attendances.slice(0, 30).map((a) => (
|
||
<div key={a.id} className="flex items-center gap-3 py-2.5">
|
||
<span className="font-num text-[13px] font-semibold text-ink-700">
|
||
#{a.session.meetingNumber}
|
||
</span>
|
||
<div className="min-w-0 flex-1">
|
||
<p className="truncate text-[13.5px] font-medium text-ink-900">{a.session.topic ?? "Pertemuan"}</p>
|
||
<p className="text-[12px] text-ink-500">{formatDateID(a.session.date, "EEE, d MMM yyyy")}</p>
|
||
</div>
|
||
{a.note ? <span className="text-[12px] text-ink-500">{a.note}</span> : null}
|
||
<AttendancePill status={a.status} />
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardBody>
|
||
</Card>
|
||
) : null}
|
||
|
||
{/* ── TAB: PEMBAYARAN ────────────────────────────── */}
|
||
{tab === "pembayaran" ? (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>Riwayat Pembayaran</CardTitle>
|
||
<Link href={`/payments/new?studentId=${student.id}`}>
|
||
<Button size="sm">
|
||
<Wallet className="size-4" /> Catat Pembayaran
|
||
</Button>
|
||
</Link>
|
||
</CardHeader>
|
||
<CardBody className="pt-0">
|
||
{student.payments.length === 0 ? (
|
||
<EmptyState icon={<Wallet className="size-6" />} title="Belum ada pembayaran" />
|
||
) : (
|
||
<div className="space-y-3">
|
||
{student.payments.map((p) => (
|
||
<div key={p.id} className="flex flex-wrap items-center gap-3 rounded-[14px] border border-ink-100 p-3.5">
|
||
<div className="min-w-0 flex-1">
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<span className="font-mono text-[12px] text-ink-500">{p.invoiceNumber}</span>
|
||
<PaymentStatusBadge status={p.status} />
|
||
</div>
|
||
<p className="mt-1 text-[14px] font-semibold text-ink-900">
|
||
{formatIDR(p.amount)}{" "}
|
||
<span className="text-[12px] font-normal text-ink-500">
|
||
= {p.meetingsPaid} pertemuan × {formatIDR(p.pricePerMeeting)}
|
||
</span>
|
||
</p>
|
||
<p className="text-[12px] text-ink-500">
|
||
{p.class.name} · {formatDateID(p.paidAt)}
|
||
</p>
|
||
</div>
|
||
<MethodChip method={p.method} />
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardBody>
|
||
</Card>
|
||
) : null}
|
||
|
||
{/* ── TAB: PLACEMENT TEST ────────────────────────── */}
|
||
{tab === "placement" ? (
|
||
<div className="space-y-4">
|
||
{student.placementTests.length === 0 ? (
|
||
<EmptyState icon={<Target className="size-6" />} title="Belum ada hasil placement test" description="Input hasil tes penempatan di menu Placement Test." />
|
||
) : (
|
||
student.placementTests.map((pt) => (
|
||
<Card key={pt.id}>
|
||
<CardHeader>
|
||
<div className="flex items-center gap-3">
|
||
<span className="grad-primary flex size-10 items-center justify-center rounded-[12px] text-white">
|
||
<Target className="size-5" />
|
||
</span>
|
||
<div>
|
||
<CardTitle>Hasil Placement Test</CardTitle>
|
||
<p className="text-[12px] text-ink-500">
|
||
{formatDateID(pt.testDate)} · Pemeriksa {pt.examiner?.name ?? "-"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="font-num text-[26px] font-bold text-ink-900">{pt.totalScore}</p>
|
||
<LevelBadge level={pt.levelResult} />
|
||
</div>
|
||
</CardHeader>
|
||
<CardBody className="pt-0">
|
||
<div className="space-y-2.5">
|
||
{pt.scores.map((s) => (
|
||
<div key={s.id}>
|
||
<div className="mb-1 flex justify-between text-[12.5px]">
|
||
<span className="font-medium text-ink-700">{s.category}</span>
|
||
<span className="font-num font-semibold text-ink-900">{s.score}</span>
|
||
</div>
|
||
<div className="h-2 overflow-hidden rounded-full bg-ink-100">
|
||
<div className="grad-primary h-full rounded-full" style={{ width: `${s.score}%` }} />
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{pt.recommendedClassId ? (
|
||
<div className="mt-4 rounded-[12px] bg-brand-50 px-3 py-2.5 text-[13px] text-brand-700">
|
||
Rekomendasi kelas:{" "}
|
||
<b>{classes.find((c) => c.id === pt.recommendedClassId)?.name ?? "-"}</b>
|
||
</div>
|
||
) : null}
|
||
{pt.notes ? <p className="mt-3 text-[12.5px] text-ink-500">{pt.notes}</p> : null}
|
||
</CardBody>
|
||
</Card>
|
||
))
|
||
)}
|
||
</div>
|
||
) : null}
|
||
|
||
{/* ── TAB: RAPORT ────────────────────────────────── */}
|
||
{tab === "raport" ? (
|
||
<div className="space-y-4">
|
||
{student.reportCards.length === 0 ? (
|
||
<EmptyState icon={<FileText className="size-6" />} title="Belum ada raport" />
|
||
) : (
|
||
student.reportCards.map((rc) => (
|
||
<Card key={rc.id}>
|
||
<CardHeader>
|
||
<div>
|
||
<CardTitle>{rc.class.name}</CardTitle>
|
||
<p className="text-[12px] text-ink-500">
|
||
{rc.period} · {rc.teacher?.name ?? "-"}
|
||
</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="font-num text-[26px] font-bold text-ink-900">{rc.finalScore}</p>
|
||
<Badge bg="#E4F8F1" fg="#0A7A58">
|
||
Grade {rc.grade ?? gradeFromScore(rc.finalScore ?? 0)}
|
||
</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardBody className="pt-0">
|
||
<div className="grid grid-cols-2 gap-2.5 sm:grid-cols-3">
|
||
{rc.scores.map((s) => (
|
||
<div key={s.id} className="rounded-[12px] bg-canvas px-3 py-2.5">
|
||
<p className="text-[12px] text-ink-500">{s.aspect}</p>
|
||
<p className="font-num text-[16px] font-bold text-ink-900">{s.score}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{rc.teacherNotes ? (
|
||
<p className="mt-3 rounded-[12px] bg-canvas px-3 py-2.5 text-[13px] text-ink-700">
|
||
“{rc.teacherNotes}”
|
||
</p>
|
||
) : null}
|
||
</CardBody>
|
||
</Card>
|
||
))
|
||
)}
|
||
</div>
|
||
) : null}
|
||
|
||
{/* ── TAB: JURNAL ────────────────────────────────── */}
|
||
{tab === "jurnal" ? (
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||
{student.journalStudents.length === 0 ? (
|
||
<div className="sm:col-span-2">
|
||
<EmptyState icon={<NotebookPen className="size-6" />} title="Belum ada jurnal untuk siswa ini" />
|
||
</div>
|
||
) : (
|
||
student.journalStudents.map((js) => (
|
||
<Card key={js.journalId} className="overflow-hidden">
|
||
{js.journal.photos[0] ? (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img src={js.journal.photos[0].url} alt={js.journal.title} className="h-40 w-full object-cover" />
|
||
) : (
|
||
<div className="grad-pink flex h-40 items-center justify-center text-white">
|
||
<NotebookPen className="size-8 opacity-80" />
|
||
</div>
|
||
)}
|
||
<CardBody className="pt-3">
|
||
<p className="text-[11px] font-medium text-ink-500">{js.journal.class.name}</p>
|
||
<h4 className="mt-0.5 text-[15px] font-semibold text-ink-900">{js.journal.title}</h4>
|
||
<p className="mt-1 line-clamp-2 text-[13px] text-ink-500">{js.journal.description}</p>
|
||
<p className="mt-2 text-[12px] text-ink-500">{formatRelativeID(js.journal.activityDate)}</p>
|
||
</CardBody>
|
||
</Card>
|
||
))
|
||
)}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Info({ label, value }: { label: string; value: string }) {
|
||
return (
|
||
<div className="flex items-start justify-between gap-3 border-b border-ink-100 pb-2.5 last:border-0">
|
||
<span className="shrink-0 text-ink-500">{label}</span>
|
||
<span className="text-right font-medium text-ink-900">{value}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function AttendancePill({ status }: { status: string }) {
|
||
const meta: Record<string, { label: string; bg: string; text: string }> = {
|
||
PRESENT: { label: "Hadir", bg: "#E4F8F1", text: "#0A7A58" },
|
||
LATE: { label: "Terlambat", bg: "#FFF4E0", text: "#9A6100" },
|
||
EXCUSED: { label: "Izin", bg: "#E3F3FF", text: "#0B6FB8" },
|
||
SICK: { label: "Sakit", bg: "#F1ECFE", text: "#5B34C9" },
|
||
ABSENT: { label: "Alpa", bg: "#FEE4E2", text: "#B42318" },
|
||
};
|
||
const m = meta[status] ?? { label: status, bg: "#EEF1F7", text: "#4A5470" };
|
||
return (
|
||
<Badge bg={m.bg} fg={m.text}>
|
||
{m.label}
|
||
</Badge>
|
||
);
|
||
}
|