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

@@ -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">