diff --git a/public/locale/en.json b/public/locale/en.json index 3fbcfc3de72..8e08fb3f22d 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -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", @@ -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", @@ -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", @@ -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", diff --git a/src/components/Users/CreateUserForm.tsx b/src/components/Users/CreateUserForm.tsx index beafb4be950..ef3c8660a06 100644 --- a/src/components/Users/CreateUserForm.tsx +++ b/src/components/Users/CreateUserForm.tsx @@ -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, @@ -25,69 +29,11 @@ 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; - interface Props { onSubmitSuccess?: (user: UserBase) => void; } @@ -95,7 +41,70 @@ interface Props { export default function CreateUserForm({ onSubmitSuccess }: Props) { const { t } = useTranslation(); - const form = useForm({ + 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>({ + mode: "onChange", resolver: zodResolver(userFormSchema), defaultValues: { user_type: "staff", @@ -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) => { + 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 ( @@ -180,7 +193,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="first_name" render={({ field }) => ( - First Name + First Name @@ -194,7 +207,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="last_name" render={({ field }) => ( - Last Name + Last Name @@ -209,7 +222,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="username" render={({ field }) => ( - Username + Username @@ -224,7 +237,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="password" render={({ field }) => ( - Password + Password @@ -238,7 +251,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="c_password" render={({ field }) => ( - Confirm Password + Confirm Password ( - Email + Email @@ -272,9 +285,14 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="phone_number" render={({ field }) => ( - Phone Number + Phone Number - + @@ -286,12 +304,13 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="alt_phone_number" render={({ field }) => ( - Alternative Phone Number + WhatsApp Number @@ -325,9 +344,15 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="date_of_birth" render={({ field }) => ( - Date of Birth + Date of Birth - + field.onChange(date)} + disabled={(date) => + date > new Date() || date < new Date("1900-01-01") + } + /> @@ -437,8 +462,15 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { )} /> - diff --git a/src/components/ui/date-picker.tsx b/src/components/ui/date-picker.tsx index 30ca74560e0..32d23132362 100644 --- a/src/components/ui/date-picker.tsx +++ b/src/components/ui/date-picker.tsx @@ -16,9 +16,10 @@ import { interface DatePickerProps { date?: Date; onChange?: (date?: Date) => void; + disabled?: (date: Date) => boolean; } -export function DatePicker({ date, onChange }: DatePickerProps) { +export function DatePicker({ date, onChange, disabled }: DatePickerProps) { const [open, setOpen] = useState(false); return ( @@ -44,6 +45,7 @@ export function DatePicker({ date, onChange }: DatePickerProps) { setOpen(false); }} initialFocus + disabled={disabled} /> diff --git a/src/types/user/user.ts b/src/types/user/user.ts index 010e1614807..1a81a695be1 100644 --- a/src/types/user/user.ts +++ b/src/types/user/user.ts @@ -14,3 +14,20 @@ export type UserBase = { phone_number: string; gender: (typeof GENDER_TYPES)[number]["id"]; }; + +export type UserCreateRequest = { + user_type: UserType; + gender: (typeof GENDER_TYPES)[number]["id"]; + password: string; + geo_organization: string; + username: string; + first_name: string; + last_name: string; + email: string; + phone_number: string; + alt_phone_number?: string; + date_of_birth: Date; + qualification?: string; + doctor_experience_commenced_on?: string; + doctor_medical_council_registration?: string; +}; diff --git a/src/types/user/userApi.ts b/src/types/user/userApi.ts index 61292d5c473..1413afdb12a 100644 --- a/src/types/user/userApi.ts +++ b/src/types/user/userApi.ts @@ -1,7 +1,7 @@ import { HttpMethod, Type } from "@/Utils/request/api"; import { PaginatedResponse } from "@/Utils/request/types"; -import { UserBase } from "./user"; +import { UserBase, UserCreateRequest } from "./user"; export default { list: { @@ -13,7 +13,7 @@ export default { path: "/api/v1/users/", method: HttpMethod.POST, TRes: Type(), - TBody: Type(), + TBody: Type(), }, get: { path: "/api/v1/users/{username}/",