feat: 實作使用者管理與公共事業費分頁標準化
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 50s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped

This commit is contained in:
2026-01-20 15:53:15 +08:00
parent 74728c47b9
commit 89183ca124
6 changed files with 207 additions and 31 deletions

View File

@@ -339,21 +339,32 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u
from={activities.from}
/>
<div className="mt-6">
<Pagination
links={activities.links}
total={activities.total}
current_page={activities.current_page}
last_page={activities.last_page}
per_page={perPage}
onPerPageChange={handlePerPageChange}
/>
<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
isOpen={detailOpen}
onClose={() => setDetailOpen(false)}
open={detailOpen}
onOpenChange={setDetailOpen}
activity={selectedActivity}
/>
</AuthenticatedLayout>

View File

@@ -1,7 +1,9 @@
import { useState } from 'react';
import { useState, useCallback } from 'react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, router } from '@inertiajs/react';
import { Users, Plus, Pencil, Trash2, Mail, Shield, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
import { Users, Plus, Pencil, Trash2, Mail, Shield, ArrowUpDown, ArrowUp, ArrowDown, Search, X } from 'lucide-react';
import { Input } from "@/Components/ui/input";
import { debounce } from "lodash";
import { Button } from '@/Components/ui/button';
import {
Table,
@@ -58,11 +60,16 @@ interface Props {
per_page?: string;
sort_by?: string;
sort_order?: 'asc' | 'desc';
search?: string;
role?: string;
};
roles: Role[];
}
export default function UserIndex({ users, filters }: Props) {
export default function UserIndex({ users, roles, filters }: Props) {
const [perPage, setPerPage] = useState<string>(filters.per_page || "10");
const [searchTerm, setSearchTerm] = useState(filters.search || "");
const [roleFilter, setRoleFilter] = useState<string>(filters.role || "all");
const [deleteId, setDeleteId] = useState<number | null>(null);
const [deleteName, setDeleteName] = useState<string>('');
const [modelOpen, setModelOpen] = useState(false);
@@ -88,11 +95,46 @@ export default function UserIndex({ users, filters }: Props) {
setPerPage(value);
router.get(
route('users.index'),
{ ...filters, per_page: value },
{ ...filters, per_page: value, search: searchTerm, role: roleFilter },
{ preserveState: false, replace: true, preserveScroll: true }
);
};
// Debounced Search Handler
const debouncedSearch = useCallback(
debounce((term: string, role: string) => {
router.get(
route('users.index'),
{ ...filters, search: term, role: role },
{ preserveState: true, replace: true, preserveScroll: true }
);
}, 500),
[]
);
const handleSearchChange = (term: string) => {
setSearchTerm(term);
debouncedSearch(term, roleFilter);
};
const handleRoleChange = (value: string) => {
setRoleFilter(value);
router.get(
route('users.index'),
{ ...filters, search: searchTerm, role: value },
{ preserveState: false, replace: true, preserveScroll: true }
);
};
const handleClearSearch = () => {
setSearchTerm("");
router.get(
route('users.index'),
{ ...filters, search: "", role: roleFilter },
{ preserveState: true, replace: true, preserveScroll: true }
);
};
const handleSort = (field: string) => {
let newSortBy: string | undefined = field;
let newSortOrder: 'asc' | 'desc' | undefined = 'asc';
@@ -145,14 +187,54 @@ export default function UserIndex({ users, filters }: Props) {
使
</p>
</div>
<Can permission="users.create">
<Link href={route('users.create')}>
<Button className="button-filled-primary">
<Plus className="h-4 w-4 mr-2" />
使
</Button>
</Link>
</Can>
</div>
{/* Toolbar */}
<div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
<div className="flex flex-col md:flex-row gap-4">
{/* Search */}
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="搜尋姓名、Email、帳號..."
value={searchTerm}
onChange={(e) => handleSearchChange(e.target.value)}
className="pl-10 pr-10"
/>
{searchTerm && (
<button
onClick={handleClearSearch}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
<X className="h-4 w-4" />
</button>
)}
</div>
{/* Role Filter */}
<SearchableSelect
value={roleFilter}
onValueChange={handleRoleChange}
options={[
{ label: "全部角色", value: "all" },
...roles.map((role) => ({ label: role.display_name, value: role.id.toString() }))
]}
placeholder="角色篩選"
className="w-full md:w-[180px]"
/>
{/* Action Buttons */}
<div className="flex gap-2 w-full md:w-auto">
<Can permission="users.create">
<Link href={route('users.create')} className="w-full md:w-auto">
<Button className="w-full md:w-auto button-filled-primary">
<Plus className="h-4 w-4 mr-2" />
使
</Button>
</Link>
</Can>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">