Files
star-erp/resources/js/Pages/Admin/ActivityLog/Index.tsx
sky121113 299cf37054 fix: 修正全系統側邊欄捲軸重置問題
在所有報表與管理頁面的 router.get 調用中加入 preserveScroll: true。
受影響模組包括:
- 財務管理 (會計報表、公用事業費)
- 庫存管理 (庫存查詢、倉庫管理、進貨、調整、調撥)
- 生產管理 (工單管理、配方管理)
- 採購管理 (採購單)
- 銷售與發貨管理 (銷售單、發貨單、匯入管理)
- 系統管理 (使用者、角色、操作紀錄)
2026-02-25 14:04:22 +08:00

352 lines
16 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 Pagination from '@/Components/shared/Pagination';
import { SearchableSelect } from "@/Components/ui/searchable-select";
import { FileText, Search, RotateCcw, Calendar } from 'lucide-react';
import LogTable, { Activity } from '@/Components/ActivityLog/LogTable';
import ActivityDetailDialog from '@/Components/ActivityLog/ActivityDetailDialog';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { getDateRange } from "@/utils/format";
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';
search?: string;
date_start?: string;
date_end?: string;
event?: string;
subject_type?: string;
causer_id?: string;
};
subject_types: { label: string; value: string }[];
users: { label: string; value: string }[];
}
export default function ActivityLogIndex({ activities, filters, subject_types, users }: Props) {
const [perPage, setPerPage] = useState<string>(filters.per_page || "10");
const [selectedActivity, setSelectedActivity] = useState<Activity | null>(null);
const [detailOpen, setDetailOpen] = useState(false);
// Filter States
const [search, setSearch] = useState(filters.search || '');
const [dateStart, setDateStart] = useState(filters.date_start || '');
const [dateEnd, setDateEnd] = useState(filters.date_end || '');
const [event, setEvent] = useState(filters.event || 'all');
const [subjectType, setSubjectType] = useState(filters.subject_type || 'all');
const [causer, setCauser] = useState(filters.causer_id || 'all');
const [dateRangeType, setDateRangeType] = useState('custom');
const handleDateRangeChange = (type: string) => {
setDateRangeType(type);
if (type === 'custom') return;
const { start, end } = getDateRange(type);
setDateStart(start);
setDateEnd(end);
};
const handleFilter = () => {
router.get(
route('activity-logs.index'),
{
...filters,
search: search,
date_start: dateStart,
date_end: dateEnd,
event: event === 'all' ? undefined : event,
subject_type: subjectType === 'all' ? undefined : subjectType,
causer_id: causer === 'all' ? undefined : causer,
page: 1 // Reset to first page on filter
},
{ preserveState: true, replace: true, preserveScroll: true }
);
};
const handleReset = () => {
setSearch('');
setDateStart('');
setDateEnd('');
setEvent('all');
setSubjectType('all');
setCauser('all');
setDateRangeType('custom');
router.get(
route('activity-logs.index'),
{ per_page: perPage, sort_by: filters.sort_by, sort_order: filters.sort_order },
{ preserveState: true, replace: true, preserveScroll: true }
);
};
const handleViewDetail = (activity: Activity) => {
setSelectedActivity(activity);
setDetailOpen(true);
};
const handlePerPageChange = (value: string) => {
setPerPage(value);
router.get(
route('activity-logs.index'),
{ ...filters, per_page: value, page: 1 },
{ 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, 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 shadow-sm border border-grey-4 p-5 mb-6">
<div className="space-y-4">
{/* Top Config: Date Range & Quick Buttons */}
<div className="flex flex-col lg:flex-row gap-4 lg:items-end">
<div className="flex-none space-y-2">
<Label className="text-xs font-medium text-grey-2"></Label>
<div className="flex flex-wrap gap-2">
{[
{ label: "今日", value: "today" },
{ label: "昨日", value: "yesterday" },
{ label: "本週", value: "this_week" },
{ label: "本月", value: "this_month" },
{ label: "上月", value: "last_month" },
].map((opt) => (
<Button
key={opt.value}
size="sm"
onClick={() => handleDateRangeChange(opt.value)}
className={
dateRangeType === opt.value
? 'button-filled-primary h-9 px-4 shadow-sm'
: 'button-outlined-primary h-9 px-4 bg-white'
}
>
{opt.label}
</Button>
))}
</div>
</div>
{/* Date Inputs */}
<div className="w-full lg:flex-1">
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs text-grey-2 font-medium"></Label>
<div className="relative">
<Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
<Input
type="date"
value={dateStart}
onChange={(e) => {
setDateStart(e.target.value);
setDateRangeType('custom');
}}
className="pl-9 block w-full h-9 bg-white"
/>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs text-grey-2 font-medium"></Label>
<div className="relative">
<Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
<Input
type="date"
value={dateEnd}
onChange={(e) => {
setDateEnd(e.target.value);
setDateRangeType('custom');
}}
className="pl-9 block w-full h-9 bg-white"
/>
</div>
</div>
</div>
</div>
</div>
{/* Detailed Filters row */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 items-end">
{/* 事件類型 */}
<div className="md:col-span-2 space-y-1">
<Label className="text-xs font-medium text-grey-2"></Label>
<Select value={event} onValueChange={setEvent}>
<SelectTrigger className="h-9 bg-white">
<SelectValue placeholder="所有事件" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="created"> (Created)</SelectItem>
<SelectItem value="updated"> (Updated)</SelectItem>
<SelectItem value="deleted"> (Deleted)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 操作對象 */}
<div className="md:col-span-2 space-y-1">
<Label className="text-xs font-medium text-grey-2"></Label>
<SearchableSelect
value={subjectType}
onValueChange={setSubjectType}
options={[
{ label: "所有對象", value: "all" },
...subject_types
]}
placeholder="選擇對象"
className="w-full h-9 bg-white"
/>
</div>
{/* 操作人員 */}
<div className="md:col-span-2 space-y-1">
<Label className="text-xs font-medium text-grey-2"></Label>
<SearchableSelect
value={causer}
onValueChange={setCauser}
options={[
{ label: "所有人員", value: "all" },
...users
]}
placeholder="選擇人員"
className="w-full h-9 bg-white"
/>
</div>
{/* 關鍵字搜尋 */}
<div className="md:col-span-3 space-y-1">
<Label className="text-xs font-medium text-grey-2"></Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="搜尋內容..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 h-9 block bg-white"
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
/>
</div>
</div>
{/* Action Buttons Integrated */}
<div className="md:col-span-3 flex items-center gap-2">
<Button
variant="outline"
onClick={handleReset}
className="flex-1 items-center gap-2 button-outlined-primary h-9"
>
<RotateCcw className="h-4 w-4" />
</Button>
<Button
onClick={handleFilter}
className="flex-1 button-filled-primary h-9 gap-2 shadow-sm"
>
<Search className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
<LogTable
activities={activities.data}
sortField={filters.sort_by}
sortOrder={filters.sort_order}
onSort={handleSort}
onViewDetail={handleViewDetail}
from={activities.from}
/>
<div className="mt-4 flex flex-col md: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-[100px] h-8"
showSearch={false}
/>
<span></span>
</div>
<div className="w-full md:w-auto flex justify-center md:justify-end">
<Pagination links={activities.links} />
</div>
</div>
</div>
<ActivityDetailDialog
open={detailOpen}
onOpenChange={setDetailOpen}
activity={selectedActivity}
/>
</AuthenticatedLayout>
);
}