Skip to content

Commit

Permalink
robustify UserCreateForm
Browse files Browse the repository at this point in the history
  • Loading branch information
rajku-dev committed Jan 8, 2025
1 parent 8bec8bc commit 2f7d401
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 103 deletions.
8 changes: 8 additions & 0 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,7 @@
"date_of_admission": "Date of Admission",
"date_of_birth": "Date of Birth",
"date_of_birth_age": "Date of Birth/Age",
"date_of_birth_cannot_be_in_future": "Date of birth cannot be in the future",
"date_of_birth_or_age": "Date of Birth or Age",
"date_of_positive_covid_19_swab": "Date of Positive Covid 19 Swab",
"date_of_result": "Covid confirmation date",
Expand Down Expand Up @@ -1416,6 +1417,7 @@
"phone_number": "Phone Number",
"phone_number_at_current_facility": "Phone Number of Contact person at current Facility",
"phone_number_min_error": "Phone number must be at least 10 characters long",
"phone_number_must_start": "Phone number must start with +91 followed by 10 digits",
"phone_number_verified": "Phone Number Verified",
"pincode": "Pincode",
"pincode_autofill": "State and District auto-filled from Pincode",
Expand Down Expand Up @@ -1743,6 +1745,7 @@
"thank_you_for_choosing": "Thank you for choosing our care service",
"the_request_for_resources_placed_by_yourself_is": "The request for resource (details below) placed by yourself is",
"third_party_software_licenses": "Third Party Software Licenses",
"this_field_is_required": "This field is required",
"time": "Time",
"time_slot": "Time Slot",
"title_of_request": "Title of Request",
Expand Down Expand Up @@ -1862,7 +1865,12 @@
"username": "Username",
"username_already_exists": "This username already exists",
"username_available": "Username is available",
"username_consecutive_special_characters": "Username can't contain consecutive special characters",
"username_contain_lowercase_special": "Username can only contain lowercase letters, numbers, and . _ -",
"username_less_than": "Username must be less than 16 characters",
"username_more_than": "Username must be at least 4 characters",
"username_not_available": "Username is not available",
"username_start_end_letter_number": "Username must start and end with a letter or number",
"username_userdetails_not_found": "Unable to fetch details as username or user details not found",
"users": "Users",
"vacant": "Vacant",
Expand Down
232 changes: 132 additions & 100 deletions src/components/Users/CreateUserForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { Loader2 } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import * as z from "zod";

import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { DatePicker } from "@/components/ui/date-picker";
import {
Form,
FormControl,
Expand All @@ -25,77 +29,82 @@ import {

import { GENDER_TYPES } from "@/common/constants";

import * as Notification from "@/Utils/Notifications";
import request from "@/Utils/request/request";
import mutate from "@/Utils/request/mutate";
import OrganizationSelector from "@/pages/Organization/components/OrganizationSelector";
import { UserBase } from "@/types/user/user";
import UserApi from "@/types/user/userApi";

const userFormSchema = z
.object({
user_type: z.enum(["doctor", "nurse", "staff", "volunteer"]),
username: z
.string()
.min(4, "Username must be at least 4 characters")
.max(16, "Username must be less than 16 characters")
.regex(
/^[a-z0-9._-]*$/,
"Username can only contain lowercase letters, numbers, and . _ -",
)
.regex(
/^[a-z0-9].*[a-z0-9]$/,
"Username must start and end with a letter or number",
)
.refine(
(val) => !val.match(/(?:[._-]{2,})/),
"Username can't contain consecutive special characters",
),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
.regex(/[0-9]/, "Password must contain at least one number"),
c_password: z.string(),
first_name: z.string().min(1, "First name is required"),
last_name: z.string().min(1, "Last name is required"),
email: z.string().email("Invalid email address"),
phone_number: z
.string()
.regex(
/^\+91[0-9]{10}$/,
"Phone number must start with +91 followed by 10 digits",
),
alt_phone_number: z
.string()
.regex(
/^\+91[0-9]{10}$/,
"Phone number must start with +91 followed by 10 digits",
)
.optional(),
phone_number_is_whatsapp: z.boolean().default(true),
date_of_birth: z.string().min(1, "Date of birth is required"),
gender: z.enum(["male", "female", "other"]),
qualification: z.string().optional(),
doctor_experience_commenced_on: z.string().optional(),
doctor_medical_council_registration: z.string().optional(),
geo_organization: z.string().min(1, "Organization is required"),
})
.refine((data) => data.password === data.c_password, {
message: "Passwords don't match",
path: ["c_password"],
});

type UserFormValues = z.infer<typeof userFormSchema>;

interface Props {
onSubmitSuccess?: (user: UserBase) => void;
}

export default function CreateUserForm({ onSubmitSuccess }: Props) {
const { t } = useTranslation();

const form = useForm<UserFormValues>({
const userFormSchema = z
.object({
user_type: z.enum(["doctor", "nurse", "staff", "volunteer"]),
username: z
.string()
.min(4, t("username_more_than"))
.max(16, t("username_less_than"))
.regex(/^[a-z0-9._-]*$/, t("username_contain_lowercase_special"))
.regex(/^[a-z0-9].*[a-z0-9]$/, t("username_start_end_letter_number"))
.refine(
(val) => !val.match(/(?:[._-]{2,})/),
t("username_consecutive_special_characters"),
),
password: z
.string()
.min(8, t("password_length_validation"))
.regex(/[a-z]/, t("password_lowercase_validation"))
.regex(/[A-Z]/, t("password_uppercase_validation"))
.regex(/[0-9]/, t("password_number_validation")),
c_password: z.string(),
first_name: z.string().min(1, t("this_field_is_required")),
last_name: z.string().min(1, t("this_field_is_required")),
email: z.string().email(t("invalid_email")),
phone_number: z
.string()
.regex(/^\+91[0-9]{10}$/, t("phone_number_must_start")),
alt_phone_number: z
.string()
.refine(
(val) => val === "" || /^\+91[0-9]{10}$/.test(val),
t("phone_number_must_start"),
)
.transform((val) => val || undefined)
.optional(),
phone_number_is_whatsapp: z.boolean().default(true),
date_of_birth: z
.date({
required_error: t("this_field_is_required"),
})
.refine((dob) => dob <= new Date(), {
message: t("date_of_birth_cannot_be_in_future"),
}),
gender: z.enum(["male", "female", "transgender", "non_binary"]),
qualification: z
.string()
.optional()
.transform((val) => val || undefined),
doctor_experience_commenced_on: z
.string()
.optional()
.transform((val) => val || undefined),
doctor_medical_council_registration: z
.string()
.optional()
.transform((val) => val || undefined),
geo_organization: z.string().min(1, t("this_field_is_required")),
})
.refine((data) => data.password === data.c_password, {
message: t("password_mismatch"),
path: ["c_password"],
});

const form = useForm<z.infer<typeof userFormSchema>>({
mode: "onChange",
resolver: zodResolver(userFormSchema),
defaultValues: {
user_type: "staff",
Expand All @@ -116,35 +125,39 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
}
}, [phoneNumber, isWhatsApp, form]);

const onSubmit = async (data: UserFormValues) => {
try {
const {
res,
data: user,
error,
} = await request(UserApi.create, {
body: {
...data,
// Omit c_password as it's not needed in the API
c_password: undefined,
} as unknown as UserBase,
const { mutate: createUser, isPending } = useMutation({
mutationFn: mutate(UserApi.create),
onSuccess: (user: UserBase) => {
toast.success(t("user_added_successfully"));
onSubmitSuccess?.(user!);
},
onError: (error) => {
const errors = (error.cause?.errors as any[]) || [];
errors.forEach((err) => {
const field = err.loc[0];
form.setError(field, { message: err.ctx.error });
});
},
});

if (res?.ok) {
Notification.Success({
msg: t("user_added_successfully"),
});
onSubmitSuccess?.(user!);
} else {
Notification.Error({
msg: error?.message ?? t("user_add_error"),
});
}
} catch (error) {
Notification.Error({
msg: t("user_add_error"),
});
}
const onSubmit = (data: z.infer<typeof userFormSchema>) => {
createUser({
username: data.username,
first_name: data.first_name,
last_name: data.last_name,
email: data.email,
phone_number: data.phone_number,
alt_phone_number: data.alt_phone_number,
date_of_birth: data.date_of_birth,
geo_organization: data.geo_organization,
user_type: data.user_type,
gender: data.gender,
password: data.password,
qualification: data.qualification,
doctor_experience_commenced_on: data.doctor_experience_commenced_on,
doctor_medical_council_registration:
data.doctor_medical_council_registration,
});
};

return (
Expand Down Expand Up @@ -180,7 +193,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
name="first_name"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormLabel required>First Name</FormLabel>
<FormControl>
<Input placeholder="First name" {...field} />
</FormControl>
Expand All @@ -194,7 +207,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
name="last_name"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormLabel required>Last Name</FormLabel>
<FormControl>
<Input placeholder="Last name" {...field} />
</FormControl>
Expand All @@ -209,7 +222,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormLabel required>Username</FormLabel>
<FormControl>
<Input placeholder="Username" {...field} />
</FormControl>
Expand All @@ -224,7 +237,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel required>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="Password" {...field} />
</FormControl>
Expand All @@ -238,7 +251,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
name="c_password"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormLabel required>Confirm Password</FormLabel>
<FormControl>
<Input
type="password"
Expand All @@ -257,7 +270,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel required>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="Email" {...field} />
</FormControl>
Expand All @@ -272,9 +285,14 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
name="phone_number"
render={({ field }) => (
<FormItem>
<FormLabel>Phone Number</FormLabel>
<FormLabel required>Phone Number</FormLabel>
<FormControl>
<Input type="tel" placeholder="+91XXXXXXXXXX" {...field} />
<Input
type="tel"
placeholder="+91XXXXXXXXXX"
maxLength={13}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
Expand All @@ -286,12 +304,13 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
name="alt_phone_number"
render={({ field }) => (
<FormItem>
<FormLabel>Alternative Phone Number</FormLabel>
<FormLabel>WhatsApp Number</FormLabel>
<FormControl>
<Input
placeholder="+91XXXXXXXXXX"
type="tel"
{...field}
maxLength={13}
disabled={isWhatsApp}
/>
</FormControl>
Expand Down Expand Up @@ -325,9 +344,15 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
name="date_of_birth"
render={({ field }) => (
<FormItem>
<FormLabel>Date of Birth</FormLabel>
<FormLabel required>Date of Birth</FormLabel>
<FormControl>
<Input type="date" {...field} />
<DatePicker
date={field.value}
onChange={(date) => field.onChange(date)}
disabled={(date) =>
date > new Date() || date < new Date("1900-01-01")
}
/>
</FormControl>
<FormMessage />
</FormItem>
Expand Down Expand Up @@ -437,8 +462,15 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) {
)}
/>

<Button type="submit" className="w-full">
Create User
<Button
type="submit"
className="w-full"
disabled={
!form.formState.isDirty || !form.formState.isValid || isPending
}
>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}{" "}
{t("create")}
</Button>
</form>
</Form>
Expand Down
Loading

0 comments on commit 2f7d401

Please sign in to comment.