feat: 實作使用者管理與公共事業費分頁標準化
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user