From 89183ca124af758b24a6a6e5b1c01244a6c87639 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 20 Jan 2026 15:53:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=A6=E4=BD=9C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E8=80=85=E7=AE=A1=E7=90=86=E8=88=87=E5=85=AC=E5=85=B1=E4=BA=8B?= =?UTF-8?q?=E6=A5=AD=E8=B2=BB=E5=88=86=E9=A0=81=E6=A8=99=E6=BA=96=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Admin/UserController.php | 22 +++- app/Http/Controllers/UtilityFeeController.php | 4 +- resources/js/Components/shared/Pagination.tsx | 30 ++++- .../js/Pages/Admin/ActivityLog/Index.tsx | 33 ++++-- resources/js/Pages/Admin/User/Index.tsx | 106 ++++++++++++++++-- resources/js/Pages/UtilityFee/Index.tsx | 43 ++++++- 6 files changed, 207 insertions(+), 31 deletions(-) diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 8a41d80..d646092 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -20,9 +20,27 @@ class UserController extends Controller $perPage = $request->input('per_page', 10); $sortBy = $request->input('sort_by', 'id'); $sortOrder = $request->input('sort_order', 'asc'); + $search = $request->input('search'); + $roleId = $request->input('role'); $query = User::with(['roles:id,name,display_name']); + // Handle Search + if ($search) { + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%") + ->orWhere('username', 'like', "%{$search}%"); + }); + } + + // Handle Role Filter + if ($roleId && $roleId !== 'all') { + $query->whereHas('roles', function ($q) use ($roleId) { + $q->where('id', $roleId); + }); + } + // Handle sorting if (in_array($sortBy, ['name', 'created_at'])) { $query->orderBy($sortBy, $sortOrder); @@ -31,10 +49,12 @@ class UserController extends Controller } $users = $query->paginate($perPage)->withQueryString(); + $roles = Role::select('id', 'name', 'display_name')->get(); return Inertia::render('Admin/User/Index', [ 'users' => $users, - 'filters' => $request->only(['per_page', 'sort_by', 'sort_order']), + 'roles' => $roles, + 'filters' => $request->only(['per_page', 'sort_by', 'sort_order', 'search', 'role']), ]); } diff --git a/app/Http/Controllers/UtilityFeeController.php b/app/Http/Controllers/UtilityFeeController.php index 7fdfbec..663b137 100644 --- a/app/Http/Controllers/UtilityFeeController.php +++ b/app/Http/Controllers/UtilityFeeController.php @@ -45,14 +45,14 @@ class UtilityFeeController extends Controller $query->orderBy('created_at', 'desc'); } - $fees = $query->paginate($request->input('per_page', 15))->withQueryString(); + $fees = $query->paginate($request->input('per_page', 10))->withQueryString(); $availableCategories = UtilityFee::distinct()->pluck('category'); return Inertia::render('UtilityFee/Index', [ 'fees' => $fees, 'availableCategories' => $availableCategories, - 'filters' => $request->only(['search', 'category', 'date_start', 'date_end', 'sort_field', 'sort_direction']), + 'filters' => $request->only(['search', 'category', 'date_start', 'date_end', 'sort_field', 'sort_direction', 'per_page']), ]); } diff --git a/resources/js/Components/shared/Pagination.tsx b/resources/js/Components/shared/Pagination.tsx index 63b5396..7a9cbd6 100644 --- a/resources/js/Components/shared/Pagination.tsx +++ b/resources/js/Components/shared/Pagination.tsx @@ -25,6 +25,25 @@ export default function Pagination({ links, className }: PaginationProps) { const isPrevious = label === "Previous"; const isNext = label === "Next"; + const activeIndex = links.findIndex(l => l.active); + + // Tablet/Mobile visibility logic (< md): + // Show: Previous, Next, Active, and up to 2 neighbors (Total ~5 numeric pages) + // Hide others on small screens (hidden md:flex) + // User requested: "small than 800... display 5 pages" + const isVisibleOnTablet = + isPrevious || + isNext || + link.active || + key === activeIndex - 1 || + key === activeIndex + 1 || + key === activeIndex - 2 || + key === activeIndex + 2; + + const baseClasses = cn( + isVisibleOnTablet ? "flex" : "hidden md:flex", + "h-9 items-center justify-center rounded-md border px-3 text-sm" + ); // 如果是 Previous/Next 但沒有 URL,則不渲染(或者渲染為 disabled) if ((isPrevious || isNext) && !link.url) { @@ -32,7 +51,8 @@ export default function Pagination({ links, className }: PaginationProps) {
@@ -49,7 +69,8 @@ export default function Pagination({ links, className }: PaginationProps) { href={link.url} preserveScroll className={cn( - "flex h-9 items-center justify-center rounded-md border px-3 text-sm transition-colors hover:bg-accent hover:text-accent-foreground", + baseClasses, + "transition-colors hover:bg-accent hover:text-accent-foreground", link.active ? "border-primary bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground" : "border-input bg-transparent text-foreground", @@ -63,7 +84,10 @@ export default function Pagination({ links, className }: PaginationProps) { ) : (
diff --git a/resources/js/Pages/Admin/ActivityLog/Index.tsx b/resources/js/Pages/Admin/ActivityLog/Index.tsx index 1449fac..f8a3cab 100644 --- a/resources/js/Pages/Admin/ActivityLog/Index.tsx +++ b/resources/js/Pages/Admin/ActivityLog/Index.tsx @@ -339,21 +339,32 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u from={activities.from} /> -
- +
+
+ 每頁顯示 + + +
+
+ +
setDetailOpen(false)} + open={detailOpen} + onOpenChange={setDetailOpen} activity={selectedActivity} /> diff --git a/resources/js/Pages/Admin/User/Index.tsx b/resources/js/Pages/Admin/User/Index.tsx index dac2aff..d1664fe 100644 --- a/resources/js/Pages/Admin/User/Index.tsx +++ b/resources/js/Pages/Admin/User/Index.tsx @@ -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(filters.per_page || "10"); + const [searchTerm, setSearchTerm] = useState(filters.search || ""); + const [roleFilter, setRoleFilter] = useState(filters.role || "all"); const [deleteId, setDeleteId] = useState(null); const [deleteName, setDeleteName] = useState(''); 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) { 管理系統使用者帳號與角色分配

- - - - - + + + {/* Toolbar */} +
+
+ {/* Search */} +
+ + handleSearchChange(e.target.value)} + className="pl-10 pr-10" + /> + {searchTerm && ( + + )} +
+ + {/* Role Filter */} + ({ label: role.display_name, value: role.id.toString() })) + ]} + placeholder="角色篩選" + className="w-full md:w-[180px]" + /> + + {/* Action Buttons */} +
+ + + + + +
+
diff --git a/resources/js/Pages/UtilityFee/Index.tsx b/resources/js/Pages/UtilityFee/Index.tsx index c43dea5..fe31f9b 100644 --- a/resources/js/Pages/UtilityFee/Index.tsx +++ b/resources/js/Pages/UtilityFee/Index.tsx @@ -74,6 +74,7 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: const [dateStart, setDateStart] = useState(filters.date_start || ""); const [dateEnd, setDateEnd] = useState(filters.date_end || ""); const [dateRangeType, setDateRangeType] = useState("custom"); + const [perPage, setPerPage] = useState(filters.per_page || "10"); const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); @@ -108,18 +109,36 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: date_end: dateEnd, sort_field: sortField, sort_direction: sortDirection, + per_page: perPage, }, { preserveState: true } ); }; + const handlePerPageChange = (value: string) => { + setPerPage(value); + router.get( + route("utility-fees.index"), + { + search: searchTerm, + category: categoryFilter, + date_start: dateStart, + date_end: dateEnd, + sort_field: sortField, + sort_direction: sortDirection, + per_page: value, + }, + { preserveState: false, preserveScroll: true } + ); + }; + const handleClearFilters = () => { setSearchTerm(""); setCategoryFilter("all"); setDateStart(""); setDateEnd(""); setDateRangeType("custom"); - router.get(route("utility-fees.index")); + router.get(route("utility-fees.index"), { per_page: perPage }, { preserveState: false }); }; const handleSort = (field: string) => { @@ -147,6 +166,7 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: date_end: dateEnd, sort_field: newField, sort_direction: newDirection, + per_page: perPage, }, { preserveState: true } ); @@ -469,7 +489,26 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: -
+
+ +
+
+ 每頁顯示 + + +
+