105 lines
4.6 KiB
TypeScript
105 lines
4.6 KiB
TypeScript
import { ClipboardListIcon } from "lucide-react";
|
|
import { redirect } from "next/navigation";
|
|
import { connection } from "next/server";
|
|
|
|
import { AdminHeader } from "@/components/admin/admin-header";
|
|
import { AuditLogFilters } from "@/components/admin/audit-log-filters";
|
|
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty";
|
|
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
|
|
import { listAuditActors, listAuditLogs } from "@/lib/audit-log";
|
|
import { getAdminSession } from "@/lib/auth/server";
|
|
|
|
function pageHref(page: number, actorId?: string) {
|
|
const query = new URLSearchParams({ page: String(page) });
|
|
if (actorId) query.set("actor", actorId);
|
|
return `/admin/activity?${query}`;
|
|
}
|
|
|
|
export default async function AuditLogPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
}) {
|
|
await connection();
|
|
if (!(await getAdminSession())) redirect("/auth/login?next=%2Fadmin");
|
|
|
|
const query = await searchParams;
|
|
const rawPage = Array.isArray(query.page) ? query.page[0] : query.page;
|
|
const parsedPage = Number(rawPage);
|
|
const page = Number.isSafeInteger(parsedPage) && parsedPage > 0 ? parsedPage : 1;
|
|
const rawActorId = Array.isArray(query.actor) ? query.actor[0] : query.actor;
|
|
const actorId = rawActorId && rawActorId.length <= 128 ? rawActorId : undefined;
|
|
const [{ events, pageCount, total }, actors] = await Promise.all([
|
|
listAuditLogs({ page, pageSize: 100, actorId }),
|
|
listAuditActors(),
|
|
]);
|
|
const dateTime = new Intl.DateTimeFormat("th-TH", {
|
|
dateStyle: "medium",
|
|
timeStyle: "medium",
|
|
timeZone: "Asia/Bangkok",
|
|
});
|
|
|
|
return (
|
|
<div className="min-h-svh">
|
|
<AdminHeader />
|
|
<main className="mx-auto flex w-full max-w-5xl flex-col gap-6 p-4 sm:p-6 lg:p-8">
|
|
<header>
|
|
<p className="text-sm font-medium text-primary">Admin audit log</p>
|
|
<h1 className="font-heading text-3xl font-semibold">Logs</h1>
|
|
<p className="mt-2 text-muted-foreground">
|
|
บันทึกการทำงานของผู้ดูแล {total.toLocaleString("th-TH")} รายการ
|
|
</p>
|
|
</header>
|
|
<AuditLogFilters key={actorId ?? "all"} actors={actors} initialActorId={actorId} />
|
|
{events.length ? (
|
|
<>
|
|
<div className="overflow-hidden rounded-xl border">
|
|
{events.map((event) => (
|
|
<article className="border-b px-4 py-3 last:border-b-0 hover:bg-muted/40" key={event.id}>
|
|
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
|
<span className="font-semibold">{event.author ?? "System"}</span>
|
|
<span>{event.text}</span>
|
|
<time className="text-xs text-muted-foreground">{dateTime.format(event.time)}</time>
|
|
</div>
|
|
{event.details && Object.keys(event.details).length ? (
|
|
<details className="mt-2">
|
|
<summary className="cursor-pointer text-sm text-muted-foreground">รายละเอียด</summary>
|
|
<pre className="mt-2 max-h-96 overflow-auto rounded-lg bg-muted p-3 text-xs">
|
|
{JSON.stringify(event.details, null, 2)}
|
|
</pre>
|
|
</details>
|
|
) : null}
|
|
</article>
|
|
))}
|
|
</div>
|
|
{pageCount > 1 ? (
|
|
<Pagination>
|
|
<PaginationContent>
|
|
{page > 1 ? (
|
|
<PaginationItem>
|
|
<PaginationPrevious href={pageHref(page - 1, actorId)} text="ก่อนหน้า" />
|
|
</PaginationItem>
|
|
) : null}
|
|
{page < pageCount ? (
|
|
<PaginationItem>
|
|
<PaginationNext href={pageHref(page + 1, actorId)} text="ถัดไป" />
|
|
</PaginationItem>
|
|
) : null}
|
|
</PaginationContent>
|
|
</Pagination>
|
|
) : null}
|
|
</>
|
|
) : (
|
|
<Empty className="border">
|
|
<EmptyHeader>
|
|
<EmptyMedia variant="icon"><ClipboardListIcon /></EmptyMedia>
|
|
<EmptyTitle>ยังไม่มี Log</EmptyTitle>
|
|
<EmptyDescription>Log จะแสดงหลังจากผู้ดูแลแก้ไขข้อมูล</EmptyDescription>
|
|
</EmptyHeader>
|
|
</Empty>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|