feat: 實作使用者啟停用功能與安全性強化
- 新增使用者「啟用/停用」狀態切換功能 (含後端 API、權限控管、活動紀錄) - 強化安全性:隱藏超級管理員角色的可見度與操作權限 - 更新開發規範:加入多租戶資料同步規範於 framework.md - 前端優化:使用 Switch 元件進行狀態快速切換,調整表格欄位順序
This commit is contained in:
@@ -18,6 +18,7 @@ import { Can } from '@/Components/Permission/Can';
|
||||
import { cn } from "@/lib/utils";
|
||||
import Pagination from "@/Components/shared/Pagination";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
import { Switch } from "@/Components/ui/switch";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -42,6 +43,7 @@ interface User {
|
||||
username: string | null;
|
||||
created_at: string;
|
||||
roles: Role[];
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
interface PaginationLinks {
|
||||
@@ -62,6 +64,7 @@ interface Props {
|
||||
sort_order?: 'asc' | 'desc';
|
||||
search?: string;
|
||||
role?: string;
|
||||
is_active?: string;
|
||||
};
|
||||
roles: Role[];
|
||||
}
|
||||
@@ -70,6 +73,7 @@ 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 [isActiveFilter, setIsActiveFilter] = useState<string>(filters.is_active || "all");
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
const [deleteName, setDeleteName] = useState<string>('');
|
||||
const [modelOpen, setModelOpen] = useState(false);
|
||||
@@ -95,17 +99,17 @@ export default function UserIndex({ users, roles, filters }: Props) {
|
||||
setPerPage(value);
|
||||
router.get(
|
||||
route('users.index'),
|
||||
{ ...filters, per_page: value, search: searchTerm, role: roleFilter },
|
||||
{ ...filters, per_page: value, search: searchTerm, role: roleFilter, is_active: isActiveFilter },
|
||||
{ preserveState: false, replace: true, preserveScroll: true }
|
||||
);
|
||||
};
|
||||
|
||||
// Debounced Search Handler
|
||||
const debouncedSearch = useCallback(
|
||||
debounce((term: string, role: string) => {
|
||||
debounce((term: string, role: string, isActive: string) => {
|
||||
router.get(
|
||||
route('users.index'),
|
||||
{ ...filters, search: term, role: role },
|
||||
{ ...filters, search: term, role: role, is_active: isActive },
|
||||
{ preserveState: true, replace: true, preserveScroll: true }
|
||||
);
|
||||
}, 500),
|
||||
@@ -114,14 +118,23 @@ export default function UserIndex({ users, roles, filters }: Props) {
|
||||
|
||||
const handleSearchChange = (term: string) => {
|
||||
setSearchTerm(term);
|
||||
debouncedSearch(term, roleFilter);
|
||||
debouncedSearch(term, roleFilter, isActiveFilter);
|
||||
};
|
||||
|
||||
const handleRoleChange = (value: string) => {
|
||||
setRoleFilter(value);
|
||||
router.get(
|
||||
route('users.index'),
|
||||
{ ...filters, search: searchTerm, role: value },
|
||||
{ ...filters, search: searchTerm, role: value, is_active: isActiveFilter },
|
||||
{ preserveState: false, replace: true, preserveScroll: true }
|
||||
);
|
||||
};
|
||||
|
||||
const handleIsActiveChange = (value: string) => {
|
||||
setIsActiveFilter(value);
|
||||
router.get(
|
||||
route('users.index'),
|
||||
{ ...filters, search: searchTerm, role: roleFilter, is_active: value },
|
||||
{ preserveState: false, replace: true, preserveScroll: true }
|
||||
);
|
||||
};
|
||||
@@ -130,7 +143,7 @@ export default function UserIndex({ users, roles, filters }: Props) {
|
||||
setSearchTerm("");
|
||||
router.get(
|
||||
route('users.index'),
|
||||
{ ...filters, search: "", role: roleFilter },
|
||||
{ ...filters, search: "", role: roleFilter, is_active: isActiveFilter },
|
||||
{ preserveState: true, replace: true, preserveScroll: true }
|
||||
);
|
||||
};
|
||||
@@ -165,8 +178,6 @@ export default function UserIndex({ users, roles, filters }: Props) {
|
||||
return <ArrowDown className="h-4 w-4 text-primary-main ml-1" />;
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout
|
||||
breadcrumbs={[
|
||||
@@ -223,6 +234,19 @@ export default function UserIndex({ users, roles, filters }: Props) {
|
||||
className="w-full md:w-[180px]"
|
||||
/>
|
||||
|
||||
{/* Status Filter */}
|
||||
<SearchableSelect
|
||||
value={isActiveFilter}
|
||||
onValueChange={handleIsActiveChange}
|
||||
options={[
|
||||
{ label: "全部狀態", value: "all" },
|
||||
{ label: "啟用", value: "1" },
|
||||
{ label: "停用", value: "0" },
|
||||
]}
|
||||
placeholder="狀態篩選"
|
||||
className="w-full md:w-[120px]"
|
||||
/>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Can permission="users.create">
|
||||
@@ -251,6 +275,7 @@ export default function UserIndex({ users, roles, filters }: Props) {
|
||||
</button>
|
||||
</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead className="w-[150px] text-center">狀態</TableHead>
|
||||
<TableHead>
|
||||
<button
|
||||
onClick={() => handleSort('created_at')}
|
||||
@@ -308,6 +333,30 @@ export default function UserIndex({ users, roles, filters }: Props) {
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<div className={cn(
|
||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border whitespace-nowrap",
|
||||
user.is_active
|
||||
? "bg-green-50 text-green-700 border-green-200"
|
||||
: "bg-gray-100 text-gray-700 border-gray-200"
|
||||
)}>
|
||||
<span className={cn(
|
||||
"w-1.5 h-1.5 rounded-full mr-1.5",
|
||||
user.is_active ? "bg-green-500" : "bg-gray-400"
|
||||
)} />
|
||||
{user.is_active ? '啟用' : '停用'}
|
||||
</div>
|
||||
|
||||
<Can permission="users.activate">
|
||||
<Switch
|
||||
checked={user.is_active}
|
||||
onCheckedChange={() => router.patch(route('users.toggle-active', user.id), {}, { preserveScroll: true })}
|
||||
className="data-[state=checked]:bg-green-500"
|
||||
/>
|
||||
</Can>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-500 text-sm">
|
||||
{format(new Date(user.created_at), 'yyyy/MM/dd')}
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user