TTMT.ManageWebGUI/src/routes/_auth/user/create/index.tsx

325 lines
11 KiB
TypeScript
Raw Normal View History

2026-03-06 17:54:09 +07:00
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/")({
head: () => ({
meta: [{ title: "Tạo người dùng mới" }],
}),
2026-03-06 17:54:09 +07:00
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ự)";
2026-03-06 17:54:09 +07:00
}
// 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!";
2026-03-06 17:54:09 +07:00
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>
2026-03-06 17:54:09 +07:00
<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: "/user" })}>
2026-03-06 17:54:09 +07:00
<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)
}
2026-03-06 17:54:09 +07:00
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ọ 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)
}
2026-03-06 17:54:09 +07:00
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)
}
2026-03-06 17:54:09 +07:00
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>
2026-03-06 17:54:09 +07:00
)}
</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 người dùng 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"
2026-03-06 17:54:09 +07:00
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>
);
}