Files
star-erp/resources/js/Pages/Admin/Role/Index.tsx
sky121113 299cf37054 fix: 修正全系統側邊欄捲軸重置問題
在所有報表與管理頁面的 router.get 調用中加入 preserveScroll: true。
受影響模組包括:
- 財務管理 (會計報表、公用事業費)
- 庫存管理 (庫存查詢、倉庫管理、進貨、調整、調撥)
- 生產管理 (工單管理、配方管理)
- 採購管理 (採購單)
- 銷售與發貨管理 (銷售單、發貨單、匯入管理)
- 系統管理 (使用者、角色、操作紀錄)
2026-02-25 14:04:22 +08:00

318 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import { Head, Link, router } from '@inertiajs/react';
import { cn } from "@/lib/utils";
import { Shield, Plus, Pencil, Trash2, Users, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
import { Button } from '@/Components/ui/button';
import { Badge } from '@/Components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import { format } from 'date-fns';
import { Can } from '@/Components/Permission/Can';
import { useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/Components/ui/alert-dialog";
interface User {
id: number;
name: string;
username: string;
}
interface Role {
id: number;
name: string;
display_name: string;
users_count: number;
permissions_count: number;
created_at: string;
users?: User[];
}
interface Props {
roles: Role[];
filters: {
sort_by?: string;
sort_order?: 'asc' | 'desc';
};
}
export default function RoleIndex({ roles, filters = {} }: Props) {
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
const [deleteId, setDeleteId] = useState<number | null>(null);
const [deleteName, setDeleteName] = useState<string>('');
const [modelOpen, setModelOpen] = useState(false);
const confirmDelete = (id: number, name: string) => {
setDeleteId(id);
setDeleteName(name);
setModelOpen(true);
};
const handleDelete = () => {
if (deleteId) {
router.delete(route('roles.destroy', deleteId), {
onSuccess: () => {
setModelOpen(false);
},
onFinish: () => setModelOpen(false),
});
}
};
const handleSort = (field: string) => {
let newSortBy: string | undefined = field;
let newSortOrder: 'asc' | 'desc' | undefined = 'asc';
if (filters.sort_by === field) {
if (filters.sort_order === 'asc') {
newSortOrder = 'desc';
} else {
newSortBy = undefined;
newSortOrder = undefined;
}
}
router.get(
route('roles.index'),
{ ...filters, sort_by: newSortBy, sort_order: newSortOrder },
{ preserveState: true, replace: true, preserveScroll: true }
);
};
const SortIcon = ({ field }: { field: string }) => {
if (filters.sort_by !== field) {
return <ArrowUpDown className="h-4 w-4 text-muted-foreground ml-1" />;
}
if (filters.sort_order === "asc") {
return <ArrowUp className="h-4 w-4 text-primary-main ml-1" />;
}
return <ArrowDown className="h-4 w-4 text-primary-main ml-1" />;
};
return (
<AuthenticatedLayout
breadcrumbs={[
{ label: '系統管理', href: '#' },
{ label: '角色與權限', href: route('roles.index'), isPage: true },
]}
>
<Head title="角色管理" />
<div className="container mx-auto p-6 max-w-7xl">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<Shield className="h-6 w-6 text-primary-main" />
</h1>
<p className="text-gray-500 mt-1">
</p>
</div>
<Can permission="roles.create">
<Link href={route('roles.create')}>
<Button className="button-filled-primary">
<Plus className="h-4 w-4 mr-2" />
</Button>
</Link>
</Can>
</div>
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-[50px] text-center">#</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-center">
<button
onClick={() => handleSort('permissions_count')}
className="flex items-center justify-center gap-1 w-full hover:text-gray-900 transition-colors"
>
<SortIcon field="permissions_count" />
</button>
</TableHead>
<TableHead className="text-center">
<button
onClick={() => handleSort('users_count')}
className="flex items-center justify-center gap-1 w-full hover:text-gray-900 transition-colors"
>
使 <SortIcon field="users_count" />
</button>
</TableHead>
<TableHead className="text-left">
<button
onClick={() => handleSort('created_at')}
className="flex items-center gap-1 hover:text-gray-900 transition-colors"
>
<SortIcon field="created_at" />
</button>
</TableHead>
<TableHead className="text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{roles.map((role, index) => (
<TableRow key={role.id}>
<TableCell className="text-gray-500 font-medium text-center">
{index + 1}
</TableCell>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
{role.display_name}
</div>
</TableCell>
<TableCell className="text-gray-500 font-mono text-xs">
{role.name}
</TableCell>
<TableCell className="text-center">
<Badge variant="default" className="bg-blue-100 text-blue-800 border-blue-200">
{role.permissions_count}
</Badge>
</TableCell>
<TableCell className="text-center">
<button
onClick={() => role.users_count > 0 && setSelectedRole(role)}
className={cn(
"flex items-center justify-center gap-1 w-full h-full py-2 rounded-md transition-colors",
role.users_count > 0
? "text-primary-main hover:bg-primary-main/10 font-bold"
: "text-gray-400 cursor-default"
)}
title={role.users_count > 0 ? "點擊查看成員名單" : ""}
>
<Users className="h-3.5 w-3.5" />
{role.users_count}
</button>
</TableCell>
<TableCell className="text-left text-gray-500 text-sm">
{format(new Date(role.created_at), 'yyyy/MM/dd')}
</TableCell>
<TableCell className="text-center">
{role.name !== 'super-admin' && (
<div className="flex items-center justify-center gap-2">
<Can permission="roles.edit">
<Link href={route('roles.edit', role.id)}>
<Button
variant="outline"
size="sm"
className="button-outlined-primary"
title="編輯"
>
<Pencil className="h-4 w-4" />
</Button>
</Link>
</Can>
<Can permission="roles.delete">
<Button
variant="outline"
size="sm"
className="button-outlined-error"
title="刪除"
disabled={role.users_count > 0}
onClick={() => confirmDelete(role.id, role.display_name)}
>
<Trash2 className="h-4 w-4" />
</Button>
</Can>
</div>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
{/* 成員名單對話框 */}
<Dialog open={!!selectedRole} onOpenChange={(open) => !open && setSelectedRole(null)}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Users className="h-5 w-5 text-primary-main" />
{selectedRole?.display_name} -
</DialogTitle>
<DialogDescription>
{selectedRole?.users_count} 使
</DialogDescription>
</DialogHeader>
<div className="max-h-[60vh] overflow-y-auto pr-2">
{selectedRole?.users && selectedRole.users.length > 0 ? (
<div className="space-y-3">
{selectedRole.users.map((user) => (
<div
key={user.id}
className="flex items-center justify-between p-3 rounded-lg border border-gray-100 bg-gray-50/50"
>
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-white border border-gray-200 flex items-center justify-center text-xs font-bold text-gray-500 shadow-sm">
{user.name.charAt(0)}
</div>
<div>
<p className="text-sm font-semibold text-gray-900">{user.name}</p>
<p className="text-xs text-gray-500 font-mono italic">@{user.username}</p>
</div>
</div>
<Link
href={route('users.edit', user.id)}
className="text-xs text-primary-main hover:underline font-medium"
>
</Link>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500 italic">
</div>
)}
</div>
</DialogContent>
</Dialog>
<AlertDialog open={modelOpen} onOpenChange={setModelOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{deleteName}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} className="button-filled-error">
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</AuthenticatedLayout>
);
}