Files
star-erp/resources/js/Pages/Admin/ActivityLog/Index.tsx
sky121113 74417e2e31
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 56s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
style: 統一所有表格標題樣式為一般粗細並修正排序功能
2026-01-19 09:30:02 +08:00

248 lines
10 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, ArrowUpDown, ArrowUp, ArrowDown } 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;
sort_by?: string;
sort_order?: 'asc' | 'desc';
};
}
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'),
{ ...filters, per_page: value },
{ preserveState: false, replace: true, preserveScroll: true }
);
};
const handleSort = (field: string) => {
let newSortBy: string | undefined = field;
let newSortOrder: 'asc' | 'desc' | undefined = 'asc';
if (filters.sort_by === field) {
if (filters.sort_order === 'asc') {
newSortOrder = 'desc';
} else {
newSortBy = undefined;
newSortOrder = undefined;
}
}
router.get(
route('activity-logs.index'),
{ ...filters, sort_by: newSortBy, sort_order: newSortOrder },
{ preserveState: true, replace: true }
);
};
const SortIcon = ({ field }: { field: string }) => {
if (filters.sort_by !== field) {
return <ArrowUpDown className="h-4 w-4 text-muted-foreground ml-1" />;
}
if (filters.sort_order === "asc") {
return <ArrowUp className="h-4 w-4 text-primary-main ml-1" />;
}
return <ArrowDown className="h-4 w-4 text-primary-main ml-1" />;
};
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-[50px] text-center">#</TableHead>
<TableHead className="w-[180px]">
<button
onClick={() => handleSort('created_at')}
className="flex items-center gap-1 hover:text-gray-900 transition-colors"
>
<SortIcon field="created_at" />
</button>
</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, index) => (
<TableRow key={activity.id}>
<TableCell className="text-gray-500 font-medium text-center">
{activities.from + index}
</TableCell>
<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-primary"
title="檢視詳情"
>
<Eye className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={7} 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>
);
}