Files
star-erp/resources/js/Pages/Admin/ActivityLog/Index.tsx
sky121113 0d7bb2758d
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 58s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
feat: 實作操作紀錄與商品分類單位異動紀錄 (Operation Logs for System, Products, Categories, Units)
2026-01-16 17:36:37 +08:00

204 lines
8.5 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.
import { useState } from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, router } from '@inertiajs/react';
import { PageProps } from '@/types/global';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import { Badge } from "@/Components/ui/badge";
import Pagination from '@/Components/shared/Pagination';
import { SearchableSelect } from "@/Components/ui/searchable-select";
import { FileText, Eye } from 'lucide-react';
import { format } from 'date-fns';
import { Button } from '@/Components/ui/button';
import ActivityDetailDialog from './ActivityDetailDialog';
interface Activity {
id: number;
description: string;
subject_type: string;
event: string;
causer: string;
created_at: string;
properties: any;
}
interface PaginationLinks {
url: string | null;
label: string;
active: boolean;
}
interface Props extends PageProps {
activities: {
data: Activity[];
links: PaginationLinks[];
current_page: number;
last_page: number;
total: number;
from: number;
};
filters: {
per_page?: string;
};
}
export default function ActivityLogIndex({ activities, filters }: Props) {
const [perPage, setPerPage] = useState<string>(filters.per_page || "10");
const [selectedActivity, setSelectedActivity] = useState<Activity | null>(null);
const [detailOpen, setDetailOpen] = useState(false);
const getEventBadgeColor = (event: string) => {
switch (event) {
case 'created': return 'bg-green-500 hover:bg-green-600';
case 'updated': return 'bg-blue-500 hover:bg-blue-600';
case 'deleted': return 'bg-red-500 hover:bg-red-600';
default: return 'bg-gray-500 hover:bg-gray-600';
}
};
const getEventLabel = (event: string) => {
switch (event) {
case 'created': return '新增';
case 'updated': return '更新';
case 'deleted': return '刪除';
default: return event;
}
};
const handleViewDetail = (activity: Activity) => {
setSelectedActivity(activity);
setDetailOpen(true);
};
const handlePerPageChange = (value: string) => {
setPerPage(value);
router.get(
route('activity-logs.index'),
{ per_page: value },
{ preserveState: false, replace: true, preserveScroll: true }
);
};
return (
<AuthenticatedLayout
breadcrumbs={[
{ label: '系統管理', href: '#' },
{ label: '操作紀錄', href: route('activity-logs.index'), isPage: true },
]}
>
<Head title="操作紀錄" />
<div className="container mx-auto p-6 max-w-7xl">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<FileText className="h-6 w-6 text-primary-main" />
</h1>
<p className="text-gray-500 mt-1">
</p>
</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-[180px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[100px] text-center"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[100px] text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{activities.data.length > 0 ? (
activities.data.map((activity) => (
<TableRow key={activity.id}>
<TableCell className="text-gray-500 font-medium whitespace-nowrap">
{activity.created_at}
</TableCell>
<TableCell>
<span className="font-medium text-gray-900">{activity.causer}</span>
</TableCell>
<TableCell className="text-center">
<Badge className={getEventBadgeColor(activity.event)}>
{getEventLabel(activity.event)}
</Badge>
</TableCell>
<TableCell>
<Badge variant="outline" className="bg-slate-50">
{activity.subject_type}
</Badge>
</TableCell>
<TableCell className="text-gray-600" title={activity.description}>
<div className="flex items-center gap-2">
<span>{activity.causer}</span>
<span className="text-gray-400"></span>
<span className="font-medium text-gray-700">{activity.description}</span>
<span className="text-gray-400"></span>
</div>
</TableCell>
<TableCell className="text-center">
<Button
variant="outline"
size="sm"
onClick={() => handleViewDetail(activity)}
className="button-outlined-info"
>
<Eye className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center py-8 text-gray-500">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-2 text-sm text-gray-500">
<span></span>
<SearchableSelect
value={perPage}
onValueChange={handlePerPageChange}
options={[
{ label: "10", value: "10" },
{ label: "20", value: "20" },
{ label: "50", value: "50" },
{ label: "100", value: "100" }
]}
className="w-[80px] h-8"
showSearch={false}
/>
<span></span>
</div>
<Pagination links={activities.links} />
</div>
</div>
<ActivityDetailDialog
open={detailOpen}
onOpenChange={setDetailOpen}
activity={selectedActivity}
/>
</AuthenticatedLayout>
);
}