313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
|
|
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||
|
|
import { useGetRoleList, useCreateAccount } from "@/hooks/queries";
|
||
|
|
import { useState } from "react";
|
||
|
|
import {
|
||
|
|
Card,
|
||
|
|
CardContent,
|
||
|
|
CardDescription,
|
||
|
|
CardHeader,
|
||
|
|
CardTitle,
|
||
|
|
} from "@/components/ui/card";
|
||
|
|
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 { UserPlus, ArrowLeft, Save, Loader2 } from "lucide-react";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
|
||
|
|
export const Route = createFileRoute("/_auth/user/create/")({
|
||
|
|
component: CreateUserComponent,
|
||
|
|
loader: async ({ context }) => {
|
||
|
|
context.breadcrumbs = [
|
||
|
|
{ title: "Quản lý người dùng", path: "#" },
|
||
|
|
{ title: "Tạo người dùng mới", path: "/user/create" },
|
||
|
|
];
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
function CreateUserComponent() {
|
||
|
|
const navigate = useNavigate();
|
||
|
|
const { data: roles = [], isLoading: rolesLoading } = useGetRoleList();
|
||
|
|
const createMutation = useCreateAccount();
|
||
|
|
|
||
|
|
const [formData, setFormData] = useState({
|
||
|
|
userName: "",
|
||
|
|
name: "",
|
||
|
|
password: "",
|
||
|
|
confirmPassword: "",
|
||
|
|
roleId: "",
|
||
|
|
});
|
||
|
|
|
||
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||
|
|
|
||
|
|
// Validate username theo regex backend
|
||
|
|
const validateUserName = (userName: string): boolean => {
|
||
|
|
const regex = /^[a-zA-Z0-9_.]{3,20}$/;
|
||
|
|
return regex.test(userName);
|
||
|
|
};
|
||
|
|
|
||
|
|
const validateForm = (): boolean => {
|
||
|
|
const newErrors: Record<string, string> = {};
|
||
|
|
|
||
|
|
// Validate username
|
||
|
|
if (!formData.userName) {
|
||
|
|
newErrors.userName = "Tên đăng nhập không được để trống";
|
||
|
|
} else if (!validateUserName(formData.userName)) {
|
||
|
|
newErrors.userName = "Tên đăng nhập chỉ cho phép chữ cái, số, dấu chấm và gạch dưới (3-20 ký tự)";
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate name
|
||
|
|
if (!formData.name.trim()) {
|
||
|
|
newErrors.name = "Họ và tên không được để trống";
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate password
|
||
|
|
if (!formData.password) {
|
||
|
|
newErrors.password = "Mật khẩu không được để trống";
|
||
|
|
} else if (formData.password.length < 6) {
|
||
|
|
newErrors.password = "Mật khẩu phải có ít nhất 6 ký tự";
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate confirm password
|
||
|
|
if (formData.password !== formData.confirmPassword) {
|
||
|
|
newErrors.confirmPassword = "Mật khẩu xác nhận không khớp";
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate roleId
|
||
|
|
if (!formData.roleId) {
|
||
|
|
newErrors.roleId = "Vui lòng chọn vai trò";
|
||
|
|
}
|
||
|
|
|
||
|
|
setErrors(newErrors);
|
||
|
|
return Object.keys(newErrors).length === 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
|
||
|
|
if (!validateForm()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
await createMutation.mutateAsync({
|
||
|
|
userName: formData.userName,
|
||
|
|
name: formData.name,
|
||
|
|
password: formData.password,
|
||
|
|
roleId: Number(formData.roleId),
|
||
|
|
accessRooms: [0], // Default value, will be updated when Room API provides IDs
|
||
|
|
});
|
||
|
|
toast.success("Tạo tài khoản thành công!");
|
||
|
|
navigate({ to: "/dashboard" }); // TODO: Navigate to user list page when it exists
|
||
|
|
} catch (error: any) {
|
||
|
|
const errorMessage = error.response?.data?.message || "Tạo tài khoản thất bại!";
|
||
|
|
toast.error(errorMessage);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleInputChange = (field: string, value: string) => {
|
||
|
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||
|
|
// Clear error when user starts typing
|
||
|
|
if (errors[field]) {
|
||
|
|
setErrors((prev) => {
|
||
|
|
const newErrors = { ...prev };
|
||
|
|
delete newErrors[field];
|
||
|
|
return newErrors;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="w-full px-6 py-6 space-y-6">
|
||
|
|
{/* Header */}
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<h1 className="text-3xl font-bold tracking-tight">Tạo người dùng mới</h1>
|
||
|
|
<p className="text-muted-foreground mt-1">
|
||
|
|
Thêm tài khoản người dùng mới vào hệ thống
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<Button
|
||
|
|
variant="outline"
|
||
|
|
onClick={() => navigate({ to: "/dashboard" })}
|
||
|
|
>
|
||
|
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||
|
|
Quay lại
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Form */}
|
||
|
|
<form onSubmit={handleSubmit} className="w-full">
|
||
|
|
<Card className="shadow-sm">
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="flex items-center gap-2">
|
||
|
|
<UserPlus className="h-5 w-5" />
|
||
|
|
Thông tin tài khoản
|
||
|
|
</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
Điền thông tin để tạo tài khoản người dùng mới
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-6 pt-6">
|
||
|
|
{/* Username and Name - Grid Layout */}
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="userName">
|
||
|
|
Tên đăng nhập <span className="text-destructive">*</span>
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="userName"
|
||
|
|
value={formData.userName}
|
||
|
|
onChange={(e) => handleInputChange("userName", e.target.value)}
|
||
|
|
placeholder="Nhập tên đăng nhập (3-20 ký tự, chỉ chữ, số, . và _)"
|
||
|
|
disabled={createMutation.isPending}
|
||
|
|
className="h-10"
|
||
|
|
/>
|
||
|
|
{errors.userName && (
|
||
|
|
<p className="text-sm text-destructive">{errors.userName}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="name">
|
||
|
|
Họ và tên <span className="text-destructive">*</span>
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="name"
|
||
|
|
value={formData.name}
|
||
|
|
onChange={(e) => handleInputChange("name", e.target.value)}
|
||
|
|
placeholder="Nhập họ và tên đầy đủ"
|
||
|
|
disabled={createMutation.isPending}
|
||
|
|
className="h-10"
|
||
|
|
/>
|
||
|
|
{errors.name && (
|
||
|
|
<p className="text-sm text-destructive">{errors.name}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Password */}
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="password">
|
||
|
|
Mật khẩu <span className="text-destructive">*</span>
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="password"
|
||
|
|
type="password"
|
||
|
|
value={formData.password}
|
||
|
|
onChange={(e) => handleInputChange("password", e.target.value)}
|
||
|
|
placeholder="Nhập mật khẩu (tối thiểu 6 ký tự)"
|
||
|
|
disabled={createMutation.isPending}
|
||
|
|
className="h-10"
|
||
|
|
/>
|
||
|
|
{errors.password && (
|
||
|
|
<p className="text-sm text-destructive">{errors.password}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="confirmPassword">
|
||
|
|
Xác nhận mật khẩu <span className="text-destructive">*</span>
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="confirmPassword"
|
||
|
|
type="password"
|
||
|
|
value={formData.confirmPassword}
|
||
|
|
onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
|
||
|
|
placeholder="Nhập lại mật khẩu"
|
||
|
|
disabled={createMutation.isPending}
|
||
|
|
className="h-10"
|
||
|
|
/>
|
||
|
|
{errors.confirmPassword && (
|
||
|
|
<p className="text-sm text-destructive">{errors.confirmPassword}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Role Selection */}
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="roleId">
|
||
|
|
Vai trò <span className="text-destructive">*</span>
|
||
|
|
</Label>
|
||
|
|
{rolesLoading ? (
|
||
|
|
<div className="flex items-center justify-center p-4 border rounded-md bg-muted/50">
|
||
|
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<Select
|
||
|
|
value={formData.roleId}
|
||
|
|
onValueChange={(value) => handleInputChange("roleId", value)}
|
||
|
|
disabled={createMutation.isPending}
|
||
|
|
>
|
||
|
|
<SelectTrigger className="h-10">
|
||
|
|
<SelectValue placeholder="Chọn vai trò cho người dùng" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{Array.isArray(roles) &&
|
||
|
|
roles.map((role: any) => (
|
||
|
|
<SelectItem key={role.id} value={String(role.id)}>
|
||
|
|
{role.roleName} (Priority: {role.priority})
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
)}
|
||
|
|
{errors.roleId && (
|
||
|
|
<p className="text-sm text-destructive">{errors.roleId}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* TODO: Add Room/Building Access selection when API is ready */}
|
||
|
|
{/*
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label>Quyền truy cập phòng (Tùy chọn)</Label>
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
Chọn các phòng mà người dùng có quyền truy cập
|
||
|
|
</p>
|
||
|
|
// Add multi-select component here when Room API provides IDs
|
||
|
|
</div>
|
||
|
|
*/}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Actions */}
|
||
|
|
<div className="flex justify-end gap-3 mt-6">
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant="outline"
|
||
|
|
onClick={() => navigate({ to: "/dashboard" })}
|
||
|
|
disabled={createMutation.isPending}
|
||
|
|
className="min-w-[100px]"
|
||
|
|
>
|
||
|
|
Hủy
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
type="submit"
|
||
|
|
disabled={createMutation.isPending}
|
||
|
|
className="min-w-[140px]"
|
||
|
|
>
|
||
|
|
{createMutation.isPending ? (
|
||
|
|
<>
|
||
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||
|
|
Đang tạo...
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<Save className="h-4 w-4 mr-2" />
|
||
|
|
Tạo tài khoản
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|