Files
star-erp/resources/js/Pages/Admin/Role/Create.tsx
sky121113 d671c08338
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m1s
feat: 實作使用者啟停用功能與安全性強化
- 新增使用者「啟用/停用」狀態切換功能 (含後端 API、權限控管、活動紀錄)
- 強化安全性:隱藏超級管理員角色的可見度與操作權限
- 更新開發規範:加入多租戶資料同步規範於 framework.md
- 前端優化:使用 Switch 元件進行狀態快速切換,調整表格欄位順序
2026-02-03 11:51:46 +08:00

226 lines
10 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, useForm } from '@inertiajs/react';
import { Shield, ArrowLeft, Check } from 'lucide-react';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { Checkbox } from '@/Components/ui/checkbox';
import { FormEvent } from 'react';
interface Permission {
id: number;
name: string;
}
interface GroupedPermission {
key: string;
name: string;
permissions: Permission[];
}
interface Props {
groupedPermissions: GroupedPermission[];
}
export default function RoleCreate({ groupedPermissions }: Props) {
const { data, setData, post, processing, errors } = useForm({
name: '',
display_name: '',
permissions: [] as string[],
});
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
post(route('roles.store'));
};
const togglePermission = (name: string) => {
if (data.permissions.includes(name)) {
setData('permissions', data.permissions.filter(p => p !== name));
} else {
setData('permissions', [...data.permissions, name]);
}
};
const toggleGroup = (groupPermissions: Permission[]) => {
const groupNames = groupPermissions.map(p => p.name);
const allSelected = groupNames.every(name => data.permissions.includes(name));
if (allSelected) {
// Unselect all
setData('permissions', data.permissions.filter(p => !groupNames.includes(p)));
} else {
// Select all
const newPermissions = [...data.permissions];
groupNames.forEach(name => {
if (!newPermissions.includes(name)) newPermissions.push(name);
});
setData('permissions', newPermissions);
}
};
// 翻譯權限後綴
const translateAction = (permissionName: string) => {
const parts = permissionName.split('.');
if (parts.length < 2) return permissionName;
const action = parts[1];
const map: Record<string, string> = {
'view': '檢視',
'create': '新增',
'edit': '編輯',
'delete': '刪除',
'publish': '發布',
'adjust': '調整',
'transfer': '調撥',
'safety_stock': '安全庫存設定',
'export': '匯出',
'complete': '完成',
'view_cost': '檢視成本',
'view_logs': '檢視日誌',
'activate': '啟用/停用',
};
return map[action] || action;
};
return (
<AuthenticatedLayout
breadcrumbs={[
{ label: '系統管理', href: '#' },
{ label: '角色與權限', href: route('roles.index') },
{ label: '建立角色', href: route('roles.create'), isPage: true },
]}
>
<Head title="建立角色" />
<div className="container mx-auto p-6 max-w-7xl">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Header Area */}
<div>
<Link href={route('roles.index')}>
<Button
variant="outline"
type="button"
className="gap-2 button-outlined-primary mb-2"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="flex items-center justify-between">
<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>
<Button
type="submit"
className="button-filled-primary"
disabled={processing}
>
<Check className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* Role Name */}
<div className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="display_name"> (/)</Label>
<Input
id="display_name"
placeholder="例如:行政管理員"
value={data.display_name}
onChange={e => setData('display_name', e.target.value)}
/>
{errors.display_name && (
<p className="text-sm text-red-500">{errors.display_name}</p>
)}
<p className="text-xs text-gray-500">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="name"> ()</Label>
<Input
id="name"
placeholder="e.g. sales-manager"
value={data.name}
onChange={e => setData('name', e.target.value)}
className="font-mono"
/>
{errors.name && (
<p className="text-sm text-red-500">{errors.name}</p>
)}
<p className="text-xs text-gray-500">
使: <code>warehouse-staff</code>
</p>
</div>
</div>
</div>
{/* Permissions Matrix */}
<div className="space-y-4">
<h2 className="text-lg font-bold text-grey-0"></h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{groupedPermissions.map((group) => {
const allGroupSelected = group.permissions.every(p => data.permissions.includes(p.name));
return (
<div key={group.key} className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden flex flex-col">
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200 flex items-center justify-between">
<span className="font-medium text-gray-700">{group.name}</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => toggleGroup(group.permissions)}
className="text-xs h-7 text-primary-main hover:text-primary-main hover:bg-primary-main/10"
>
{allGroupSelected ? '取消全選' : '全選'}
</Button>
</div>
<div className="p-4 flex-1">
<div className="space-y-3">
{group.permissions.map((permission) => (
<div key={permission.id} className="flex items-start space-x-3">
<Checkbox
id={permission.name}
checked={data.permissions.includes(permission.name)}
onCheckedChange={() => togglePermission(permission.name)}
/>
<div className="grid gap-1.5 leading-none">
<label
htmlFor={permission.name}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
{translateAction(permission.name)}
</label>
<p className="text-[10px] text-gray-400 font-mono">
{permission.name}
</p>
</div>
</div>
))}
</div>
</div>
</div>
);
})}
</div>
</div>
</form>
</div>
</AuthenticatedLayout>
);
}