223 lines
6.4 KiB
TypeScript
223 lines
6.4 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import { Input } from '@/components/ui/input';
|
||
|
|
import { Label } from '@/components/ui/label';
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from '@/components/ui/select';
|
||
|
|
import { Switch } from '@/components/ui/switch';
|
||
|
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet';
|
||
|
|
import { apiFetch } from '@/lib/api/client';
|
||
|
|
|
||
|
|
interface Role {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface UserFormProps {
|
||
|
|
open: boolean;
|
||
|
|
onOpenChange: (open: boolean) => void;
|
||
|
|
user?: {
|
||
|
|
userId: string;
|
||
|
|
displayName: string;
|
||
|
|
email: string;
|
||
|
|
phone: string | null;
|
||
|
|
isActive: boolean;
|
||
|
|
role: { id: string; name: string };
|
||
|
|
} | null;
|
||
|
|
onSuccess: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function UserForm({ open, onOpenChange, user, onSuccess }: UserFormProps) {
|
||
|
|
const [roles, setRoles] = useState<Role[]>([]);
|
||
|
|
const [name, setName] = useState('');
|
||
|
|
const [email, setEmail] = useState('');
|
||
|
|
const [password, setPassword] = useState('');
|
||
|
|
const [displayName, setDisplayName] = useState('');
|
||
|
|
const [phone, setPhone] = useState('');
|
||
|
|
const [roleId, setRoleId] = useState('');
|
||
|
|
const [isActive, setIsActive] = useState(true);
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
const isEdit = !!user;
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (open) {
|
||
|
|
void apiFetch<{ data: Role[] }>('/api/v1/admin/roles').then((res) => setRoles(res.data));
|
||
|
|
}
|
||
|
|
}, [open]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (open) {
|
||
|
|
if (user) {
|
||
|
|
setName(user.displayName);
|
||
|
|
setEmail(user.email);
|
||
|
|
setDisplayName(user.displayName);
|
||
|
|
setPhone(user.phone ?? '');
|
||
|
|
setRoleId(user.role.id);
|
||
|
|
setIsActive(user.isActive);
|
||
|
|
setPassword('');
|
||
|
|
} else {
|
||
|
|
setName('');
|
||
|
|
setEmail('');
|
||
|
|
setDisplayName('');
|
||
|
|
setPhone('');
|
||
|
|
setRoleId('');
|
||
|
|
setIsActive(true);
|
||
|
|
setPassword('');
|
||
|
|
}
|
||
|
|
setError(null);
|
||
|
|
}
|
||
|
|
}, [open, user]);
|
||
|
|
|
||
|
|
async function handleSubmit(e: React.FormEvent) {
|
||
|
|
e.preventDefault();
|
||
|
|
setError(null);
|
||
|
|
setLoading(true);
|
||
|
|
|
||
|
|
try {
|
||
|
|
if (isEdit) {
|
||
|
|
await apiFetch(`/api/v1/admin/users/${user.userId}`, {
|
||
|
|
method: 'PATCH',
|
||
|
|
body: {
|
||
|
|
displayName,
|
||
|
|
phone: phone || null,
|
||
|
|
roleId,
|
||
|
|
isActive,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
await apiFetch('/api/v1/admin/users', {
|
||
|
|
method: 'POST',
|
||
|
|
body: {
|
||
|
|
name: name || displayName,
|
||
|
|
email,
|
||
|
|
password,
|
||
|
|
displayName,
|
||
|
|
phone: phone || undefined,
|
||
|
|
roleId,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
onSuccess();
|
||
|
|
onOpenChange(false);
|
||
|
|
} catch (err: unknown) {
|
||
|
|
const message = err instanceof Error ? err.message : 'Something went wrong';
|
||
|
|
setError(message);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||
|
|
<SheetContent className="overflow-y-auto">
|
||
|
|
<SheetHeader>
|
||
|
|
<SheetTitle>{isEdit ? 'Edit User' : 'New User'}</SheetTitle>
|
||
|
|
</SheetHeader>
|
||
|
|
|
||
|
|
<form onSubmit={handleSubmit} className="mt-6 space-y-4">
|
||
|
|
{!isEdit && (
|
||
|
|
<>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="user-email">Email</Label>
|
||
|
|
<Input
|
||
|
|
id="user-email"
|
||
|
|
type="email"
|
||
|
|
value={email}
|
||
|
|
onChange={(e) => setEmail(e.target.value)}
|
||
|
|
placeholder="user@example.com"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="user-password">Password</Label>
|
||
|
|
<Input
|
||
|
|
id="user-password"
|
||
|
|
type="password"
|
||
|
|
value={password}
|
||
|
|
onChange={(e) => setPassword(e.target.value)}
|
||
|
|
placeholder="Min 12 characters"
|
||
|
|
minLength={12}
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="user-display-name">Display Name</Label>
|
||
|
|
<Input
|
||
|
|
id="user-display-name"
|
||
|
|
value={displayName}
|
||
|
|
onChange={(e) => setDisplayName(e.target.value)}
|
||
|
|
placeholder="John Smith"
|
||
|
|
required
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="user-phone">Phone</Label>
|
||
|
|
<Input
|
||
|
|
id="user-phone"
|
||
|
|
type="tel"
|
||
|
|
value={phone}
|
||
|
|
onChange={(e) => setPhone(e.target.value)}
|
||
|
|
placeholder="+1 555-0123"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="user-role">Role</Label>
|
||
|
|
<Select value={roleId} onValueChange={setRoleId} required>
|
||
|
|
<SelectTrigger id="user-role">
|
||
|
|
<SelectValue placeholder="Select a role" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{roles.map((r) => (
|
||
|
|
<SelectItem key={r.id} value={r.id}>
|
||
|
|
{r.name}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{isEdit && (
|
||
|
|
<div className="flex items-center justify-between rounded-lg border p-3">
|
||
|
|
<div>
|
||
|
|
<Label htmlFor="user-active">Account Active</Label>
|
||
|
|
<p className="text-xs text-muted-foreground">Disabled users cannot sign in</p>
|
||
|
|
</div>
|
||
|
|
<Switch id="user-active" checked={isActive} onCheckedChange={setIsActive} />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||
|
|
|
||
|
|
<SheetFooter>
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
onClick={() => onOpenChange(false)}
|
||
|
|
disabled={loading}
|
||
|
|
>
|
||
|
|
Cancel
|
||
|
|
</Button>
|
||
|
|
<Button type="submit" disabled={loading || !displayName.trim() || !roleId}>
|
||
|
|
{loading ? 'Saving...' : isEdit ? 'Save Changes' : 'Create User'}
|
||
|
|
</Button>
|
||
|
|
</SheetFooter>
|
||
|
|
</form>
|
||
|
|
</SheetContent>
|
||
|
|
</Sheet>
|
||
|
|
);
|
||
|
|
}
|