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 }:
-
+
+
+