Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

robustify UserCreateForm #9852

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"),
)
rajku-dev marked this conversation as resolved.
Show resolved Hide resolved
.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 });
});
},
rajku-dev marked this conversation as resolved.
Show resolved Hide resolved
});

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,
});
rajku-dev marked this conversation as resolved.
Show resolved Hide resolved
};

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
Loading