Files
star-erp/resources/js/Pages/Admin/User/Edit.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

243 lines
12 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 { Users, ArrowLeft, Check, Lock, Mail, User, AlertCircle } from 'lucide-react';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/Components/ui/radio-group';
import { FormEvent } from 'react';
import { Switch } from '@/Components/ui/switch';
import { Can } from '@/Components/Permission/Can';
interface Role {
id: number;
name: string;
display_name: string;
}
interface UserData {
id: number;
name: string;
email: string;
username: string | null;
is_active: boolean;
}
interface Props {
user: UserData;
roles: Role[];
currentRoles: string[];
}
export default function UserEdit({ user, roles, currentRoles }: Props) {
const { data, setData, put, processing, errors } = useForm({
name: user.name,
email: user.email || '',
username: user.username || '',
password: '',
password_confirmation: '',
roles: currentRoles,
is_active: user.is_active,
});
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
put(route('users.update', user.id));
};
return (
<AuthenticatedLayout
breadcrumbs={[
{ label: '系統管理', href: '#' },
{ label: '使用者管理', href: route('users.index') },
{ label: '編輯使用者', href: route('users.edit', user.id), isPage: true },
]}
>
<Head title={`編輯使用者 - ${user.name}`} />
<div className="container mx-auto p-6 max-w-7xl">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Header Area */}
<div>
<Link href={route('users.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">
<Users 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>
<div className="space-y-6">
{/* Basic Info */}
<div className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm space-y-6">
<h3 className="font-bold text-gray-900 border-b pb-2 mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="username" className="flex items-center gap-2">
<User className="h-4 w-4" /> 使 ()
</Label>
<Input
id="username"
value={data.username}
onChange={e => setData('username', e.target.value)}
placeholder="請輸入登入帳號"
/>
{errors.username && <p className="text-sm text-red-500">{errors.username}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="name" className="flex items-center gap-2">
<User className="h-4 w-4" />
</Label>
<Input
id="name"
value={data.name}
onChange={e => setData('name', e.target.value)}
placeholder="例如:王小明"
/>
{errors.name && <p className="text-sm text-red-500">{errors.name}</p>}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email" className="flex items-center gap-2">
<Mail className="h-4 w-4" /> ()
</Label>
<Input
id="email"
type="email"
value={data.email}
onChange={e => setData('email', e.target.value)}
placeholder="user@example.com (可省略)"
/>
{errors.email && <p className="text-sm text-red-500">{errors.email}</p>}
</div>
{/* Status */}
<Can permission="users.activate">
<div className="space-y-2">
<Label htmlFor="is_active" className="flex items-center gap-2">
<Check className="h-4 w-4" />
</Label>
<div className="flex items-center gap-2">
<Switch
id="is_active"
checked={data.is_active}
onCheckedChange={(checked: boolean) => setData('is_active', checked)}
/>
<span className="text-sm text-gray-500">
{data.is_active ? '啟用' : '停用'}
</span>
</div>
<p className="text-xs text-gray-400">
使
</p>
</div>
</Can>
</div>
{/* Roles */}
<div className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<h3 className="font-bold text-gray-900 border-b pb-2 mb-4"> ()</h3>
<RadioGroup
value={data.roles[0] || ''}
onValueChange={(value) => setData('roles', [value])}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
>
{roles.map((role) => (
<Label
key={role.id}
htmlFor={`role-${role.id}`}
className={`flex items-center space-x-3 p-3 border rounded-lg transition-colors cursor-pointer ${data.roles.includes(role.name)
? 'border-primary-main bg-primary-lightest'
: 'border-gray-100 bg-gray-50/50 hover:bg-gray-100'
}`}
>
<RadioGroupItem
value={role.name}
id={`role-${role.id}`}
className="text-primary-main border-gray-300"
/>
<div className="grid gap-1 leading-none">
<span className="text-sm font-medium leading-none">
{role.display_name}
</span>
<span className="text-xs text-gray-500 font-mono">
{role.name}
</span>
</div>
</Label>
))}
</RadioGroup>
{errors.roles && <p className="text-sm text-red-500 mt-2">{errors.roles}</p>}
</div>
{/* Password Reset */}
<div className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm space-y-6">
<h3 className="font-bold text-gray-900 border-b pb-2 mb-4"></h3>
<div className="bg-amber-50 text-amber-800 p-3 rounded-lg text-sm flex items-start gap-2 mb-4">
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="password" className="flex items-center gap-2">
<Lock className="h-4 w-4" />
</Label>
<Input
id="password"
type="password"
value={data.password}
onChange={e => setData('password', e.target.value)}
placeholder="••••••••"
/>
{errors.password && <p className="text-sm text-red-500">{errors.password}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="password_confirmation" className="flex items-center gap-2">
<Lock className="h-4 w-4" />
</Label>
<Input
id="password_confirmation"
type="password"
value={data.password_confirmation}
onChange={e => setData('password_confirmation', e.target.value)}
placeholder="••••••••"
/>
</div>
</div>
</div>
</div>
</form>
</div>
</AuthenticatedLayout>
);
}