Files
star-erp/resources/js/Pages/Admin/Role/Create.tsx
sky121113 4c959efc8b
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 50s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
feat: 補齊生產管理與進貨單權限、功能實作及 UI 優化
2026-01-27 17:40:56 +08:00

225 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': '檢視日誌',
};
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>
);
}