feat : add audit logs
CI / Verify (push) Successful in 1m28s
CI / Build immutable images and deploy (push) Successful in 2m40s

This commit is contained in:
2026-09-27 16:03:53 +07:00 Unverified
parent 094456ecba
commit 8ddb5896b6
18 changed files with 4027 additions and 115 deletions
+10
View File
@@ -8,6 +8,7 @@ import {
ImageIcon,
LayoutDashboardIcon,
BookOpenIcon,
ClipboardListIcon,
LogOutIcon,
PlusIcon,
UserPlusIcon,
@@ -31,6 +32,15 @@ export function AdminHeader() {
<span className="hidden sm:inline">ไกด์เกนชินไม่ใช่เกมมือถือ</span>
</Link>
<nav aria-label="Admin navigation" className="flex items-center gap-1">
<Button
variant={pathname === "/admin/activity" ? "secondary" : "ghost"}
size="sm"
nativeButton={false}
render={<Link href="/admin/activity" />}
>
<ClipboardListIcon data-icon="inline-start" />
<span className="hidden sm:inline">Activity</span>
</Button>
<Button
variant={pathname === "/admin/glossary" ? "secondary" : "ghost"}
size="sm"
+101
View File
@@ -0,0 +1,101 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { FilterIcon, RotateCcwIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const ALL = "all";
export function AuditLogFilters({
actors,
targetTypes,
initialActorId,
initialTargetType,
}: {
actors: Array<{ id: string; label: string }>;
targetTypes: Array<{ value: string; label: string }>;
initialActorId?: string;
initialTargetType?: string;
}) {
const router = useRouter();
const [actorId, setActorId] = useState(initialActorId ?? ALL);
const [targetType, setTargetType] = useState(initialTargetType ?? ALL);
function applyFilters() {
const query = new URLSearchParams();
if (actorId !== ALL) query.set("actor", actorId);
if (targetType !== ALL) query.set("type", targetType);
const suffix = query.toString();
router.push(suffix ? `/admin/activity?${suffix}` : "/admin/activity");
}
return (
<FieldGroup className="rounded-xl border p-4 md:flex-row md:items-end">
<Field>
<FieldLabel htmlFor="audit-actor">ผู้ดูแล</FieldLabel>
<Select value={actorId} onValueChange={(value) => value && setActorId(value)}>
<SelectTrigger id="audit-actor" className="w-full md:w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value={ALL}>ทั้งหมด</SelectItem>
{actors.map((actor) => (
<SelectItem key={actor.id} value={actor.id}>
{actor.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="audit-target-type">ประเภทเป้าหมาย</FieldLabel>
<Select value={targetType} onValueChange={(value) => value && setTargetType(value)}>
<SelectTrigger id="audit-target-type" className="w-full md:w-56">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value={ALL}>ทั้งหมด</SelectItem>
{targetTypes.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
<div className="flex gap-2">
<Button type="button" onClick={applyFilters}>
<FilterIcon data-icon="inline-start" />
กรอง
</Button>
<Button
type="button"
variant="outline"
onClick={() => {
setActorId(ALL);
setTargetType(ALL);
router.push("/admin/activity");
}}
>
<RotateCcwIcon data-icon="inline-start" />
ล้าง
</Button>
</div>
</FieldGroup>
);
}