From 24a9dac3d193d66120a5596fa155d6cffa9bd0fc Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Fri, 1 Nov 2024 18:42:39 +0530 Subject: [PATCH 01/55] User page and components - Creating a separate user detail page (for user management) Pt 1 --- src/Locale/en.json | 10 + src/Routers/routes/UserRoutes.tsx | 7 + src/components/Common/UserDetails.tsx | 4 +- src/components/Form/Form.tsx | 13 +- src/components/Users/ManageUsers.tsx | 327 ++----------- src/components/Users/UserBanner.tsx | 134 +++++ src/components/Users/UserHome.tsx | 100 ++++ src/components/Users/UserInformation.tsx | 537 +++++++++++++++++++++ src/components/Users/UserResetPassword.tsx | 182 +++++++ src/components/Users/UserSummary.tsx | 104 ++++ 10 files changed, 1118 insertions(+), 300 deletions(-) create mode 100644 src/components/Users/UserBanner.tsx create mode 100644 src/components/Users/UserHome.tsx create mode 100644 src/components/Users/UserInformation.tsx create mode 100644 src/components/Users/UserResetPassword.tsx create mode 100644 src/components/Users/UserSummary.tsx diff --git a/src/Locale/en.json b/src/Locale/en.json index fa5bdeb6aff..0fc5f603622 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -204,6 +204,7 @@ "URINATION_FREQUENCY__DECREASED": "Decreased", "URINATION_FREQUENCY__INCREASED": "Increased", "URINATION_FREQUENCY__NORMAL": "Normal", + "USERMANAGEMENT_TAB__USERINFO": "User Information", "VENTILATOR": "Detailed Update", "VENTILATOR_MODE__CMV": "Control Mechanical Ventilation (CMV)", "VENTILATOR_MODE__PCV": "Pressure Control Ventilation (PCV)", @@ -369,6 +370,8 @@ "category": "Category", "caution": "Caution", "central_nursing_station": "Central Nursing Station", + "change_avatar": "Change Avatar", + "change_avatar_note": "JPG, GIF or PNG. 1MB max.", "change_file": "Change File", "change_password": "Change Password", "check_eligibility": "Check Eligibility", @@ -517,6 +520,9 @@ "date_of_test": "Date of sample collection for Covid testing", "days": "Days", "delete": "Delete", + "delete_account": "Delete account", + "delete_account_btn": "Yes, delete this account", + "delete_account_note": "Deleting this account will remove all associated data and cannot be undone.", "delete_facility": "Delete Facility", "delete_item": "Delete {{name}}", "delete_record": "Delete Record", @@ -954,6 +960,8 @@ "patient_transfer_birth_match_note": "Note: Year of birth must match the patient to process the transfer request.", "patients": "Patients", "personal_information": "Personal Information", + "personal_information_note": "Update User's Personal Information", + "personal_information_note_user": "Update Your Personal Information", "phone": "Phone", "phone_no": "Phone no.", "phone_number": "Phone Number", @@ -1052,6 +1060,7 @@ "resend_otp": "Resend OTP", "reset": "Reset", "reset_password": "Reset Password", + "reset_password_note": "Enter your current password, then create and confirm your new password", "resource": "Resource", "resource_approving_facility": "Resource approving facility", "resource_origin_facility": "Origin Facility", @@ -1242,6 +1251,7 @@ "view_details": "View Details", "view_faciliy": "View Facility", "view_patients": "View Patients", + "view_user": "View User", "view_users": "View Users", "virtual_nursing_assistant": "Virtual Nursing Assistant", "vitals": "Vitals", diff --git a/src/Routers/routes/UserRoutes.tsx b/src/Routers/routes/UserRoutes.tsx index d7542c15a15..53cb13414a2 100644 --- a/src/Routers/routes/UserRoutes.tsx +++ b/src/Routers/routes/UserRoutes.tsx @@ -2,9 +2,16 @@ import ManageUsers from "@/components/Users/ManageUsers"; import { UserAdd } from "@/components/Users/UserAdd"; import UserProfile from "@/components/Users/UserProfile"; import { AppRoutes } from "../AppRouter"; +import UserHome from "@/components/Users/UserHome"; const UserRoutes: AppRoutes = { "/users": () => , + "/users/:username": ({ username }) => ( + + ), + "/users/:username/:tab": ({ username, tab }) => ( + + ), "/users/add": () => , "/user/profile": () => , }; diff --git a/src/components/Common/UserDetails.tsx b/src/components/Common/UserDetails.tsx index 2e4355f98bb..96139d03d6e 100644 --- a/src/components/Common/UserDetails.tsx +++ b/src/components/Common/UserDetails.tsx @@ -7,8 +7,8 @@ function UserDetails(props: { }) { return (
-
- {props.title}: +
+ {props.title}
{props.children}
diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index 36b542f889d..b23e46d3eec 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -1,5 +1,5 @@ import { isEmpty, omitBy } from "lodash-es"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { classNames } from "../../Utils/utils"; import { Cancel, Submit } from "@/components/Common/components/ButtonV2"; import { FieldValidator } from "./FieldValidators"; @@ -23,6 +23,7 @@ type Props = { onDraftRestore?: (newState: FormState) => void; children: (props: FormContextValue) => React.ReactNode; hideRestoreDraft?: boolean; + resetFormVals?: boolean; }; const Form = ({ @@ -33,6 +34,7 @@ const Form = ({ const initial = { form: props.defaults, errors: {} }; const [isLoading, setIsLoading] = useState(!!asyncGetDefaults); const [state, dispatch] = useAutoSaveReducer(formReducer, initial); + const formVals = useRef(props.defaults); useEffect(() => { if (!asyncGetDefaults) return; @@ -68,6 +70,13 @@ const Form = ({ } }; + const handleCancel = () => { + if (props.resetFormVals) { + dispatch({ type: "set_form", form: formVals.current }); + } + props.onCancel?.(); + }; + const { Provider, Consumer } = useMemo(() => createFormContext(), []); const disabled = isLoading || props.disabled; @@ -112,7 +121,7 @@ const Form = ({
(null); - const [expandWorkingHours, setExpandWorkingHours] = useState(false); const authUser = useAuthUser(); - const [weeklyHours, setWeeklyHours] = useState("0"); const userIndex = USER_TYPES.indexOf(authUser.user_type); const userTypes = authUser.is_superuser ? [...USER_TYPES] : USER_TYPES.slice(0, userIndex + 1); - const [userData, setUserData] = useState<{ - show: boolean; - username: string; - name: string; - }>({ show: false, username: "", name: "" }); - - const [weeklyHoursError, setWeeklyHoursError] = useState(""); - const extremeSmallScreenBreakpoint = 320; const isExtremeSmallScreen = width <= extremeSmallScreenBreakpoint; @@ -80,27 +61,26 @@ export default function ManageUsers() { prefetch: !!qParams.home_facility && qParams.home_facility !== "NONE", }); - const { - data: userListData, - loading: userListLoading, - refetch: refetchUserList, - } = useQuery(routes.userList, { - query: { - limit: resultsPerPage.toString(), - offset: ( - (qParams.page ? qParams.page - 1 : 0) * resultsPerPage - ).toString(), - username: qParams.username, - first_name: qParams.first_name, - last_name: qParams.last_name, - phone_number: qParams.phone_number, - alt_phone_number: qParams.alt_phone_number, - user_type: qParams.user_type, - district_id: qParams.district, - home_facility: qParams.home_facility, - last_active_days: qParams.last_active_days, + const { data: userListData, loading: userListLoading } = useQuery( + routes.userList, + { + query: { + limit: resultsPerPage.toString(), + offset: ( + (qParams.page ? qParams.page - 1 : 0) * resultsPerPage + ).toString(), + username: qParams.username, + first_name: qParams.first_name, + last_name: qParams.last_name, + phone_number: qParams.phone_number, + alt_phone_number: qParams.alt_phone_number, + user_type: qParams.user_type, + district_id: qParams.district, + home_facility: qParams.home_facility, + last_active_days: qParams.last_active_days, + }, }, - }); + ); useEffect(() => { if (!qParams.state && qParams.district) { @@ -130,62 +110,6 @@ export default function ManageUsers() { ); - const handleCancel = () => { - setUserData({ show: false, username: "", name: "" }); - }; - - const handleWorkingHourSubmit = async () => { - const username = selectedUser; - if (!username || !weeklyHours || +weeklyHours < 0 || +weeklyHours > 168) { - setWeeklyHoursError("Value should be between 0 and 168"); - return; - } - const { res, data, error } = await request(routes.partialUpdateUser, { - pathParams: { username }, - body: { weekly_working_hours: weeklyHours }, - }); - if (res && res.status === 200 && data) { - Notification.Success({ - msg: "Working hours updated successfully", - }); - setExpandWorkingHours(false); - setSelectedUser(null); - } else { - Notification.Error({ - msg: "Error while updating working hours: " + (error || ""), - }); - } - setWeeklyHours("0"); - setWeeklyHoursError(""); - await refetchUserList(); - }; - - const handleSubmit = async () => { - const { res, error } = await request(routes.deleteUser, { - pathParams: { username: userData.username }, - }); - if (res?.status === 204) { - Notification.Success({ - msg: "User deleted successfully", - }); - } else { - Notification.Error({ - msg: "Error while deleting User: " + (error || ""), - }); - } - - setUserData({ show: false, username: "", name: "" }); - await refetchUserList(); - }; - - const handleDelete = (user: any) => { - setUserData({ - show: true, - username: user.username, - name: formatName(user), - }); - }; - let userList: any[] = []; userListData?.results && userListData.results.length && @@ -246,14 +170,6 @@ export default function ManageUsers() { aria-label="Online" /> ) : null} - {showUserDelete(authUser, user) && ( -
handleDelete(user)} - > - -
- )}
)} - {user.user_type === "Doctor" && ( - <> -
- - {user.qualification ? ( - - {user.qualification} - - ) : ( - - {t("unknown")} - - )} - -
-
- - {user.doctor_experience_commenced_on ? ( - - {dayjs().diff( - user.doctor_experience_commenced_on, - "years", - false, - )}{" "} - years - - ) : ( - - {t("unknown")} - - )} - -
-
- - {user.doctor_medical_council_registration ? ( - - {user.doctor_medical_council_registration} - - ) : ( - - {t("unknown")} - - )} - -
- - )}
{user.local_body_object && ( @@ -345,24 +207,6 @@ export default function ManageUsers() { isExtremeSmallScreen ? "flex flex-wrap" : "grid grid-cols-2" }`} > - {user.user_type === "Nurse" && ( -
- - {user.qualification ? ( - - {user.qualification} - - ) : ( - - {t("unknown")} - - )} - -
- )} {user.created_by && (
@@ -388,69 +232,18 @@ export default function ManageUsers() {
)} -
- - {user.weekly_working_hours ? ( - - {user.weekly_working_hours} hours - - ) : ( - - - )} - -
- {user.username && ( -
-
- { - setExpandFacilityList(!expandFacilityList); - setSelectedUser(user); - }} - > - -

{t("linked_facilities")}

-
- { - setExpandSkillList(true); - setSelectedUser(user.username); - }} - > - -

{t("linked_skills")}

-
-
- {["DistrictAdmin", "StateAdmin"].includes( - authUser.user_type, - ) && ( -
- { - setExpandWorkingHours(true); - setSelectedUser(user.username); - setWeeklyHours(user.weekly_working_hours); - }} - > - -

- Set Average weekly working hours -

-
-
- )} -
- )} +
+ navigate(`/users/${user.username}`)} + > + {t("view_user")} + +
@@ -482,57 +275,6 @@ export default function ManageUsers() { return ( - {expandSkillList && ( - - )} - - - - { - setExpandWorkingHours(state); - setWeeklyHours("0"); - setWeeklyHoursError(""); - }} - slideFrom="right" - title={t("average_weekly_working_hours")} - dialogClass="md:w-[400px]" - > -
-
- {t("set_average_weekly_working_hours_for")} {selectedUser} -
- { - setWeeklyHours(e.value); - }} - error={weeklyHoursError} - required - label="" - type="number" - min={0} - max={168} - /> -
- -
-
-
-
{manageUsers}
- {userData.show && ( - - )}
); } diff --git a/src/components/Users/UserBanner.tsx b/src/components/Users/UserBanner.tsx new file mode 100644 index 00000000000..9e8c2d5d1f7 --- /dev/null +++ b/src/components/Users/UserBanner.tsx @@ -0,0 +1,134 @@ +import { UserModel } from "./models"; +import { useEffect, useState } from "react"; +import CareIcon from "@/CAREUI/icons/CareIcon"; +import { + isUserOnline, + formatName, + classNames, + relativeTime, +} from "@/Utils/utils"; +import dayjs from "dayjs"; +import { t } from "i18next"; +import UserDetails from "../Common/UserDetails"; +import UserDetailComponent from "../Common/UserDetailsComponet"; +import { Avatar } from "../Common/Avatar"; + +export default function UserBanner({ userData }: { userData: UserModel }) { + const [userOnline, setUserOnline] = useState(false); + + useEffect(() => { + if (!userData) return; + setUserOnline(isUserOnline(userData)); + }, [userData]); + + if (!userData) { + return; + } + + return ( + <> + { + <> +
+
+
+ +
+
+

+ {formatName(userData)} +

+
+ {userData.last_login && userOnline ? ( + + {" "} + {" "} + Currently Online + + ) : ( + <> +
+ + + {userData.last_login + ? relativeTime(userData.last_login) + : "Never"} + +
+ + )} +
+
+ + + {userData.username} + +
+
+
+ {userData.user_type && ( + + )} + {userData.district_object && ( + + )} + + + {userData.home_facility_object?.name || "No Home Facility"} + + + {["Doctor", "Nurse"].includes(userData.user_type) && ( + + {userData.qualification ? ( + + {userData.qualification} + + ) : ( + {t("unknown")} + )} + + )} + {userData.user_type === "Doctor" && ( + + {userData.doctor_experience_commenced_on ? ( + + {dayjs().diff( + userData.doctor_experience_commenced_on, + "years", + false, + )}{" "} + years + + ) : ( + {t("unknown")} + )} + + )} +
+
+
+ + } + + ); +} diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx new file mode 100644 index 00000000000..f68e0a5289a --- /dev/null +++ b/src/components/Users/UserHome.tsx @@ -0,0 +1,100 @@ +import { useState } from "react"; +import Page from "../Common/components/Page"; +import UserBanner from "./UserBanner"; +import UserSummaryTab from "./UserSummary"; +import routes from "@/Redux/api"; +import useQuery from "@/Utils/request/useQuery"; +import { UserModel } from "./models"; +import Loading from "../Common/Loading"; +import Error404 from "../ErrorPages/404"; +import { classNames, keysOf } from "@/Utils/utils"; +import { Link } from "raviger"; +import { useTranslation } from "react-i18next"; + +export interface UserDetailProps { + username: string; + tab: string; +} + +export default function UserHome(props: UserDetailProps) { + const { username, tab } = props; + const [userData, setUserData] = useState(); + const { t } = useTranslation(); + + const { loading } = useQuery(routes.getUserDetails, { + pathParams: { + username, + }, + onResponse: ({ res, data }) => { + if (res?.status === 200 && data) { + setUserData(data); + } + }, + }); + + const TABS = { + profile: UserSummaryTab, + }; + + let currentTab = undefined; + if (Object.keys(TABS).includes(tab.toUpperCase())) { + currentTab = tab.toUpperCase() as keyof typeof TABS; + } + + if (!currentTab) { + return ; + } + + if (loading || !userData) { + return ; + } + + const SelectedTab = TABS[currentTab]; + + return ( + <> + + { + <> + +
+
+
+ +
+
+
+ + + } +
+ + ); +} diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx new file mode 100644 index 00000000000..cb097e5b492 --- /dev/null +++ b/src/components/Users/UserInformation.tsx @@ -0,0 +1,537 @@ +import { useState, useReducer, useRef } from "react"; +import { GENDER_TYPES, LocalStorageKeys } from "@/common/constants"; +import { validateEmailAddress } from "@/common/validation"; +import * as Notification from "../../Utils/Notifications"; +import TextFormField from "../Form/FormFields/TextFormField"; +import ButtonV2 from "@/components/Common/components/ButtonV2"; +import { + dateQueryString, + formatDisplayName, + isValidUrl, + parsePhoneNumber, + sleep, +} from "@/Utils/utils"; +import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; +import { GenderType } from "./models"; +import dayjs from "../../Utils/dayjs"; +import useAuthUser from "@/common/hooks/useAuthUser"; +import { + FieldError, + PhoneNumberValidator, + RequiredFieldValidator, +} from "../Form/FieldValidators"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; +import DateFormField from "../Form/FormFields/DateFormField"; +import { useTranslation } from "react-i18next"; +import Loading from "@/components/Common/Loading"; +import AvatarEditModal from "@/components/Common/AvatarEditModal"; +import uploadFile from "@/Utils/request/uploadFile"; +import careConfig from "@careConfig"; +import { Avatar } from "../Common/Avatar"; +import Form from "../Form/Form"; +import RadioFormField from "../Form/FormFields/RadioFormField"; + +type EditForm = { + firstName: string; + lastName: string; + date_of_birth: Date | null | string; + gender: GenderType; + email: string; + video_connect_link: string | undefined; + phoneNumber: string; + altPhoneNumber: string; + user_type: string | undefined; + qualification: string | undefined; + doctor_experience_commenced_on: number | string | undefined; + doctor_medical_council_registration: string | undefined; + weekly_working_hours: string | null | undefined; +}; +type ErrorForm = { + firstName: string; + lastName: string; + date_of_birth: string | null; + gender: string; + email: string; + video_connect_link: string | undefined; + phoneNumber: string; + altPhoneNumber: string; + user_type: string | undefined; + qualification: string | undefined; + doctor_experience_commenced_on: number | string | undefined; + doctor_medical_council_registration: string | undefined; + weekly_working_hours: string | undefined; +}; +type State = { + form: EditForm; + errors: ErrorForm; +}; +type Action = + | { type: "set_form"; form: EditForm } + | { type: "set_error"; errors: ErrorForm }; + +const initForm: EditForm = { + firstName: "", + lastName: "", + date_of_birth: null, + gender: "Male", + video_connect_link: "", + email: "", + phoneNumber: "", + altPhoneNumber: "", + user_type: "", + qualification: undefined, + doctor_experience_commenced_on: undefined, + doctor_medical_council_registration: undefined, + weekly_working_hours: undefined, +}; + +const initError: ErrorForm = Object.assign( + {}, + ...Object.keys(initForm).map((k) => ({ [k]: "" })), +); + +const initialState: State = { + form: { ...initForm }, + errors: { ...initError }, +}; + +const editFormReducer = (state: State, action: Action) => { + switch (action.type) { + case "set_form": { + return { + ...state, + form: action.form, + }; + } + case "set_error": { + return { + ...state, + errors: action.errors, + }; + } + } +}; + +export default function UserInformation({ username }: { username: string }) { + const { t } = useTranslation(); + const [states, dispatch] = useReducer(editFormReducer, initialState); + const formVals = useRef(initForm); + const [editAvatar, setEditAvatar] = useState(false); + + const authUser = useAuthUser(); + + const { + data: userData, + loading: isLoading, + refetch: refetchUserData, + } = useQuery(routes.getUserDetails, { + pathParams: { + username, + }, + onResponse: (result) => { + if (!result || !result.res || !result.data) return; + + const formData: EditForm = { + firstName: result.data.first_name, + lastName: result.data.last_name, + date_of_birth: result.data.date_of_birth || null, + gender: result.data.gender || "Male", + email: result.data.email, + video_connect_link: result.data.video_connect_link, + phoneNumber: result.data.phone_number?.toString() || "", + altPhoneNumber: result.data.alt_phone_number?.toString() || "", + user_type: result.data.user_type, + qualification: result.data.qualification, + doctor_experience_commenced_on: dayjs().diff( + dayjs(result.data.doctor_experience_commenced_on), + "years", + ), + doctor_medical_council_registration: + result.data.doctor_medical_council_registration, + weekly_working_hours: result.data.weekly_working_hours, + }; + dispatch({ + type: "set_form", + form: formData, + }); + formVals.current = formData; + }, + }); + + const validateForm = () => { + const errors: Partial> = {}; + Object.keys(states.form).forEach((field) => { + switch (field) { + case "firstName": + case "lastName": + case "gender": + errors[field] = RequiredFieldValidator()(states.form[field]); + return; + case "date_of_birth": + if (!states.form[field]) { + errors[field] = "Enter a valid date of birth"; + } else if ( + !dayjs(states.form[field]).isValid() || + dayjs(states.form[field]).isAfter(dayjs().subtract(17, "year")) + ) { + errors[field] = "Enter a valid date of birth"; + } + return; + case "phoneNumber": + // eslint-disable-next-line no-case-declarations + const phoneNumber = parsePhoneNumber(states.form[field]); + + // eslint-disable-next-line no-case-declarations + let is_valid = false; + if (phoneNumber) { + is_valid = PhoneNumberValidator()(phoneNumber) === undefined; + } + + if (!states.form[field] || !is_valid) { + errors[field] = "Please enter valid phone number"; + } + return; + case "altPhoneNumber": + // eslint-disable-next-line no-case-declarations + let alt_is_valid = false; + if (states.form[field] && states.form[field] !== "+91") { + const altPhoneNumber = parsePhoneNumber(states.form[field]); + if (altPhoneNumber) { + alt_is_valid = + PhoneNumberValidator(["mobile"])(altPhoneNumber) === undefined; + } + } + + if ( + states.form[field] && + states.form[field] !== "+91" && + !alt_is_valid + ) { + errors[field] = "Please enter valid mobile number"; + } + return; + case "email": + if (!states.form[field]) { + errors[field] = t("field_required"); + } else if (!validateEmailAddress(states.form[field])) { + errors[field] = "Enter a valid email address"; + } + return; + case "doctor_experience_commenced_on": + if (states.form.user_type === "Doctor" && !states.form[field]) { + errors[field] = t("field_required"); + } else if ( + (states.form.user_type === "Doctor" && + Number(states.form.doctor_experience_commenced_on) >= 100) || + Number(states.form.doctor_experience_commenced_on) < 0 + ) { + errors[field] = + "Doctor experience should be at least 0 years and less than 100 years."; + } + return; + case "qualification": + if ( + (states.form.user_type === "Doctor" || + states.form.user_type === "Nurse") && + !states.form[field] + ) { + errors[field] = t("field_required"); + } + return; + case "doctor_medical_council_registration": + if (states.form.user_type === "Doctor" && !states.form[field]) { + errors[field] = t("field_required"); + } + return; + case "weekly_working_hours": + if ( + states.form[field] && + (Number(states.form[field]) < 0 || + Number(states.form[field]) > 168 || + !/^\d+$/.test(states.form[field] ?? "")) + ) { + errors[field] = + "Average weekly working hours must be a number between 0 and 168"; + } + return; + case "video_connect_link": + if (states.form[field]) { + if (isValidUrl(states.form[field]) === false) { + errors[field] = "Please enter a valid url"; + } + } + return; + } + }); + return errors; + }; + + const getDate = (value: any) => + value && dayjs(value).isValid() && dayjs(value).toDate(); + + const handleCancel = () => { + dispatch({ + type: "set_form", + form: formVals.current, + }); + }; + + if (isLoading || !userData) { + return ; + } + + const handleSubmit = async (e: any) => { + e.preventDefault(); + const data = { + username: userData.username, + first_name: states.form.firstName, + last_name: states.form.lastName, + email: states.form.email, + video_connect_link: states.form.video_connect_link, + phone_number: parsePhoneNumber(states.form.phoneNumber) ?? "", + alt_phone_number: parsePhoneNumber(states.form.altPhoneNumber) ?? "", + gender: states.form.gender, + date_of_birth: dateQueryString(states.form.date_of_birth), + qualification: + states.form.user_type === "Doctor" || states.form.user_type === "Nurse" + ? states.form.qualification + : undefined, + doctor_experience_commenced_on: + states.form.user_type === "Doctor" + ? dayjs() + .subtract( + parseInt( + (states.form.doctor_experience_commenced_on as string) ?? "0", + ), + "years", + ) + .format("YYYY-MM-DD") + : undefined, + doctor_medical_council_registration: + states.form.user_type === "Doctor" + ? states.form.doctor_medical_council_registration + : undefined, + weekly_working_hours: + states.form.weekly_working_hours && + states.form.weekly_working_hours !== "" + ? states.form.weekly_working_hours + : null, + }; + const { res } = await request(routes.partialUpdateUser, { + pathParams: { username: authUser.username }, + body: data, + }); + if (res?.ok) { + Notification.Success({ + msg: "Details updated successfully", + }); + await refetchUserData(); + } + }; + + const handleAvatarUpload = async (file: File, onError: () => void) => { + const formData = new FormData(); + formData.append("profile_picture", file); + const url = `${careConfig.apiUrl}/api/v1/users/${authUser.username}/profile_picture/`; + + uploadFile( + url, + formData, + "POST", + { + Authorization: + "Bearer " + localStorage.getItem(LocalStorageKeys.accessToken), + }, + async (xhr: XMLHttpRequest) => { + if (xhr.status === 200) { + await sleep(1000); + refetchUserData(); + Notification.Success({ msg: "Profile picture updated." }); + setEditAvatar(false); + } + }, + null, + () => { + onError(); + }, + ); + }; + + const handleAvatarDelete = async (onError: () => void) => { + const { res } = await request(routes.deleteProfilePicture, { + pathParams: { username: authUser.username }, + }); + if (res?.ok) { + Notification.Success({ msg: "Profile picture deleted" }); + await refetchUserData(); + setEditAvatar(false); + } else { + onError(); + } + }; + + return ( + <> + setEditAvatar(false)} + /> +
+
+
+ +
+ setEditAvatar(!editAvatar)} + type="button" + id="edit-cancel-profile-button" + className="border border-gray-200 bg-gray-50 text-black hover:bg-gray-100" + shadow={false} + > + {t("change_avatar")} + +

+ {t("change_avatar_note")} +

+
+
+
+ {!isLoading && ( +
+ + disabled={isLoading} + defaults={userData ? states.form : initForm} + validate={validateForm} + onCancel={handleCancel} + onSubmit={handleSubmit} + hideRestoreDraft + noPadding + resetFormVals + > + {(field) => ( + <> +
+ + +
+
+ + o.text} + optionValue={(o) => o.text} + options={GENDER_TYPES} + /> +
+
+ + +
+
+ + {(states.form.user_type === "Doctor" || + states.form.user_type === "Nurse") && ( + + )} +
+ {states.form.user_type === "Doctor" && ( + <> + + + + )} +
+ + +
+ + )} + +
+ )} +
+ + ); +} diff --git a/src/components/Users/UserResetPassword.tsx b/src/components/Users/UserResetPassword.tsx new file mode 100644 index 00000000000..3104a1bcc8e --- /dev/null +++ b/src/components/Users/UserResetPassword.tsx @@ -0,0 +1,182 @@ +import { useState } from "react"; +import * as Notification from "../../Utils/Notifications"; +import TextFormField from "../Form/FormFields/TextFormField"; +import { Submit } from "@/components/Common/components/ButtonV2"; +import { UpdatePasswordForm } from "./models"; +import useAuthUser from "@/common/hooks/useAuthUser"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; +import { validateRule } from "./UserAdd"; +import { useTranslation } from "react-i18next"; + +export default function UserResetPassword() { + const { t } = useTranslation(); + const authUser = useAuthUser(); + + const [changePasswordForm, setChangePasswordForm] = useState<{ + username: string; + old_password: string; + new_password_1: string; + new_password_2: string; + }>({ + username: authUser.username, + old_password: "", + new_password_1: "", + new_password_2: "", + }); + + const [changePasswordErrors] = useState<{ + old_password: string; + password_confirmation: string; + }>({ + old_password: "", + password_confirmation: "", + }); + + const validateNewPassword = (password: string) => { + if ( + password.length < 8 || + !/\d/.test(password) || + password === password.toUpperCase() || + password === password.toLowerCase() + ) { + return false; + } + return true; + }; + + const changePassword = async (e: any) => { + e.preventDefault(); + //validating form + if ( + changePasswordForm.new_password_1 !== changePasswordForm.new_password_2 + ) { + Notification.Error({ + msg: "Passwords are different in new password and confirmation password column.", + }); + } else if (!validateNewPassword(changePasswordForm.new_password_1)) { + Notification.Error({ + msg: "Entered New Password is not valid, please check!", + }); + } else if ( + changePasswordForm.new_password_1 === changePasswordForm.old_password + ) { + Notification.Error({ + msg: "New password is same as old password, Please enter a different new password.", + }); + } else { + const form: UpdatePasswordForm = { + old_password: changePasswordForm.old_password, + username: authUser.username, + new_password: changePasswordForm.new_password_1, + }; + const { res, data, error } = await request(routes.updatePassword, { + body: form, + }); + if (res?.ok) { + Notification.Success({ msg: data?.message }); + } else if (!error) { + Notification.Error({ + msg: "There was some error. Please try again in some time.", + }); + } + setChangePasswordForm({ + ...changePasswordForm, + new_password_1: "", + new_password_2: "", + old_password: "", + }); + } + }; + + return ( + <> +
+
+
+
+ + setChangePasswordForm({ + ...changePasswordForm, + old_password: e.value, + }) + } + error={changePasswordErrors.old_password} + required + /> +
+ { + setChangePasswordForm({ + ...changePasswordForm, + new_password_1: e.value, + }); + }} + required + /> +
+ {validateRule( + changePasswordForm.new_password_1?.length >= 8, + "Password should be atleast 8 characters long", + )} + {validateRule( + changePasswordForm.new_password_1 !== + changePasswordForm.new_password_1.toUpperCase(), + "Password should contain at least 1 lowercase letter", + )} + {validateRule( + changePasswordForm.new_password_1 !== + changePasswordForm.new_password_1.toLowerCase(), + "Password should contain at least 1 uppercase letter", + )} + {validateRule( + /\d/.test(changePasswordForm.new_password_1), + "Password should contain at least 1 number", + )} +
+
+
+ { + setChangePasswordForm({ + ...changePasswordForm, + new_password_2: e.value, + }); + }} + /> + {changePasswordForm.new_password_2.length > 0 && ( +
+ {validateRule( + changePasswordForm.new_password_1 === + changePasswordForm.new_password_2, + "Confirm password should match the new password", + )} +
+ )} +
+
+
+ +
+
+
+
+ + ); +} diff --git a/src/components/Users/UserSummary.tsx b/src/components/Users/UserSummary.tsx new file mode 100644 index 00000000000..8e193f274e1 --- /dev/null +++ b/src/components/Users/UserSummary.tsx @@ -0,0 +1,104 @@ +import { useTranslation } from "react-i18next"; +import UserResetPassword from "./UserResetPassword"; +import UserInformation from "./UserInformation"; +import { useState } from "react"; +import routes from "@/Redux/api"; +import CareIcon from "@/CAREUI/icons/CareIcon"; +import useAppHistory from "@/common/hooks/useAppHistory"; +import useAuthUser from "@/common/hooks/useAuthUser"; +import { showUserDelete } from "@/Utils/permissions"; +import request from "@/Utils/request/request"; +import UserDeleteDialog from "./UserDeleteDialog"; +import * as Notification from "../../Utils/Notifications"; +import { UserModel } from "./models"; +import ButtonV2 from "../Common/components/ButtonV2"; + +export default function UserSummaryTab({ userData }: { userData?: UserModel }) { + const { t } = useTranslation(); + const [showDeleteDialog, setshowDeleteDialog] = useState(false); + const authUser = useAuthUser(); + const { goBack } = useAppHistory(); + + if (!userData) { + return; + } + + const handleSubmit = async () => { + const { res, error } = await request(routes.deleteUser, { + pathParams: { username: userData.username }, + }); + if (res?.status === 204) { + Notification.Success({ + msg: "User deleted successfully", + }); + } else { + Notification.Error({ + msg: "Error while deleting User: " + (error || ""), + }); + } + setshowDeleteDialog(!showDeleteDialog); + goBack(); + }; + + return ( + <> + {showDeleteDialog && ( + { + setshowDeleteDialog(false); + }} + /> + )} +
+
+
+

+

{t("personal_information")}

+

+ {t("personal_information_note")} +

+

+
+
+ +
+
+ +
+
+

+

{t("reset_password")}

+

{t("reset_password_note")}

+

+
+
+ +
+
+ +
+
+

+

{t("delete_account")}

+

{t("delete_account_note")}

+

+
+
+ showUserDelete(authUser, userData)} + onClick={() => setshowDeleteDialog(true)} + variant="danger" + data-testid="user-delete-button" + className="my-1 inline-flex" + > + + {t("delete_account_btn")} + +
+
+
+ + ); +} From c74888f45a34b71b171ab1b8a80486e4a6c60839 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Fri, 1 Nov 2024 20:14:57 +0530 Subject: [PATCH 02/55] Minor fixes/refactoring --- src/Locale/en.json | 2 +- src/components/Common/UserColumns.tsx | 29 +++++++++++++++++ src/components/Users/UserHome.tsx | 6 ++-- src/components/Users/UserInformation.tsx | 15 ++++----- src/components/Users/UserSummary.tsx | 41 +++++++++--------------- 5 files changed, 54 insertions(+), 39 deletions(-) create mode 100644 src/components/Common/UserColumns.tsx diff --git a/src/Locale/en.json b/src/Locale/en.json index 0fc5f603622..7c284829c12 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -204,7 +204,7 @@ "URINATION_FREQUENCY__DECREASED": "Decreased", "URINATION_FREQUENCY__INCREASED": "Increased", "URINATION_FREQUENCY__NORMAL": "Normal", - "USERMANAGEMENT_TAB__USERINFO": "User Information", + "USERMANAGEMENT_TAB__PROFILE": "User Information", "VENTILATOR": "Detailed Update", "VENTILATOR_MODE__CMV": "Control Mechanical Ventilation (CMV)", "VENTILATOR_MODE__PCV": "Pressure Control Ventilation (PCV)", diff --git a/src/components/Common/UserColumns.tsx b/src/components/Common/UserColumns.tsx new file mode 100644 index 00000000000..e805b94ff89 --- /dev/null +++ b/src/components/Common/UserColumns.tsx @@ -0,0 +1,29 @@ +import { UserModel } from "../Users/models"; + +export type userChildProps = { + userData: UserModel; + username: string; +}; + +export default function userColumns( + heading: string, + note: string, + Child: (childProps: userChildProps) => JSX.Element, + childProps: userChildProps, +) { + return ( + <> +
+
+

+

{heading}

+

{note}

+

+
+
+ +
+
+ + ); +} diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index f68e0a5289a..f8639f3ab7d 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -11,12 +11,12 @@ import { classNames, keysOf } from "@/Utils/utils"; import { Link } from "raviger"; import { useTranslation } from "react-i18next"; -export interface UserDetailProps { +export interface UserHomeProps { username: string; tab: string; } -export default function UserHome(props: UserDetailProps) { +export default function UserHome(props: UserHomeProps) { const { username, tab } = props; const [userData, setUserData] = useState(); const { t } = useTranslation(); @@ -33,7 +33,7 @@ export default function UserHome(props: UserDetailProps) { }); const TABS = { - profile: UserSummaryTab, + PROFILE: UserSummaryTab, }; let currentTab = undefined; diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx index cb097e5b492..af5875c1371 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserInformation.tsx @@ -14,7 +14,6 @@ import { import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import { GenderType } from "./models"; import dayjs from "../../Utils/dayjs"; -import useAuthUser from "@/common/hooks/useAuthUser"; import { FieldError, PhoneNumberValidator, @@ -120,8 +119,6 @@ export default function UserInformation({ username }: { username: string }) { const formVals = useRef(initForm); const [editAvatar, setEditAvatar] = useState(false); - const authUser = useAuthUser(); - const { data: userData, loading: isLoading, @@ -320,7 +317,7 @@ export default function UserInformation({ username }: { username: string }) { : null, }; const { res } = await request(routes.partialUpdateUser, { - pathParams: { username: authUser.username }, + pathParams: { username: userData.username }, body: data, }); if (res?.ok) { @@ -334,7 +331,7 @@ export default function UserInformation({ username }: { username: string }) { const handleAvatarUpload = async (file: File, onError: () => void) => { const formData = new FormData(); formData.append("profile_picture", file); - const url = `${careConfig.apiUrl}/api/v1/users/${authUser.username}/profile_picture/`; + const url = `${careConfig.apiUrl}/api/v1/users/${userData.username}/profile_picture/`; uploadFile( url, @@ -361,7 +358,7 @@ export default function UserInformation({ username }: { username: string }) { const handleAvatarDelete = async (onError: () => void) => { const { res } = await request(routes.deleteProfilePicture, { - pathParams: { username: authUser.username }, + pathParams: { username }, }); if (res?.ok) { Notification.Success({ msg: "Profile picture deleted" }); @@ -377,7 +374,7 @@ export default function UserInformation({ username }: { username: string }) { setEditAvatar(false)} @@ -386,8 +383,8 @@ export default function UserInformation({ username }: { username: string }) {
diff --git a/src/components/Users/UserSummary.tsx b/src/components/Users/UserSummary.tsx index 8e193f274e1..c647cd26fcb 100644 --- a/src/components/Users/UserSummary.tsx +++ b/src/components/Users/UserSummary.tsx @@ -12,6 +12,7 @@ import UserDeleteDialog from "./UserDeleteDialog"; import * as Notification from "../../Utils/Notifications"; import { UserModel } from "./models"; import ButtonV2 from "../Common/components/ButtonV2"; +import userColumns from "../Common/UserColumns"; export default function UserSummaryTab({ userData }: { userData?: UserModel }) { const { t } = useTranslation(); @@ -40,6 +41,8 @@ export default function UserSummaryTab({ userData }: { userData?: UserModel }) { goBack(); }; + const userColumnsData = { userData, username: userData.username }; + return ( <> {showDeleteDialog && ( @@ -52,32 +55,18 @@ export default function UserSummaryTab({ userData }: { userData?: UserModel }) { /> )}
-
-
-

-

{t("personal_information")}

-

- {t("personal_information_note")} -

-

-
-
- -
-
- -
-
-

-

{t("reset_password")}

-

{t("reset_password_note")}

-

-
-
- -
-
- + {userColumns( + t("personal_information"), + t("personal_information_note"), + UserInformation, + userColumnsData, + )} + {userColumns( + t("reset_password"), + t("reset_password_note"), + UserResetPassword, + userColumnsData, + )}

From 64b7176cf536876d70fa857adeec5b137e87da16 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Sun, 3 Nov 2024 10:14:25 +0530 Subject: [PATCH 03/55] Added LinkedFacilitiesTab and components --- src/Locale/en.json | 14 + src/components/Users/ConfirmFacilityModal.tsx | 78 +++++ .../Users/ConfirmHomeFacilityUpdateDialog.tsx | 51 --- src/components/Users/LinkedFacilities.tsx | 297 ++++++++++++++++++ src/components/Users/LinkedFacilitiesTab.tsx | 31 ++ src/components/Users/ManageUsers.tsx | 50 --- src/components/Users/SkillsSlideOver.tsx | 168 ---------- .../Users/SkillsSlideOverComponents.tsx | 63 ---- src/components/Users/UserHome.tsx | 2 + 9 files changed, 422 insertions(+), 332 deletions(-) create mode 100644 src/components/Users/ConfirmFacilityModal.tsx delete mode 100644 src/components/Users/ConfirmHomeFacilityUpdateDialog.tsx create mode 100644 src/components/Users/LinkedFacilities.tsx create mode 100644 src/components/Users/LinkedFacilitiesTab.tsx delete mode 100644 src/components/Users/SkillsSlideOver.tsx delete mode 100644 src/components/Users/SkillsSlideOverComponents.tsx diff --git a/src/Locale/en.json b/src/Locale/en.json index 7c284829c12..e2716fd44bf 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -204,6 +204,7 @@ "URINATION_FREQUENCY__DECREASED": "Decreased", "URINATION_FREQUENCY__INCREASED": "Increased", "URINATION_FREQUENCY__NORMAL": "Normal", + "USERMANAGEMENT_TAB__FACILITIES": "Linked Facilities", "USERMANAGEMENT_TAB__PROFILE": "User Information", "VENTILATOR": "Detailed Update", "VENTILATOR_MODE__CMV": "Control Mechanical Ventilation (CMV)", @@ -262,6 +263,7 @@ "add_beds": "Add Bed(s)", "add_beds_to_configure_presets": "Add beds to this location to configure presets for them.", "add_details_of_patient": "Add Details of Patient", + "add_facility": "Add Facility", "add_location": "Add Location", "add_new_beds": "Add New Bed(s)", "add_new_user": "Add New User", @@ -421,6 +423,7 @@ "clear": "Clear", "clear_all_filters": "Clear All Filters", "clear_home_facility": "Clear Home Facility", + "clear_home_facility_confirm": "Are you sure you want to clear the home facility", "clear_selection": "Clear selection", "close": "Close", "close_scanner": "Close Scanner", @@ -673,6 +676,7 @@ "forget_password": "Forgot password?", "forget_password_instruction": "Enter your username, and if it exists, we will send you a link to reset your password.", "frequency": "Frequency", + "from_user": "from User", "full_name": "Full Name", "full_screen": "Full Screen", "gender": "Gender", @@ -779,6 +783,7 @@ "link_camera_and_bed": "Link bed to Camera", "link_existing_abha_profile": "Already have an ABHA number", "linked_facilities": "Linked Facilities", + "linked_facilities_note": "Add or remove facilities and set or change the Home Facility", "linked_skills": "Linked Skills", "liquid_oxygen_capacity": "Liquid Oxygen Capacity", "list_view": "List View", @@ -1044,6 +1049,9 @@ "reload": "Reload", "remove": "Remove", "rename": "Rename", + "replace_home_facility": "Replace Home Facility", + "replace_home_facility_confirm": "Are you sure you want to replace", + "replace_home_facility_confirm_as": "as home facility for user", "reply": "Reply", "report": "Report", "req_atleast_one_digit": "Require at least one digit", @@ -1120,6 +1128,7 @@ "session_expired": "Session Expired", "session_expired_msg": "It appears that your session has expired. This could be due to inactivity. Please login again to continue.", "set_average_weekly_working_hours_for": "Set Average weekly working hours for", + "set_home_facility": "Set as home facility", "set_your_local_language": "Set your local language", "settings_and_filters": "Settings and Filters", "severity_of_breathlessness": "Severity of Breathlessness", @@ -1199,6 +1208,10 @@ "unlink_asset_bed_and_presets": "Delete linked presets and unlink bed", "unlink_asset_bed_caution": "This action will also delete all presets that are associated to this camera and bed.", "unlink_camera_and_bed": "Unlink this bed from this camera", + "unlink_facility": "Unlink Facility", + "unlink_facility_access": "The user will lose access to the facility", + "unlink_facility_confirm": "Are you sure you want to unlink the facility", + "unlink_this_facility": "Unlink Facility", "unsubscribe": "Unsubscribe", "unsubscribe_failed": "Unsubscribe failed.", "unsupported_browser": "Unsupported Browser", @@ -1263,6 +1276,7 @@ "whatsapp_number": "Whatsapp Number", "why_the_asset_is_not_working": "Why the asset is not working?", "width": "Width ({{unit}})", + "with": "with", "working_status": "Working Status", "years": "years", "years_of_experience": "Years of Experience", diff --git a/src/components/Users/ConfirmFacilityModal.tsx b/src/components/Users/ConfirmFacilityModal.tsx new file mode 100644 index 00000000000..3395091406d --- /dev/null +++ b/src/components/Users/ConfirmFacilityModal.tsx @@ -0,0 +1,78 @@ +import ConfirmDialog from "@/components/Common/ConfirmDialog"; +import { FacilityModel } from "../Facility/models"; +import { useTranslation } from "react-i18next"; + +const ConfirmFacilityModal = ({ + username, + currentFacility, + homeFacility, + handleCancel, + handleOk, + type, +}: { + username: string; + currentFacility?: FacilityModel; + homeFacility?: FacilityModel; + handleCancel: () => void; + handleOk: () => void; + type: string; +}) => { + const { t } = useTranslation(); + const title = t(type); + let action = ""; + let body; + switch (type) { + case "unlink_facility": + action = "Unlink"; + body = ( +

+
+ {t("unlink_facility_confirm")}{" "} + {currentFacility?.name} {t("from_user")}{" "} + {username} ? +
+ {t("unlink_facility_access")} +
+
+ ); + break; + case "clear_home_facility": + action = "Clear"; + body = ( +
+
+ {t("clear_home_facility_confirm")}{" "} + {currentFacility?.name} {t("from_user")}{" "} + {username} ? +
+
+
+ ); + break; + case "replace_home_facility": + action = "Replace"; + body = ( +
+ {t("replace_home_facility_confirm")}{" "} + {homeFacility?.name} {t("with")}{" "} + {currentFacility?.name}{" "} + {t("replace_home_facility_confirm_as")} {username}? +
+ ); + break; + } + return ( + {title}} + show={true} + action={action} + onClose={handleCancel} + onConfirm={handleOk} + variant="danger" + > +
{body}
+
+ ); +}; + +export default ConfirmFacilityModal; diff --git a/src/components/Users/ConfirmHomeFacilityUpdateDialog.tsx b/src/components/Users/ConfirmHomeFacilityUpdateDialog.tsx deleted file mode 100644 index d31c281fb4e..00000000000 --- a/src/components/Users/ConfirmHomeFacilityUpdateDialog.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useState } from "react"; -import ConfirmDialog from "@/components/Common/ConfirmDialog"; - -interface ConfirmDialogProps { - previousFacilityName: string; - userName: string; - newFacilityName: string; - handleCancel: () => void; - handleOk: () => void; -} - -const ConfirmHomeFacilityUpdateDialog = (props: ConfirmDialogProps) => { - const { - previousFacilityName, - userName, - newFacilityName, - handleCancel, - handleOk, - } = props; - - const [disable, setDisable] = useState(false); - - const handleSubmit = () => { - handleOk(); - setDisable(true); - }; - return ( - Replace Home Facility} - show={true} - action={"Replace"} - onClose={handleCancel} - onConfirm={handleSubmit} - disabled={disable} - variant="danger" - > -
-
- Are you sure you want to replace{" "} - {previousFacilityName} with{" "} - {newFacilityName} as home facility for user{" "} - {userName} - ? -
-
-
-
- ); -}; - -export default ConfirmHomeFacilityUpdateDialog; diff --git a/src/components/Users/LinkedFacilities.tsx b/src/components/Users/LinkedFacilities.tsx new file mode 100644 index 00000000000..8c67d3facc2 --- /dev/null +++ b/src/components/Users/LinkedFacilities.tsx @@ -0,0 +1,297 @@ +import { useState } from "react"; +import ButtonV2 from "../Common/components/ButtonV2"; +import { FacilitySelect } from "../Common/FacilitySelect"; +import routes from "@/Redux/api"; +import request from "@/Utils/request/request"; +import useQuery from "@/Utils/request/useQuery"; +import { FacilityModel } from "../Facility/models"; +import { UserModel } from "./models"; +import * as Notification from "../../Utils/Notifications"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import CareIcon from "@/CAREUI/icons/CareIcon"; +import ConfirmFacilityModal from "./ConfirmFacilityModal"; +import { useTranslation } from "react-i18next"; + +const initModalProps: { + selectedFacility?: FacilityModel; + type: string; + toggle: boolean; +} = { + toggle: false, + selectedFacility: undefined, + type: "", +}; + +export default function LinkedFacilities({ + userData, +}: { + userData: UserModel; +}) { + const [facility, setFacility] = useState(null); + const [userFacilities, setUserFacilities] = useState< + FacilityModel[] | null + >(); + const [homeFacility, setHomeFacility] = useState(); + const [modalProps, setModalProps] = useState(initModalProps); + const { t } = useTranslation(); + + const { refetch: refetchUserFacilities } = useQuery(routes.userListFacility, { + pathParams: { username: userData.username }, + query: { limit: 36 }, + onResponse({ res, data }) { + if (res?.status === 200 && data) { + let userFacilities = data?.results; + if (userData.home_facility_object) { + const homeFacility = data?.results.find( + (facility) => facility.id === userData.home_facility_object?.id, + ); + userFacilities = userFacilities.filter( + (facility) => facility.id !== homeFacility?.id, + ); + setHomeFacility(homeFacility); + } + setUserFacilities(userFacilities); + } + }, + }); + + const handleOnClick = (type: string, selectedFacility: FacilityModel) => { + switch (type) { + case "clear_home_facility": + case "unlink_facility": + case "replace_home_facility": + setModalProps({ + selectedFacility, + type: type, + toggle: true, + }); + break; + case "set_home_facility": + replaceHomeFacility(selectedFacility); + break; + } + }; + + const handleModalCancel = () => { + setModalProps(initModalProps); + }; + + const handleModalOk = () => { + switch (modalProps.type) { + case "unlink_facility": + unlinkFacility(); + break; + case "clear_home_facility": + clearHomeFacility(); + break; + case "replace_home_facility": + replaceHomeFacility(); + break; + } + setModalProps(initModalProps); + }; + + const replaceHomeFacility = async (facility?: FacilityModel) => { + const selectedFacility = facility ?? modalProps.selectedFacility; + const { res } = await request(routes.partialUpdateUser, { + pathParams: { username: userData.username }, + body: { home_facility: selectedFacility?.id?.toString() }, + }); + if (!res?.ok) { + Notification.Error({ + msg: "Error while updating Home facility", + }); + } else { + userData.home_facility_object = selectedFacility; + setHomeFacility(selectedFacility); + Notification.Success({ + msg: "Home Facility updated successfully", + }); + } + await refetchUserFacilities(); + }; + + const clearHomeFacility = async () => { + const { res } = await request(routes.clearHomeFacility, { + pathParams: { username: userData.username }, + }); + + if (!res?.ok) { + Notification.Error({ + msg: "Error while clearing home facility", + }); + } else { + userData.home_facility_object = undefined; + setHomeFacility(undefined); + Notification.Success({ + msg: "Home Facility cleared successfully", + }); + } + await refetchUserFacilities(); + }; + + const unlinkFacility = async () => { + const { res } = await request(routes.deleteUserFacility, { + pathParams: { username: userData.username }, + body: { facility: modalProps.selectedFacility?.id?.toString() }, + }); + if (!res?.ok) { + Notification.Error({ + msg: "Error while unlinking home facility", + }); + } else { + Notification.Success({ + msg: "Facility unlinked successfully", + }); + } + await refetchUserFacilities(); + }; + + const linkFacility = async ( + username: string, + facility: FacilityModel | null, + ) => { + //setIsLoading(true); + const { res } = await request(routes.addUserFacility, { + pathParams: { username }, + body: { facility: facility?.id?.toString() }, + }); + + if (!res?.ok) { + Notification.Error({ + msg: "Error while linking facility", + }); + } else { + Notification.Success({ + msg: "Facility linked successfully", + }); + } + await refetchUserFacilities(); + //setIsLoading(false); + setFacility(null); + }; + + const renderFacilityButtons = (facility: FacilityModel) => { + if (!facility) return; + return ( +
+ +
+
{facility.name}
+ +
+ +
+
+
+ + + + handleOnClick( + homeFacility ? "replace_home_facility" : "set_home_facility", + facility, + ) + } + > + {t("set_home_facility")} + + handleOnClick("unlink_facility", facility)} + > + {t("unlink_this_facility")} + + +
+
+ ); + }; + + const renderHomeFacilityButton = (homeFacility: FacilityModel) => { + return ( +
+
+
{homeFacility.name}
+
+ +
+
+
+ ); + }; + + return ( + <> + {modalProps.toggle && ( + + )} +
+
+ + linkFacility(userData.username, facility)} + > + {t("add_facility")} + +
+ + {homeFacility && ( +
+

{t("home_facility")}

+
+ {renderHomeFacilityButton(homeFacility)} +
+
+ )} + {userFacilities && userFacilities.length > 0 && ( +
+

{t("linked_facilities")}

+ +
+ {userFacilities.map((facility: FacilityModel) => { + if (homeFacility?.id === facility.id) { + return null; + } + return renderFacilityButtons(facility); + })} +
+
+ )} +
+ + ); +} diff --git a/src/components/Users/LinkedFacilitiesTab.tsx b/src/components/Users/LinkedFacilitiesTab.tsx new file mode 100644 index 00000000000..c59af7dfc77 --- /dev/null +++ b/src/components/Users/LinkedFacilitiesTab.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from "react-i18next"; +import LinkededFacilities from "./LinkedFacilities"; +import { UserModel } from "./models"; +import userColumns from "../Common/UserColumns"; + +type Props = { + userData: UserModel; + username: string; +}; + +export default function LinkedFacilitiesTab(props: Props) { + const { userData } = props; + const { t } = useTranslation(); + + if (!userData) { + return; + } + + return ( + <> +
+ {userColumns( + t("linked_facilities"), + t("linked_facilities_note"), + LinkededFacilities, + props, + )} +
+ + ); +} diff --git a/src/components/Users/ManageUsers.tsx b/src/components/Users/ManageUsers.tsx index 007aa55f78f..72815f4c3c1 100644 --- a/src/components/Users/ManageUsers.tsx +++ b/src/components/Users/ManageUsers.tsx @@ -27,7 +27,6 @@ import CircularProgress from "@/components/Common/components/CircularProgress"; import Page from "@/components/Common/components/Page"; import { FacilityModel } from "../Facility/models"; import SearchInput from "../Form/SearchInput"; -import ConfirmHomeFacilityUpdateDialog from "./ConfirmHomeFacilityUpdateDialog"; import UnlinkFacilityDialog from "./UnlinkFacilityDialog"; import UserFilter from "./UserFilter"; @@ -364,26 +363,6 @@ export function UserFacilities(props: { user: any }) { isHomeFacility: boolean; }>({ show: false, userName: "", facility: undefined, isHomeFacility: false }); const authUser = useAuthUser(); - - const [replaceHomeFacility, setReplaceHomeFacility] = useState<{ - show: boolean; - userName: string; - previousFacility?: FacilityModel; - newFacility?: FacilityModel; - }>({ - show: false, - userName: "", - previousFacility: undefined, - newFacility: undefined, - }); - const hideReplaceHomeFacilityModal = () => { - setReplaceHomeFacility({ - show: false, - previousFacility: undefined, - userName: "", - newFacility: undefined, - }); - }; const hideUnlinkFacilityModal = () => { setUnlinkFacilityData({ show: false, @@ -609,13 +588,6 @@ export function UserFacilities(props: { user: any }) { onClick={() => { if (user?.home_facility_object) { // has previous home facility - setReplaceHomeFacility({ - show: true, - userName: username, - previousFacility: - user?.home_facility_object, - newFacility: facility, - }); } else { // no previous home facility updateHomeFacility(username, facility); @@ -681,28 +653,6 @@ export function UserFacilities(props: { user: any }) { )}
)} - {replaceHomeFacility.show && ( - { - updateHomeFacility( - replaceHomeFacility.userName, - replaceHomeFacility.newFacility, - ); - setReplaceHomeFacility({ - show: false, - previousFacility: undefined, - userName: "", - newFacility: undefined, - }); - }} - /> - )}
); } diff --git a/src/components/Users/SkillsSlideOver.tsx b/src/components/Users/SkillsSlideOver.tsx deleted file mode 100644 index 40f9a29e905..00000000000 --- a/src/components/Users/SkillsSlideOver.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { useCallback, useState } from "react"; -import SlideOverCustom from "../../CAREUI/interactive/SlideOver"; -import { SkillModel, SkillObjectModel } from "./models"; -import { SkillSelect } from "@/components/Common/SkillSelect"; -import UnlinkSkillDialog from "./UnlinkSkillDialog"; -import * as Notification from "../../Utils/Notifications"; -import ButtonV2 from "@/components/Common/components/ButtonV2"; -import AuthorizeFor from "../../Utils/AuthorizeFor"; -import { useIsAuthorized } from "@/common/hooks/useIsAuthorized"; -import { AddSkillsPlaceholder, SkillsArray } from "./SkillsSlideOverComponents"; -import { useTranslation } from "react-i18next"; -import CircularProgress from "@/components/Common/components/CircularProgress"; -import useQuery from "../../Utils/request/useQuery"; -import request from "../../Utils/request/request"; -import routes from "../../Redux/api"; - -interface IProps { - username: string; - show: boolean; - setShow: (show: boolean) => void; -} - -export default ({ show, setShow, username }: IProps) => { - /* added const {t} hook here and relevant text to Common.json to avoid eslint error */ - const { t } = useTranslation(); - const [selectedSkill, setSelectedSkill] = useState( - null, - ); - const [isLoading, setIsLoading] = useState(false); - const [deleteSkill, setDeleteSkill] = useState(null); - - const { - data: skills, - loading: skillsLoading, - refetch: refetchUserSkills, - } = useQuery(routes.userListSkill, { - pathParams: { username }, - }); - - const addSkill = useCallback( - async (username: string, skill: SkillObjectModel | null) => { - if (!skill) return; - setIsLoading(true); - const { res } = await request(routes.addUserSkill, { - pathParams: { username }, - body: { skill: skill.id }, - }); - - if (!res?.ok) { - Notification.Error({ - msg: "Error while adding skill", - }); - } else { - Notification.Success({ - msg: "Skill added successfully", - }); - } - setSelectedSkill(null); - setIsLoading(false); - await refetchUserSkills(); - }, - [refetchUserSkills], - ); - - const removeSkill = useCallback( - async (username: string, skillId: string) => { - const { res } = await request(routes.deleteUserSkill, { - pathParams: { username, id: skillId }, - }); - if (res?.status !== 204) { - Notification.Error({ - msg: "Error while unlinking skill", - }); - } else { - Notification.Success({ - msg: "Skill unlinked successfully", - }); - } - setDeleteSkill(null); - await refetchUserSkills(); - }, - [refetchUserSkills], - ); - - const authorizeForAddSkill = useIsAuthorized( - AuthorizeFor(["DistrictAdmin", "StateAdmin"]), - ); - - const hasSkills = skills?.results?.length || 0 > 0; - - return ( -
- {deleteSkill && ( - setDeleteSkill(null)} - onSubmit={() => removeSkill(username, deleteSkill.id)} - /> - )} - { - !deleteSkill && setShow(openState); - }} - slideFrom="right" - title="Skills" - dialogClass="md:w-[400px]" - > -
-
- {(!isLoading || !skillsLoading) && ( -
- - addSkill(username, selectedSkill)} - className="mt-1 h-[45px] w-[74px] text-base" - > - {t("add")} - - {!authorizeForAddSkill && ( - - {t("contact_your_admin_to_add_skills")} - - )} -
- )} - {isLoading || skillsLoading ? ( -
- -
- ) : ( -
- {hasSkills ? ( - - ) : ( - - )} -
- )} -
-
-
-
- ); -}; diff --git a/src/components/Users/SkillsSlideOverComponents.tsx b/src/components/Users/SkillsSlideOverComponents.tsx deleted file mode 100644 index 0593718588e..00000000000 --- a/src/components/Users/SkillsSlideOverComponents.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useTranslation } from "react-i18next"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import { classNames } from "../../Utils/utils"; -import ButtonV2 from "@/components/Common/components/ButtonV2"; -import { SkillModel } from "./models"; -import { Fragment } from "react"; - -export const AddSkillsPlaceholder = () => { - const { t } = useTranslation(); - return ( -
-
- Error 404 -
-

- {t("select_skills")} -

-
- ); -}; - -type SkillsArrayProps = { - isLoading: boolean; - skills: SkillModel[]; - authorizeForAddSkill: boolean; - setDeleteSkill: (skill: SkillModel) => void; -}; - -export const SkillsArray = ({ - isLoading, - skills, - authorizeForAddSkill, - setDeleteSkill, -}: SkillsArrayProps) => { - return ( - - {skills.map((skill, i) => ( -
-
-
{skill.skill_object.name}
-
- setDeleteSkill(skill)} - > - - -
-
-
- ))} -
- ); -}; diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index f8639f3ab7d..765a70ea76f 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -10,6 +10,7 @@ import Error404 from "../ErrorPages/404"; import { classNames, keysOf } from "@/Utils/utils"; import { Link } from "raviger"; import { useTranslation } from "react-i18next"; +import LinkedFacilitiesTab from "./LinkedFacilitiesTab"; export interface UserHomeProps { username: string; @@ -34,6 +35,7 @@ export default function UserHome(props: UserHomeProps) { const TABS = { PROFILE: UserSummaryTab, + FACILITIES: LinkedFacilitiesTab, }; let currentTab = undefined; From 3c9d0148a697e06e96dbeb35d3d8d1808bdd43cd Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Mon, 4 Nov 2024 18:36:03 +0530 Subject: [PATCH 04/55] Adding Roles and LinkedSkills --- src/Locale/en.json | 8 + src/components/Common/SkillSelect.tsx | 6 +- src/components/Users/ConfirmSkillsModal.tsx | 37 +++ src/components/Users/LinkedSkills.tsx | 163 +++++++++++ src/components/Users/RoleAndSkillsTab.tsx | 33 +++ src/components/Users/UserHome.tsx | 4 +- src/components/Users/UserRoles.tsx | 285 ++++++++++++++++++++ 7 files changed, 532 insertions(+), 4 deletions(-) create mode 100644 src/components/Users/ConfirmSkillsModal.tsx create mode 100644 src/components/Users/LinkedSkills.tsx create mode 100644 src/components/Users/RoleAndSkillsTab.tsx create mode 100644 src/components/Users/UserRoles.tsx diff --git a/src/Locale/en.json b/src/Locale/en.json index e2716fd44bf..4734414ea75 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -206,6 +206,7 @@ "URINATION_FREQUENCY__NORMAL": "Normal", "USERMANAGEMENT_TAB__FACILITIES": "Linked Facilities", "USERMANAGEMENT_TAB__PROFILE": "User Information", + "USERMANAGEMENT_TAB__ROLE_SKILLS": "Role & Skills", "VENTILATOR": "Detailed Update", "VENTILATOR_MODE__CMV": "Control Mechanical Ventilation (CMV)", "VENTILATOR_MODE__PCV": "Pressure Control Ventilation (PCV)", @@ -274,6 +275,7 @@ "add_preset": "Add preset", "add_prn_prescription": "Add PRN Prescription", "add_remarks": "Add remarks", + "add_skill": "Add Skill", "add_spoke": "Add Spoke Facility", "address": "Address", "administer": "Administer", @@ -785,6 +787,7 @@ "linked_facilities": "Linked Facilities", "linked_facilities_note": "Add or remove facilities and set or change the Home Facility", "linked_skills": "Linked Skills", + "linked_skills_note": "Search and select skills to add to the skill set", "liquid_oxygen_capacity": "Liquid Oxygen Capacity", "list_view": "List View", "litres": "Litres", @@ -1211,6 +1214,9 @@ "unlink_facility": "Unlink Facility", "unlink_facility_access": "The user will lose access to the facility", "unlink_facility_confirm": "Are you sure you want to unlink the facility", + "unlink_skill": "Unlink Skill", + "unlink_skill_access": "The user will not have the skill associated anymore.", + "unlink_skill_confirm": "Are you sure you want to unlink the skill", "unlink_this_facility": "Unlink Facility", "unsubscribe": "Unsubscribe", "unsubscribe_failed": "Unsubscribe failed.", @@ -1245,6 +1251,8 @@ "use_existing_abha_address": "Use Existing ABHA Address", "user_deleted_successfuly": "User Deleted Successfuly", "user_management": "User Management", + "user_role": "Role", + "user_role_note": "Search and assign the appropriate role for this user.", "username": "Username", "users": "Users", "vacant": "Vacant", diff --git a/src/components/Common/SkillSelect.tsx b/src/components/Common/SkillSelect.tsx index 117df4f536d..6ff082e448f 100644 --- a/src/components/Common/SkillSelect.tsx +++ b/src/components/Common/SkillSelect.tsx @@ -1,6 +1,6 @@ import { useCallback } from "react"; import AutoCompleteAsync from "../Form/AutoCompleteAsync"; -import { SkillModel, SkillObjectModel } from "../Users/models"; +import { SkillModel } from "../Users/models"; import request from "../../Utils/request/request"; import routes from "../../Redux/api"; @@ -13,8 +13,8 @@ interface SkillSelectProps { multiple?: boolean; showNOptions?: number; disabled?: boolean; - selected: SkillObjectModel | SkillObjectModel[] | null; - setSelected: (selected: SkillObjectModel) => void; + selected: SkillModel | SkillModel[] | null; + setSelected: (selected: SkillModel) => void; userSkills?: SkillModel[]; } diff --git a/src/components/Users/ConfirmSkillsModal.tsx b/src/components/Users/ConfirmSkillsModal.tsx new file mode 100644 index 00000000000..da69c7111cd --- /dev/null +++ b/src/components/Users/ConfirmSkillsModal.tsx @@ -0,0 +1,37 @@ +import ConfirmDialog from "@/components/Common/ConfirmDialog"; +import { useTranslation } from "react-i18next"; + +const ConfirmSkillsModal = ({ + username, + currentSkillName, + handleCancel, + handleOk, +}: { + username: string; + currentSkillName?: string; + handleCancel: () => void; + handleOk: () => void; +}) => { + const { t } = useTranslation(); + const title = t("unlink_skill"); + const body = ( + + {t("unlink_skill_confirm")} {currentSkillName}{" "} + {t("from_user")} {username}? {t("unlink_skill_access")} + + ); + return ( + {title}} + show={true} + action={"Unlink"} + onClose={handleCancel} + onConfirm={handleOk} + variant="danger" + > +
{body}
+
+ ); +}; + +export default ConfirmSkillsModal; diff --git a/src/components/Users/LinkedSkills.tsx b/src/components/Users/LinkedSkills.tsx new file mode 100644 index 00000000000..7df40dce4e3 --- /dev/null +++ b/src/components/Users/LinkedSkills.tsx @@ -0,0 +1,163 @@ +import { useState } from "react"; +import ButtonV2 from "../Common/components/ButtonV2"; +import routes from "@/Redux/api"; +import request from "@/Utils/request/request"; +import useQuery from "@/Utils/request/useQuery"; +import { SkillModel } from "./models"; +import * as Notification from "../../Utils/Notifications"; +import CareIcon from "@/CAREUI/icons/CareIcon"; +import { useTranslation } from "react-i18next"; +import ConfirmSkillsModal from "./ConfirmSkillsModal"; +import { SkillSelect } from "../Common/SkillSelect"; +import { useIsAuthorized } from "@/common/hooks/useIsAuthorized"; +import AuthorizeFor from "@/Utils/AuthorizeFor"; + +const initModalProps: { + selectedSkill: SkillModel | null; + toggle: boolean; +} = { + toggle: false, + selectedSkill: null, +}; + +export default function LinkedSkills({ username }: { username: string }) { + const [modalProps, setModalProps] = useState(initModalProps); + const [selectedSkill, setSelectedSkill] = useState(null); + const { t } = useTranslation(); + + const { data: skills, refetch: refetchUserSkills } = useQuery( + routes.userListSkill, + { + pathParams: { username }, + }, + ); + + const handleOnClick = (selectedSkill: SkillModel) => { + setModalProps({ + selectedSkill, + toggle: true, + }); + }; + + const handleModalCancel = () => { + setModalProps(initModalProps); + }; + + const handleModalOk = () => { + removeSkill(username, modalProps.selectedSkill?.id.toString() ?? ""); + setModalProps(initModalProps); + }; + + const authorizeForAddSkill = useIsAuthorized( + AuthorizeFor(["DistrictAdmin", "StateAdmin"]), + ); + + const addSkill = async (username: string, skill: SkillModel | null) => { + if (!skill) return; + const { res } = await request(routes.addUserSkill, { + pathParams: { username }, + body: { skill: skill.id }, + }); + + if (!res?.ok) { + Notification.Error({ + msg: "Error while adding skill", + }); + } else { + Notification.Success({ + msg: "Skill added successfully", + }); + } + setSelectedSkill(null); + setModalProps(initModalProps); + await refetchUserSkills(); + }; + + const removeSkill = async (username: string, skillId: string) => { + const { res } = await request(routes.deleteUserSkill, { + pathParams: { username, id: skillId }, + }); + if (res?.status !== 204) { + Notification.Error({ + msg: "Error while unlinking skill", + }); + } else { + Notification.Success({ + msg: "Skill unlinked successfully", + }); + } + await refetchUserSkills(); + }; + + const renderSkillButtons = (skill: SkillModel) => { + return ( +
+
+
{skill.skill_object.name}
+
+ +
+
+
+ ); + }; + + return ( + <> + {modalProps.toggle && ( + + )} +
+
+ + addSkill(username, selectedSkill)} + className="mt-1 rounded-lg px-6 py-[11px] text-base" + > + {t("add_skill")} + + {!authorizeForAddSkill && ( + + {t("contact_your_admin_to_add_skills")} + + )} +
+ {skills && skills?.count > 0 && ( +
+

{t("linked_skills")}

+ +
+ {skills?.results.map((skill: SkillModel) => { + return renderSkillButtons(skill); + })} +
+
+ )} +
+ + ); +} diff --git a/src/components/Users/RoleAndSkillsTab.tsx b/src/components/Users/RoleAndSkillsTab.tsx new file mode 100644 index 00000000000..24202a6c0a7 --- /dev/null +++ b/src/components/Users/RoleAndSkillsTab.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from "react-i18next"; +import { UserModel } from "./models"; +import userColumns from "../Common/UserColumns"; +import LinkedSkills from "./LinkedSkills"; +import UserRoles from "./UserRoles"; + +type Props = { + userData: UserModel; + username: string; +}; + +export default function RoleAndSkillsTab(props: Props) { + const { userData } = props; + const { t } = useTranslation(); + + if (!userData) { + return; + } + + return ( + <> +
+ {userColumns(t("user_role"), t("user_role_note"), UserRoles, props)} + {userColumns( + t("linked_skills"), + t("linked_skills_note"), + LinkedSkills, + props, + )} +
+ + ); +} diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index 765a70ea76f..a3850cb5264 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -11,6 +11,7 @@ import { classNames, keysOf } from "@/Utils/utils"; import { Link } from "raviger"; import { useTranslation } from "react-i18next"; import LinkedFacilitiesTab from "./LinkedFacilitiesTab"; +import RoleAndSkillsTab from "./RoleAndSkillsTab"; export interface UserHomeProps { username: string; @@ -35,6 +36,7 @@ export default function UserHome(props: UserHomeProps) { const TABS = { PROFILE: UserSummaryTab, + ROLE_SKILLS: RoleAndSkillsTab, FACILITIES: LinkedFacilitiesTab, }; @@ -68,7 +70,7 @@ export default function UserHome(props: UserHomeProps) {
- {states.form.user_type === "Doctor" && ( - <> - - - - )}
Date: Mon, 4 Nov 2024 18:46:10 +0530 Subject: [PATCH 06/55] adjusted user detail page urls --- src/Routers/routes/UserRoutes.tsx | 4 ++-- src/components/Users/ManageUsers.tsx | 2 +- src/components/Users/UserHome.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Routers/routes/UserRoutes.tsx b/src/Routers/routes/UserRoutes.tsx index 53cb13414a2..065d0039b84 100644 --- a/src/Routers/routes/UserRoutes.tsx +++ b/src/Routers/routes/UserRoutes.tsx @@ -6,10 +6,10 @@ import UserHome from "@/components/Users/UserHome"; const UserRoutes: AppRoutes = { "/users": () => , - "/users/:username": ({ username }) => ( + "/users/detail/:username": ({ username }) => ( ), - "/users/:username/:tab": ({ username, tab }) => ( + "/users/detail/:username/:tab": ({ username, tab }) => ( ), "/users/add": () => , diff --git a/src/components/Users/ManageUsers.tsx b/src/components/Users/ManageUsers.tsx index 72815f4c3c1..db736d662f8 100644 --- a/src/components/Users/ManageUsers.tsx +++ b/src/components/Users/ManageUsers.tsx @@ -238,7 +238,7 @@ export default function ManageUsers() { className="mr-2 mt-1 h-[35px] w-[80px] self-center text-sm" ghost border - onClick={() => navigate(`/users/${user.username}`)} + onClick={() => navigate(`/users/detail/${user.username}`)} > {t("view_user")} diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index a3850cb5264..35de1899751 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -83,7 +83,7 @@ export default function UserHome(props: UserHomeProps) { ? "border-b-2 border-primary-500 text-primary-600 hover:border-secondary-300" : "text-secondary-700 hover:text-secondary-700", )} - href={`/users/${username}/${p.toLocaleLowerCase()}`} + href={`/users/detail/${username}/${p.toLocaleLowerCase()}`} >
{t(`USERMANAGEMENT_TAB__${p}`)} From 3256e7ccd08c26fe9f4fb3940c5c80d98a72c93d Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Mon, 4 Nov 2024 20:43:57 +0530 Subject: [PATCH 07/55] Page title adjustments --- src/Locale/en.json | 1 + src/components/Common/PageTitle.tsx | 6 +++++- src/components/Common/components/Page.tsx | 2 ++ src/components/Users/UserHome.tsx | 5 +++-- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Locale/en.json b/src/Locale/en.json index 4734414ea75..035e9fab69d 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -812,6 +812,7 @@ "manage_bed_presets": "Manage Presets of Bed", "manage_prescriptions": "Manage Prescriptions", "manage_preset": "Manage preset {{ name }}", + "manage_user": "Manage User", "manufacturer": "Manufacturer", "map_acronym": "M.A.P.", "mark_all_as_read": "Mark all as Read", diff --git a/src/components/Common/PageTitle.tsx b/src/components/Common/PageTitle.tsx index f3ca0146f3e..b948d58fc3f 100644 --- a/src/components/Common/PageTitle.tsx +++ b/src/components/Common/PageTitle.tsx @@ -17,6 +17,7 @@ export interface PageTitleProps { // New props for Breadcrumbs hideBack?: boolean; backUrl?: string; + hideTitleOnPage?: boolean; onBackClick?: () => boolean | void; } @@ -33,6 +34,7 @@ export default function PageTitle({ hideBack = false, backUrl, onBackClick, + hideTitleOnPage, }: PageTitleProps) { const divRef = useRef(); @@ -68,7 +70,9 @@ export default function PageTitle({ )} >
-

{title}

+ {!hideTitleOnPage && ( +

{title}

+ )}
{componentRight}
diff --git a/src/components/Common/components/Page.tsx b/src/components/Common/components/Page.tsx index 79e5348b120..a32d596f96a 100644 --- a/src/components/Common/components/Page.tsx +++ b/src/components/Common/components/Page.tsx @@ -15,6 +15,7 @@ interface PageProps extends PageTitleProps { * @default false **/ collapseSidebar?: boolean; + hideTitleOnPage?: boolean; } export default function Page(props: PageProps) { @@ -49,6 +50,7 @@ export default function Page(props: PageProps) { focusOnLoad={props.focusOnLoad} onBackClick={props.onBackClick} isInsidePage={true} + hideTitleOnPage={props.hideTitleOnPage} /> {props.options}
diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index 35de1899751..d01c93810c5 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -7,7 +7,7 @@ import useQuery from "@/Utils/request/useQuery"; import { UserModel } from "./models"; import Loading from "../Common/Loading"; import Error404 from "../ErrorPages/404"; -import { classNames, keysOf } from "@/Utils/utils"; +import { classNames, formatName, keysOf } from "@/Utils/utils"; import { Link } from "raviger"; import { useTranslation } from "react-i18next"; import LinkedFacilitiesTab from "./LinkedFacilitiesTab"; @@ -58,10 +58,11 @@ export default function UserHome(props: UserHomeProps) { return ( <> { <> From 4cd3142ff95933ac070318eb6a9d7d973b723a2f Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Mon, 4 Nov 2024 21:08:59 +0530 Subject: [PATCH 08/55] minor tweaks --- src/Routers/routes/UserRoutes.tsx | 6 +++--- src/components/Users/ManageUsers.tsx | 2 +- src/components/Users/UserHome.tsx | 2 +- src/components/Users/UserRoles.tsx | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Routers/routes/UserRoutes.tsx b/src/Routers/routes/UserRoutes.tsx index 065d0039b84..5b9dca749ca 100644 --- a/src/Routers/routes/UserRoutes.tsx +++ b/src/Routers/routes/UserRoutes.tsx @@ -6,13 +6,13 @@ import UserHome from "@/components/Users/UserHome"; const UserRoutes: AppRoutes = { "/users": () => , - "/users/detail/:username": ({ username }) => ( + "/users/add": () => , + "/users/:username": ({ username }) => ( ), - "/users/detail/:username/:tab": ({ username, tab }) => ( + "/users/:username/:tab": ({ username, tab }) => ( ), - "/users/add": () => , "/user/profile": () => , }; diff --git a/src/components/Users/ManageUsers.tsx b/src/components/Users/ManageUsers.tsx index db736d662f8..72815f4c3c1 100644 --- a/src/components/Users/ManageUsers.tsx +++ b/src/components/Users/ManageUsers.tsx @@ -238,7 +238,7 @@ export default function ManageUsers() { className="mr-2 mt-1 h-[35px] w-[80px] self-center text-sm" ghost border - onClick={() => navigate(`/users/detail/${user.username}`)} + onClick={() => navigate(`/users/${user.username}`)} > {t("view_user")} diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index d01c93810c5..4184c30883d 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -84,7 +84,7 @@ export default function UserHome(props: UserHomeProps) { ? "border-b-2 border-primary-500 text-primary-600 hover:border-secondary-300" : "text-secondary-700 hover:text-secondary-700", )} - href={`/users/detail/${username}/${p.toLocaleLowerCase()}`} + href={`/users/${username}/${p.toLocaleLowerCase()}`} >
{t(`USERMANAGEMENT_TAB__${p}`)} diff --git a/src/components/Users/UserRoles.tsx b/src/components/Users/UserRoles.tsx index bf447e36a81..69295259c8a 100644 --- a/src/components/Users/UserRoles.tsx +++ b/src/components/Users/UserRoles.tsx @@ -216,7 +216,7 @@ export default function UserRoles({ username }: { username: string }) { return ( <> -
+
{!isLoading && (
@@ -241,7 +241,7 @@ export default function UserRoles({ username }: { username: string }) { o.role + (o.readOnly ? " (Read Only)" : "") } optionValue={(o) => o.id} - className="flex-1" + className="z-40 flex-1" /> {(states.form.user_type === "Doctor" || states.form.user_type === "Nurse") && ( From 98f9f0bb23c3881635d471bd59fdd0bab18315c7 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Tue, 5 Nov 2024 20:26:52 +0530 Subject: [PATCH 09/55] FormEdits, fixes, customization - Remove role editing from role management component - Use passable formData for formValidation - Minor customizations for role management issues - Only show role management options for pages of users with Roles: Doctor and Nurse --- src/Locale/en.json | 1 + src/components/Common/UserColumns.tsx | 2 +- src/components/Users/RoleAndSkillsTab.tsx | 8 ++- src/components/Users/UserHome.tsx | 30 ++++++++-- src/components/Users/UserInformation.tsx | 71 ++++++++++------------- src/components/Users/UserRoles.tsx | 69 ++++------------------ 6 files changed, 75 insertions(+), 106 deletions(-) diff --git a/src/Locale/en.json b/src/Locale/en.json index 035e9fab69d..21954afd107 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -207,6 +207,7 @@ "USERMANAGEMENT_TAB__FACILITIES": "Linked Facilities", "USERMANAGEMENT_TAB__PROFILE": "User Information", "USERMANAGEMENT_TAB__ROLE_SKILLS": "Role & Skills", + "USERMANAGEMENT_TAB__SKILLS": "Skills", "VENTILATOR": "Detailed Update", "VENTILATOR_MODE__CMV": "Control Mechanical Ventilation (CMV)", "VENTILATOR_MODE__PCV": "Pressure Control Ventilation (PCV)", diff --git a/src/components/Common/UserColumns.tsx b/src/components/Common/UserColumns.tsx index e805b94ff89..27b2fd9026c 100644 --- a/src/components/Common/UserColumns.tsx +++ b/src/components/Common/UserColumns.tsx @@ -8,7 +8,7 @@ export type userChildProps = { export default function userColumns( heading: string, note: string, - Child: (childProps: userChildProps) => JSX.Element, + Child: (childProps: userChildProps) => JSX.Element | undefined, childProps: userChildProps, ) { return ( diff --git a/src/components/Users/RoleAndSkillsTab.tsx b/src/components/Users/RoleAndSkillsTab.tsx index 24202a6c0a7..d5ac40a0797 100644 --- a/src/components/Users/RoleAndSkillsTab.tsx +++ b/src/components/Users/RoleAndSkillsTab.tsx @@ -10,17 +10,19 @@ type Props = { }; export default function RoleAndSkillsTab(props: Props) { - const { userData } = props; + const { userData, username } = props; const { t } = useTranslation(); - if (!userData) { + if (!userData || !username) { return; } return ( <>
- {userColumns(t("user_role"), t("user_role_note"), UserRoles, props)} + {userData.user_type && + ["Doctor", "Nurse"].includes(userData.user_type) && + userColumns(t("user_role"), t("user_role_note"), UserRoles, props)} {userColumns( t("linked_skills"), t("linked_skills_note"), diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index 4184c30883d..84bdd35fce0 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -12,11 +12,16 @@ import { Link } from "raviger"; import { useTranslation } from "react-i18next"; import LinkedFacilitiesTab from "./LinkedFacilitiesTab"; import RoleAndSkillsTab from "./RoleAndSkillsTab"; +import { userChildProps } from "../Common/UserColumns"; export interface UserHomeProps { username: string; tab: string; } +export interface tabChildProp { + body: (childProps: userChildProps) => JSX.Element | undefined; + name?: string; +} export default function UserHome(props: UserHomeProps) { const { username, tab } = props; @@ -34,10 +39,22 @@ export default function UserHome(props: UserHomeProps) { }, }); - const TABS = { - PROFILE: UserSummaryTab, - ROLE_SKILLS: RoleAndSkillsTab, - FACILITIES: LinkedFacilitiesTab, + const roleInfoBeVisible = () => { + if (["Doctor", "Nurse"].includes(userData?.user_type ?? "")) return true; + return false; + }; + + const TABS: { + PROFILE: tabChildProp; + ROLE_SKILLS: tabChildProp; + FACILITIES: tabChildProp; + } = { + PROFILE: { body: UserSummaryTab }, + ROLE_SKILLS: { + body: RoleAndSkillsTab, + name: roleInfoBeVisible() ? "ROLE_SKILLS" : "SKILLS", + }, + FACILITIES: { body: LinkedFacilitiesTab }, }; let currentTab = undefined; @@ -53,7 +70,7 @@ export default function UserHome(props: UserHomeProps) { return ; } - const SelectedTab = TABS[currentTab]; + const SelectedTab = TABS[currentTab].body; return ( <> @@ -75,6 +92,7 @@ export default function UserHome(props: UserHomeProps) { id="usermanagement_tab_nav" > {keysOf(TABS).map((p) => { + const tabName = TABS[p]?.name ?? p; return (
- {t(`USERMANAGEMENT_TAB__${p}`)} + {t(`USERMANAGEMENT_TAB__${tabName}`)}
); diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx index 4cc45573452..5cfa04d3c02 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserInformation.tsx @@ -137,28 +137,28 @@ export default function UserInformation({ username }: { username: string }) { }, }); - const validateForm = () => { + const validateForm = (form: EditForm) => { const errors: Partial> = {}; - Object.keys(states.form).forEach((field) => { + Object.keys(form).forEach((field) => { switch (field) { case "firstName": case "lastName": case "gender": - errors[field] = RequiredFieldValidator()(states.form[field]); + errors[field] = RequiredFieldValidator()(form[field]); return; case "date_of_birth": - if (!states.form[field]) { + if (!form[field]) { errors[field] = "Enter a valid date of birth"; } else if ( - !dayjs(states.form[field]).isValid() || - dayjs(states.form[field]).isAfter(dayjs().subtract(17, "year")) + !dayjs(form[field]).isValid() || + dayjs(form[field]).isAfter(dayjs().subtract(17, "year")) ) { errors[field] = "Enter a valid date of birth"; } return; case "phoneNumber": // eslint-disable-next-line no-case-declarations - const phoneNumber = parsePhoneNumber(states.form[field]); + const phoneNumber = parsePhoneNumber(form[field]); // eslint-disable-next-line no-case-declarations let is_valid = false; @@ -166,50 +166,46 @@ export default function UserInformation({ username }: { username: string }) { is_valid = PhoneNumberValidator()(phoneNumber) === undefined; } - if (!states.form[field] || !is_valid) { + if (!form[field] || !is_valid) { errors[field] = "Please enter valid phone number"; } return; case "altPhoneNumber": // eslint-disable-next-line no-case-declarations let alt_is_valid = false; - if (states.form[field] && states.form[field] !== "+91") { - const altPhoneNumber = parsePhoneNumber(states.form[field]); + if (form[field] && form[field] !== "+91") { + const altPhoneNumber = parsePhoneNumber(form[field]); if (altPhoneNumber) { alt_is_valid = PhoneNumberValidator(["mobile"])(altPhoneNumber) === undefined; } } - if ( - states.form[field] && - states.form[field] !== "+91" && - !alt_is_valid - ) { + if (form[field] && form[field] !== "+91" && !alt_is_valid) { errors[field] = "Please enter valid mobile number"; } return; case "email": - if (!states.form[field]) { + if (!form[field]) { errors[field] = t("field_required"); - } else if (!validateEmailAddress(states.form[field])) { + } else if (!validateEmailAddress(form[field])) { errors[field] = "Enter a valid email address"; } return; case "weekly_working_hours": if ( - states.form[field] && - (Number(states.form[field]) < 0 || - Number(states.form[field]) > 168 || - !/^\d+$/.test(states.form[field] ?? "")) + form[field] && + (Number(form[field]) < 0 || + Number(form[field]) > 168 || + !/^\d+$/.test(form[field] ?? "")) ) { errors[field] = "Average weekly working hours must be a number between 0 and 168"; } return; case "video_connect_link": - if (states.form[field]) { - if (isValidUrl(states.form[field]) === false) { + if (form[field]) { + if (isValidUrl(form[field]) === false) { errors[field] = "Please enter a valid url"; } } @@ -219,8 +215,8 @@ export default function UserInformation({ username }: { username: string }) { return errors; }; - const getDate = (value: any) => - value && dayjs(value).isValid() && dayjs(value).toDate(); + const getDate = (value: string | Date | null) => + value && dayjs(value).isValid() ? dayjs(value).toDate() : undefined; const handleCancel = () => { dispatch({ @@ -233,22 +229,19 @@ export default function UserInformation({ username }: { username: string }) { return ; } - const handleSubmit = async (e: any) => { - e.preventDefault(); + const handleSubmit = async (formData: EditForm) => { const data = { - username: userData.username, - first_name: states.form.firstName, - last_name: states.form.lastName, - email: states.form.email, - video_connect_link: states.form.video_connect_link, - phone_number: parsePhoneNumber(states.form.phoneNumber) ?? "", - alt_phone_number: parsePhoneNumber(states.form.altPhoneNumber) ?? "", - gender: states.form.gender, - date_of_birth: dateQueryString(states.form.date_of_birth), + first_name: formData.firstName, + last_name: formData.lastName, + email: formData.email, + video_connect_link: formData.video_connect_link, + phone_number: parsePhoneNumber(formData.phoneNumber) ?? "", + alt_phone_number: parsePhoneNumber(formData.altPhoneNumber) ?? "", + gender: formData.gender, + date_of_birth: dateQueryString(formData.date_of_birth), weekly_working_hours: - states.form.weekly_working_hours && - states.form.weekly_working_hours !== "" - ? states.form.weekly_working_hours + formData.weekly_working_hours && formData.weekly_working_hours !== "" + ? formData.weekly_working_hours : null, }; const { res } = await request(routes.partialUpdateUser, { diff --git a/src/components/Users/UserRoles.tsx b/src/components/Users/UserRoles.tsx index 69295259c8a..66fa7e61aae 100644 --- a/src/components/Users/UserRoles.tsx +++ b/src/components/Users/UserRoles.tsx @@ -9,9 +9,6 @@ import request from "../../Utils/request/request"; import { useTranslation } from "react-i18next"; import Loading from "@/components/Common/Loading"; import Form from "../Form/Form"; -import { USER_TYPE_OPTIONS, USER_TYPES, UserRole } from "@/common/constants"; -import { SelectFormField } from "../Form/FormFields/SelectFormField"; -import useAuthUser from "@/common/hooks/useAuthUser"; type EditForm = { user_type: string | undefined; @@ -71,7 +68,6 @@ export default function UserRoles({ username }: { username: string }) { const { t } = useTranslation(); const [states, dispatch] = useReducer(editFormReducer, initialState); const formVals = useRef(initForm); - const authUser = useAuthUser(); const { data: userData, @@ -148,58 +144,28 @@ export default function UserRoles({ username }: { username: string }) { return ; } - const userIndex = USER_TYPES.indexOf(authUser.user_type); - const readOnlyUsers = USER_TYPE_OPTIONS.filter((user) => user.readOnly); - const defaultAllowedUserTypes = USER_TYPE_OPTIONS.slice(0, userIndex + 1); - let userTypes; - if (authUser.is_superuser) { - userTypes = [...USER_TYPE_OPTIONS]; - } else { - switch (authUser.user_type) { - case "StaffReadOnly": - userTypes = readOnlyUsers.slice(0, 1); - break; - case "DistrictReadOnlyAdmin": - userTypes = readOnlyUsers.slice(0, 2); - break; - case "StateReadOnlyAdmin": - userTypes = readOnlyUsers.slice(0, 3); - break; - case "Pharmacist": - userTypes = USER_TYPE_OPTIONS.slice(0, 1); - break; - default: - // Exception to allow Staff to Create Doctors - userTypes = defaultAllowedUserTypes; - } - // Temporarily allows creation of users with elevated permissions due to introduction of new roles. - if (authUser.user_type === "Nurse" || authUser.user_type === "Staff") { - userTypes.push(USER_TYPE_OPTIONS[6]); - } - } + if (!["Doctor", "Nurse"].includes(states.form.user_type ?? "")) return; - const handleSubmit = async (e: any) => { - e.preventDefault(); + const handleSubmit = async (formData: EditForm) => { const data = { - user_type: states.form.user_type as UserRole, qualification: - states.form.user_type === "Doctor" || states.form.user_type === "Nurse" - ? states.form.qualification + formData.user_type === "Doctor" || formData.user_type === "Nurse" + ? formData.qualification : undefined, doctor_experience_commenced_on: - states.form.user_type === "Doctor" + formData.user_type === "Doctor" ? dayjs() .subtract( parseInt( - (states.form.doctor_experience_commenced_on as string) ?? "0", + (formData.doctor_experience_commenced_on as string) ?? "0", ), "years", ) .format("YYYY-MM-DD") : undefined, doctor_medical_council_registration: - states.form.user_type === "Doctor" - ? states.form.doctor_medical_council_registration + formData.user_type === "Doctor" + ? formData.doctor_medical_council_registration : undefined, }; const { res } = await request(routes.partialUpdateUser, { @@ -232,17 +198,6 @@ export default function UserRoles({ username }: { username: string }) { {(field) => ( <>
- - o.role + (o.readOnly ? " (Read Only)" : "") - } - optionValue={(o) => o.id} - className="z-40 flex-1" - /> {(states.form.user_type === "Doctor" || states.form.user_type === "Nurse") && ( {states.form.user_type === "Doctor" && ( - <> +
- +
)} )} From 3b8803d142c2a40662977712b1fdcad36c018b01 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Wed, 6 Nov 2024 13:10:33 +0530 Subject: [PATCH 10/55] Editing userAdd component to be reusable --- src/Routers/routes/UserRoutes.tsx | 1 + src/components/Users/UserAdd.tsx | 1122 +++++++++++++++++------------ 2 files changed, 646 insertions(+), 477 deletions(-) diff --git a/src/Routers/routes/UserRoutes.tsx b/src/Routers/routes/UserRoutes.tsx index 5b9dca749ca..e2cce4ecc2c 100644 --- a/src/Routers/routes/UserRoutes.tsx +++ b/src/Routers/routes/UserRoutes.tsx @@ -7,6 +7,7 @@ import UserHome from "@/components/Users/UserHome"; const UserRoutes: AppRoutes = { "/users": () => , "/users/add": () => , + "/users/edit/:username": ({ username }) => , "/users/:username": ({ username }) => ( ), diff --git a/src/components/Users/UserAdd.tsx b/src/components/Users/UserAdd.tsx index 3c59d9eeab2..3e2eac21ff4 100644 --- a/src/components/Users/UserAdd.tsx +++ b/src/components/Users/UserAdd.tsx @@ -1,5 +1,5 @@ import { Link, navigate } from "raviger"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { GENDER_TYPES, USER_TYPES, @@ -18,10 +18,10 @@ import { FacilityModel } from "../Facility/models"; import { classNames, dateQueryString, + isValidUrl, parsePhoneNumber, scrollTo, } from "../../Utils/utils"; -import { Cancel, Submit } from "@/components/Common/components/ButtonV2"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import TextFormField from "../Form/FormFields/TextFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; @@ -30,12 +30,11 @@ import DateFormField from "../Form/FormFields/DateFormField"; import { FieldLabel } from "../Form/FormFields/FormField"; import useAppHistory from "@/common/hooks/useAppHistory"; import Page from "@/components/Common/components/Page"; -import Card from "../../CAREUI/display/Card"; import CircularProgress from "@/components/Common/components/CircularProgress"; -import { DraftSection, useAutoSaveReducer } from "../../Utils/AutoSave"; +import { useAutoSaveReducer } from "../../Utils/AutoSave"; import dayjs from "../../Utils/dayjs"; import useAuthUser from "@/common/hooks/useAuthUser"; -import { PhoneNumberValidator } from "../Form/FieldValidators"; +import { FieldError, PhoneNumberValidator } from "../Form/FieldValidators"; import routes from "../../Redux/api"; import request from "../../Utils/request/request"; import useQuery from "../../Utils/request/useQuery"; @@ -44,8 +43,10 @@ import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import { useTranslation } from "react-i18next"; import Loading from "@/components/Common/Loading"; +import { GenderType } from "./models"; +import Form from "../Form/Form"; interface UserProps { - userId?: number; + username?: string; } interface StateObj { @@ -54,26 +55,28 @@ interface StateObj { } type UserForm = { - user_type: string; + user_type?: string; gender: string; - password: string; - c_password: string; - facilities: Array; - home_facility: FacilityModel | null; - username: string; + password?: string; + c_password?: string; + facilities?: Array; + home_facility?: FacilityModel | null; + username?: string; first_name: string; last_name: string; email: string; phone_number: string; alt_phone_number: string; - phone_number_is_whatsapp: boolean; - date_of_birth: Date | null; - state: number; - district: number; - local_body: number; - qualification: string | undefined; - doctor_experience_commenced_on: string | undefined; - doctor_medical_council_registration: string | undefined; + phone_number_is_whatsapp?: boolean; + date_of_birth: Date | null | string; + state?: number; + district?: number; + local_body?: number; + qualification?: string | undefined; + doctor_experience_commenced_on?: string | undefined; + doctor_medical_council_registration?: string | undefined; + video_connect_link?: string; + weekly_working_hours?: string | null; }; const initForm: UserForm = { @@ -165,7 +168,84 @@ export const validateRule = ( export const UserAdd = (props: UserProps) => { const { t } = useTranslation(); const { goBack } = useAppHistory(); - const { userId } = props; + const { username } = props; + const editUser = username ? true : false; + const formVals = useRef(initForm); + + const { data: userData, refetch: refetchUserData } = useQuery( + routes.getUserDetails, + { + pathParams: { + username: username ?? "", + }, + prefetch: editUser, + onResponse: (result) => { + if (!editUser || !result || !result.res || !result.data) return; + const formData: UserForm = { + first_name: result.data.first_name, + last_name: result.data.last_name, + date_of_birth: result.data.date_of_birth || null, + gender: result.data.gender || "Male", + email: result.data.email, + video_connect_link: result.data.video_connect_link, + phone_number: result.data.phone_number?.toString() || "", + alt_phone_number: result.data.alt_phone_number?.toString() || "", + weekly_working_hours: result.data.weekly_working_hours, + }; + dispatch({ + type: "set_form", + form: formData, + }); + formVals.current = formData; + }, + }, + ); + + const handleEditSubmit = async (formData: UserForm) => { + const data = { + first_name: formData.first_name, + last_name: formData.last_name, + email: formData.email, + video_connect_link: formData.video_connect_link, + phone_number: parsePhoneNumber(formData.phone_number) ?? "", + alt_phone_number: parsePhoneNumber(formData.alt_phone_number) ?? "", + gender: formData.gender as GenderType, + date_of_birth: dateQueryString(formData.date_of_birth), + qualification: + formData.user_type === "Doctor" || formData.user_type === "Nurse" + ? formData.qualification + : undefined, + doctor_experience_commenced_on: + formData.user_type === "Doctor" + ? dayjs() + .subtract( + parseInt( + (formData.doctor_experience_commenced_on as string) ?? "0", + ), + "years", + ) + .format("YYYY-MM-DD") + : undefined, + doctor_medical_council_registration: + formData.user_type === "Doctor" + ? formData.doctor_medical_council_registration + : undefined, + weekly_working_hours: + formData.weekly_working_hours && formData.weekly_working_hours !== "" + ? formData.weekly_working_hours + : null, + }; + const { res } = await request(routes.partialUpdateUser, { + pathParams: { username: authUser.username }, + body: data, + }); + if (res?.ok) { + Notification.Success({ + msg: "Details updated successfully", + }); + await refetchUserData(); + } + }; const [state, dispatch] = useAutoSaveReducer( user_create_reducer, @@ -243,14 +323,13 @@ export const UserAdd = (props: UserProps) => { userTypes.push(USER_TYPE_OPTIONS[6]); // Temperorily allows creation of users with elevated permissions due to introduction of new roles. } - const headerText = !userId ? "Add User" : "Update User"; - const buttonText = !userId ? "Save User" : "Update Details"; + const headerText = !editUser ? "Add User" : "Update User"; const showLocalbody = ![ "Pharmacist", "Volunteer", "Doctor", ...STAFF_OR_NURSE_USER, - ].includes(state.form.user_type); + ].includes(state.form.user_type ?? ""); const { loading: isDistrictLoading } = useQuery(routes.getDistrictByState, { prefetch: !!(selectedStateId > 0), @@ -306,7 +385,7 @@ export const UserAdd = (props: UserProps) => { } }; - const handleFieldChange = (event: FieldChangeEvent) => { + const handleFieldChange = (event: FieldChangeEvent, field?: any) => { const errors = { ...state.errors, [event.name]: "" }; dispatch({ type: "set_form", @@ -316,6 +395,7 @@ export const UserAdd = (props: UserProps) => { }, }); dispatch({ type: "set_errors", errors }); + if (field) field(event.name).onChange(event); }; useAbortableEffect(() => { @@ -341,178 +421,169 @@ export const UserAdd = (props: UserProps) => { dispatch({ type: "set_form", form }); }; - const validateForm = () => { - const errors = { ...initError }; - let invalidForm = false; - Object.keys(state.form).forEach((field) => { + const validateForm = (formData: UserForm) => { + const errors: Partial> = {}; + Object.keys(formData).forEach((field) => { switch (field) { case "facilities": if ( - state.form[field].length === 0 && + formData.facilities && + formData.user_type && + formData["facilities"].length === 0 && STAFF_OR_NURSE_USER.includes(authUser.user_type) && - STAFF_OR_NURSE_USER.includes(state.form.user_type) + STAFF_OR_NURSE_USER.includes(formData.user_type) ) { errors[field] = "Please select atleast one of the facilities you are linked to"; - invalidForm = true; } return; case "user_type": - if (!state.form[field]) { + if (!formData[field]) { errors[field] = "Please select the User Type"; - invalidForm = true; } return; case "doctor_experience_commenced_on": - if (state.form.user_type === "Doctor" && !state.form[field]) { + if (formData.user_type === "Doctor" && !formData[field]) { errors[field] = t("field_required"); - invalidForm = true; } else if ( - state.form.user_type === "Doctor" && - Number(state.form.doctor_experience_commenced_on) > 100 + formData.user_type === "Doctor" && + Number(formData.doctor_experience_commenced_on) > 100 ) { errors[field] = "Doctor experience should be less than 100 years"; - invalidForm = true; } return; case "qualification": if ( - (state.form.user_type === "Doctor" || - state.form.user_type === "Nurse") && - !state.form[field] + (formData.user_type === "Doctor" || + formData.user_type === "Nurse") && + !formData[field] ) { errors[field] = t("field_required"); - invalidForm = true; } return; case "doctor_medical_council_registration": - if (state.form.user_type === "Doctor" && !state.form[field]) { + if (formData.user_type === "Doctor" && !formData[field]) { errors[field] = t("field_required"); - invalidForm = true; } return; case "first_name": case "last_name": - state.form[field] = state.form[field].trim(); - if (!state.form[field]) { + formData[field] = formData[field].trim(); + if (!formData[field]) { errors[field] = `${field .split("_") .map((word) => word[0].toUpperCase() + word.slice(1)) .join(" ")} is required`; - invalidForm = true; - } else if (!validateName(state.form[field])) { + } else if (!validateName(formData[field])) { errors[field] = "Please enter a valid name"; - invalidForm = true; } return; case "gender": - if (!state.form[field]) { + if (!formData[field]) { errors[field] = "Please select the Gender"; - invalidForm = true; } return; case "username": - if (!state.form[field]) { + if (!formData[field]) { errors[field] = "Please enter the username"; - invalidForm = true; - } else if (!validateUsername(state.form[field])) { + } else if (!validateUsername(formData[field])) { errors[field] = "Please enter a 4-16 characters long username with lowercase letters, digits and . _ - only and it should not start or end with . _ -"; - invalidForm = true; } else if (usernameExists !== userExistsEnums.available) { errors[field] = "This username already exists"; - invalidForm = true; } return; case "password": - if (!state.form[field]) { + if (!formData[field]) { errors[field] = "Please enter the password"; - invalidForm = true; - } else if (!validatePassword(state.form[field])) { + } else if (!validatePassword(formData[field])) { errors.password = "Password should have 1 lowercase letter, 1 uppercase letter, 1 number, and be at least 8 characters long"; - invalidForm = true; } return; case "c_password": - if (!state.form.password) { + if (!formData.password) { errors.c_password = "Confirm password is required"; - invalidForm = true; - } else if (state.form.password !== state.form.c_password) { + } else if (formData.password !== formData.c_password) { errors.c_password = "Passwords not matching"; - invalidForm = true; } return; case "phone_number": // eslint-disable-next-line no-case-declarations - const phoneNumber = parsePhoneNumber(state.form[field]); + const phoneNumber = parsePhoneNumber(formData[field]); // eslint-disable-next-line no-case-declarations let is_valid = false; if (phoneNumber) { is_valid = PhoneNumberValidator()(phoneNumber) === undefined; } - if (!state.form[field] || !is_valid) { + if (!formData[field] || !is_valid) { errors[field] = "Please enter valid phone number"; - invalidForm = true; } return; case "alt_phone_number": // eslint-disable-next-line no-case-declarations let alt_is_valid = false; - if (state.form[field] && state.form[field] !== "+91") { - const altPhoneNumber = parsePhoneNumber(state.form[field]); + if (formData[field] && formData[field] !== "+91") { + const altPhoneNumber = parsePhoneNumber(formData[field]); if (altPhoneNumber) { alt_is_valid = PhoneNumberValidator(["mobile"])(altPhoneNumber) === undefined; } } - if ( - state.form[field] && - state.form[field] !== "+91" && - !alt_is_valid - ) { + if (formData[field] && formData[field] !== "+91" && !alt_is_valid) { errors[field] = "Please enter valid mobile number"; - invalidForm = true; } return; case "email": - state.form[field] = state.form[field].trim(); + formData[field] = formData[field].trim(); if ( - state.form[field].length === 0 || - !validateEmailAddress(state.form[field]) + formData[field].length === 0 || + !validateEmailAddress(formData[field]) ) { errors[field] = "Please enter a valid email address"; - invalidForm = true; } return; case "date_of_birth": - if (!state.form[field]) { + if (!formData[field]) { errors[field] = "Please enter date in DD/MM/YYYY format"; - invalidForm = true; } else if ( - dayjs(state.form[field]).isAfter(dayjs().subtract(1, "year")) + dayjs(formData[field]).isAfter(dayjs().subtract(1, "year")) ) { errors[field] = "Enter a valid date of birth"; - invalidForm = true; } return; case "state": - if (!Number(state.form[field])) { + if (!Number(formData[field])) { errors[field] = "Please select the state"; - invalidForm = true; } return; case "district": - if (!Number(state.form[field])) { + if (!Number(formData[field])) { errors[field] = "Please select the district"; - invalidForm = true; } return; case "local_body": - if (showLocalbody && !Number(state.form[field])) { + if (showLocalbody && !Number(formData[field])) { errors[field] = "Please select the local body"; - invalidForm = true; + } + return; + case "weekly_working_hours": + if ( + formData[field] && + (Number(formData[field]) < 0 || + Number(formData[field]) > 168 || + !/^\d+$/.test(formData[field] ?? "")) + ) { + errors[field] = + "Average weekly working hours must be a number between 0 and 168"; + } + return; + case "video_connect_link": + if (formData[field]) { + if (isValidUrl(formData[field]) === false) { + errors[field] = "Please enter a valid url"; + } } return; @@ -520,102 +591,92 @@ export const UserAdd = (props: UserProps) => { return; } }); - if (invalidForm) { - dispatch({ type: "set_errors", errors }); - const firstError = Object.keys(errors).find((e) => errors[e]); - if (firstError) { - scrollTo(firstError); - } - return false; + + const firstError = Object.values(errors).find((e) => e); + if (firstError) { + scrollTo(firstError); } dispatch({ type: "set_errors", errors }); - return true; + return errors; }; - const handleSubmit = async (e: any) => { - e.preventDefault(); - const validated = validateForm(); - if (validated) { - setIsLoading(true); - const data = { - user_type: state.form.user_type, - gender: state.form.gender, - password: state.form.password, - facilities: state.form.facilities ? state.form.facilities : undefined, - home_facility: state.form.home_facility ?? undefined, - username: state.form.username, - first_name: state.form.first_name ? state.form.first_name : undefined, - last_name: state.form.last_name ? state.form.last_name : undefined, - email: state.form.email, - state: state.form.state, - district: state.form.district, - local_body: showLocalbody ? state.form.local_body : null, - phone_number: - state.form.phone_number === "+91" - ? "" - : parsePhoneNumber(state.form.phone_number), - alt_phone_number: - parsePhoneNumber( - state.form.phone_number_is_whatsapp - ? state.form.phone_number === "+91" - ? "" - : state.form.phone_number - : state.form.alt_phone_number === "+91" - ? "" - : state.form.alt_phone_number, - ) ?? "", - date_of_birth: dateQueryString(state.form.date_of_birth), - qualification: - state.form.user_type === "Doctor" || state.form.user_type == "Nurse" - ? state.form.qualification - : undefined, - doctor_experience_commenced_on: - state.form.user_type === "Doctor" - ? dayjs() - .subtract( - parseInt(state.form.doctor_experience_commenced_on ?? "0"), - "years", - ) - .format("YYYY-MM-DD") - : undefined, - doctor_medical_council_registration: - state.form.user_type === "Doctor" - ? state.form.doctor_medical_council_registration - : undefined, - }; + const handleSubmit = async (formData: UserForm) => { + setIsLoading(true); + const data = { + user_type: formData.user_type, + gender: formData.gender, + password: formData.password, + facilities: formData.facilities ? formData.facilities : undefined, + home_facility: formData.home_facility ?? undefined, + username: formData.username, + first_name: formData.first_name ? formData.first_name : undefined, + last_name: formData.last_name ? formData.last_name : undefined, + email: formData.email, + state: formData.state, + district: formData.district, + local_body: showLocalbody ? formData.local_body : null, + phone_number: + formData.phone_number === "+91" + ? "" + : parsePhoneNumber(formData.phone_number), + alt_phone_number: + parsePhoneNumber( + formData.phone_number_is_whatsapp + ? formData.phone_number === "+91" + ? "" + : formData.phone_number + : formData.alt_phone_number === "+91" + ? "" + : formData.alt_phone_number, + ) ?? "", + date_of_birth: dateQueryString(formData.date_of_birth), + qualification: + formData.user_type === "Doctor" || formData.user_type == "Nurse" + ? formData.qualification + : undefined, + doctor_experience_commenced_on: + formData.user_type === "Doctor" + ? dayjs() + .subtract( + parseInt(formData.doctor_experience_commenced_on ?? "0"), + "years", + ) + .format("YYYY-MM-DD") + : undefined, + doctor_medical_council_registration: + formData.user_type === "Doctor" + ? formData.doctor_medical_council_registration + : undefined, + }; - const { res } = await request(routes.addUser, { - body: data, - }); - if (res?.ok) { - dispatch({ type: "set_form", form: initForm }); - if (!userId) { - Notification.Success({ - msg: "User added successfully", - }); - } else { - Notification.Success({ - msg: "User updated successfully", - }); - } - navigate("/users"); + const { res } = await request(routes.addUser, { + body: data, + }); + if (res?.ok) { + dispatch({ type: "set_form", form: initForm }); + if (!username) { + Notification.Success({ + msg: "User added successfully", + }); + } else { + Notification.Success({ + msg: "User updated successfully", + }); } - setIsLoading(false); + navigate("/users"); } + setIsLoading(false); }; if (isLoading) { return ; } - const field = (name: string) => { - return { - id: name, - name, - onChange: handleFieldChange, - value: (state.form as any)[name], - error: (state.errors as any)[name], - }; + const handleCancel = () => { + dispatch({ + type: "set_form", + form: formVals.current, + }); }; return ( @@ -633,329 +694,436 @@ export const UserAdd = (props: UserProps) => { } backUrl="/users" > - -
handleSubmit(e)}> - { - dispatch({ type: "set_state", state: newState }); - }} - formData={state.form} - /> -
-
- Facilities - -
- o.role + (o.readOnly ? " (Read Only)" : "")} - optionValue={(o) => o.id} - /> - - {(state.form.user_type === "Doctor" || - state.form.user_type === "Nurse") && ( - - )} - {state.form.user_type === "Doctor" && ( - <> + + disabled={isLoading} + defaults={userData ? state.form : initForm} + validate={validateForm} + onCancel={editUser ? handleCancel : () => goBack()} + onSubmit={editUser ? handleEditSubmit : handleSubmit} + onDraftRestore={(newState) => { + dispatch({ type: "set_state", state: newState }); + }} + hideRestoreDraft={editUser} + noPadding + resetFormVals + > + {(field) => ( + <> +
+ {!editUser && ( +
+ Facilities + +
+ )} + {!editUser && ( +
+ + o.role + (o.readOnly ? " (Read Only)" : "") + } + onChange={(e) => { + handleFieldChange(e, field); + }} + optionValue={(o) => o.id} + className="flex-1" + /> + option.name} + optionValue={(option) => option.id} + onChange={(e) => { + handleFieldChange(e, field); + }} + className="flex-1" + /> +
+ )} + {(state.form.user_type === "Doctor" || + state.form.user_type === "Nurse") && ( { + handleFieldChange(e, field); + }} + className="flex-1" /> + )} + {state.form.user_type === "Doctor" && ( +
+ { + handleFieldChange(e, field); + }} + className="flex-1" + /> - { + handleFieldChange(e, field); + }} + className="flex-1" + /> +
+ )} + +
+
+ { + handleFieldChange(e, field); + }} + className="" + /> + {!editUser && ( + { + handleFieldChange(e, field); + }} + label="Is the phone number a WhatsApp number?" + /> + )} +
+ { + handleFieldChange(e, field); + }} + className="flex-1" /> - - )} - - option.name} - optionValue={(option) => option.id} - onChange={handleFieldChange} - /> - -
- - -
+
- +
+ {!editUser && ( + { + handleFieldChange(e, field); + setUsernameInput(e.value); + }} + onFocus={() => setUsernameInputInFocus(true)} + onBlur={() => { + setUsernameInputInFocus(false); + }} + /> + )} + {!editUser && usernameInputInFocus && ( +
+
+ {usernameExists !== userExistsEnums.idle && ( + <> + {usernameExists === userExistsEnums.checking ? ( + + {" "} + checking... + + ) : ( + <> + {usernameExists === userExistsEnums.exists ? ( +
+ {" "} + + Username is not available + +
+ ) : ( +
+ {" "} + + Username is available + +
+ )} + + )} + + )} +
+
+ {validateRule( + usernameInput.length >= 4 && usernameInput.length <= 16, + "Username should be 4-16 characters long", + )} +
+
+ {validateRule( + /^[a-z0-9._-]*$/.test(usernameInput), + "Username can only contain lowercase letters, numbers, and . _ -", + )} +
+
+ {validateRule( + /^[a-z0-9].*[a-z0-9]$/i.test(usernameInput), + "Username must start and end with a letter or number", + )} +
+
+ {validateRule( + !/(?:[._-]{2,})/.test(usernameInput), + "Username can't contain consecutive special characters . _ -", + )} +
+
+ )} +
-
+ {!editUser && ( + <> +
+
+ setPasswordInputInFocus(true)} + onBlur={() => setPasswordInputInFocus(false)} + onChange={(e) => { + handleFieldChange(e, field); + }} + /> + {passwordInputInFocus && state.form.password && ( +
+ {validateRule( + state.form.password.length >= 8, + "Password should be atleast 8 characters long", + )} + {validateRule( + state.form.password !== + state.form.password.toUpperCase(), + "Password should contain at least 1 lowercase letter", + )} + {validateRule( + state.form.password !== + state.form.password.toLowerCase(), + "Password should contain at least 1 uppercase letter", + )} + {validateRule( + /\d/.test(state.form.password), + "Password should contain at least 1 number", + )} +
+ )} +
+
+ setConfirmPasswordInputInFocus(true)} + onBlur={() => setConfirmPasswordInputInFocus(false)} + onChange={(e) => { + handleFieldChange(e, field); + }} + /> + {confirmPasswordInputInFocus && + state.form.c_password && + state.form.c_password.length > 0 && + validateRule( + state.form.c_password === state.form.password, + "Confirm password should match the entered password", + )} +
+
+ + )} +
+ { + handleFieldChange(e, field); + }} + /> + { + handleFieldChange(e, field); + }} + /> +
{ - handleFieldChange(e); - setUsernameInput(e.value); - }} - onFocus={() => setUsernameInputInFocus(true)} - onBlur={() => { - setUsernameInputInFocus(false); + handleFieldChange(e, field); }} /> - {usernameInputInFocus && ( -
-
- {usernameExists !== userExistsEnums.idle && ( - <> - {usernameExists === userExistsEnums.checking ? ( - - {" "} - checking... - - ) : ( - <> - {usernameExists === userExistsEnums.exists ? ( -
- {" "} - - Username is not available - -
- ) : ( -
- {" "} - - Username is available - -
- )} - - )} - - )} -
-
- {validateRule( - usernameInput.length >= 4 && usernameInput.length <= 16, - "Username should be 4-16 characters long", - )} -
-
- {validateRule( - /^[a-z0-9._-]*$/.test(usernameInput), - "Username can only contain lowercase letters, numbers, and . _ -", - )} -
-
- {validateRule( - /^[a-z0-9].*[a-z0-9]$/i.test(usernameInput), - "Username must start and end with a letter or number", - )} -
-
- {validateRule( - !/(?:[._-]{2,})/.test(usernameInput), - "Username can't contain consecutive special characters . _ -", - )} +
+ + o.text} + optionValue={(o) => o.text} + onChange={(e) => { + handleFieldChange(e, field); + }} + className="flex-1" + /> +
+ + {editUser && ( + <> +
+ { + handleFieldChange(e, field); + }} + /> + { + handleFieldChange(e, field); + }} + />
-
+ )} -
- - -
- setPasswordInputInFocus(true)} - onBlur={() => setPasswordInputInFocus(false)} - /> - {passwordInputInFocus && ( -
- {validateRule( - state.form.password?.length >= 8, - "Password should be atleast 8 characters long", - )} - {validateRule( - state.form.password !== state.form.password.toUpperCase(), - "Password should contain at least 1 lowercase letter", - )} - {validateRule( - state.form.password !== state.form.password.toLowerCase(), - "Password should contain at least 1 uppercase letter", + {!editUser && ( + <> + {isStateLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e, field); + if (e) setSelectedStateId(e.value); + }} + /> )} - {validateRule( - /\d/.test(state.form.password), - "Password should contain at least 1 number", + + {isDistrictLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e, field); + if (e) setSelectedDistrictId(e.value); + }} + /> )} -
+ + {showLocalbody && + (isLocalbodyLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + /> + ))} + )}
-
- setConfirmPasswordInputInFocus(true)} - onBlur={() => setConfirmPasswordInputInFocus(false)} - /> - {confirmPasswordInputInFocus && - state.form.c_password.length > 0 && - validateRule( - state.form.c_password === state.form.password, - "Confirm password should match the entered password", - )} -
- - - - o.text} - optionValue={(o) => o.text} - /> - - {isStateLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - onChange={(e) => { - handleFieldChange(e); - if (e) setSelectedStateId(e.value); - }} - /> - )} - - {isDistrictLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - onChange={(e) => { - handleFieldChange(e); - if (e) setSelectedDistrictId(e.value); - }} - /> - )} - - {showLocalbody && - (isLocalbodyLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - /> - ))} -
-
- goBack()} /> - -
- - + + )} + ); }; From 2e6bb42048cea98426e5312a720c28b79502ba06 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Wed, 6 Nov 2024 17:14:00 +0530 Subject: [PATCH 11/55] Convert addUser to be reusable Pt2 - Import the form into UserInformationTab (removing the current form) - Moved out the form components into its own file - Import the form into UserAdd --- src/Routers/routes/UserRoutes.tsx | 3 +- .../ABDM/LinkAbhaNumber/CreateWithAadhaar.tsx | 2 +- src/components/Auth/ResetPassword.tsx | 2 +- src/components/Users/UserAdd.tsx | 1112 +--------------- src/components/Users/UserAddEditForm.tsx | 1124 +++++++++++++++++ src/components/Users/UserInformation.tsx | 328 +---- src/components/Users/UserProfile.tsx | 2 +- src/components/Users/UserResetPassword.tsx | 2 +- 8 files changed, 1141 insertions(+), 1434 deletions(-) create mode 100644 src/components/Users/UserAddEditForm.tsx diff --git a/src/Routers/routes/UserRoutes.tsx b/src/Routers/routes/UserRoutes.tsx index e2cce4ecc2c..47b45a5eb63 100644 --- a/src/Routers/routes/UserRoutes.tsx +++ b/src/Routers/routes/UserRoutes.tsx @@ -1,5 +1,5 @@ import ManageUsers from "@/components/Users/ManageUsers"; -import { UserAdd } from "@/components/Users/UserAdd"; +import UserAdd from "@/components/Users/UserAdd"; import UserProfile from "@/components/Users/UserProfile"; import { AppRoutes } from "../AppRouter"; import UserHome from "@/components/Users/UserHome"; @@ -7,7 +7,6 @@ import UserHome from "@/components/Users/UserHome"; const UserRoutes: AppRoutes = { "/users": () => , "/users/add": () => , - "/users/edit/:username": ({ username }) => , "/users/:username": ({ username }) => ( ), diff --git a/src/components/ABDM/LinkAbhaNumber/CreateWithAadhaar.tsx b/src/components/ABDM/LinkAbhaNumber/CreateWithAadhaar.tsx index dc2b5ff7ef6..1b02963358b 100644 --- a/src/components/ABDM/LinkAbhaNumber/CreateWithAadhaar.tsx +++ b/src/components/ABDM/LinkAbhaNumber/CreateWithAadhaar.tsx @@ -9,7 +9,7 @@ import ButtonV2, { import OtpFormField from "../../Form/FormFields/OtpFormField"; import PhoneNumberFormField from "../../Form/FormFields/PhoneNumberFormField"; import { AbhaNumberModel } from "../types/abha"; -import { validateRule } from "../../Users/UserAdd"; +import { validateRule } from "../../Users/UserAddEditForm"; import request from "../../../Utils/request/request"; import routes from "../../../Redux/api"; import * as Notify from "../../../Utils/Notifications"; diff --git a/src/components/Auth/ResetPassword.tsx b/src/components/Auth/ResetPassword.tsx index 15fc6cbac69..d31e7998000 100644 --- a/src/components/Auth/ResetPassword.tsx +++ b/src/components/Auth/ResetPassword.tsx @@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next"; import { LocalStorageKeys } from "@/common/constants"; import { Cancel, Submit } from "@/components/Common/components/ButtonV2"; import TextFormField from "../Form/FormFields/TextFormField"; -import { validateRule } from "../Users/UserAdd"; +import { validateRule } from "../Users/UserAddEditForm"; import { validatePassword } from "@/common/validation"; import routes from "../../Redux/api"; diff --git a/src/components/Users/UserAdd.tsx b/src/components/Users/UserAdd.tsx index 3e2eac21ff4..552312199d7 100644 --- a/src/components/Users/UserAdd.tsx +++ b/src/components/Users/UserAdd.tsx @@ -1,687 +1,16 @@ -import { Link, navigate } from "raviger"; -import { useEffect, useRef, useState } from "react"; -import { - GENDER_TYPES, - USER_TYPES, - USER_TYPE_OPTIONS, -} from "@/common/constants"; -import { useAbortableEffect } from "@/common/utils"; -import { - validateEmailAddress, - validateName, - validatePassword, - validateUsername, -} from "@/common/validation"; -import * as Notification from "../../Utils/Notifications"; -import { FacilitySelect } from "@/components/Common/FacilitySelect"; -import { FacilityModel } from "../Facility/models"; -import { - classNames, - dateQueryString, - isValidUrl, - parsePhoneNumber, - scrollTo, -} from "../../Utils/utils"; -import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; -import TextFormField from "../Form/FormFields/TextFormField"; -import { FieldChangeEvent } from "../Form/FormFields/Utils"; -import { SelectFormField } from "../Form/FormFields/SelectFormField"; -import DateFormField from "../Form/FormFields/DateFormField"; -import { FieldLabel } from "../Form/FormFields/FormField"; -import useAppHistory from "@/common/hooks/useAppHistory"; +import { Link } from "raviger"; import Page from "@/components/Common/components/Page"; -import CircularProgress from "@/components/Common/components/CircularProgress"; -import { useAutoSaveReducer } from "../../Utils/AutoSave"; -import dayjs from "../../Utils/dayjs"; -import useAuthUser from "@/common/hooks/useAuthUser"; -import { FieldError, PhoneNumberValidator } from "../Form/FieldValidators"; -import routes from "../../Redux/api"; -import request from "../../Utils/request/request"; -import useQuery from "../../Utils/request/useQuery"; import CareIcon from "../../CAREUI/icons/CareIcon"; -import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import { useTranslation } from "react-i18next"; -import Loading from "@/components/Common/Loading"; -import { GenderType } from "./models"; -import Form from "../Form/Form"; -interface UserProps { - username?: string; -} +import UserAddEditForm from "./UserAddEditForm"; -interface StateObj { - id: number; - name: string; -} - -type UserForm = { - user_type?: string; - gender: string; - password?: string; - c_password?: string; - facilities?: Array; - home_facility?: FacilityModel | null; - username?: string; - first_name: string; - last_name: string; - email: string; - phone_number: string; - alt_phone_number: string; - phone_number_is_whatsapp?: boolean; - date_of_birth: Date | null | string; - state?: number; - district?: number; - local_body?: number; - qualification?: string | undefined; - doctor_experience_commenced_on?: string | undefined; - doctor_medical_council_registration?: string | undefined; - video_connect_link?: string; - weekly_working_hours?: string | null; -}; - -const initForm: UserForm = { - user_type: "", - gender: "", - password: "", - c_password: "", - facilities: [], - home_facility: null, - username: "", - first_name: "", - last_name: "", - email: "", - phone_number: "+91", - alt_phone_number: "+91", - phone_number_is_whatsapp: true, - date_of_birth: null, - state: 0, - district: 0, - local_body: 0, - qualification: undefined, - doctor_experience_commenced_on: undefined, - doctor_medical_council_registration: undefined, -}; - -const STAFF_OR_NURSE_USER = [ - "Staff", - "StaffReadOnly", - "Nurse", - "NurseReadOnly", -]; - -const initError = Object.assign( - {}, - ...Object.keys(initForm).map((k) => ({ [k]: "" })), -); - -const initialState = { - form: { ...initForm }, - errors: { ...initError }, -}; - -const user_create_reducer = (state = initialState, action: any) => { - switch (action.type) { - case "set_form": { - return { - ...state, - form: action.form, - }; - } - case "set_errors": { - return { - ...state, - errors: action.errors, - }; - } - case "set_state": { - if (action.state) return action.state; - return state; - } - default: - return state; - } -}; - -const getDate = (value: any) => - value && dayjs(value).isValid() && dayjs(value).toDate(); - -export const validateRule = ( - condition: boolean, - content: JSX.Element | string, -) => { - return ( -
- {condition ? ( - - ) : ( - - )}{" "} - - {content} - -
- ); -}; - -export const UserAdd = (props: UserProps) => { +const UserAdd = () => { const { t } = useTranslation(); - const { goBack } = useAppHistory(); - const { username } = props; - const editUser = username ? true : false; - const formVals = useRef(initForm); - - const { data: userData, refetch: refetchUserData } = useQuery( - routes.getUserDetails, - { - pathParams: { - username: username ?? "", - }, - prefetch: editUser, - onResponse: (result) => { - if (!editUser || !result || !result.res || !result.data) return; - const formData: UserForm = { - first_name: result.data.first_name, - last_name: result.data.last_name, - date_of_birth: result.data.date_of_birth || null, - gender: result.data.gender || "Male", - email: result.data.email, - video_connect_link: result.data.video_connect_link, - phone_number: result.data.phone_number?.toString() || "", - alt_phone_number: result.data.alt_phone_number?.toString() || "", - weekly_working_hours: result.data.weekly_working_hours, - }; - dispatch({ - type: "set_form", - form: formData, - }); - formVals.current = formData; - }, - }, - ); - - const handleEditSubmit = async (formData: UserForm) => { - const data = { - first_name: formData.first_name, - last_name: formData.last_name, - email: formData.email, - video_connect_link: formData.video_connect_link, - phone_number: parsePhoneNumber(formData.phone_number) ?? "", - alt_phone_number: parsePhoneNumber(formData.alt_phone_number) ?? "", - gender: formData.gender as GenderType, - date_of_birth: dateQueryString(formData.date_of_birth), - qualification: - formData.user_type === "Doctor" || formData.user_type === "Nurse" - ? formData.qualification - : undefined, - doctor_experience_commenced_on: - formData.user_type === "Doctor" - ? dayjs() - .subtract( - parseInt( - (formData.doctor_experience_commenced_on as string) ?? "0", - ), - "years", - ) - .format("YYYY-MM-DD") - : undefined, - doctor_medical_council_registration: - formData.user_type === "Doctor" - ? formData.doctor_medical_council_registration - : undefined, - weekly_working_hours: - formData.weekly_working_hours && formData.weekly_working_hours !== "" - ? formData.weekly_working_hours - : null, - }; - const { res } = await request(routes.partialUpdateUser, { - pathParams: { username: authUser.username }, - body: data, - }); - if (res?.ok) { - Notification.Success({ - msg: "Details updated successfully", - }); - await refetchUserData(); - } - }; - - const [state, dispatch] = useAutoSaveReducer( - user_create_reducer, - initialState, - ); - const [isLoading, setIsLoading] = useState(false); - const [states, setStates] = useState([]); - const [selectedStateId, setSelectedStateId] = useState(0); - const [selectedDistrictId, setSelectedDistrictId] = useState(0); - const [districts, setDistricts] = useState([]); - const [localBodies, setLocalBodies] = useState([]); - const [selectedFacility, setSelectedFacility] = useState([]); - const [usernameInputInFocus, setUsernameInputInFocus] = useState(false); - const [passwordInputInFocus, setPasswordInputInFocus] = useState(false); - const [confirmPasswordInputInFocus, setConfirmPasswordInputInFocus] = - useState(false); - const [usernameInput, setUsernameInput] = useState(""); - - const userExistsEnums = { - idle: 0, - checking: 1, - exists: 2, - available: 3, - }; - - const [usernameExists, setUsernameExists] = useState(0); - - const check_username = async (username: string) => { - setUsernameExists(userExistsEnums.checking); - const { res: usernameCheck } = await request(routes.checkUsername, { - pathParams: { username }, - silent: true, - }); - if (usernameCheck === undefined || usernameCheck.status === 409) - setUsernameExists(userExistsEnums.exists); - else if (usernameCheck.status === 200) - setUsernameExists(userExistsEnums.available); - else - Notification.Error({ - msg: "Some error checking username availabality. Please try again later.", - }); - }; - - useEffect(() => { - setUsernameExists(userExistsEnums.idle); - if (validateUsername(usernameInput)) { - const timeout = setTimeout(() => { - check_username(usernameInput); - }, 500); - return () => clearTimeout(timeout); - } - }, [usernameInput]); - - const authUser = useAuthUser(); - - const userIndex = USER_TYPES.indexOf(authUser.user_type); - const readOnlyUsers = USER_TYPE_OPTIONS.filter((user) => user.readOnly); - - const defaultAllowedUserTypes = USER_TYPE_OPTIONS.slice(0, userIndex + 1); - const userTypes = authUser.is_superuser - ? [...USER_TYPE_OPTIONS] - : authUser.user_type === "StaffReadOnly" - ? readOnlyUsers.slice(0, 1) - : authUser.user_type === "DistrictReadOnlyAdmin" - ? readOnlyUsers.slice(0, 2) - : authUser.user_type === "StateReadOnlyAdmin" - ? readOnlyUsers.slice(0, 3) - : authUser.user_type === "Pharmacist" - ? USER_TYPE_OPTIONS.slice(0, 1) - : // Exception to allow Staff to Create Doctors - defaultAllowedUserTypes; - - // TODO: refactor lines 227 through 248 to be more readable. This is messy. - if (authUser.user_type === "Nurse" || authUser.user_type === "Staff") { - userTypes.push(USER_TYPE_OPTIONS[6]); // Temperorily allows creation of users with elevated permissions due to introduction of new roles. - } - - const headerText = !editUser ? "Add User" : "Update User"; - const showLocalbody = ![ - "Pharmacist", - "Volunteer", - "Doctor", - ...STAFF_OR_NURSE_USER, - ].includes(state.form.user_type ?? ""); - - const { loading: isDistrictLoading } = useQuery(routes.getDistrictByState, { - prefetch: !!(selectedStateId > 0), - pathParams: { id: selectedStateId.toString() }, - onResponse: (result) => { - if (!result || !result.res || !result.data) return; - if (userIndex <= USER_TYPES.indexOf("DistrictAdmin")) { - setDistricts([authUser.district_object!]); - } else { - setDistricts(result.data); - } - }, - }); - - const { loading: isLocalbodyLoading } = useQuery( - routes.getAllLocalBodyByDistrict, - { - prefetch: !!(selectedDistrictId > 0), - pathParams: { id: selectedDistrictId.toString() }, - onResponse: (result) => { - if (!result || !result.res || !result.data) return; - if (userIndex <= USER_TYPES.indexOf("LocalBodyAdmin")) { - setLocalBodies([authUser.local_body_object!]); - } else { - setLocalBodies(result.data); - } - }, - }, - ); - - const { loading: isStateLoading } = useQuery(routes.statesList, { - onResponse: (result) => { - if (!result || !result.res || !result.data) return; - if (userIndex <= USER_TYPES.indexOf("StateAdmin")) { - setStates([authUser.state_object!]); - } else { - setStates(result.data.results); - } - }, - }); - - const handleDateChange = (e: FieldChangeEvent) => { - if (dayjs(e.value).isValid()) { - const errors = { ...state.errors, [e.name]: "" }; - dispatch({ - type: "set_form", - form: { - ...state.form, - [e.name]: dayjs(e.value).format("YYYY-MM-DD"), - }, - }); - dispatch({ type: "set_errors", errors }); - } - }; - - const handleFieldChange = (event: FieldChangeEvent, field?: any) => { - const errors = { ...state.errors, [event.name]: "" }; - dispatch({ - type: "set_form", - form: { - ...state.form, - [event.name]: event.value, - }, - }); - dispatch({ type: "set_errors", errors }); - if (field) field(event.name).onChange(event); - }; - - useAbortableEffect(() => { - if (state.form.phone_number_is_whatsapp) { - handleFieldChange({ - name: "alt_phone_number", - value: state.form.phone_number, - }); - } - }, [state.form.phone_number_is_whatsapp, state.form.phone_number]); - - const setFacility = (selected: FacilityModel | FacilityModel[] | null) => { - const newSelectedFacilities = selected - ? Array.isArray(selected) - ? selected - : [selected] - : []; - setSelectedFacility(newSelectedFacilities as FacilityModel[]); - const form = { ...state.form }; - form.facilities = selected - ? (selected as FacilityModel[]).map((i) => i.id!) - : []; - dispatch({ type: "set_form", form }); - }; - - const validateForm = (formData: UserForm) => { - const errors: Partial> = {}; - Object.keys(formData).forEach((field) => { - switch (field) { - case "facilities": - if ( - formData.facilities && - formData.user_type && - formData["facilities"].length === 0 && - STAFF_OR_NURSE_USER.includes(authUser.user_type) && - STAFF_OR_NURSE_USER.includes(formData.user_type) - ) { - errors[field] = - "Please select atleast one of the facilities you are linked to"; - } - return; - case "user_type": - if (!formData[field]) { - errors[field] = "Please select the User Type"; - } - return; - case "doctor_experience_commenced_on": - if (formData.user_type === "Doctor" && !formData[field]) { - errors[field] = t("field_required"); - } else if ( - formData.user_type === "Doctor" && - Number(formData.doctor_experience_commenced_on) > 100 - ) { - errors[field] = "Doctor experience should be less than 100 years"; - } - return; - case "qualification": - if ( - (formData.user_type === "Doctor" || - formData.user_type === "Nurse") && - !formData[field] - ) { - errors[field] = t("field_required"); - } - return; - case "doctor_medical_council_registration": - if (formData.user_type === "Doctor" && !formData[field]) { - errors[field] = t("field_required"); - } - return; - case "first_name": - case "last_name": - formData[field] = formData[field].trim(); - if (!formData[field]) { - errors[field] = `${field - .split("_") - .map((word) => word[0].toUpperCase() + word.slice(1)) - .join(" ")} is required`; - } else if (!validateName(formData[field])) { - errors[field] = "Please enter a valid name"; - } - return; - case "gender": - if (!formData[field]) { - errors[field] = "Please select the Gender"; - } - return; - case "username": - if (!formData[field]) { - errors[field] = "Please enter the username"; - } else if (!validateUsername(formData[field])) { - errors[field] = - "Please enter a 4-16 characters long username with lowercase letters, digits and . _ - only and it should not start or end with . _ -"; - } else if (usernameExists !== userExistsEnums.available) { - errors[field] = "This username already exists"; - } - return; - case "password": - if (!formData[field]) { - errors[field] = "Please enter the password"; - } else if (!validatePassword(formData[field])) { - errors.password = - "Password should have 1 lowercase letter, 1 uppercase letter, 1 number, and be at least 8 characters long"; - } - return; - case "c_password": - if (!formData.password) { - errors.c_password = "Confirm password is required"; - } else if (formData.password !== formData.c_password) { - errors.c_password = "Passwords not matching"; - } - return; - case "phone_number": - // eslint-disable-next-line no-case-declarations - const phoneNumber = parsePhoneNumber(formData[field]); - // eslint-disable-next-line no-case-declarations - let is_valid = false; - if (phoneNumber) { - is_valid = PhoneNumberValidator()(phoneNumber) === undefined; - } - if (!formData[field] || !is_valid) { - errors[field] = "Please enter valid phone number"; - } - return; - - case "alt_phone_number": - // eslint-disable-next-line no-case-declarations - let alt_is_valid = false; - if (formData[field] && formData[field] !== "+91") { - const altPhoneNumber = parsePhoneNumber(formData[field]); - if (altPhoneNumber) { - alt_is_valid = - PhoneNumberValidator(["mobile"])(altPhoneNumber) === undefined; - } - } - if (formData[field] && formData[field] !== "+91" && !alt_is_valid) { - errors[field] = "Please enter valid mobile number"; - } - return; - case "email": - formData[field] = formData[field].trim(); - if ( - formData[field].length === 0 || - !validateEmailAddress(formData[field]) - ) { - errors[field] = "Please enter a valid email address"; - } - return; - case "date_of_birth": - if (!formData[field]) { - errors[field] = "Please enter date in DD/MM/YYYY format"; - } else if ( - dayjs(formData[field]).isAfter(dayjs().subtract(1, "year")) - ) { - errors[field] = "Enter a valid date of birth"; - } - return; - case "state": - if (!Number(formData[field])) { - errors[field] = "Please select the state"; - } - return; - case "district": - if (!Number(formData[field])) { - errors[field] = "Please select the district"; - } - return; - case "local_body": - if (showLocalbody && !Number(formData[field])) { - errors[field] = "Please select the local body"; - } - return; - case "weekly_working_hours": - if ( - formData[field] && - (Number(formData[field]) < 0 || - Number(formData[field]) > 168 || - !/^\d+$/.test(formData[field] ?? "")) - ) { - errors[field] = - "Average weekly working hours must be a number between 0 and 168"; - } - return; - case "video_connect_link": - if (formData[field]) { - if (isValidUrl(formData[field]) === false) { - errors[field] = "Please enter a valid url"; - } - } - return; - - default: - return; - } - }); - - const firstError = Object.values(errors).find((e) => e); - if (firstError) { - scrollTo(firstError); - } - dispatch({ type: "set_errors", errors }); - return errors; - }; - - const handleSubmit = async (formData: UserForm) => { - setIsLoading(true); - const data = { - user_type: formData.user_type, - gender: formData.gender, - password: formData.password, - facilities: formData.facilities ? formData.facilities : undefined, - home_facility: formData.home_facility ?? undefined, - username: formData.username, - first_name: formData.first_name ? formData.first_name : undefined, - last_name: formData.last_name ? formData.last_name : undefined, - email: formData.email, - state: formData.state, - district: formData.district, - local_body: showLocalbody ? formData.local_body : null, - phone_number: - formData.phone_number === "+91" - ? "" - : parsePhoneNumber(formData.phone_number), - alt_phone_number: - parsePhoneNumber( - formData.phone_number_is_whatsapp - ? formData.phone_number === "+91" - ? "" - : formData.phone_number - : formData.alt_phone_number === "+91" - ? "" - : formData.alt_phone_number, - ) ?? "", - date_of_birth: dateQueryString(formData.date_of_birth), - qualification: - formData.user_type === "Doctor" || formData.user_type == "Nurse" - ? formData.qualification - : undefined, - doctor_experience_commenced_on: - formData.user_type === "Doctor" - ? dayjs() - .subtract( - parseInt(formData.doctor_experience_commenced_on ?? "0"), - "years", - ) - .format("YYYY-MM-DD") - : undefined, - doctor_medical_council_registration: - formData.user_type === "Doctor" - ? formData.doctor_medical_council_registration - : undefined, - }; - - const { res } = await request(routes.addUser, { - body: data, - }); - if (res?.ok) { - dispatch({ type: "set_form", form: initForm }); - if (!username) { - Notification.Success({ - msg: "User added successfully", - }); - } else { - Notification.Success({ - msg: "User updated successfully", - }); - } - navigate("/users"); - } - setIsLoading(false); - }; - - if (isLoading) { - return ; - } - - const handleCancel = () => { - dispatch({ - type: "set_form", - form: formVals.current, - }); - }; return ( { } backUrl="/users" > - - disabled={isLoading} - defaults={userData ? state.form : initForm} - validate={validateForm} - onCancel={editUser ? handleCancel : () => goBack()} - onSubmit={editUser ? handleEditSubmit : handleSubmit} - onDraftRestore={(newState) => { - dispatch({ type: "set_state", state: newState }); - }} - hideRestoreDraft={editUser} - noPadding - resetFormVals - > - {(field) => ( - <> -
- {!editUser && ( -
- Facilities - -
- )} - {!editUser && ( -
- - o.role + (o.readOnly ? " (Read Only)" : "") - } - onChange={(e) => { - handleFieldChange(e, field); - }} - optionValue={(o) => o.id} - className="flex-1" - /> - option.name} - optionValue={(option) => option.id} - onChange={(e) => { - handleFieldChange(e, field); - }} - className="flex-1" - /> -
- )} - {(state.form.user_type === "Doctor" || - state.form.user_type === "Nurse") && ( - { - handleFieldChange(e, field); - }} - className="flex-1" - /> - )} - {state.form.user_type === "Doctor" && ( -
- { - handleFieldChange(e, field); - }} - className="flex-1" - /> - - { - handleFieldChange(e, field); - }} - className="flex-1" - /> -
- )} - -
-
- { - handleFieldChange(e, field); - }} - className="" - /> - {!editUser && ( - { - handleFieldChange(e, field); - }} - label="Is the phone number a WhatsApp number?" - /> - )} -
- { - handleFieldChange(e, field); - }} - className="flex-1" - /> -
- -
- {!editUser && ( - { - handleFieldChange(e, field); - setUsernameInput(e.value); - }} - onFocus={() => setUsernameInputInFocus(true)} - onBlur={() => { - setUsernameInputInFocus(false); - }} - /> - )} - {!editUser && usernameInputInFocus && ( -
-
- {usernameExists !== userExistsEnums.idle && ( - <> - {usernameExists === userExistsEnums.checking ? ( - - {" "} - checking... - - ) : ( - <> - {usernameExists === userExistsEnums.exists ? ( -
- {" "} - - Username is not available - -
- ) : ( -
- {" "} - - Username is available - -
- )} - - )} - - )} -
-
- {validateRule( - usernameInput.length >= 4 && usernameInput.length <= 16, - "Username should be 4-16 characters long", - )} -
-
- {validateRule( - /^[a-z0-9._-]*$/.test(usernameInput), - "Username can only contain lowercase letters, numbers, and . _ -", - )} -
-
- {validateRule( - /^[a-z0-9].*[a-z0-9]$/i.test(usernameInput), - "Username must start and end with a letter or number", - )} -
-
- {validateRule( - !/(?:[._-]{2,})/.test(usernameInput), - "Username can't contain consecutive special characters . _ -", - )} -
-
- )} -
- - {!editUser && ( - <> -
-
- setPasswordInputInFocus(true)} - onBlur={() => setPasswordInputInFocus(false)} - onChange={(e) => { - handleFieldChange(e, field); - }} - /> - {passwordInputInFocus && state.form.password && ( -
- {validateRule( - state.form.password.length >= 8, - "Password should be atleast 8 characters long", - )} - {validateRule( - state.form.password !== - state.form.password.toUpperCase(), - "Password should contain at least 1 lowercase letter", - )} - {validateRule( - state.form.password !== - state.form.password.toLowerCase(), - "Password should contain at least 1 uppercase letter", - )} - {validateRule( - /\d/.test(state.form.password), - "Password should contain at least 1 number", - )} -
- )} -
-
- setConfirmPasswordInputInFocus(true)} - onBlur={() => setConfirmPasswordInputInFocus(false)} - onChange={(e) => { - handleFieldChange(e, field); - }} - /> - {confirmPasswordInputInFocus && - state.form.c_password && - state.form.c_password.length > 0 && - validateRule( - state.form.c_password === state.form.password, - "Confirm password should match the entered password", - )} -
-
- - )} -
- { - handleFieldChange(e, field); - }} - /> - { - handleFieldChange(e, field); - }} - /> -
- { - handleFieldChange(e, field); - }} - /> -
- - o.text} - optionValue={(o) => o.text} - onChange={(e) => { - handleFieldChange(e, field); - }} - className="flex-1" - /> -
- - {editUser && ( - <> -
- { - handleFieldChange(e, field); - }} - /> - { - handleFieldChange(e, field); - }} - /> -
- - )} - - {!editUser && ( - <> - {isStateLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - onChange={(e) => { - handleFieldChange(e, field); - if (e) setSelectedStateId(e.value); - }} - /> - )} - - {isDistrictLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - onChange={(e) => { - handleFieldChange(e, field); - if (e) setSelectedDistrictId(e.value); - }} - /> - )} - - {showLocalbody && - (isLocalbodyLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - /> - ))} - - )} -
- - )} - +
); }; + +export default UserAdd; diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx new file mode 100644 index 00000000000..c848d097712 --- /dev/null +++ b/src/components/Users/UserAddEditForm.tsx @@ -0,0 +1,1124 @@ +import { navigate } from "raviger"; +import { useEffect, useRef, useState } from "react"; +import { + GENDER_TYPES, + USER_TYPES, + USER_TYPE_OPTIONS, +} from "@/common/constants"; +import { useAbortableEffect } from "@/common/utils"; +import { + validateEmailAddress, + validateName, + validatePassword, + validateUsername, +} from "@/common/validation"; +import * as Notification from "../../Utils/Notifications"; +import { FacilitySelect } from "@/components/Common/FacilitySelect"; +import { FacilityModel } from "../Facility/models"; +import { + classNames, + dateQueryString, + isValidUrl, + parsePhoneNumber, + scrollTo, +} from "../../Utils/utils"; +import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; +import TextFormField from "../Form/FormFields/TextFormField"; +import { FieldChangeEvent } from "../Form/FormFields/Utils"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; +import DateFormField from "../Form/FormFields/DateFormField"; +import { FieldLabel } from "../Form/FormFields/FormField"; +import useAppHistory from "@/common/hooks/useAppHistory"; +import CircularProgress from "@/components/Common/components/CircularProgress"; +import { useAutoSaveReducer } from "../../Utils/AutoSave"; +import dayjs from "../../Utils/dayjs"; +import useAuthUser from "@/common/hooks/useAuthUser"; +import { FieldError, PhoneNumberValidator } from "../Form/FieldValidators"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; +import useQuery from "../../Utils/request/useQuery"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; +import { useTranslation } from "react-i18next"; + +import Loading from "@/components/Common/Loading"; +import { GenderType } from "./models"; +import Form from "../Form/Form"; +interface UserProps { + username?: string; +} + +interface StateObj { + id: number; + name: string; +} + +type UserForm = { + user_type?: string; + gender: string; + password?: string; + c_password?: string; + facilities?: Array; + home_facility?: FacilityModel | null; + username?: string; + first_name: string; + last_name: string; + email: string; + phone_number: string; + alt_phone_number: string; + phone_number_is_whatsapp?: boolean; + date_of_birth: Date | null | string; + state?: number; + district?: number; + local_body?: number; + qualification?: string | undefined; + doctor_experience_commenced_on?: string | undefined; + doctor_medical_council_registration?: string | undefined; + video_connect_link?: string; + weekly_working_hours?: string | null; +}; + +const initForm: UserForm = { + user_type: "", + gender: "", + password: "", + c_password: "", + facilities: [], + home_facility: null, + username: "", + first_name: "", + last_name: "", + email: "", + phone_number: "+91", + alt_phone_number: "+91", + phone_number_is_whatsapp: true, + date_of_birth: null, + state: 0, + district: 0, + local_body: 0, + qualification: undefined, + doctor_experience_commenced_on: undefined, + doctor_medical_council_registration: undefined, + weekly_working_hours: "", + video_connect_link: "", +}; + +const STAFF_OR_NURSE_USER = [ + "Staff", + "StaffReadOnly", + "Nurse", + "NurseReadOnly", +]; + +const initError = Object.assign( + {}, + ...Object.keys(initForm).map((k) => ({ [k]: "" })), +); + +const initialState = { + form: { ...initForm }, + errors: { ...initError }, +}; + +const user_create_reducer = (state = initialState, action: any) => { + switch (action.type) { + case "set_form": { + return { + ...state, + form: action.form, + }; + } + case "set_errors": { + return { + ...state, + errors: action.errors, + }; + } + case "set_state": { + if (action.state) return action.state; + return state; + } + default: + return state; + } +}; + +const getDate = (value: any) => + value && dayjs(value).isValid() && dayjs(value).toDate(); + +export const validateRule = ( + condition: boolean, + content: JSX.Element | string, +) => { + return ( +
+ {condition ? ( + + ) : ( + + )}{" "} + + {content} + +
+ ); +}; + +const UserAddEditForm = (props: UserProps) => { + const { t } = useTranslation(); + const { goBack } = useAppHistory(); + const { username } = props; + const editUser = username ? true : false; + const formVals = useRef(initForm); + + const { + loading: userDataLoading, + data: userData, + refetch: refetchUserData, + } = useQuery(routes.getUserDetails, { + pathParams: { + username: username ?? "", + }, + prefetch: editUser, + onResponse: (result) => { + if (!editUser || !result || !result.res || !result.data) return; + const formData: UserForm = { + first_name: result.data.first_name, + last_name: result.data.last_name, + date_of_birth: result.data.date_of_birth || null, + gender: result.data.gender || "Male", + email: result.data.email, + video_connect_link: result.data.video_connect_link, + phone_number: result.data.phone_number?.toString() || "", + alt_phone_number: result.data.alt_phone_number?.toString() || "", + weekly_working_hours: result.data.weekly_working_hours, + }; + dispatch({ + type: "set_form", + form: formData, + }); + formVals.current = formData; + }, + }); + + const handleEditSubmit = async (formData: UserForm) => { + if (!username) return; + const data = { + first_name: formData.first_name, + last_name: formData.last_name, + email: formData.email, + video_connect_link: formData.video_connect_link, + phone_number: parsePhoneNumber(formData.phone_number) ?? "", + alt_phone_number: parsePhoneNumber(formData.alt_phone_number) ?? "", + gender: formData.gender as GenderType, + date_of_birth: dateQueryString(formData.date_of_birth), + qualification: + formData.user_type === "Doctor" || formData.user_type === "Nurse" + ? formData.qualification + : undefined, + doctor_experience_commenced_on: + formData.user_type === "Doctor" + ? dayjs() + .subtract( + parseInt( + (formData.doctor_experience_commenced_on as string) ?? "0", + ), + "years", + ) + .format("YYYY-MM-DD") + : undefined, + doctor_medical_council_registration: + formData.user_type === "Doctor" + ? formData.doctor_medical_council_registration + : undefined, + weekly_working_hours: + formData.weekly_working_hours && formData.weekly_working_hours !== "" + ? formData.weekly_working_hours + : null, + }; + const { res } = await request(routes.partialUpdateUser, { + pathParams: { username }, + body: data, + }); + if (res?.ok) { + Notification.Success({ + msg: "Details updated successfully", + }); + await refetchUserData(); + } + }; + + const [state, dispatch] = useAutoSaveReducer( + user_create_reducer, + initialState, + ); + const [isLoading, setIsLoading] = useState(false); + const [states, setStates] = useState([]); + const [selectedStateId, setSelectedStateId] = useState(0); + const [selectedDistrictId, setSelectedDistrictId] = useState(0); + const [districts, setDistricts] = useState([]); + const [localBodies, setLocalBodies] = useState([]); + const [selectedFacility, setSelectedFacility] = useState([]); + const [usernameInputInFocus, setUsernameInputInFocus] = useState(false); + const [passwordInputInFocus, setPasswordInputInFocus] = useState(false); + const [confirmPasswordInputInFocus, setConfirmPasswordInputInFocus] = + useState(false); + const [usernameInput, setUsernameInput] = useState(""); + + const userExistsEnums = { + idle: 0, + checking: 1, + exists: 2, + available: 3, + }; + + const [usernameExists, setUsernameExists] = useState(0); + + const check_username = async (username: string) => { + setUsernameExists(userExistsEnums.checking); + const { res: usernameCheck } = await request(routes.checkUsername, { + pathParams: { username }, + silent: true, + }); + if (usernameCheck === undefined || usernameCheck.status === 409) + setUsernameExists(userExistsEnums.exists); + else if (usernameCheck.status === 200) + setUsernameExists(userExistsEnums.available); + else + Notification.Error({ + msg: "Some error checking username availabality. Please try again later.", + }); + }; + + useEffect(() => { + setUsernameExists(userExistsEnums.idle); + if (validateUsername(usernameInput)) { + const timeout = setTimeout(() => { + check_username(usernameInput); + }, 500); + return () => clearTimeout(timeout); + } + }, [usernameInput]); + + const authUser = useAuthUser(); + + const userIndex = USER_TYPES.indexOf(authUser.user_type); + const readOnlyUsers = USER_TYPE_OPTIONS.filter((user) => user.readOnly); + + const defaultAllowedUserTypes = USER_TYPE_OPTIONS.slice(0, userIndex + 1); + const userTypes = authUser.is_superuser + ? [...USER_TYPE_OPTIONS] + : authUser.user_type === "StaffReadOnly" + ? readOnlyUsers.slice(0, 1) + : authUser.user_type === "DistrictReadOnlyAdmin" + ? readOnlyUsers.slice(0, 2) + : authUser.user_type === "StateReadOnlyAdmin" + ? readOnlyUsers.slice(0, 3) + : authUser.user_type === "Pharmacist" + ? USER_TYPE_OPTIONS.slice(0, 1) + : // Exception to allow Staff to Create Doctors + defaultAllowedUserTypes; + + // TODO: refactor lines 227 through 248 to be more readable. This is messy. + if (authUser.user_type === "Nurse" || authUser.user_type === "Staff") { + userTypes.push(USER_TYPE_OPTIONS[6]); // Temperorily allows creation of users with elevated permissions due to introduction of new roles. + } + + const showLocalbody = ![ + "Pharmacist", + "Volunteer", + "Doctor", + ...STAFF_OR_NURSE_USER, + ].includes(state.form.user_type ?? ""); + + const { loading: isDistrictLoading } = useQuery(routes.getDistrictByState, { + prefetch: !!(selectedStateId > 0), + pathParams: { id: selectedStateId.toString() }, + onResponse: (result) => { + if (!result || !result.res || !result.data) return; + if (userIndex <= USER_TYPES.indexOf("DistrictAdmin")) { + setDistricts([authUser.district_object!]); + } else { + setDistricts(result.data); + } + }, + }); + + const { loading: isLocalbodyLoading } = useQuery( + routes.getAllLocalBodyByDistrict, + { + prefetch: !!(selectedDistrictId > 0), + pathParams: { id: selectedDistrictId.toString() }, + onResponse: (result) => { + if (!result || !result.res || !result.data) return; + if (userIndex <= USER_TYPES.indexOf("LocalBodyAdmin")) { + setLocalBodies([authUser.local_body_object!]); + } else { + setLocalBodies(result.data); + } + }, + }, + ); + + const { loading: isStateLoading } = useQuery(routes.statesList, { + onResponse: (result) => { + if (!result || !result.res || !result.data) return; + if (userIndex <= USER_TYPES.indexOf("StateAdmin")) { + setStates([authUser.state_object!]); + } else { + setStates(result.data.results); + } + }, + }); + + const handleDateChange = (e: FieldChangeEvent, field?: any) => { + if (dayjs(e.value).isValid()) { + const errors = { ...state.errors, [e.name]: "" }; + dispatch({ + type: "set_form", + form: { + ...state.form, + [e.name]: dayjs(e.value).format("YYYY-MM-DD"), + }, + }); + if (field) field(e.name).onChange(e); + dispatch({ type: "set_errors", errors }); + } + }; + + const handleFieldChange = (event: FieldChangeEvent, field?: any) => { + const errors = { ...state.errors, [event.name]: "" }; + dispatch({ + type: "set_form", + form: { + ...state.form, + [event.name]: event.value, + }, + }); + dispatch({ type: "set_errors", errors }); + if (field) field(event.name).onChange(event); + }; + + useAbortableEffect(() => { + if (state.form.phone_number_is_whatsapp) { + handleFieldChange({ + name: "alt_phone_number", + value: state.form.phone_number, + }); + } + }, [state.form.phone_number_is_whatsapp, state.form.phone_number]); + + const setFacility = (selected: FacilityModel | FacilityModel[] | null) => { + const newSelectedFacilities = selected + ? Array.isArray(selected) + ? selected + : [selected] + : []; + setSelectedFacility(newSelectedFacilities as FacilityModel[]); + const form = { ...state.form }; + form.facilities = selected + ? (selected as FacilityModel[]).map((i) => i.id!) + : []; + dispatch({ type: "set_form", form }); + }; + + const validateForm = (formData: UserForm) => { + const errors: Partial> = {}; + Object.keys(formData).forEach((field) => { + switch (field) { + case "facilities": + if ( + formData.facilities && + formData.user_type && + formData["facilities"].length === 0 && + STAFF_OR_NURSE_USER.includes(authUser.user_type) && + STAFF_OR_NURSE_USER.includes(formData.user_type) + ) { + errors[field] = + "Please select atleast one of the facilities you are linked to"; + } + return; + case "user_type": + if (!formData[field]) { + errors[field] = "Please select the User Type"; + } + return; + case "doctor_experience_commenced_on": + if (formData.user_type === "Doctor" && !formData[field]) { + errors[field] = t("field_required"); + } else if ( + formData.user_type === "Doctor" && + Number(formData.doctor_experience_commenced_on) > 100 + ) { + errors[field] = "Doctor experience should be less than 100 years"; + } + return; + case "qualification": + if ( + (formData.user_type === "Doctor" || + formData.user_type === "Nurse") && + !formData[field] + ) { + errors[field] = t("field_required"); + } + return; + case "doctor_medical_council_registration": + if (formData.user_type === "Doctor" && !formData[field]) { + errors[field] = t("field_required"); + } + return; + case "first_name": + case "last_name": + formData[field] = formData[field].trim(); + if (!formData[field]) { + errors[field] = `${field + .split("_") + .map((word) => word[0].toUpperCase() + word.slice(1)) + .join(" ")} is required`; + } else if (!validateName(formData[field])) { + errors[field] = "Please enter a valid name"; + } + return; + case "gender": + if (!formData[field]) { + errors[field] = "Please select the Gender"; + } + return; + case "username": + if (!formData[field]) { + errors[field] = "Please enter the username"; + } else if (!validateUsername(formData[field])) { + errors[field] = + "Please enter a 4-16 characters long username with lowercase letters, digits and . _ - only and it should not start or end with . _ -"; + } else if (usernameExists !== userExistsEnums.available) { + errors[field] = "This username already exists"; + } + return; + case "password": + if (!formData[field]) { + errors[field] = "Please enter the password"; + } else if (!validatePassword(formData[field])) { + errors.password = + "Password should have 1 lowercase letter, 1 uppercase letter, 1 number, and be at least 8 characters long"; + } + return; + case "c_password": + if (!formData.password) { + errors.c_password = "Confirm password is required"; + } else if (formData.password !== formData.c_password) { + errors.c_password = "Passwords not matching"; + } + return; + case "phone_number": + // eslint-disable-next-line no-case-declarations + const phoneNumber = parsePhoneNumber(formData[field]); + // eslint-disable-next-line no-case-declarations + let is_valid = false; + if (phoneNumber) { + is_valid = PhoneNumberValidator()(phoneNumber) === undefined; + } + if (!formData[field] || !is_valid) { + errors[field] = "Please enter valid phone number"; + } + return; + + case "alt_phone_number": + // eslint-disable-next-line no-case-declarations + let alt_is_valid = false; + if (formData[field] && formData[field] !== "+91") { + const altPhoneNumber = parsePhoneNumber(formData[field]); + if (altPhoneNumber) { + alt_is_valid = + PhoneNumberValidator(["mobile"])(altPhoneNumber) === undefined; + } + } + if (formData[field] && formData[field] !== "+91" && !alt_is_valid) { + errors[field] = "Please enter valid mobile number"; + } + return; + case "email": + formData[field] = formData[field].trim(); + if ( + formData[field].length === 0 || + !validateEmailAddress(formData[field]) + ) { + errors[field] = "Please enter a valid email address"; + } + return; + case "date_of_birth": + if (!formData[field]) { + errors[field] = "Please enter date in DD/MM/YYYY format"; + } else if ( + dayjs(formData[field]).isAfter(dayjs().subtract(1, "year")) + ) { + errors[field] = "Enter a valid date of birth"; + } + return; + case "state": + if (!Number(formData[field])) { + errors[field] = "Please select the state"; + } + return; + case "district": + if (!Number(formData[field])) { + errors[field] = "Please select the district"; + } + return; + case "local_body": + if (showLocalbody && !Number(formData[field])) { + errors[field] = "Please select the local body"; + } + return; + case "weekly_working_hours": + if ( + formData[field] && + (Number(formData[field]) < 0 || + Number(formData[field]) > 168 || + !/^\d+$/.test(formData[field] ?? "")) + ) { + errors[field] = + "Average weekly working hours must be a number between 0 and 168"; + } + return; + case "video_connect_link": + if (formData[field]) { + if (isValidUrl(formData[field]) === false) { + errors[field] = "Please enter a valid url"; + } + } + return; + + default: + return; + } + }); + + const firstError = Object.values(errors).find((e) => e); + if (firstError) { + scrollTo(firstError); + } + dispatch({ type: "set_errors", errors }); + return errors; + }; + + const handleSubmit = async (formData: UserForm) => { + setIsLoading(true); + const data = { + user_type: formData.user_type, + gender: formData.gender, + password: formData.password, + facilities: formData.facilities ? formData.facilities : undefined, + home_facility: formData.home_facility ?? undefined, + username: formData.username, + first_name: formData.first_name ? formData.first_name : undefined, + last_name: formData.last_name ? formData.last_name : undefined, + email: formData.email, + state: formData.state, + district: formData.district, + local_body: showLocalbody ? formData.local_body : null, + phone_number: + formData.phone_number === "+91" + ? "" + : parsePhoneNumber(formData.phone_number), + alt_phone_number: + parsePhoneNumber( + formData.phone_number_is_whatsapp + ? formData.phone_number === "+91" + ? "" + : formData.phone_number + : formData.alt_phone_number === "+91" + ? "" + : formData.alt_phone_number, + ) ?? "", + date_of_birth: dateQueryString(formData.date_of_birth), + qualification: + formData.user_type === "Doctor" || formData.user_type == "Nurse" + ? formData.qualification + : undefined, + doctor_experience_commenced_on: + formData.user_type === "Doctor" + ? dayjs() + .subtract( + parseInt(formData.doctor_experience_commenced_on ?? "0"), + "years", + ) + .format("YYYY-MM-DD") + : undefined, + doctor_medical_council_registration: + formData.user_type === "Doctor" + ? formData.doctor_medical_council_registration + : undefined, + }; + + const { res } = await request(routes.addUser, { + body: data, + }); + if (res?.ok) { + dispatch({ type: "set_form", form: initForm }); + if (!username) { + Notification.Success({ + msg: "User added successfully", + }); + } else { + Notification.Success({ + msg: "User updated successfully", + }); + } + navigate("/users"); + } + setIsLoading(false); + }; + + if (isLoading || (editUser && userDataLoading)) { + return ; + } + + const handleCancel = () => { + dispatch({ + type: "set_form", + form: formVals.current, + }); + }; + + return ( + + disabled={isLoading} + defaults={userData ? state.form : initForm} + validate={validateForm} + onCancel={editUser ? handleCancel : () => goBack()} + onSubmit={editUser ? handleEditSubmit : handleSubmit} + onDraftRestore={(newState) => { + dispatch({ type: "set_state", state: newState }); + }} + hideRestoreDraft={editUser} + noPadding + resetFormVals + > + {(field) => ( + <> +
+ {!editUser && ( +
+ Facilities + +
+ )} + {!editUser && ( +
+ + o.role + (o.readOnly ? " (Read Only)" : "") + } + onChange={(e) => { + handleFieldChange(e, field); + }} + optionValue={(o) => o.id} + className="flex-1" + /> + option.name} + optionValue={(option) => option.id} + onChange={(e) => { + handleFieldChange(e, field); + }} + className="flex-1" + /> +
+ )} + {(state.form.user_type === "Doctor" || + state.form.user_type === "Nurse") && ( + { + handleFieldChange(e, field); + }} + className="flex-1" + /> + )} + {state.form.user_type === "Doctor" && ( +
+ { + handleFieldChange(e, field); + }} + className="flex-1" + /> + + { + handleFieldChange(e, field); + }} + className="flex-1" + /> +
+ )} + +
+
+ { + handleFieldChange(e, field); + }} + className="" + /> + {!editUser && ( + { + handleFieldChange(e, field); + }} + label="Is the phone number a WhatsApp number?" + /> + )} +
+ { + handleFieldChange(e, field); + }} + className="flex-1" + /> +
+ +
+ {!editUser && ( + { + handleFieldChange(e, field); + setUsernameInput(e.value); + }} + onFocus={() => setUsernameInputInFocus(true)} + onBlur={() => { + setUsernameInputInFocus(false); + }} + /> + )} + {!editUser && usernameInputInFocus && ( +
+
+ {usernameExists !== userExistsEnums.idle && ( + <> + {usernameExists === userExistsEnums.checking ? ( + + {" "} + checking... + + ) : ( + <> + {usernameExists === userExistsEnums.exists ? ( +
+ {" "} + + Username is not available + +
+ ) : ( +
+ {" "} + + Username is available + +
+ )} + + )} + + )} +
+
+ {validateRule( + usernameInput.length >= 4 && usernameInput.length <= 16, + "Username should be 4-16 characters long", + )} +
+
+ {validateRule( + /^[a-z0-9._-]*$/.test(usernameInput), + "Username can only contain lowercase letters, numbers, and . _ -", + )} +
+
+ {validateRule( + /^[a-z0-9].*[a-z0-9]$/i.test(usernameInput), + "Username must start and end with a letter or number", + )} +
+
+ {validateRule( + !/(?:[._-]{2,})/.test(usernameInput), + "Username can't contain consecutive special characters . _ -", + )} +
+
+ )} +
+ + {!editUser && ( + <> +
+
+ setPasswordInputInFocus(true)} + onBlur={() => setPasswordInputInFocus(false)} + onChange={(e) => { + handleFieldChange(e, field); + }} + /> + {passwordInputInFocus && state.form.password && ( +
+ {validateRule( + state.form.password.length >= 8, + "Password should be atleast 8 characters long", + )} + {validateRule( + state.form.password !== + state.form.password.toUpperCase(), + "Password should contain at least 1 lowercase letter", + )} + {validateRule( + state.form.password !== + state.form.password.toLowerCase(), + "Password should contain at least 1 uppercase letter", + )} + {validateRule( + /\d/.test(state.form.password), + "Password should contain at least 1 number", + )} +
+ )} +
+
+ setConfirmPasswordInputInFocus(true)} + onBlur={() => setConfirmPasswordInputInFocus(false)} + onChange={(e) => { + handleFieldChange(e, field); + }} + /> + {confirmPasswordInputInFocus && + state.form.c_password && + state.form.c_password.length > 0 && + validateRule( + state.form.c_password === state.form.password, + "Confirm password should match the entered password", + )} +
+
+ + )} +
+ { + handleFieldChange(e, field); + }} + /> + { + handleFieldChange(e, field); + }} + /> +
+ { + handleFieldChange(e, field); + }} + /> +
+ { + handleDateChange(e, field); + }} + position="LEFT" + disableFuture + className="flex-1" + /> + o.text} + optionValue={(o) => o.text} + onChange={(e) => { + handleFieldChange(e, field); + }} + className="flex-1" + /> +
+ + {editUser && ( + <> +
+ { + handleFieldChange(e, field); + }} + /> + { + handleFieldChange(e, field); + }} + /> +
+ + )} + + {!editUser && ( + <> + {isStateLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e, field); + if (e) setSelectedStateId(e.value); + }} + /> + )} + + {isDistrictLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e, field); + if (e) setSelectedDistrictId(e.value); + }} + /> + )} + + {showLocalbody && + (isLocalbodyLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e, field); + }} + /> + ))} + + )} +
+ + )} + + ); +}; + +export default UserAddEditForm; diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx index 5cfa04d3c02..bae3464ea1f 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserInformation.tsx @@ -1,110 +1,21 @@ -import { useState, useReducer, useRef } from "react"; -import { GENDER_TYPES, LocalStorageKeys } from "@/common/constants"; -import { validateEmailAddress } from "@/common/validation"; +import { useState } from "react"; +import { LocalStorageKeys } from "@/common/constants"; import * as Notification from "../../Utils/Notifications"; -import TextFormField from "../Form/FormFields/TextFormField"; import ButtonV2 from "@/components/Common/components/ButtonV2"; -import { - dateQueryString, - formatDisplayName, - isValidUrl, - parsePhoneNumber, - sleep, -} from "@/Utils/utils"; -import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; -import { GenderType } from "./models"; -import dayjs from "../../Utils/dayjs"; -import { - FieldError, - PhoneNumberValidator, - RequiredFieldValidator, -} from "../Form/FieldValidators"; +import { formatDisplayName, sleep } from "@/Utils/utils"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; import request from "../../Utils/request/request"; -import DateFormField from "../Form/FormFields/DateFormField"; import { useTranslation } from "react-i18next"; import Loading from "@/components/Common/Loading"; import AvatarEditModal from "@/components/Common/AvatarEditModal"; import uploadFile from "@/Utils/request/uploadFile"; import careConfig from "@careConfig"; import { Avatar } from "../Common/Avatar"; -import Form from "../Form/Form"; -import RadioFormField from "../Form/FormFields/RadioFormField"; - -type EditForm = { - firstName: string; - lastName: string; - date_of_birth: Date | null | string; - gender: GenderType; - email: string; - video_connect_link: string | undefined; - phoneNumber: string; - altPhoneNumber: string; - weekly_working_hours: string | null | undefined; -}; -type ErrorForm = { - firstName: string; - lastName: string; - date_of_birth: string | null; - gender: string; - email: string; - video_connect_link: string | undefined; - phoneNumber: string; - altPhoneNumber: string; - weekly_working_hours: string | undefined; -}; -type State = { - form: EditForm; - errors: ErrorForm; -}; -type Action = - | { type: "set_form"; form: EditForm } - | { type: "set_error"; errors: ErrorForm }; - -const initForm: EditForm = { - firstName: "", - lastName: "", - date_of_birth: null, - gender: "Male", - video_connect_link: "", - email: "", - phoneNumber: "", - altPhoneNumber: "", - weekly_working_hours: undefined, -}; - -const initError: ErrorForm = Object.assign( - {}, - ...Object.keys(initForm).map((k) => ({ [k]: "" })), -); - -const initialState: State = { - form: { ...initForm }, - errors: { ...initError }, -}; - -const editFormReducer = (state: State, action: Action) => { - switch (action.type) { - case "set_form": { - return { - ...state, - form: action.form, - }; - } - case "set_error": { - return { - ...state, - errors: action.errors, - }; - } - } -}; +import UserAddEditForm from "./UserAddEditForm"; export default function UserInformation({ username }: { username: string }) { const { t } = useTranslation(); - const [states, dispatch] = useReducer(editFormReducer, initialState); - const formVals = useRef(initForm); const [editAvatar, setEditAvatar] = useState(false); const { @@ -115,147 +26,12 @@ export default function UserInformation({ username }: { username: string }) { pathParams: { username, }, - onResponse: (result) => { - if (!result || !result.res || !result.data) return; - - const formData: EditForm = { - firstName: result.data.first_name, - lastName: result.data.last_name, - date_of_birth: result.data.date_of_birth || null, - gender: result.data.gender || "Male", - email: result.data.email, - video_connect_link: result.data.video_connect_link, - phoneNumber: result.data.phone_number?.toString() || "", - altPhoneNumber: result.data.alt_phone_number?.toString() || "", - weekly_working_hours: result.data.weekly_working_hours, - }; - dispatch({ - type: "set_form", - form: formData, - }); - formVals.current = formData; - }, }); - const validateForm = (form: EditForm) => { - const errors: Partial> = {}; - Object.keys(form).forEach((field) => { - switch (field) { - case "firstName": - case "lastName": - case "gender": - errors[field] = RequiredFieldValidator()(form[field]); - return; - case "date_of_birth": - if (!form[field]) { - errors[field] = "Enter a valid date of birth"; - } else if ( - !dayjs(form[field]).isValid() || - dayjs(form[field]).isAfter(dayjs().subtract(17, "year")) - ) { - errors[field] = "Enter a valid date of birth"; - } - return; - case "phoneNumber": - // eslint-disable-next-line no-case-declarations - const phoneNumber = parsePhoneNumber(form[field]); - - // eslint-disable-next-line no-case-declarations - let is_valid = false; - if (phoneNumber) { - is_valid = PhoneNumberValidator()(phoneNumber) === undefined; - } - - if (!form[field] || !is_valid) { - errors[field] = "Please enter valid phone number"; - } - return; - case "altPhoneNumber": - // eslint-disable-next-line no-case-declarations - let alt_is_valid = false; - if (form[field] && form[field] !== "+91") { - const altPhoneNumber = parsePhoneNumber(form[field]); - if (altPhoneNumber) { - alt_is_valid = - PhoneNumberValidator(["mobile"])(altPhoneNumber) === undefined; - } - } - - if (form[field] && form[field] !== "+91" && !alt_is_valid) { - errors[field] = "Please enter valid mobile number"; - } - return; - case "email": - if (!form[field]) { - errors[field] = t("field_required"); - } else if (!validateEmailAddress(form[field])) { - errors[field] = "Enter a valid email address"; - } - return; - case "weekly_working_hours": - if ( - form[field] && - (Number(form[field]) < 0 || - Number(form[field]) > 168 || - !/^\d+$/.test(form[field] ?? "")) - ) { - errors[field] = - "Average weekly working hours must be a number between 0 and 168"; - } - return; - case "video_connect_link": - if (form[field]) { - if (isValidUrl(form[field]) === false) { - errors[field] = "Please enter a valid url"; - } - } - return; - } - }); - return errors; - }; - - const getDate = (value: string | Date | null) => - value && dayjs(value).isValid() ? dayjs(value).toDate() : undefined; - - const handleCancel = () => { - dispatch({ - type: "set_form", - form: formVals.current, - }); - }; - if (isLoading || !userData) { return ; } - const handleSubmit = async (formData: EditForm) => { - const data = { - first_name: formData.firstName, - last_name: formData.lastName, - email: formData.email, - video_connect_link: formData.video_connect_link, - phone_number: parsePhoneNumber(formData.phoneNumber) ?? "", - alt_phone_number: parsePhoneNumber(formData.altPhoneNumber) ?? "", - gender: formData.gender, - date_of_birth: dateQueryString(formData.date_of_birth), - weekly_working_hours: - formData.weekly_working_hours && formData.weekly_working_hours !== "" - ? formData.weekly_working_hours - : null, - }; - const { res } = await request(routes.partialUpdateUser, { - pathParams: { username: userData.username }, - body: data, - }); - if (res?.ok) { - Notification.Success({ - msg: "Details updated successfully", - }); - await refetchUserData(); - } - }; - const handleAvatarUpload = async (file: File, onError: () => void) => { const formData = new FormData(); formData.append("profile_picture", file); @@ -331,101 +107,7 @@ export default function UserInformation({ username }: { username: string }) {
- {!isLoading && ( -
- - disabled={isLoading} - defaults={userData ? states.form : initForm} - validate={validateForm} - onCancel={handleCancel} - onSubmit={handleSubmit} - hideRestoreDraft - noPadding - resetFormVals - > - {(field) => ( - <> -
- - -
-
- - o.text} - optionValue={(o) => o.text} - options={GENDER_TYPES} - /> -
-
- - -
-
- -
-
- - -
- - )} - -
- )} +
); diff --git a/src/components/Users/UserProfile.tsx b/src/components/Users/UserProfile.tsx index 7aa3c9eff34..13e27c37f95 100644 --- a/src/components/Users/UserProfile.tsx +++ b/src/components/Users/UserProfile.tsx @@ -27,7 +27,7 @@ import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; import request from "../../Utils/request/request"; import DateFormField from "../Form/FormFields/DateFormField"; -import { validateRule } from "./UserAdd"; +import { validateRule } from "./UserAddEditForm"; import { useTranslation } from "react-i18next"; import AvatarEditable from "@/components/Common/AvatarEditable"; import Page from "@/components/Common/components/Page"; diff --git a/src/components/Users/UserResetPassword.tsx b/src/components/Users/UserResetPassword.tsx index 3104a1bcc8e..b46b10cef86 100644 --- a/src/components/Users/UserResetPassword.tsx +++ b/src/components/Users/UserResetPassword.tsx @@ -6,7 +6,7 @@ import { UpdatePasswordForm } from "./models"; import useAuthUser from "@/common/hooks/useAuthUser"; import routes from "../../Redux/api"; import request from "../../Utils/request/request"; -import { validateRule } from "./UserAdd"; +import { validateRule } from "./UserAddEditForm"; import { useTranslation } from "react-i18next"; export default function UserResetPassword() { From 86aee7a11b3426ca866846593e48d4576cb3490a Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Thu, 7 Nov 2024 12:01:26 +0530 Subject: [PATCH 12/55] form validation edits and translations --- src/Locale/en.json | 21 ++ src/components/Users/UserAddEditForm.tsx | 380 +++++++++++++---------- 2 files changed, 238 insertions(+), 163 deletions(-) diff --git a/src/Locale/en.json b/src/Locale/en.json index 21954afd107..4ea7d5b2d01 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -442,6 +442,7 @@ "confirm_delete": "Confirm Delete", "confirm_discontinue": "Confirm Discontinue", "confirm_password": "Confirm Password", + "confirm_password_required": "Confirm password is required", "confirm_transfer_complete": "Confirm Transfer Complete!", "confirmed": "Confirmed", "consent__hi_range": "Health Information Range", @@ -568,6 +569,8 @@ "disease_status": "Disease status", "district": "District", "district_program_management_supporting_unit": "District Program Management Supporting Unit", + "dob_format": "Please enter date in DD/MM/YYYY format", + "doctor_experience_less_than_100_years": "Doctor experience should be less than 100 years", "doctor_s_medical_council_registration": "Doctor's Medical Council Registration", "domestic_healthcare_support": "Domestic healthcare support", "done": "Done", @@ -630,6 +633,9 @@ "enter_mobile_otp": "Enter OTP sent to the given mobile number", "enter_otp": "Enter OTP sent to the registered mobile with the respective ID", "enter_valid_age": "Please Enter Valid Age", + "enter_valid_dob": "Enter a valid date of birth", + "enter_valid_dob_age": "Please enter an age greater than 15 years", + "enter_valid_name": "Please enter a valid name", "entered-in-error": "Entered in error", "error_404": "Error 404", "error_deleting_shifting": "Error while deleting Shifting record", @@ -675,6 +681,7 @@ "filter_by_category": "Filter by category", "filters": "Filters", "first_name": "First Name", + "first_name_required": "First Name is required", "footer_body": "Open Healthcare Network is an open-source public utility designed by a multi-disciplinary team of innovators and volunteers. Open Healthcare Network CARE is a Digital Public Good recognised by the United Nations.", "forget_password": "Forgot password?", "forget_password_instruction": "Enter your username, and if it exists, we will send you a link to reset your password.", @@ -745,7 +752,9 @@ "invalid_phone_number": "Invalid Phone Number", "invalid_pincode": "Invalid Pincode", "invalid_reset": "Invalid Reset", + "invalid_url": "Please enter a valid url", "invalid_username": "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + "invalid_username_format": "Please enter a 4-16 characters long username with lowercase letters, digits and . _ - only and it should not start or end with . _ -", "inventory_management": "Inventory Management", "investigation_report": "Investigation Report", "investigation_report_for_{{name}}": "Investigation Report for {{name}}", @@ -776,6 +785,7 @@ "last_edited": "Last Edited", "last_modified": "Last Modified", "last_name": "Last Name", + "last_name_required": "Last Name is required", "last_online": "Last Online", "last_serviced_on": "Last Serviced On", "latitude_invalid": "Latitude must be between -90 and 90", @@ -934,6 +944,8 @@ "password_reset_failure": "Password Reset Failed", "password_reset_success": "Password Reset successfully", "password_sent": "Password Reset Email Sent", + "password_validation": "Password should have 1 lowercase letter, 1 uppercase letter, 1 number, and be at least 8 characters long", + "passwords_not_matching": "Passwords not matching", "patient_address": "Patient Address", "patient_body": "Patient Body", "patient_category": "Patient Category", @@ -978,12 +990,19 @@ "phone_number_at_current_facility": "Phone Number of Contact person at current Facility", "pincode": "Pincode", "please_enter_a_reason_for_the_shift": "Please enter a reason for the shift.", + "please_enter_password": "Please enter the password", + "please_enter_username": "Please enter the username", "please_select_a_facility": "Please select a facility", "please_select_breathlessness_level": "Please select Breathlessness Level", + "please_select_district": "Please select the district", "please_select_facility_type": "Please select Facility Type", + "please_select_gender": "Please select the Gender", + "please_select_localbody": "Please select the local body", "please_select_patient_category": "Please select Patient Category", "please_select_preferred_vehicle_type": "Please select Preferred Vehicle Type", + "please_select_state": "Please select the state", "please_select_status": "Please select Status", + "please_select_user_type": "Please select the User Type", "please_upload_a_csv_file": "Please Upload A CSV file", "policy": "Policy", "policy__insurer": "Insurer", @@ -1256,6 +1275,7 @@ "user_role": "Role", "user_role_note": "Search and assign the appropriate role for this user.", "username": "Username", + "username_already_exists": "This username already exists", "users": "Users", "vacant": "Vacant", "vehicle_preference": "Vehicle preference", @@ -1282,6 +1302,7 @@ "vitals_present": "Vitals Monitor present", "ward": "Ward", "warranty_amc_expiry": "Warranty / AMC Expiry", + "weekly_working_hours_error": "Average weekly working hours must be a number between 0 and 168", "what_facility_assign_the_patient_to": "What facility would you like to assign the patient to", "whatsapp_number": "Whatsapp Number", "why_the_asset_is_not_working": "Why the asset is not working?", diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index c848d097712..7c7fac3c284 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -5,7 +5,6 @@ import { USER_TYPES, USER_TYPE_OPTIONS, } from "@/common/constants"; -import { useAbortableEffect } from "@/common/utils"; import { validateEmailAddress, validateName, @@ -42,8 +41,9 @@ import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import { useTranslation } from "react-i18next"; import Loading from "@/components/Common/Loading"; -import { GenderType } from "./models"; +import { GenderType, UserModel } from "./models"; import Form from "../Form/Form"; +import { FormContextValue } from "../Form/FormContext"; interface UserProps { username?: string; } @@ -128,12 +128,6 @@ const user_create_reducer = (state = initialState, action: any) => { form: action.form, }; } - case "set_errors": { - return { - ...state, - errors: action.errors, - }; - } case "set_state": { if (action.state) return action.state; return state; @@ -143,8 +137,8 @@ const user_create_reducer = (state = initialState, action: any) => { } }; -const getDate = (value: any) => - value && dayjs(value).isValid() && dayjs(value).toDate(); +const getDate = (value: string | Date | null) => + value && dayjs(value).isValid() ? dayjs(value).toDate() : undefined; export const validateRule = ( condition: boolean, @@ -172,6 +166,7 @@ const UserAddEditForm = (props: UserProps) => { const { username } = props; const editUser = username ? true : false; const formVals = useRef(initForm); + const [facilityErrors, setFacilityErrors] = useState(""); const { loading: userDataLoading, @@ -308,23 +303,33 @@ const UserAddEditForm = (props: UserProps) => { const readOnlyUsers = USER_TYPE_OPTIONS.filter((user) => user.readOnly); const defaultAllowedUserTypes = USER_TYPE_OPTIONS.slice(0, userIndex + 1); - const userTypes = authUser.is_superuser - ? [...USER_TYPE_OPTIONS] - : authUser.user_type === "StaffReadOnly" - ? readOnlyUsers.slice(0, 1) - : authUser.user_type === "DistrictReadOnlyAdmin" - ? readOnlyUsers.slice(0, 2) - : authUser.user_type === "StateReadOnlyAdmin" - ? readOnlyUsers.slice(0, 3) - : authUser.user_type === "Pharmacist" - ? USER_TYPE_OPTIONS.slice(0, 1) - : // Exception to allow Staff to Create Doctors - defaultAllowedUserTypes; - // TODO: refactor lines 227 through 248 to be more readable. This is messy. - if (authUser.user_type === "Nurse" || authUser.user_type === "Staff") { - userTypes.push(USER_TYPE_OPTIONS[6]); // Temperorily allows creation of users with elevated permissions due to introduction of new roles. - } + const getUserTypes = (authUser: UserModel) => { + // Superuser gets all options + if (authUser.is_superuser) { + return [...USER_TYPE_OPTIONS]; + } + + switch (authUser.user_type) { + case "StaffReadOnly": + return readOnlyUsers.slice(0, 1); + case "DistrictReadOnlyAdmin": + return readOnlyUsers.slice(0, 2); + case "StateReadOnlyAdmin": + return readOnlyUsers.slice(0, 3); + case "Pharmacist": + return USER_TYPE_OPTIONS.slice(0, 1); + case "Nurse": + case "Staff": + // Allow creation of users with elevated permissions + return [...defaultAllowedUserTypes, USER_TYPE_OPTIONS[6]]; + default: + // Exception to allow Staff to Create Doctors + return defaultAllowedUserTypes; + } + }; + + const userTypes = getUserTypes(authUser); const showLocalbody = ![ "Pharmacist", @@ -373,23 +378,26 @@ const UserAddEditForm = (props: UserProps) => { }, }); - const handleDateChange = (e: FieldChangeEvent, field?: any) => { - if (dayjs(e.value).isValid()) { - const errors = { ...state.errors, [e.name]: "" }; + const handleDateChange = ( + event: FieldChangeEvent, + field?: FormContextValue, + ) => { + if (dayjs(event.value).isValid()) { dispatch({ type: "set_form", form: { ...state.form, - [e.name]: dayjs(e.value).format("YYYY-MM-DD"), + [event.name]: dayjs(event.value).format("YYYY-MM-DD"), }, }); - if (field) field(e.name).onChange(e); - dispatch({ type: "set_errors", errors }); + if (field) field(event.name as keyof UserForm).onChange(event); } }; - const handleFieldChange = (event: FieldChangeEvent, field?: any) => { - const errors = { ...state.errors, [event.name]: "" }; + const handleFieldChange = ( + event: FieldChangeEvent, + field?: FormContextValue, + ) => { dispatch({ type: "set_form", form: { @@ -397,18 +405,63 @@ const UserAddEditForm = (props: UserProps) => { [event.name]: event.value, }, }); - dispatch({ type: "set_errors", errors }); - if (field) field(event.name).onChange(event); + if (field) field(event.name as keyof UserForm).onChange(event); }; - useAbortableEffect(() => { - if (state.form.phone_number_is_whatsapp) { - handleFieldChange({ - name: "alt_phone_number", - value: state.form.phone_number, - }); + const handlePhoneChange = ( + event: FieldChangeEvent, + field: FormContextValue, + ) => { + let formData = { ...state.form }; + let phoneNumberVal = ""; + switch (event.name) { + case "phone_number": + phoneNumberVal = event.value as string; + field("phone_number").onChange({ + name: field("phone_number").name, + value: phoneNumberVal, + }); + formData = { ...formData, phone_number: phoneNumberVal }; + if (state.form.phone_number_is_whatsapp) { + field("alt_phone_number").onChange({ + name: field("alt_phone_number").name, + value: phoneNumberVal, + }); + formData = { ...formData, alt_phone_number: phoneNumberVal }; + } + break; + case "alt_phone_number": + phoneNumberVal = event.value as string; + if (!state.form.phone_number_is_whatsapp) { + field("alt_phone_number").onChange({ + name: field("alt_phone_number").name, + value: phoneNumberVal, + }); + formData = { ...formData, alt_phone_number: phoneNumberVal }; + } + break; + case "phone_number_is_whatsapp": + phoneNumberVal = state.form.phone_number; + formData = { + ...formData, + alt_phone_number: phoneNumberVal, + phone_number_is_whatsapp: event.value as boolean, + }; + field("alt_phone_number").onChange({ + name: field("alt_phone_number").name, + value: phoneNumberVal, + }); + field("phone_number_is_whatsapp").onChange({ + name: field("phone_number_is_whatsapp").name, + value: event.value, + }); + break; } - }, [state.form.phone_number_is_whatsapp, state.form.phone_number]); + dispatch({ + type: "set_form", + form: formData, + }); + }; const setFacility = (selected: FacilityModel | FacilityModel[] | null) => { const newSelectedFacilities = selected @@ -424,37 +477,40 @@ const UserAddEditForm = (props: UserProps) => { dispatch({ type: "set_form", form }); }; + const validateFacility = ( + formData: UserForm, + selectedFacility: FacilityModel[], + ) => { + if ( + selectedFacility && + formData.user_type && + selectedFacility.length === 0 && + STAFF_OR_NURSE_USER.includes(authUser.user_type) && + STAFF_OR_NURSE_USER.includes(formData.user_type) + ) { + return "Please select atleast one of the facilities you are linked to"; + } + }; + + const validatePhoneNumber = (phoneNumber: string) => { + const parsedPhoneNumber = parsePhoneNumber(phoneNumber); + if (!parsedPhoneNumber) return false; + return PhoneNumberValidator()(parsedPhoneNumber) === undefined; + }; + const validateForm = (formData: UserForm) => { const errors: Partial> = {}; + const facilityError = validateFacility(formData, selectedFacility); + if (facilityError) { + errors.facilities = facilityError; + } Object.keys(formData).forEach((field) => { switch (field) { - case "facilities": - if ( - formData.facilities && - formData.user_type && - formData["facilities"].length === 0 && - STAFF_OR_NURSE_USER.includes(authUser.user_type) && - STAFF_OR_NURSE_USER.includes(formData.user_type) - ) { - errors[field] = - "Please select atleast one of the facilities you are linked to"; - } - return; case "user_type": if (!formData[field]) { - errors[field] = "Please select the User Type"; + errors[field] = t("please_select_user_type"); } - return; - case "doctor_experience_commenced_on": - if (formData.user_type === "Doctor" && !formData[field]) { - errors[field] = t("field_required"); - } else if ( - formData.user_type === "Doctor" && - Number(formData.doctor_experience_commenced_on) > 100 - ) { - errors[field] = "Doctor experience should be less than 100 years"; - } - return; + break; case "qualification": if ( (formData.user_type === "Doctor" || @@ -463,143 +519,138 @@ const UserAddEditForm = (props: UserProps) => { ) { errors[field] = t("field_required"); } - return; + break; + case "doctor_experience_commenced_on": + if (formData.user_type === "Doctor" && !formData[field]) { + errors[field] = t("field_required"); + } else if ( + formData.user_type === "Doctor" && + Number(formData.doctor_experience_commenced_on) > 100 + ) { + errors[field] = t("doctor_experience_less_than_100_years"); + } + break; case "doctor_medical_council_registration": if (formData.user_type === "Doctor" && !formData[field]) { errors[field] = t("field_required"); } - return; - case "first_name": - case "last_name": - formData[field] = formData[field].trim(); - if (!formData[field]) { - errors[field] = `${field - .split("_") - .map((word) => word[0].toUpperCase() + word.slice(1)) - .join(" ")} is required`; - } else if (!validateName(formData[field])) { - errors[field] = "Please enter a valid name"; + break; + case "phone_number": + if (!formData[field] || !validatePhoneNumber(formData[field])) { + errors[field] = t("invalid_phone"); } - return; - case "gender": - if (!formData[field]) { - errors[field] = "Please select the Gender"; + break; + case "alt_phone_number": + if ( + formData[field] && + formData[field] !== "+91" && + !validatePhoneNumber(formData[field]) + ) { + errors[field] = t("mobile_number_validation_error"); } - return; + break; case "username": if (!formData[field]) { - errors[field] = "Please enter the username"; + errors[field] = t("please_enter_username"); } else if (!validateUsername(formData[field])) { - errors[field] = - "Please enter a 4-16 characters long username with lowercase letters, digits and . _ - only and it should not start or end with . _ -"; + errors[field] = t("invalid_username"); } else if (usernameExists !== userExistsEnums.available) { - errors[field] = "This username already exists"; + errors[field] = t("username_already_exists"); } - return; + break; case "password": if (!formData[field]) { - errors[field] = "Please enter the password"; + errors[field] = t("please_enter_password"); } else if (!validatePassword(formData[field])) { - errors.password = - "Password should have 1 lowercase letter, 1 uppercase letter, 1 number, and be at least 8 characters long"; + errors.password = t("password_validation"); } - return; + break; case "c_password": if (!formData.password) { - errors.c_password = "Confirm password is required"; + errors.c_password = t("confirm_password_required"); } else if (formData.password !== formData.c_password) { - errors.c_password = "Passwords not matching"; + errors.c_password = t("passwords_not_matching"); } - return; - case "phone_number": - // eslint-disable-next-line no-case-declarations - const phoneNumber = parsePhoneNumber(formData[field]); - // eslint-disable-next-line no-case-declarations - let is_valid = false; - if (phoneNumber) { - is_valid = PhoneNumberValidator()(phoneNumber) === undefined; - } - if (!formData[field] || !is_valid) { - errors[field] = "Please enter valid phone number"; - } - return; - - case "alt_phone_number": - // eslint-disable-next-line no-case-declarations - let alt_is_valid = false; - if (formData[field] && formData[field] !== "+91") { - const altPhoneNumber = parsePhoneNumber(formData[field]); - if (altPhoneNumber) { - alt_is_valid = - PhoneNumberValidator(["mobile"])(altPhoneNumber) === undefined; - } - } - if (formData[field] && formData[field] !== "+91" && !alt_is_valid) { - errors[field] = "Please enter valid mobile number"; + break; + case "first_name": + case "last_name": + formData[field] = formData[field].trim(); + if (!formData[field]) { + errors[field] = t(`${field}_required`); + } else if (!validateName(formData[field])) { + errors[field] = t("enter_valid_name"); } - return; + break; case "email": formData[field] = formData[field].trim(); if ( formData[field].length === 0 || !validateEmailAddress(formData[field]) ) { - errors[field] = "Please enter a valid email address"; + errors[field] = t("invalid_email"); } - return; + break; case "date_of_birth": if (!formData[field]) { - errors[field] = "Please enter date in DD/MM/YYYY format"; + errors[field] = t("dob_format"); } else if ( dayjs(formData[field]).isAfter(dayjs().subtract(1, "year")) ) { - errors[field] = "Enter a valid date of birth"; + errors[field] = t("enter_valid_dob"); + } else if ( + dayjs(formData[field]).isAfter(dayjs().subtract(16, "year")) + ) { + errors[field] = t("enter_valid_dob_age"); + } + break; + case "gender": + if (!formData[field]) { + errors[field] = t("please_select_gender"); } - return; + break; case "state": if (!Number(formData[field])) { - errors[field] = "Please select the state"; + errors[field] = t("please_select_state"); } - return; + break; case "district": if (!Number(formData[field])) { - errors[field] = "Please select the district"; + errors[field] = t("please_select_district"); } - return; + break; case "local_body": if (showLocalbody && !Number(formData[field])) { - errors[field] = "Please select the local body"; + errors[field] = t("please_select_localbody"); } - return; + break; case "weekly_working_hours": - if ( - formData[field] && - (Number(formData[field]) < 0 || - Number(formData[field]) > 168 || - !/^\d+$/.test(formData[field] ?? "")) - ) { - errors[field] = - "Average weekly working hours must be a number between 0 and 168"; + if (formData[field] !== null && formData[field] !== undefined) { + const hours = Number(formData[field]); + if ( + isNaN(hours) || + hours < 0 || + hours > 168 || + !/^\d+$/.test(formData[field] ?? "") + ) { + errors[field] = t("weekly_working_hours_error"); + } } - return; + break; case "video_connect_link": if (formData[field]) { if (isValidUrl(formData[field]) === false) { - errors[field] = "Please enter a valid url"; + errors[field] = t("invalid_url"); } } - return; - + break; default: - return; + break; } }); - - const firstError = Object.values(errors).find((e) => e); + const firstError = Object.keys(errors).find((e) => e); if (firstError) { scrollTo(firstError); } - dispatch({ type: "set_errors", errors }); return errors; }; @@ -609,7 +660,7 @@ const UserAddEditForm = (props: UserProps) => { user_type: formData.user_type, gender: formData.gender, password: formData.password, - facilities: formData.facilities ? formData.facilities : undefined, + facilities: selectedFacility ? selectedFacility : undefined, home_facility: formData.home_facility ?? undefined, username: formData.username, first_name: formData.first_name ? formData.first_name : undefined, @@ -671,6 +722,11 @@ const UserAddEditForm = (props: UserProps) => { setIsLoading(false); }; + useEffect(() => { + const facilityError = validateFacility(state.form, selectedFacility); + setFacilityErrors(facilityError || ""); + }, [state.form, selectedFacility]); + if (isLoading || (editUser && userDataLoading)) { return ; } @@ -707,7 +763,7 @@ const UserAddEditForm = (props: UserProps) => { name="facilities" selected={selectedFacility} setSelected={setFacility} - errors={state.errors.facilities} + errors={facilityErrors} showAll={false} />
@@ -791,20 +847,18 @@ const UserAddEditForm = (props: UserProps) => { required types={["mobile", "landline"]} onChange={(e) => { - handleFieldChange(e, field); + handlePhoneChange(e, field); }} className="" /> - {!editUser && ( - { - handleFieldChange(e, field); - }} - label="Is the phone number a WhatsApp number?" - /> - )} + { + handlePhoneChange(e, field); + }} + label="Is the phone number a WhatsApp number?" + />
{ disabled={state.form.phone_number_is_whatsapp} types={["mobile"]} onChange={(e) => { - handleFieldChange(e, field); + handlePhoneChange(e, field); }} className="flex-1" /> From 3dc2ee21cc88499e5bd30924775fa596d2074012 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Thu, 7 Nov 2024 14:58:43 +0530 Subject: [PATCH 13/55] Manage Users: card modifications --- src/components/Users/ManageUsers.tsx | 238 +++++++++-------------- src/components/Users/UserInformation.tsx | 2 +- 2 files changed, 92 insertions(+), 148 deletions(-) diff --git a/src/components/Users/ManageUsers.tsx b/src/components/Users/ManageUsers.tsx index c6196317f6f..c82807d9115 100644 --- a/src/components/Users/ManageUsers.tsx +++ b/src/components/Users/ManageUsers.tsx @@ -2,6 +2,7 @@ import { navigate } from "raviger"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import Card from "@/CAREUI/display/Card"; import CountBlock from "@/CAREUI/display/Count"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { AdvancedFilterButton } from "@/CAREUI/interactive/FiltersSlideover"; @@ -13,8 +14,6 @@ import { FacilitySelect } from "@/components/Common/FacilitySelect"; import Loading from "@/components/Common/Loading"; import Page from "@/components/Common/Page"; import Pagination from "@/components/Common/Pagination"; -import UserDetails from "@/components/Common/UserDetails"; -import UserDetailComponent from "@/components/Common/UserDetailsComponet"; import { FacilityModel } from "@/components/Facility/models"; import SearchInput from "@/components/Form/SearchInput"; import UnlinkFacilityDialog from "@/components/Users/UnlinkFacilityDialog"; @@ -22,7 +21,6 @@ import UserFilter from "@/components/Users/UserFilter"; import useAuthUser from "@/hooks/useAuthUser"; import useFilters from "@/hooks/useFilters"; -import useWindowDimensions from "@/hooks/useWindowDimensions"; import { USER_TYPES } from "@/common/constants"; @@ -32,15 +30,15 @@ import request from "@/Utils/request/request"; import useQuery from "@/Utils/request/useQuery"; import { classNames, - formatDisplayName, formatName, isUserOnline, relativeTime, } from "@/Utils/utils"; +import { UserModel } from "./models"; + export default function ManageUsers() { const { t } = useTranslation(); - const { width } = useWindowDimensions(); const { qParams, updateQuery, @@ -59,9 +57,6 @@ export default function ManageUsers() { ? [...USER_TYPES] : USER_TYPES.slice(0, userIndex + 1); - const extremeSmallScreenBreakpoint = 320; - const isExtremeSmallScreen = width <= extremeSmallScreenBreakpoint; - const { data: homeFacilityData } = useQuery(routes.getAnyFacility, { pathParams: { id: qParams.home_facility }, prefetch: !!qParams.home_facility && qParams.home_facility !== "NONE", @@ -116,163 +111,112 @@ export default function ManageUsers() { ); - let userList: any[] = []; - userListData?.results && - userListData.results.length && - (userList = userListData.results.map((user: any, idx) => { - const cur_online = isUserOnline(user); - return ( -
-
-
-
-
- -
-
- {user.username && ( -
- {user.username} -
+ const getCard = (user: UserModel, idx: number) => { + const cur_online = isUserOnline(user); + return ( + +
+
+ +
+
+

+ {formatName(user)} +

+
- {user.last_login && cur_online ? ( - - {" "} - {" "} - Currently Online - - ) : ( - <> - - Last - Online:{" "} - - - - {user.last_login + > + {user && ( + <> + + + {cur_online + ? "Online" + : user.last_login ? relativeTime(user.last_login) : "Never"} - - - )} -
+ + + )}
-
-
- {formatName(user)} -
+
+ {user.username} - {user.last_login && cur_online ? ( -
- ) : null} +
+
+
Role
+
{user.user_type}
- -
- {user.user_type && ( - - )} - {user.district_object && ( - - )} +
+
Home facility
+
+ {user.home_facility_object?.name || "No Home Facility"} +
- {user.local_body_object && ( - -
- {user.local_body_object.name} + {user.district_object && ( +
+
District
+
+ {user.district_object.name}
- +
)} - -
- {user.created_by && ( -
- -
-
- {user.created_by} -
-
-
-
- )} - {user.username && ( -
- - - {user.home_facility_object?.name || - "No Home Facility"} - - + {user.weekly_working_hours && ( +
+
Average Weekly Hours
+
+ {user.weekly_working_hours}
- )} -
-
-
- navigate(`/users/${user.username}`)} - > - {t("view_user")} - +
+ )}
+ +
- ); - })); + + ); + }; if (userListLoading || districtDataLoading || !userListData?.results) { - manageUsers = ; - } else if (userListData?.results.length) { + return ; + } + + let userList: any[] = []; + + userList = userListData.results.map((user: UserModel, idx) => { + return getCard(user, idx); + }); + + if (userListData?.results.length) { manageUsers = (
-
- {userList} -
+
{userList}
); diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx index b39bb37249c..af91420da85 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserInformation.tsx @@ -86,7 +86,7 @@ export default function UserInformation({ username }: { username: string }) { handleDelete={handleAvatarDelete} onClose={() => setEditAvatar(false)} /> -
+
Date: Fri, 8 Nov 2024 01:32:55 +0530 Subject: [PATCH 14/55] Added list and card views --- src/components/Users/ManageUsers.tsx | 268 +++++++++++++++++++++------ 1 file changed, 208 insertions(+), 60 deletions(-) diff --git a/src/components/Users/ManageUsers.tsx b/src/components/Users/ManageUsers.tsx index c82807d9115..02059f08882 100644 --- a/src/components/Users/ManageUsers.tsx +++ b/src/components/Users/ManageUsers.tsx @@ -14,6 +14,7 @@ import { FacilitySelect } from "@/components/Common/FacilitySelect"; import Loading from "@/components/Common/Loading"; import Page from "@/components/Common/Page"; import Pagination from "@/components/Common/Pagination"; +import Tabs from "@/components/Common/Tabs"; import { FacilityModel } from "@/components/Facility/models"; import SearchInput from "@/components/Form/SearchInput"; import UnlinkFacilityDialog from "@/components/Users/UnlinkFacilityDialog"; @@ -21,6 +22,7 @@ import UserFilter from "@/components/Users/UserFilter"; import useAuthUser from "@/hooks/useAuthUser"; import useFilters from "@/hooks/useFilters"; +import useWindowDimensions from "@/hooks/useWindowDimensions"; import { USER_TYPES } from "@/common/constants"; @@ -56,6 +58,10 @@ export default function ManageUsers() { const userTypes = authUser.is_superuser ? [...USER_TYPES] : USER_TYPES.slice(0, userIndex + 1); + const { width } = useWindowDimensions(); + const mediumScreenBreakpoint = 640; + const isMediumScreen = width <= mediumScreenBreakpoint; + const [activeTab, setActiveTab] = useState(0); const { data: homeFacilityData } = useQuery(routes.getAnyFacility, { pathParams: { id: qParams.home_facility }, @@ -111,55 +117,65 @@ export default function ManageUsers() { ); - const getCard = (user: UserModel, idx: number) => { - const cur_online = isUserOnline(user); + const getNameAndStatusCard = (user: UserModel, cur_online: boolean) => { return ( - -
-
- -
-
-

- {formatName(user)} -

-
+
+

+ {formatName(user)} +

+
+ {user && ( + <> + - {user && ( - <> - - - {cur_online - ? "Online" - : user.last_login - ? relativeTime(user.last_login) - : "Never"} - - + > + -
- {user.username} + > + {cur_online + ? "Online" + : user.last_login + ? relativeTime(user.last_login) + : "Never"} + + + )} +
+
+ {user.username} +
+ ); + }; + const getCard = (user: UserModel, idx: number) => { + const cur_online = isUserOnline(user); + return ( + +
+
+
+ + {isMediumScreen && getNameAndStatusCard(user, cur_online)} +
+
+ {!isMediumScreen && getNameAndStatusCard(user, cur_online)}
Role
@@ -202,21 +218,162 @@ export default function ManageUsers() { ); }; + const getListHeader = () => ( + + + Name + Status + Role + Home facility + District + + + + ); + const getList = (user: UserModel, idx: number) => { + const cur_online = isUserOnline(user); + return ( + + +
+ +
+

+ {formatName(user)} +

+ @{user.username} +
+
+ + +
+ + + {cur_online + ? "Online" + : user.last_login + ? relativeTime(user.last_login) + : "Never"} + +
+ + {user.user_type} + + {user.home_facility_object?.name || "No Home Facility"} + + + {user.district_object?.name || ""} + + + + + + ); + }; + const renderCard = () => ( + <> + {userListData?.results.map((user: UserModel, idx: number) => + getCard(user, idx), + )} + + ); + const renderList = () => ( + + {getListHeader()} + + {userListData?.results.map((user: UserModel, idx: number) => + getList(user, idx), + )} + +
+ ); + + const tabs = [ + { + id: 0, + content: ( +
+ {renderCard()} +
+ ), + }, + { + id: 1, + content:
{renderList()}
, + }, + ]; if (userListLoading || districtDataLoading || !userListData?.results) { return ; } - let userList: any[] = []; - - userList = userListData.results.map((user: UserModel, idx) => { - return getCard(user, idx); - }); - if (userListData?.results.length) { manageUsers = (
-
{userList}
+
+
+ updateQuery({ [e.name]: e.value })} + value={qParams.username} + placeholder={t("search_by_username")} + /> +
+ + + Card +
+ ), + value: 0, + }, + { + text: ( +
+ + List +
+ ), + value: 1, + }, + ]} + currentTab={activeTab} + onTabChange={(tab) => setActiveTab(tab as number)} + className="float-right" + /> +
+
+ {tabs.find((tab) => tab.id === activeTab)?.content} +
); @@ -243,15 +400,6 @@ export default function ManageUsers() { className="flex-1" />
-
- updateQuery({ [e.name]: e.value })} - value={qParams.username} - placeholder={t("search_by_username")} - /> -
advancedFilter.setShow(true)} From eaf37d4909b3f6746a343b439bb80bcfecc93335 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Fri, 8 Nov 2024 11:27:06 +0530 Subject: [PATCH 15/55] Refactor - Refactor FacilityUsers to use similar format as ManageUsers - Extracted out the list/card logic to UserListAndCard - Changed FacilityUsers to use userList route (with facilityId as home_facility parameter) --- public/locale/en.json | 6 +- src/components/Facility/FacilityUsers.tsx | 342 +++++----------------- src/components/Users/ManageUsers.tsx | 233 +-------------- src/components/Users/UserListAndCard.tsx | 216 ++++++++++++++ 4 files changed, 298 insertions(+), 499 deletions(-) create mode 100644 src/components/Users/UserListAndCard.tsx diff --git a/public/locale/en.json b/public/locale/en.json index 0d2534337d4..bb2c82c39ca 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -888,7 +888,7 @@ "no_duplicate_facility": "You should not create duplicate facilities", "no_facilities": "No Facilities found", "no_files_found": "No {{type}} files found", - "no_home_facility": "No home facility assigned", + "no_home_facility": "No home facility", "no_image_found": "No image found", "no_investigation": "No investigation Reports found", "no_investigation_suggestions": "No Investigation Suggestions", @@ -1324,5 +1324,7 @@ "yet_to_be_decided": "Yet to be decided", "you_need_at_least_a_location_to_create_an_assest": "You need at least a location to create an assest.", "zoom_in": "Zoom In", - "zoom_out": "Zoom Out" + "zoom_out": "Zoom Out", + "role": "Role", + "more_details": "More details" } diff --git a/src/components/Facility/FacilityUsers.tsx b/src/components/Facility/FacilityUsers.tsx index c8682b516f8..10d241bf796 100644 --- a/src/components/Facility/FacilityUsers.tsx +++ b/src/components/Facility/FacilityUsers.tsx @@ -3,47 +3,26 @@ import { useTranslation } from "react-i18next"; import CountBlock from "@/CAREUI/display/Count"; import CareIcon from "@/CAREUI/icons/CareIcon"; -import SlideOver from "@/CAREUI/interactive/SlideOver"; -import ButtonV2 from "@/components/Common/ButtonV2"; -import Loading from "@/components/Common/Loading"; import Page from "@/components/Common/Page"; -import Pagination from "@/components/Common/Pagination"; -import UserDetails from "@/components/Common/UserDetails"; -import LinkFacilityDialog from "@/components/Users/LinkFacilityDialog"; -import { UserFacilities } from "@/components/Users/ManageUsers"; -import UserDeleteDialog from "@/components/Users/UserDeleteDialog"; +import Tabs from "@/components/Common/Tabs"; +import SearchInput from "@/components/Form/SearchInput"; -import { RESULTS_PER_PAGE_LIMIT } from "@/common/constants"; +import useFilters from "@/hooks/useFilters"; -import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; -import request from "@/Utils/request/request"; import useQuery from "@/Utils/request/useQuery"; -import { formatName, isUserOnline, relativeTime } from "@/Utils/utils"; -export default function FacilityUsers(props: any) { +import { UserGrid, UserList } from "../Users/UserListAndCard"; + +export default function FacilityUsers(props: { facilityId: number }) { const { t } = useTranslation(); + const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ + limit: 18, + cacheBlacklist: ["username"], + }); const { facilityId } = props; - let manageUsers: any = null; - const [isAddFacilityLoading, setIsAddFacilityLoading] = useState(false); - const [currentPage, setCurrentPage] = useState(1); - const [expandFacilityList, setExpandFacilityList] = useState(false); - const [selectedUser, setSelectedUser] = useState(null); - const [offset, setOffset] = useState(0); - - const [linkFacility, setLinkFacility] = useState<{ - show: boolean; - username: string; - }>({ show: false, username: "" }); - - const [userData, setUserData] = useState<{ - show: boolean; - username: string; - name: string; - }>({ show: false, username: "", name: "" }); - - const limit = RESULTS_PER_PAGE_LIMIT; + const [activeTab, setActiveTab] = useState(0); const { data: facilityData } = useQuery(routes.getAnyFacility, { pathParams: { @@ -52,211 +31,23 @@ export default function FacilityUsers(props: any) { prefetch: facilityId !== undefined, }); - const { - data: facilityUserData, - refetch: facilityUserFetch, - loading: isLoading, - } = useQuery(routes.getFacilityUsers, { - query: { offset: offset, limit: limit }, - pathParams: { facility_id: facilityId }, - prefetch: facilityId !== undefined, - }); - - const handlePagination = (page: number, limit: number) => { - const offset = (page - 1) * limit; - setCurrentPage(page); - setOffset(offset); - }; - - const loadFacilities = async (username: string) => { - if (isAddFacilityLoading) { - return; - } - const { res, data } = await request(routes.userListFacility, { - pathParams: { username: username }, - }); - if (res?.ok && data && facilityUserData) { - facilityUserData.results = facilityUserData.results.map((user) => { - return user.username === username - ? { - ...user, - facilities: data, - } - : user; - }); - } - }; - - const hideLinkFacilityModal = () => { - setLinkFacility({ - show: false, - username: "", - }); - }; - - const handleCancel = () => { - setUserData({ show: false, username: "", name: "" }); - }; - - const handleSubmit = async () => { - const username = userData.username; - await request(routes.deleteUser, { - pathParams: { username: username }, - onResponse: ({ res }) => { - if (res?.status === 204) { - Notification.Success({ - msg: t("user_deleted_successfuly"), - }); - } - }, - }); - setUserData({ show: false, username: "", name: "" }); - facilityUserFetch(); - }; - - const addFacility = async (username: string, facility: any) => { - hideLinkFacilityModal(); - setIsAddFacilityLoading(true); - // Remaining props of request are not specified in dispatch request - await request(routes.addUserFacility, { - body: { - facility: String(facility.id), - }, - pathParams: { - username: username, + const { data: userListData, loading: userListLoading } = useQuery( + routes.userList, + { + query: { + limit: resultsPerPage.toString(), + offset: ( + (qParams.page ? qParams.page - 1 : 0) * resultsPerPage + ).toString(), + home_facility: facilityId.toString(), + username: qParams.username, }, - }); - setIsAddFacilityLoading(false); - loadFacilities(username); - }; - - let userList: any[] = []; - - facilityUserData && - facilityUserData.results && - facilityUserData.results.length && - (userList = facilityUserData.results.map((user) => { - return ( -
-
-
-
-
- {user.username && ( -
- {user.username} -
- )} -
- {t("last_online")}{" "} - - - {user.last_login - ? relativeTime(user.last_login) - : t("never")} - -
-
-
- {formatName(user)} -
- -
- {user.user_type && ( - -
{user.user_type}
-
- )} -
-
- {user.phone_number && ( -
-
-
-
- {t("phone_number")} -
- - {user.phone_number || "-"} - -
-
-
- )} -
- - {user.username && ( - - { - setExpandFacilityList(!expandFacilityList); - setSelectedUser(user); - }} - > - -

{t("linked_facilities")}

-
-
- )} -
-
-
-
- ); - })); + prefetch: facilityId !== undefined, + }, + ); - if (!facilityUserData) { - manageUsers = ; - } else if (facilityUserData.results && facilityUserData.results.length) { - manageUsers = ( -
-
{userList}
- {facilityUserData && facilityUserData.count > limit && ( -
- -
- )} -
- ); - } else if ( - facilityUserData.results && - facilityUserData.results.length === 0 - ) { - manageUsers = ( -
-
-
{t("no_users_found")}
-
-
- ); - } + const renderCard = () => ; + const renderList = () => ; return ( - {linkFacility.show && ( - - )} - -
- {facilityUserData && ( - + +
+
+ updateQuery({ [e.name]: e.value })} + value={qParams.username} + placeholder={t("search_by_username")} /> - )} -
- - - -
-
{manageUsers}
-
- {userData.show && ( - + + + Card +
+ ), + value: 0, + }, + { + text: ( +
+ + List +
+ ), + value: 1, + }, + ]} + currentTab={activeTab} + onTabChange={(tab) => setActiveTab(tab as number)} + className="float-right" /> - )} +
+ + {activeTab === 0 ? renderCard() : renderList()} +
); } diff --git a/src/components/Users/ManageUsers.tsx b/src/components/Users/ManageUsers.tsx index 02059f08882..b6be9d55a61 100644 --- a/src/components/Users/ManageUsers.tsx +++ b/src/components/Users/ManageUsers.tsx @@ -2,12 +2,10 @@ import { navigate } from "raviger"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import Card from "@/CAREUI/display/Card"; import CountBlock from "@/CAREUI/display/Count"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { AdvancedFilterButton } from "@/CAREUI/interactive/FiltersSlideover"; -import { Avatar } from "@/components/Common/Avatar"; import ButtonV2 from "@/components/Common/ButtonV2"; import CircularProgress from "@/components/Common/CircularProgress"; import { FacilitySelect } from "@/components/Common/FacilitySelect"; @@ -22,7 +20,6 @@ import UserFilter from "@/components/Users/UserFilter"; import useAuthUser from "@/hooks/useAuthUser"; import useFilters from "@/hooks/useFilters"; -import useWindowDimensions from "@/hooks/useWindowDimensions"; import { USER_TYPES } from "@/common/constants"; @@ -30,14 +27,9 @@ import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; import useQuery from "@/Utils/request/useQuery"; -import { - classNames, - formatName, - isUserOnline, - relativeTime, -} from "@/Utils/utils"; +import { classNames } from "@/Utils/utils"; -import { UserModel } from "./models"; +import { UserGrid, UserList } from "./UserListAndCard"; export default function ManageUsers() { const { t } = useTranslation(); @@ -52,15 +44,12 @@ export default function ManageUsers() { limit: 18, cacheBlacklist: ["username"], }); - let manageUsers: any = null; + let manageUsers: JSX.Element = <>; const authUser = useAuthUser(); const userIndex = USER_TYPES.indexOf(authUser.user_type); const userTypes = authUser.is_superuser ? [...USER_TYPES] : USER_TYPES.slice(0, userIndex + 1); - const { width } = useWindowDimensions(); - const mediumScreenBreakpoint = 640; - const isMediumScreen = width <= mediumScreenBreakpoint; const [activeTab, setActiveTab] = useState(0); const { data: homeFacilityData } = useQuery(routes.getAnyFacility, { @@ -117,216 +106,8 @@ export default function ManageUsers() { ); - const getNameAndStatusCard = (user: UserModel, cur_online: boolean) => { - return ( -
-
-

- {formatName(user)} -

-
- {user && ( - <> - - - {cur_online - ? "Online" - : user.last_login - ? relativeTime(user.last_login) - : "Never"} - - - )} -
-
- {user.username} -
- ); - }; - const getCard = (user: UserModel, idx: number) => { - const cur_online = isUserOnline(user); - - return ( - -
-
-
- - {isMediumScreen && getNameAndStatusCard(user, cur_online)} -
-
- {!isMediumScreen && getNameAndStatusCard(user, cur_online)} -
-
-
Role
-
{user.user_type}
-
-
-
Home facility
-
- {user.home_facility_object?.name || "No Home Facility"} -
-
- {user.district_object && ( -
-
District
-
- {user.district_object.name} -
-
- )} - {user.weekly_working_hours && ( -
-
Average Weekly Hours
-
- {user.weekly_working_hours} -
-
- )} -
-
-
- - -
-
- ); - }; - const getListHeader = () => ( - - - Name - Status - Role - Home facility - District - - - - ); - const getList = (user: UserModel, idx: number) => { - const cur_online = isUserOnline(user); - return ( - - -
- -
-

- {formatName(user)} -

- @{user.username} -
-
- - -
- - - {cur_online - ? "Online" - : user.last_login - ? relativeTime(user.last_login) - : "Never"} - -
- - {user.user_type} - - {user.home_facility_object?.name || "No Home Facility"} - - - {user.district_object?.name || ""} - - - - - - ); - }; - const renderCard = () => ( - <> - {userListData?.results.map((user: UserModel, idx: number) => - getCard(user, idx), - )} - - ); - const renderList = () => ( - - {getListHeader()} - - {userListData?.results.map((user: UserModel, idx: number) => - getList(user, idx), - )} - -
- ); - - const tabs = [ - { - id: 0, - content: ( -
- {renderCard()} -
- ), - }, - { - id: 1, - content:
{renderList()}
, - }, - ]; + const renderCard = () => ; + const renderList = () => ; if (userListLoading || districtDataLoading || !userListData?.results) { return ; @@ -372,7 +153,7 @@ export default function ManageUsers() { />
- {tabs.find((tab) => tab.id === activeTab)?.content} + {activeTab === 0 ? renderCard() : renderList()}
@@ -391,7 +172,7 @@ export default function ManageUsers() { return ( -
+
{ + return ( +
+
+

+ {formatName(user)} +

+
+ +
+
+ {user.username} +
+ ); +}; +const UserCard = ({ user }: { user: UserModel }) => { + const cur_online = isUserOnline(user); + const { width } = useWindowDimensions(); + const mediumScreenBreakpoint = 640; + const isMediumScreen = width <= mediumScreenBreakpoint; + const { t } = useTranslation(); + + return ( + +
+
+
+ + {isMediumScreen && getNameAndStatusCard(user, cur_online)} +
+
+ {!isMediumScreen && getNameAndStatusCard(user, cur_online)} +
+
+
{t("role")}
+
{user.user_type}
+
+
+
{t("home_facility")}
+
+ {user.home_facility_object?.name || t("no_home_facility")} +
+
+ {user.district_object && ( +
+
{t("district")}
+
{user.district_object.name}
+
+ )} + {user.weekly_working_hours && ( +
+
+ {t("average_weekly_working_hours")} +
+
{user.weekly_working_hours}
+
+ )} +
+
+
+ + +
+
+ ); +}; +export const UserGrid = ({ users }: { users?: UserModel[] }) => ( +
+ {users?.map((user) => )} +
+); + +const UserListHeader = () => { + const { t } = useTranslation(); + return ( + + + {t("name")} + {t("status")} + {t("role")} + {t("home_facility")} + {t("district")} + + + + ); +}; + +const UserListRow = ({ user }: { user: UserModel }) => { + const { t } = useTranslation(); + return ( + + +
+ +
+

+ {formatName(user)} +

+ @{user.username} +
+
+ + + + + {user.user_type} + + {user.home_facility_object?.name || t("no_home_facility")} + + {user.district_object?.name || ""} + + + + + ); +}; +export const UserList = ({ users }: { users?: UserModel[] }) => ( +
+ + + + {users?.map((user) => )} + +
+
+); + +export const UserStatusIndicator = ({ + user, + className, + addPadding = false, +}: { + user: UserModel; + className?: string; + addPadding?: boolean; +}) => { + const cur_online = isUserOnline(user); + const { t } = useTranslation(); + return ( +
+ + + {cur_online + ? t("online") + : user.last_login + ? relativeTime(user.last_login) + : t("never")} + +
+ ); +}; From d581402370ee87196ec72d26df3eaf7bc575dc66 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Fri, 8 Nov 2024 22:47:15 +0530 Subject: [PATCH 16/55] Route and perms updates - Modified to use a new route instead - Added a new route to Backend rather than changing permissions for UserModel - Display view details button based on permissions --- src/Utils/request/api.tsx | 2 +- src/components/Users/UserAddEditForm.tsx | 45 ++------------- src/components/Users/UserHome.tsx | 8 ++- src/components/Users/UserInformation.tsx | 4 +- src/components/Users/UserListAndCard.tsx | 73 +++++++++++++++++++----- src/components/Users/UserRoles.tsx | 4 +- 6 files changed, 74 insertions(+), 62 deletions(-) diff --git a/src/Utils/request/api.tsx b/src/Utils/request/api.tsx index 441b9a3d8c8..93be89a4827 100644 --- a/src/Utils/request/api.tsx +++ b/src/Utils/request/api.tsx @@ -1043,7 +1043,7 @@ const routes = { }, getUserDetails: { - path: "/api/v1/users/{username}/", + path: "/api/v1/users/get_user/", method: "GET", TRes: Type(), }, diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index 2d67bfbdb5d..93f020274d3 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -25,11 +25,7 @@ import { FieldChangeEvent } from "@/components/Form/FormFields/Utils"; import useAppHistory from "@/hooks/useAppHistory"; import useAuthUser from "@/hooks/useAuthUser"; -import { - GENDER_TYPES, - USER_TYPES, - USER_TYPE_OPTIONS, -} from "@/common/constants"; +import { GENDER_TYPES, USER_TYPES } from "@/common/constants"; import { validateEmailAddress, validateName, @@ -51,7 +47,8 @@ import { scrollTo, } from "@/Utils/utils"; -import { GenderType, UserModel } from "./models"; +import { GetUserTypes } from "./UserListAndCard"; +import { GenderType } from "./models"; interface UserProps { username?: string; @@ -191,8 +188,8 @@ const UserAddEditForm = (props: UserProps) => { data: userData, refetch: refetchUserData, } = useQuery(routes.getUserDetails, { - pathParams: { - username: username ?? "", + query: { + username: username, }, prefetch: editUser, onResponse: (result) => { @@ -315,39 +312,9 @@ const UserAddEditForm = (props: UserProps) => { } }, [usernameInput]); + const userTypes = GetUserTypes(); const authUser = useAuthUser(); - const userIndex = USER_TYPES.indexOf(authUser.user_type); - const readOnlyUsers = USER_TYPE_OPTIONS.filter((user) => user.readOnly); - - const defaultAllowedUserTypes = USER_TYPE_OPTIONS.slice(0, userIndex + 1); - - const getUserTypes = (authUser: UserModel) => { - // Superuser gets all options - if (authUser.is_superuser) { - return [...USER_TYPE_OPTIONS]; - } - - switch (authUser.user_type) { - case "StaffReadOnly": - return readOnlyUsers.slice(0, 1); - case "DistrictReadOnlyAdmin": - return readOnlyUsers.slice(0, 2); - case "StateReadOnlyAdmin": - return readOnlyUsers.slice(0, 3); - case "Pharmacist": - return USER_TYPE_OPTIONS.slice(0, 1); - case "Nurse": - case "Staff": - // Allow creation of users with elevated permissions - return [...defaultAllowedUserTypes, USER_TYPE_OPTIONS[6]]; - default: - // Exception to allow Staff to Create Doctors - return defaultAllowedUserTypes; - } - }; - - const userTypes = getUserTypes(authUser); const showLocalbody = ![ "Pharmacist", diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index 3afc55d49ff..33d9066670c 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -1,4 +1,4 @@ -import { Link } from "raviger"; +import { Link, navigate } from "raviger"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -32,12 +32,14 @@ export default function UserHome(props: UserHomeProps) { const { t } = useTranslation(); const { loading } = useQuery(routes.getUserDetails, { - pathParams: { - username, + query: { + username: username, }, onResponse: ({ res, data }) => { if (res?.status === 200 && data) { setUserData(data); + } else if (res?.status === 400) { + navigate("/users"); } }, }); diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx index af91420da85..9e64db79a1b 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserInformation.tsx @@ -26,8 +26,8 @@ export default function UserInformation({ username }: { username: string }) { loading: isLoading, refetch: refetchUserData, } = useQuery(routes.getUserDetails, { - pathParams: { - username, + query: { + username: username, }, }); diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index 975fafcf1f0..a9579f09d75 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -1,4 +1,5 @@ import { navigate } from "raviger"; +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import Card from "@/CAREUI/display/Card"; @@ -7,8 +8,11 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import { Avatar } from "@/components/Common/Avatar"; import { UserModel } from "@/components/Users/models"; +import useAuthUser from "@/hooks/useAuthUser"; import useWindowDimensions from "@/hooks/useWindowDimensions"; +import { USER_TYPES, USER_TYPE_OPTIONS } from "@/common/constants"; + import { classNames, formatName, @@ -16,6 +20,42 @@ import { relativeTime, } from "@/Utils/utils"; +export const GetUserTypes = () => { + const authUser = useAuthUser(); + + return useMemo(() => { + const userIndex = USER_TYPES.indexOf(authUser.user_type); + const readOnlyUsers = USER_TYPE_OPTIONS.filter((user) => user.readOnly); + const defaultAllowedUserTypes = USER_TYPE_OPTIONS.slice(0, userIndex + 1); + + // Superuser gets all options + if (authUser.is_superuser) { + return [...USER_TYPE_OPTIONS]; + } + + switch (authUser.user_type) { + case "StaffReadOnly": + return readOnlyUsers.slice(0, 1); + case "DistrictReadOnlyAdmin": + return readOnlyUsers.slice(0, 2); + case "StateReadOnlyAdmin": + return readOnlyUsers.slice(0, 3); + case "Pharmacist": + return USER_TYPE_OPTIONS.slice(0, 1); + case "Nurse": + case "Staff": + return [...defaultAllowedUserTypes, USER_TYPE_OPTIONS[6]]; + default: + return defaultAllowedUserTypes; + } + }, [authUser.user_type, authUser.is_superuser]); +}; + +export const CanUserAccess = (user: UserModel) => { + const allowedTypes = useMemo(() => GetUserTypes().map((type) => type.id), []); + return allowedTypes.includes(user.user_type); +}; + const getNameAndStatusCard = (user: UserModel, cur_online: boolean) => { return (
@@ -85,14 +125,15 @@ const UserCard = ({ user }: { user: UserModel }) => {
- - + {CanUserAccess(user) && ( + + )}
); @@ -151,13 +192,15 @@ const UserListRow = ({ user }: { user: UserModel }) => { {user.district_object?.name || ""} - + {CanUserAccess(user) && ( + + )} ); diff --git a/src/components/Users/UserRoles.tsx b/src/components/Users/UserRoles.tsx index cb6446e7f18..e4852a1c2b5 100644 --- a/src/components/Users/UserRoles.tsx +++ b/src/components/Users/UserRoles.tsx @@ -76,8 +76,8 @@ export default function UserRoles({ username }: { username: string }) { loading: isLoading, refetch: refetchUserData, } = useQuery(routes.getUserDetails, { - pathParams: { - username, + query: { + username: username, }, onResponse: (result) => { if (!result || !result.res || !result.data) return; From f553ade1bb3971da3d0d67d4852ae56716b892ce Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Sat, 9 Nov 2024 11:09:47 +0530 Subject: [PATCH 17/55] Refactoring - Moving out additional common functionality inyo UserListAndCard - Removed memoization since it was causing hook errors - Switched back to using getFacilityUsers route in FacilityUsers as that should be faster --- src/components/Facility/FacilityUsers.tsx | 57 ++------ src/components/Users/ManageUsers.tsx | 53 +------- src/components/Users/UserListAndCard.tsx | 157 +++++++++++++++++----- 3 files changed, 135 insertions(+), 132 deletions(-) diff --git a/src/components/Facility/FacilityUsers.tsx b/src/components/Facility/FacilityUsers.tsx index 10d241bf796..32e0243ec65 100644 --- a/src/components/Facility/FacilityUsers.tsx +++ b/src/components/Facility/FacilityUsers.tsx @@ -1,20 +1,15 @@ -import { useState } from "react"; import { useTranslation } from "react-i18next"; import CountBlock from "@/CAREUI/display/Count"; -import CareIcon from "@/CAREUI/icons/CareIcon"; import Page from "@/components/Common/Page"; -import Tabs from "@/components/Common/Tabs"; -import SearchInput from "@/components/Form/SearchInput"; +import UserListView from "@/components/Users/UserListAndCard"; import useFilters from "@/hooks/useFilters"; import routes from "@/Utils/request/api"; import useQuery from "@/Utils/request/useQuery"; -import { UserGrid, UserList } from "../Users/UserListAndCard"; - export default function FacilityUsers(props: { facilityId: number }) { const { t } = useTranslation(); const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ @@ -22,7 +17,6 @@ export default function FacilityUsers(props: { facilityId: number }) { cacheBlacklist: ["username"], }); const { facilityId } = props; - const [activeTab, setActiveTab] = useState(0); const { data: facilityData } = useQuery(routes.getAnyFacility, { pathParams: { @@ -32,23 +26,20 @@ export default function FacilityUsers(props: { facilityId: number }) { }); const { data: userListData, loading: userListLoading } = useQuery( - routes.userList, + routes.getFacilityUsers, { query: { limit: resultsPerPage.toString(), offset: ( (qParams.page ? qParams.page - 1 : 0) * resultsPerPage ).toString(), - home_facility: facilityId.toString(), username: qParams.username, }, + pathParams: { facility_id: facilityId }, prefetch: facilityId !== undefined, }, ); - const renderCard = () => ; - const renderList = () => ; - return ( -
-
- updateQuery({ [e.name]: e.value })} - value={qParams.username} - placeholder={t("search_by_username")} - /> -
- - - Card -
- ), - value: 0, - }, - { - text: ( -
- - List -
- ), - value: 1, - }, - ]} - currentTab={activeTab} - onTabChange={(tab) => setActiveTab(tab as number)} - className="float-right" - /> -
+ updateQuery({ username })} + searchValue={qParams.username} + /> - {activeTab === 0 ? renderCard() : renderList()} ); diff --git a/src/components/Users/ManageUsers.tsx b/src/components/Users/ManageUsers.tsx index b6be9d55a61..8f1ab005457 100644 --- a/src/components/Users/ManageUsers.tsx +++ b/src/components/Users/ManageUsers.tsx @@ -12,11 +12,10 @@ import { FacilitySelect } from "@/components/Common/FacilitySelect"; import Loading from "@/components/Common/Loading"; import Page from "@/components/Common/Page"; import Pagination from "@/components/Common/Pagination"; -import Tabs from "@/components/Common/Tabs"; import { FacilityModel } from "@/components/Facility/models"; -import SearchInput from "@/components/Form/SearchInput"; import UnlinkFacilityDialog from "@/components/Users/UnlinkFacilityDialog"; import UserFilter from "@/components/Users/UserFilter"; +import UserListView from "@/components/Users/UserListAndCard"; import useAuthUser from "@/hooks/useAuthUser"; import useFilters from "@/hooks/useFilters"; @@ -29,8 +28,6 @@ import request from "@/Utils/request/request"; import useQuery from "@/Utils/request/useQuery"; import { classNames } from "@/Utils/utils"; -import { UserGrid, UserList } from "./UserListAndCard"; - export default function ManageUsers() { const { t } = useTranslation(); const { @@ -50,7 +47,6 @@ export default function ManageUsers() { const userTypes = authUser.is_superuser ? [...USER_TYPES] : USER_TYPES.slice(0, userIndex + 1); - const [activeTab, setActiveTab] = useState(0); const { data: homeFacilityData } = useQuery(routes.getAnyFacility, { pathParams: { id: qParams.home_facility }, @@ -106,9 +102,6 @@ export default function ManageUsers() { ); - const renderCard = () => ; - const renderList = () => ; - if (userListLoading || districtDataLoading || !userListData?.results) { return ; } @@ -116,45 +109,11 @@ export default function ManageUsers() { if (userListData?.results.length) { manageUsers = (
-
-
- updateQuery({ [e.name]: e.value })} - value={qParams.username} - placeholder={t("search_by_username")} - /> -
- - - Card -
- ), - value: 0, - }, - { - text: ( -
- - List -
- ), - value: 1, - }, - ]} - currentTab={activeTab} - onTabChange={(tab) => setActiveTab(tab as number)} - className="float-right" - /> -
-
- {activeTab === 0 ? renderCard() : renderList()} -
+ updateQuery({ username })} + searchValue={qParams.username} + />
); diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index a9579f09d75..5228f717166 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -1,12 +1,12 @@ import { navigate } from "raviger"; -import { useMemo } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import Card from "@/CAREUI/display/Card"; import CareIcon from "@/CAREUI/icons/CareIcon"; import { Avatar } from "@/components/Common/Avatar"; -import { UserModel } from "@/components/Users/models"; +import { UserAssignedModel, UserModel } from "@/components/Users/models"; import useAuthUser from "@/hooks/useAuthUser"; import useWindowDimensions from "@/hooks/useWindowDimensions"; @@ -20,43 +20,108 @@ import { relativeTime, } from "@/Utils/utils"; +import Tabs from "../Common/Tabs"; +import SearchInput from "../Form/SearchInput"; + +interface UserListViewProps { + users: UserModel[] | UserAssignedModel[]; + onSearch: (username: string) => void; + searchValue: string; +} + +export default function UserListView({ + users, + onSearch, + searchValue, +}: UserListViewProps) { + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState(0); + + return ( + <> +
+
+ onSearch(e.value)} + value={searchValue} + placeholder={t("search_by_username")} + /> +
+ + + Card +
+ ), + value: 0, + }, + { + text: ( +
+ + List +
+ ), + value: 1, + }, + ]} + currentTab={activeTab} + onTabChange={(tab) => setActiveTab(tab as number)} + className="float-right" + /> +
+ {activeTab === 0 ? ( + + ) : ( + + )} + + ); +} + export const GetUserTypes = () => { const authUser = useAuthUser(); - return useMemo(() => { - const userIndex = USER_TYPES.indexOf(authUser.user_type); - const readOnlyUsers = USER_TYPE_OPTIONS.filter((user) => user.readOnly); - const defaultAllowedUserTypes = USER_TYPE_OPTIONS.slice(0, userIndex + 1); + const userIndex = USER_TYPES.indexOf(authUser.user_type); + const readOnlyUsers = USER_TYPE_OPTIONS.filter((user) => user.readOnly); + const defaultAllowedUserTypes = USER_TYPE_OPTIONS.slice(0, userIndex + 1); - // Superuser gets all options - if (authUser.is_superuser) { - return [...USER_TYPE_OPTIONS]; - } + // Superuser gets all options + if (authUser.is_superuser) { + return [...USER_TYPE_OPTIONS]; + } - switch (authUser.user_type) { - case "StaffReadOnly": - return readOnlyUsers.slice(0, 1); - case "DistrictReadOnlyAdmin": - return readOnlyUsers.slice(0, 2); - case "StateReadOnlyAdmin": - return readOnlyUsers.slice(0, 3); - case "Pharmacist": - return USER_TYPE_OPTIONS.slice(0, 1); - case "Nurse": - case "Staff": - return [...defaultAllowedUserTypes, USER_TYPE_OPTIONS[6]]; - default: - return defaultAllowedUserTypes; - } - }, [authUser.user_type, authUser.is_superuser]); + switch (authUser.user_type) { + case "StaffReadOnly": + return readOnlyUsers.slice(0, 1); + case "DistrictReadOnlyAdmin": + return readOnlyUsers.slice(0, 2); + case "StateReadOnlyAdmin": + return readOnlyUsers.slice(0, 3); + case "Pharmacist": + return USER_TYPE_OPTIONS.slice(0, 1); + case "Nurse": + case "Staff": + return [...defaultAllowedUserTypes, USER_TYPE_OPTIONS[6]]; + default: + return defaultAllowedUserTypes; + } }; -export const CanUserAccess = (user: UserModel) => { - const allowedTypes = useMemo(() => GetUserTypes().map((type) => type.id), []); +export const CanUserAccess = (user: UserModel | UserAssignedModel) => { + const allowedTypes = GetUserTypes().map((type) => type.id); return allowedTypes.includes(user.user_type); }; -const getNameAndStatusCard = (user: UserModel, cur_online: boolean) => { +const getNameAndStatusCard = ( + user: UserModel | UserAssignedModel, + cur_online: boolean, +) => { return (
@@ -76,7 +141,7 @@ const getNameAndStatusCard = (user: UserModel, cur_online: boolean) => {
); }; -const UserCard = ({ user }: { user: UserModel }) => { +const UserCard = ({ user }: { user: UserModel | UserAssignedModel }) => { const cur_online = isUserOnline(user); const { width } = useWindowDimensions(); const mediumScreenBreakpoint = 640; @@ -108,12 +173,18 @@ const UserCard = ({ user }: { user: UserModel }) => { {user.home_facility_object?.name || t("no_home_facility")}
- {user.district_object && ( + {"district_object" in user && user.district_object && (
{t("district")}
{user.district_object.name}
)} + {"district" in user && user.district && ( +
+
{t("district")}
+
{user.district}
+
+ )} {user.weekly_working_hours && (
@@ -138,7 +209,11 @@ const UserCard = ({ user }: { user: UserModel }) => { ); }; -export const UserGrid = ({ users }: { users?: UserModel[] }) => ( +export const UserGrid = ({ + users, +}: { + users?: UserModel[] | UserAssignedModel[]; +}) => (
{users?.map((user) => )}
@@ -160,7 +235,7 @@ const UserListHeader = () => { ); }; -const UserListRow = ({ user }: { user: UserModel }) => { +const UserListRow = ({ user }: { user: UserModel | UserAssignedModel }) => { const { t } = useTranslation(); return ( { {user.home_facility_object?.name || t("no_home_facility")} - {user.district_object?.name || ""} + + {"district_object" in user && user.district + ? user.district_object?.name + : "district" in user && user.district + ? user.district + : ""} + {CanUserAccess(user) && (
+ ); -}; +} From a8ff61f20b0bdba1ac4e0f7e96958dc44dcd448f Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Mon, 11 Nov 2024 15:48:22 +0530 Subject: [PATCH 21/55] Conditionally render delete button/password reset form - Added error message notifications for the form submits --- src/components/Users/UserAddEditForm.tsx | 26 +++++----- src/components/Users/UserResetPassword.tsx | 6 ++- src/components/Users/UserSummary.tsx | 59 ++++++++++++---------- 3 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index ca07324c52b..e37caeb1369 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -248,15 +248,19 @@ const UserAddEditForm = (props: UserProps) => { ? formData.weekly_working_hours : null, }; - const { res } = await request(routes.partialUpdateUser, { + const { res, error } = await request(routes.partialUpdateUser, { pathParams: { username }, body: data, }); if (res?.ok) { Notification.Success({ - msg: "Details updated successfully", + msg: "Updated user details successfully", }); await refetchUserData(); + } else { + Notification.Error({ + msg: error?.message ?? "Error while updating user details", + }); } }; @@ -688,21 +692,19 @@ const UserAddEditForm = (props: UserProps) => { : undefined, }; - const { res } = await request(routes.addUser, { + const { res, error } = await request(routes.addUser, { body: data, }); if (res?.ok) { dispatch({ type: "set_form", form: initForm }); - if (!username) { - Notification.Success({ - msg: "User added successfully", - }); - } else { - Notification.Success({ - msg: "User updated successfully", - }); - } + Notification.Success({ + msg: "User added successfully", + }); navigate("/users"); + } else { + Notification.Error({ + msg: error?.message ?? "Error while adding user", + }); } setIsLoading(false); }; diff --git a/src/components/Users/UserResetPassword.tsx b/src/components/Users/UserResetPassword.tsx index ff3a7d2523a..513c3e35eb3 100644 --- a/src/components/Users/UserResetPassword.tsx +++ b/src/components/Users/UserResetPassword.tsx @@ -79,9 +79,11 @@ export default function UserResetPassword({ }); if (res?.ok) { Notification.Success({ msg: data?.message }); - } else if (!error) { + } else { Notification.Error({ - msg: "There was some error. Please try again in some time.", + msg: + error?.message ?? + "There was some error. Please try again in some time.", }); } setChangePasswordForm({ diff --git a/src/components/Users/UserSummary.tsx b/src/components/Users/UserSummary.tsx index c1d3ee3d095..c720f1177cd 100644 --- a/src/components/Users/UserSummary.tsx +++ b/src/components/Users/UserSummary.tsx @@ -1,3 +1,4 @@ +import { navigate } from "raviger"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -10,7 +11,6 @@ import UserInformation from "@/components/Users/UserInformation"; import UserResetPassword from "@/components/Users/UserResetPassword"; import { UserModel } from "@/components/Users/models"; -import useAppHistory from "@/hooks/useAppHistory"; import useAuthUser from "@/hooks/useAuthUser"; import * as Notification from "@/Utils/Notifications"; @@ -22,7 +22,6 @@ export default function UserSummaryTab({ userData }: { userData?: UserModel }) { const { t } = useTranslation(); const [showDeleteDialog, setshowDeleteDialog] = useState(false); const authUser = useAuthUser(); - const { goBack } = useAppHistory(); if (!userData) { return; @@ -42,10 +41,11 @@ export default function UserSummaryTab({ userData }: { userData?: UserModel }) { }); } setshowDeleteDialog(!showDeleteDialog); - goBack(); + navigate("/users"); }; const userColumnsData = { userData, username: userData.username }; + const deletePermitted = showUserDelete(authUser, userData); return ( <> @@ -65,32 +65,35 @@ export default function UserSummaryTab({ userData }: { userData?: UserModel }) { UserInformation, userColumnsData, )} - {userColumns( - t("reset_password"), - t("reset_password_note"), - UserResetPassword, - userColumnsData, - )} -
-
-

-

{t("delete_account")}

-

{t("delete_account_note")}

-

+ {deletePermitted && + userColumns( + t("reset_password"), + t("reset_password_note"), + UserResetPassword, + userColumnsData, + )} + {deletePermitted && ( +
+
+

+

{t("delete_account")}

+

{t("delete_account_note")}

+

+
+
+ deletePermitted} + onClick={() => setshowDeleteDialog(true)} + variant="danger" + data-testid="user-delete-button" + className="my-1 inline-flex" + > + + {t("delete_account_btn")} + +
-
- showUserDelete(authUser, userData)} - onClick={() => setshowDeleteDialog(true)} - variant="danger" - data-testid="user-delete-button" - className="my-1 inline-flex" - > - - {t("delete_account_btn")} - -
-
+ )}
); From 0565d849e243a460b9c04f6d7e0de28cdbb49bd0 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Mon, 11 Nov 2024 16:23:58 +0530 Subject: [PATCH 22/55] Reword role to qualifications --- public/locale/en.json | 6 +++--- src/components/Users/RoleAndSkillsTab.tsx | 10 +++++++--- src/components/Users/UserHome.tsx | 6 +++--- .../Users/{UserRoles.tsx => UserQualifications.tsx} | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) rename src/components/Users/{UserRoles.tsx => UserQualifications.tsx} (98%) diff --git a/public/locale/en.json b/public/locale/en.json index 461ea986505..26de7654a54 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -210,7 +210,7 @@ "URINATION_FREQUENCY__NORMAL": "Normal", "USERMANAGEMENT_TAB__FACILITIES": "Linked Facilities", "USERMANAGEMENT_TAB__PROFILE": "User Information", - "USERMANAGEMENT_TAB__ROLE_SKILLS": "Role & Skills", + "USERMANAGEMENT_TAB__QUALIFICATIONS_SKILLS": "Qualifications & Skills", "USERMANAGEMENT_TAB__SKILLS": "Skills", "VENTILATOR": "Detailed Update", "VENTILATOR_MODE__CMV": "Control Mechanical Ventilation (CMV)", @@ -1316,8 +1316,8 @@ "use_existing_abha_address": "Use Existing ABHA Address", "user_deleted_successfuly": "User Deleted Successfuly", "user_management": "User Management", - "user_role": "Role", - "user_role_note": "Search and assign the appropriate role for this user.", + "user_qualifications": "Qualifications", + "user_qualifications_note": "Enter appropriate qualifications for this user", "username": "Username", "username_already_exists": "This username already exists", "users": "Users", diff --git a/src/components/Users/RoleAndSkillsTab.tsx b/src/components/Users/RoleAndSkillsTab.tsx index b5528d2131e..3247329c627 100644 --- a/src/components/Users/RoleAndSkillsTab.tsx +++ b/src/components/Users/RoleAndSkillsTab.tsx @@ -2,10 +2,9 @@ import { useTranslation } from "react-i18next"; import userColumns from "@/components/Common/UserColumns"; import LinkedSkills from "@/components/Users/LinkedSkills"; +import UserQualifications from "@/components/Users/UserQualifications"; import { UserModel } from "@/components/Users/models"; -import UserRoles from "./UserRoles"; - type Props = { userData: UserModel; username: string; @@ -24,7 +23,12 @@ export default function RoleAndSkillsTab(props: Props) {
{userData.user_type && ["Doctor", "Nurse"].includes(userData.user_type) && - userColumns(t("user_role"), t("user_role_note"), UserRoles, props)} + userColumns( + t("user_qualifications"), + t("user_qualifications_note"), + UserQualifications, + props, + )} {userColumns( t("linked_skills"), t("linked_skills_note"), diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index 2280f7b975d..8065b8e5c6e 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -50,13 +50,13 @@ export default function UserHome(props: UserHomeProps) { const TABS: { PROFILE: tabChildProp; - ROLE_SKILLS: tabChildProp; + SKILLS: tabChildProp; FACILITIES: tabChildProp; } = { PROFILE: { body: UserSummaryTab }, - ROLE_SKILLS: { + SKILLS: { body: RoleAndSkillsTab, - name: roleInfoBeVisible() ? "ROLE_SKILLS" : "SKILLS", + name: roleInfoBeVisible() ? "QUALIFICATIONS_SKILLS" : "SKILLS", }, FACILITIES: { body: LinkedFacilitiesTab }, }; diff --git a/src/components/Users/UserRoles.tsx b/src/components/Users/UserQualifications.tsx similarity index 98% rename from src/components/Users/UserRoles.tsx rename to src/components/Users/UserQualifications.tsx index e4852a1c2b5..bd4c9e3108e 100644 --- a/src/components/Users/UserRoles.tsx +++ b/src/components/Users/UserQualifications.tsx @@ -66,7 +66,7 @@ const editFormReducer = (state: State, action: Action) => { } }; -export default function UserRoles({ username }: { username: string }) { +export default function UserQualifications({ username }: { username: string }) { const { t } = useTranslation(); const [states, dispatch] = useReducer(editFormReducer, initialState); const formVals = useRef(initForm); From d7bdfbc4a9a24b08cd415353e7dde263948f3fa8 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Tue, 12 Nov 2024 15:28:44 +0530 Subject: [PATCH 23/55] RabbitAi suggestions and form fixes --- public/locale/en.json | 33 ++- src/Utils/utils.ts | 4 +- src/common/validation.tsx | 5 + src/components/Common/SkillSelect.tsx | 4 +- src/components/Common/UserColumns.tsx | 24 +- src/components/Users/LinkedFacilities.tsx | 27 +- src/components/Users/LinkedFacilitiesTab.tsx | 7 +- src/components/Users/LinkedSkills.tsx | 29 +-- src/components/Users/UnlinkSkillDialog.tsx | 2 +- src/components/Users/UserAddEditForm.tsx | 103 ++++---- src/components/Users/UserBanner.tsx | 188 +++++-------- src/components/Users/UserHome.tsx | 44 ++-- src/components/Users/UserListAndCard.tsx | 10 +- src/components/Users/UserQualifications.tsx | 90 ++++--- src/components/Users/UserResetPassword.tsx | 261 +++++++++---------- src/components/Users/UserSummary.tsx | 13 +- 16 files changed, 430 insertions(+), 414 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 26de7654a54..18f5e173f12 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -404,8 +404,11 @@ "checking_for_update": "Checking for update", "checking_policy_eligibility": "Checking Policy Eligibility", "choose_date_time": "Choose Date and Time", + "choose_district": "Choose District", "choose_file": "Upload From Device", + "choose_localbody": "Choose Local Body", "choose_location": "Choose Location", + "choose_state": "Choose State", "claim__add_item": "Add Item", "claim__create_claim": "Create Claim", "claim__create_preauthorization": "Create Pre Authorization", @@ -442,6 +445,7 @@ "clear_all_filters": "Clear All Filters", "clear_home_facility": "Clear Home Facility", "clear_home_facility_confirm": "Are you sure you want to clear the home facility", + "clear_home_facility_error": "Error while clearing home facility. Try again later.", "clear_selection": "Clear selection", "close": "Close", "close_scanner": "Close Scanner", @@ -589,7 +593,8 @@ "district": "District", "district_program_management_supporting_unit": "District Program Management Supporting Unit", "dob_format": "Please enter date in DD/MM/YYYY format", - "doctor_experience_less_than_100_years": "Doctor experience should be less than 100 years", + "doctor_experience_error": "Doctor experience value should be at least 0 and less than 100.", + "doctor_experience_number_error": "Please enter a valid number", "doctor_s_medical_council_registration": "Doctor's Medical Council Registration", "doctors_name": "Doctor's Name", "domestic_healthcare_support": "Domestic healthcare support", @@ -673,6 +678,7 @@ "facilities": "Facilities", "facility": "Facility", "facility_consent_requests_page_title": "Patient Consent List", + "facility_linked_success": "Facility linked successfully", "facility_name": "Facility Name", "facility_preference": "Facility preference", "facility_search_placeholder": "Search by Facility / District Name", @@ -757,6 +763,8 @@ "hi__waiting_for_record": "Waiting for the Host HIP to send the record.", "hide": "Hide", "home_facility": "Home Facility", + "home_facility_cleared_success": "Home Facility cleared successfully", + "home_facility_updated_success": "Home Facility updated successfully", "hubs": "Hub Facilities", "i_declare": "I hereby declare that:", "icd11_as_recommended": "As per ICD-11 recommended by WHO", @@ -802,6 +810,7 @@ "is_emergency": "Is emergency", "is_emergency_case": "Is emergency case", "is_it_upshift": "is it upshift", + "is_phone_a_whatsapp_number": "Is the phone number a WhatsApp number?", "is_this_an_emergency": "Is this an emergency?", "is_this_an_upshift": "Is this an upshift?", "is_unusual_course": "Is unusual course", @@ -825,6 +834,7 @@ "link_abha_profile": "Link ABHA Profile", "link_camera_and_bed": "Link bed to Camera", "link_existing_abha_profile": "Already have an ABHA number", + "link_facility_error": "Error while linking facility. Try again later.", "linked_facilities": "Linked Facilities", "linked_facilities_note": "Add or remove facilities and set or change the Home Facility", "linked_skills": "Linked Skills", @@ -903,6 +913,8 @@ "never": "never", "new_password": "New Password", "new_password_confirmation": "Confirm New Password", + "new_password_same_as_old": "New password is same as old password, please enter a different new password.", + "new_password_validation": "New password is not valid.", "next_sessions": "Next Sessions", "no": "No", "no_attachments_found": "This communication has no attachments.", @@ -973,12 +985,17 @@ "pain_chart_description": "Mark region and intensity of pain", "passport_number": "Passport Number", "password": "Password", + "password_length_validation": "Password should be atleast 8 characters long", + "password_lowercase_validation": "Password should contain at least 1 lowercase letter", "password_mismatch": "Password and confirm password must be same.", + "password_number_validation": "Password should contain at least 1 number", "password_reset_failure": "Password Reset Failed", "password_reset_success": "Password Reset successfully", "password_sent": "Password Reset Email Sent", + "password_update_error": "Error while updating password. Try again later.", + "password_uppercase_validation": "Password should contain at least 1 uppercase letter", "password_validation": "Password should have 1 lowercase letter, 1 uppercase letter, 1 number, and be at least 8 characters long", - "passwords_not_matching": "Passwords not matching", + "passwords_not_matching": "Passwords do not match.", "patient": "Patient", "patient_address": "Patient Address", "patient_body": "Patient Body", @@ -1023,7 +1040,10 @@ "phone_number": "Phone Number", "phone_number_at_current_facility": "Phone Number of Contact person at current Facility", "pincode": "Pincode", + "please_confirm_password": "Please confirm your new password.", "please_enter_a_reason_for_the_shift": "Please enter a reason for the shift.", + "please_enter_current_password": "Please enter your current password.", + "please_enter_new_password": "Please enter your new password.", "please_enter_password": "Please enter the password", "please_enter_username": "Please enter the username", "please_select_a_facility": "Please select a facility", @@ -1211,6 +1231,8 @@ "show_patient_presets": "Show Patient Presets", "show_unread_notifications": "Show Unread", "sign_out": "Sign Out", + "skill_add_error": "Error while adding skill", + "skill_added_successfully": "Skill added successfully", "skills": "Skills", "socioeconomic_status": "Socioeconomic status", "software_update": "Software Update", @@ -1279,9 +1301,13 @@ "unlink_facility": "Unlink Facility", "unlink_facility_access": "The user will lose access to the facility", "unlink_facility_confirm": "Are you sure you want to unlink the facility", + "unlink_home_facility_error": "Error while unlinking home facility. Try again later.", + "unlink_home_facility_success": "Home Facility cleared successfully", "unlink_skill": "Unlink Skill", "unlink_skill_access": "The user will not have the skill associated anymore.", "unlink_skill_confirm": "Are you sure you want to unlink the skill", + "unlink_skill_error": "Error while unlinking skill. Try again later.", + "unlink_skill_success": "Skill unlinked successfully", "unlink_this_facility": "Unlink Facility", "unsubscribe": "Unsubscribe", "unsubscribe_failed": "Unsubscribe failed.", @@ -1318,8 +1344,11 @@ "user_management": "User Management", "user_qualifications": "Qualifications", "user_qualifications_note": "Enter appropriate qualifications for this user", + "user_type": "User Type", "username": "Username", "username_already_exists": "This username already exists", + "username_available": "Username is available", + "username_not_available": "Username is not available", "users": "Users", "vacant": "Vacant", "vehicle_preference": "Vehicle preference", diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index 3888406ad29..cd9d1fdb6be 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -217,7 +217,9 @@ export const formatCurrency = (price: number) => }); export const isUserOnline = (user: { last_login: DateLike }) => { - return dayjs().subtract(5, "minutes").isBefore(user.last_login); + return user.last_login + ? dayjs().subtract(5, "minutes").isBefore(user.last_login) + : false; }; export interface CountryData { diff --git a/src/common/validation.tsx b/src/common/validation.tsx index c7c084354a8..8c7953ed2e2 100644 --- a/src/common/validation.tsx +++ b/src/common/validation.tsx @@ -58,6 +58,11 @@ export const validatePincode = (pincode: string) => { return pattern.test(pincode); }; +export const validateNumber = (number: string) => { + const pattern = /^[0-9]+$/; + return pattern.test(number); +}; + export const checkIfValidIP = (str: string) => { // Regular expression to check if string is a IP address const regexExp = diff --git a/src/components/Common/SkillSelect.tsx b/src/components/Common/SkillSelect.tsx index 41c7fa762cb..eff16d1380e 100644 --- a/src/components/Common/SkillSelect.tsx +++ b/src/components/Common/SkillSelect.tsx @@ -15,8 +15,8 @@ interface SkillSelectProps { multiple?: boolean; showNOptions?: number; disabled?: boolean; - selected: SkillModel | SkillModel[] | null; - setSelected: (selected: SkillModel) => void; + selected: SkillModel | null; + setSelected: (selected: SkillModel | null) => void; userSkills?: SkillModel[]; } diff --git a/src/components/Common/UserColumns.tsx b/src/components/Common/UserColumns.tsx index 27b2fd9026c..81d59f5d71e 100644 --- a/src/components/Common/UserColumns.tsx +++ b/src/components/Common/UserColumns.tsx @@ -3,6 +3,7 @@ import { UserModel } from "../Users/models"; export type userChildProps = { userData: UserModel; username: string; + refetchUserData?: () => void; }; export default function userColumns( @@ -12,18 +13,19 @@ export default function userColumns( childProps: userChildProps, ) { return ( - <> -
-
-

-

{heading}

-

{note}

-

-
-
- +
+
+
+

{heading}

+

{note}

- +
+ +
+
); } diff --git a/src/components/Users/LinkedFacilities.tsx b/src/components/Users/LinkedFacilities.tsx index 3d59d3f88b7..61676818017 100644 --- a/src/components/Users/LinkedFacilities.tsx +++ b/src/components/Users/LinkedFacilities.tsx @@ -34,8 +34,10 @@ const initModalProps: { export default function LinkedFacilities({ userData, + refetchUserData, }: { userData: UserModel; + refetchUserData?: () => void; }) { const [facility, setFacility] = useState(null); const [userFacilities, setUserFacilities] = useState< @@ -109,16 +111,16 @@ export default function LinkedFacilities({ }); if (!res?.ok) { Notification.Error({ - msg: "Error while updating Home facility", + msg: t("update_home_facility_error"), }); } else { - userData.home_facility_object = selectedFacility; setHomeFacility(selectedFacility); Notification.Success({ - msg: "Home Facility updated successfully", + msg: t("home_facility_updated_success"), }); } await refetchUserFacilities(); + refetchUserData?.(); }; const clearHomeFacility = async () => { @@ -128,13 +130,13 @@ export default function LinkedFacilities({ if (!res?.ok) { Notification.Error({ - msg: "Error while clearing home facility", + msg: t("clear_home_facility_error"), }); } else { userData.home_facility_object = undefined; setHomeFacility(undefined); Notification.Success({ - msg: "Home Facility cleared successfully", + msg: t("home_facility_cleared_success"), }); } await refetchUserFacilities(); @@ -147,11 +149,11 @@ export default function LinkedFacilities({ }); if (!res?.ok) { Notification.Error({ - msg: "Error while unlinking home facility", + msg: t("unlink_home_facility_error"), }); } else { Notification.Success({ - msg: "Facility unlinked successfully", + msg: t("unlink_home_facility_success"), }); } await refetchUserFacilities(); @@ -161,23 +163,22 @@ export default function LinkedFacilities({ username: string, facility: FacilityModel | null, ) => { - //setIsLoading(true); + if (!facility) return; const { res } = await request(routes.addUserFacility, { pathParams: { username }, - body: { facility: facility?.id?.toString() }, + body: { facility: facility.id?.toString() }, }); if (!res?.ok) { Notification.Error({ - msg: "Error while linking facility", + msg: t("link_facility_error"), }); } else { Notification.Success({ - msg: "Facility linked successfully", + msg: t("facility_linked_success"), }); } await refetchUserFacilities(); - //setIsLoading(false); setFacility(null); }; @@ -189,7 +190,7 @@ export default function LinkedFacilities({
{facility.name}
-
+
diff --git a/src/components/Users/LinkedFacilitiesTab.tsx b/src/components/Users/LinkedFacilitiesTab.tsx index c4cd431a161..0f1cc848d8b 100644 --- a/src/components/Users/LinkedFacilitiesTab.tsx +++ b/src/components/Users/LinkedFacilitiesTab.tsx @@ -1,12 +1,13 @@ import { useTranslation } from "react-i18next"; import userColumns from "@/components/Common/UserColumns"; -import LinkededFacilities from "@/components/Users/LinkedFacilities"; +import LinkedFacilities from "@/components/Users/LinkedFacilities"; import { UserModel } from "@/components/Users/models"; type Props = { userData: UserModel; username: string; + refetchUserData?: () => void; }; export default function LinkedFacilitiesTab(props: Props) { @@ -14,7 +15,7 @@ export default function LinkedFacilitiesTab(props: Props) { const { t } = useTranslation(); if (!userData) { - return; + return <>; } return ( @@ -23,7 +24,7 @@ export default function LinkedFacilitiesTab(props: Props) { {userColumns( t("linked_facilities"), t("linked_facilities_note"), - LinkededFacilities, + LinkedFacilities, props, )}
diff --git a/src/components/Users/LinkedSkills.tsx b/src/components/Users/LinkedSkills.tsx index 1098df6d3e1..4afc230e633 100644 --- a/src/components/Users/LinkedSkills.tsx +++ b/src/components/Users/LinkedSkills.tsx @@ -63,14 +63,13 @@ export default function LinkedSkills({ username }: { username: string }) { pathParams: { username }, body: { skill: skill.id }, }); - - if (!res?.ok) { - Notification.Error({ - msg: "Error while adding skill", + if (res?.ok) { + Notification.Success({ + msg: t("skill_added_successfully"), }); } else { - Notification.Success({ - msg: "Skill added successfully", + Notification.Error({ + msg: t("skill_add_error"), }); } setSelectedSkill(null); @@ -84,11 +83,11 @@ export default function LinkedSkills({ username }: { username: string }) { }); if (res?.status !== 204) { Notification.Error({ - msg: "Error while unlinking skill", + msg: t("unlink_skill_error"), }); } else { Notification.Success({ - msg: "Skill unlinked successfully", + msg: t("unlink_skill_success"), }); } await refetchUserSkills(); @@ -100,7 +99,7 @@ export default function LinkedSkills({ username }: { username: string }) {
{skill.skill_object.name}
{authorizeForAddSkill && ( -
+
{skills && skills?.count > 0 && (
diff --git a/src/components/Users/UnlinkSkillDialog.tsx b/src/components/Users/UnlinkSkillDialog.tsx index d70e10f8f76..14e15b6d4b3 100644 --- a/src/components/Users/UnlinkSkillDialog.tsx +++ b/src/components/Users/UnlinkSkillDialog.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import ConfirmDialog from "@/components/Common/ConfirmDialog"; interface Props { - skillName?: string; + skillName: string; userName: string; onCancel: () => void; onSubmit: () => void; diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index e37caeb1369..64aa94cd23f 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -29,6 +29,7 @@ import { GENDER_TYPES, USER_TYPES } from "@/common/constants"; import { validateEmailAddress, validateName, + validateNumber, validatePassword, validateUsername, } from "@/common/validation"; @@ -78,8 +79,8 @@ type UserForm = { district?: number; local_body?: number; qualification?: string | undefined; - doctor_experience_commenced_on?: string | undefined; - doctor_medical_council_registration?: string | undefined; + doctor_experience_commenced_on?: string; + doctor_medical_council_registration?: string; video_connect_link?: string; weekly_working_hours?: string | null; }; @@ -397,6 +398,17 @@ const UserAddEditForm = (props: UserProps) => { if (field) field(event.name as keyof UserForm).onChange(event); }; + const changePhoneNumber = ( + field: FormContextValue, + fieldName: keyof UserForm, + phoneNumber: string, + ) => { + field(fieldName).onChange({ + name: field(fieldName).name, + value: phoneNumber, + }); + }; + const handlePhoneChange = ( event: FieldChangeEvent, field: FormContextValue, @@ -406,26 +418,17 @@ const UserAddEditForm = (props: UserProps) => { switch (event.name) { case "phone_number": phoneNumberVal = event.value as string; - field("phone_number").onChange({ - name: field("phone_number").name, - value: phoneNumberVal, - }); + changePhoneNumber(field, "phone_number", phoneNumberVal); formData = { ...formData, phone_number: phoneNumberVal }; if (state.form.phone_number_is_whatsapp) { - field("alt_phone_number").onChange({ - name: field("alt_phone_number").name, - value: phoneNumberVal, - }); + changePhoneNumber(field, "alt_phone_number", phoneNumberVal); formData = { ...formData, alt_phone_number: phoneNumberVal }; } break; case "alt_phone_number": phoneNumberVal = event.value as string; if (!state.form.phone_number_is_whatsapp) { - field("alt_phone_number").onChange({ - name: field("alt_phone_number").name, - value: phoneNumberVal, - }); + changePhoneNumber(field, "alt_phone_number", phoneNumberVal); formData = { ...formData, alt_phone_number: phoneNumberVal }; } break; @@ -436,10 +439,7 @@ const UserAddEditForm = (props: UserProps) => { alt_phone_number: phoneNumberVal, phone_number_is_whatsapp: event.value as boolean, }; - field("alt_phone_number").onChange({ - name: field("alt_phone_number").name, - value: phoneNumberVal, - }); + changePhoneNumber(field, "alt_phone_number", phoneNumberVal); field("phone_number_is_whatsapp").onChange({ name: field("phone_number_is_whatsapp").name, value: event.value, @@ -510,13 +510,14 @@ const UserAddEditForm = (props: UserProps) => { } break; case "doctor_experience_commenced_on": - if (formData.user_type === "Doctor" && !formData[field]) { - errors[field] = t("field_required"); - } else if ( - formData.user_type === "Doctor" && - Number(formData.doctor_experience_commenced_on) > 100 - ) { - errors[field] = t("doctor_experience_less_than_100_years"); + if (formData.user_type === "Doctor") { + if (!formData[field]) { + errors[field] = t("field_required"); + } else if (!validateNumber(formData[field] ?? "")) { + errors[field] = t("doctor_experience_number_error"); + } else if (Number(formData.doctor_experience_commenced_on) > 100) { + errors[field] = t("doctor_experience_less_than_100_years"); + } } break; case "doctor_medical_council_registration": @@ -616,10 +617,10 @@ const UserAddEditForm = (props: UserProps) => { if (formData[field] !== null && formData[field] !== undefined) { const hours = Number(formData[field]); if ( - isNaN(hours) || + Number.isNaN(hours) || hours < 0 || hours > 168 || - !/^\d+$/.test(formData[field] ?? "") + !validateNumber(formData[field] ?? "") ) { errors[field] = t("weekly_working_hours_error"); } @@ -744,7 +745,7 @@ const UserAddEditForm = (props: UserProps) => {
{!editUser && (
- Facilities + {t("facilities")} { o.role + (o.readOnly ? " (Read Only)" : "") @@ -773,7 +774,7 @@ const UserAddEditForm = (props: UserProps) => { /> option.name} optionValue={(option) => option.id} @@ -804,8 +805,8 @@ const UserAddEditForm = (props: UserProps) => { required min={0} type="number" - label="Years of experience" - placeholder="Years of experience of the Doctor" + label={t("years_of_experience")} + placeholder={t("years_of_experience_of_the_doctor")} onChange={(e) => { handleFieldChange(e, field); }} @@ -815,8 +816,8 @@ const UserAddEditForm = (props: UserProps) => { { handleFieldChange(e, field); }} @@ -829,8 +830,8 @@ const UserAddEditForm = (props: UserProps) => {
{ @@ -844,13 +845,13 @@ const UserAddEditForm = (props: UserProps) => { onChange={(e) => { handlePhoneChange(e, field); }} - label="Is the phone number a WhatsApp number?" + label={t("is_phone_a_whatsapp_number")} />
{ @@ -864,8 +865,8 @@ const UserAddEditForm = (props: UserProps) => { {!editUser && ( { className="text-xl text-red-500" />{" "} - Username is not available + {t("username_not_available")}
) : ( @@ -911,7 +912,7 @@ const UserAddEditForm = (props: UserProps) => { className="text-xl text-green-500" />{" "} - Username is available + {t("username_available")}
)} @@ -1045,7 +1046,7 @@ const UserAddEditForm = (props: UserProps) => { { handleFieldChange(e, field); @@ -1054,7 +1055,7 @@ const UserAddEditForm = (props: UserProps) => {
{ @@ -1112,9 +1113,9 @@ const UserAddEditForm = (props: UserProps) => { ) : ( o.name} optionValue={(o) => o.id} @@ -1130,9 +1131,9 @@ const UserAddEditForm = (props: UserProps) => { ) : ( o.name} optionValue={(o) => o.id} @@ -1149,10 +1150,10 @@ const UserAddEditForm = (props: UserProps) => { ) : ( o.name} optionValue={(o) => o.id} diff --git a/src/components/Users/UserBanner.tsx b/src/components/Users/UserBanner.tsx index 61f28697f8a..0ed57372bde 100644 --- a/src/components/Users/UserBanner.tsx +++ b/src/components/Users/UserBanner.tsx @@ -1,137 +1,91 @@ import dayjs from "dayjs"; import { t } from "i18next"; -import { useEffect, useState } from "react"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; import { Avatar } from "@/components/Common/Avatar"; import UserDetails from "@/components/Common/UserDetails"; import UserDetailComponent from "@/components/Common/UserDetailsComponet"; import { UserModel } from "@/components/Users/models"; -import { - classNames, - formatName, - isUserOnline, - relativeTime, -} from "@/Utils/utils"; - -export default function UserBanner({ userData }: { userData: UserModel }) { - const [userOnline, setUserOnline] = useState(false); +import { formatName } from "@/Utils/utils"; - useEffect(() => { - if (!userData) return; - setUserOnline(isUserOnline(userData)); - }, [userData]); +import { UserStatusIndicator } from "./UserListAndCard"; +export default function UserBanner({ userData }: { userData: UserModel }) { if (!userData) { return; } return ( - <> - { - <> -
-
-
- -
-
-

- {formatName(userData)} -

-
- {userData.last_login && userOnline ? ( - - {" "} - {" "} - Currently Online - - ) : ( - <> -
- - - {userData.last_login - ? relativeTime(userData.last_login) - : "Never"} - -
- - )} -
-
- - - {userData.username} - -
-
-
- {userData.user_type && ( - - )} - {userData.district_object && ( - - )} - - - {userData.home_facility_object?.name || "No Home Facility"} - - - {["Doctor", "Nurse"].includes(userData.user_type) && ( - - {userData.qualification ? ( - - {userData.qualification} - - ) : ( - {t("unknown")} - )} - - )} - {userData.user_type === "Doctor" && ( - - {userData.doctor_experience_commenced_on ? ( - - {dayjs().diff( - userData.doctor_experience_commenced_on, - "years", - false, - )}{" "} - years - - ) : ( - {t("unknown")} - )} - - )} +
+
+
+ +
+
+

+ {formatName(userData)} +

+
+
+ + + {userData.username} +
- - } - +
+
+ {userData.user_type && ( + + )} + {userData.district_object && ( + + )} + + + {userData.home_facility_object?.name || "No Home Facility"} + + + {["Doctor", "Nurse"].includes(userData.user_type) && ( + + {userData.qualification ? ( + {userData.qualification} + ) : ( + {t("unknown")} + )} + + )} + {userData.user_type === "Doctor" && ( + + {userData.doctor_experience_commenced_on ? ( + + {dayjs().diff( + userData.doctor_experience_commenced_on, + "years", + false, + )}{" "} + years + + ) : ( + {t("unknown")} + )} + + )} +
+
+
); } diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index 8065b8e5c6e..9c2b4616aaf 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -12,6 +12,7 @@ import UserBanner from "@/components/Users/UserBanner"; import UserSummaryTab from "@/components/Users/UserSummary"; import { UserModel } from "@/components/Users/models"; +import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; import useQuery from "@/Utils/request/useQuery"; import { classNames, formatName, keysOf } from "@/Utils/utils"; @@ -30,18 +31,25 @@ export default function UserHome(props: UserHomeProps) { const [userData, setUserData] = useState(); const { t } = useTranslation(); - const { loading } = useQuery(routes.getUserDetails, { - query: { - username: username, - }, - onResponse: ({ res, data }) => { - if (res?.status === 200 && data) { - setUserData(data); - } else if (res?.status === 400) { - navigate("/users"); - } + const { loading, refetch: refetchUserDetails } = useQuery( + routes.getUserDetails, + { + query: { + username: username, + }, + onResponse: ({ res, data, error }) => { + if (res?.status === 200 && data) { + setUserData(data); + } else if (res?.status === 400) { + navigate("/users"); + } else if (error) { + Notification.Error({ + msg: "Error while fetching user details: " + (error?.message || ""), + }); + } + }, }, - }); + ); const roleInfoBeVisible = () => { if (["Doctor", "Nurse"].includes(userData?.user_type ?? "")) return true; @@ -61,10 +69,10 @@ export default function UserHome(props: UserHomeProps) { FACILITIES: { body: LinkedFacilitiesTab }, }; - let currentTab = undefined; - if (Object.keys(TABS).includes(tab.toUpperCase())) { - currentTab = tab.toUpperCase() as keyof typeof TABS; - } + const normalizedTab = tab.toUpperCase(); + const isValidTab = (tab: string): tab is keyof typeof TABS => + Object.keys(TABS).includes(tab as keyof typeof TABS); + const currentTab = isValidTab(normalizedTab) ? normalizedTab : undefined; if (!currentTab) { return ; @@ -118,7 +126,11 @@ export default function UserHome(props: UserHomeProps) {
- + } diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index cc7f4259892..969212878b0 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -80,9 +80,7 @@ const getNameAndStatusCard = (
-

- {formatName(user)} -

+

{formatName(user)}

{ className="h-10 w-10 text-lg" />
-

- {formatName(user)} -

+

{formatName(user)}

@{user.username}
diff --git a/src/components/Users/UserQualifications.tsx b/src/components/Users/UserQualifications.tsx index bd4c9e3108e..42c56ea8bed 100644 --- a/src/components/Users/UserQualifications.tsx +++ b/src/components/Users/UserQualifications.tsx @@ -6,23 +6,28 @@ import { FieldError } from "@/components/Form/FieldValidators"; import Form from "@/components/Form/Form"; import TextFormField from "@/components/Form/FormFields/TextFormField"; +import { validateNumber } from "@/common/validation"; + import * as Notification from "@/Utils/Notifications"; import dayjs from "@/Utils/dayjs"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; import useQuery from "@/Utils/request/useQuery"; +import { FormContextValue } from "../Form/FormContext"; +import { FieldChangeEvent } from "../Form/FormFields/Utils"; + type EditForm = { - user_type: string | undefined; - qualification: string | undefined; - doctor_experience_commenced_on: number | string | undefined; - doctor_medical_council_registration: string | undefined; + user_type: string; + qualification?: string | null; + doctor_experience_commenced_on?: string | null; + doctor_medical_council_registration?: string | null; }; type ErrorForm = { - user_type: string | undefined; - qualification: string | undefined; - doctor_experience_commenced_on: number | string | undefined; - doctor_medical_council_registration: string | undefined; + user_type?: string; + qualification?: string | null; + doctor_experience_commenced_on?: string | null; + doctor_medical_council_registration?: string | null; }; type State = { form: EditForm; @@ -34,9 +39,9 @@ type Action = const initForm: EditForm = { user_type: "", - qualification: undefined, - doctor_experience_commenced_on: undefined, - doctor_medical_council_registration: undefined, + qualification: null, + doctor_experience_commenced_on: null, + doctor_medical_council_registration: null, }; const initError: ErrorForm = Object.assign( @@ -85,10 +90,9 @@ export default function UserQualifications({ username }: { username: string }) { const formData: EditForm = { user_type: result.data.user_type, qualification: result.data.qualification, - doctor_experience_commenced_on: dayjs().diff( - dayjs(result.data.doctor_experience_commenced_on), - "years", - ), + doctor_experience_commenced_on: dayjs() + .diff(dayjs(result.data.doctor_experience_commenced_on), "years") + .toString(), doctor_medical_council_registration: result.data.doctor_medical_council_registration, }; @@ -105,17 +109,21 @@ export default function UserQualifications({ username }: { username: string }) { Object.keys(states.form).forEach((field) => { switch (field) { case "doctor_experience_commenced_on": - if (states.form.user_type === "Doctor" && !states.form[field]) { - errors[field] = t("field_required"); - } else if ( - (states.form.user_type === "Doctor" && - Number(states.form.doctor_experience_commenced_on) >= 100) || - Number(states.form.doctor_experience_commenced_on) < 0 - ) { - errors[field] = - "Doctor experience should be at least 0 years and less than 100 years."; + if (states.form.user_type === "Doctor") { + if (states.form.doctor_experience_commenced_on === undefined) { + errors[field] = t("field_required"); + } else if ( + !validateNumber(states.form.doctor_experience_commenced_on ?? "") + ) { + errors[field] = t("doctor_experience_number_error"); + } else { + const years = Number(states.form.doctor_experience_commenced_on); + if (isNaN(years) || years < 0 || years > 100) { + errors[field] = t("doctor_experience_error"); + } + } } - return; + break; case "qualification": if ( (states.form.user_type === "Doctor" || @@ -124,12 +132,12 @@ export default function UserQualifications({ username }: { username: string }) { ) { errors[field] = t("field_required"); } - return; + break; case "doctor_medical_council_registration": if (states.form.user_type === "Doctor" && !states.form[field]) { errors[field] = t("field_required"); } - return; + break; } }); return errors; @@ -142,6 +150,20 @@ export default function UserQualifications({ username }: { username: string }) { }); }; + const handleFieldChange = ( + event: FieldChangeEvent, + field?: FormContextValue, + ) => { + dispatch({ + type: "set_form", + form: { + ...states.form, + [event.name]: event.value, + }, + }); + if (field) field(event.name as keyof EditForm).onChange(event); + }; + if (isLoading || !userData) { return ; } @@ -152,7 +174,7 @@ export default function UserQualifications({ username }: { username: string }) { const data = { qualification: formData.user_type === "Doctor" || formData.user_type === "Nurse" - ? formData.qualification + ? (formData.qualification ?? undefined) : undefined, doctor_experience_commenced_on: formData.user_type === "Doctor" @@ -167,7 +189,7 @@ export default function UserQualifications({ username }: { username: string }) { : undefined, doctor_medical_council_registration: formData.user_type === "Doctor" - ? formData.doctor_medical_council_registration + ? (formData.doctor_medical_council_registration ?? undefined) : undefined, }; const { res } = await request(routes.partialUpdateUser, { @@ -208,6 +230,9 @@ export default function UserQualifications({ username }: { username: string }) { className="flex-1" label={t("qualification")} placeholder={t("qualification")} + onChange={(e) => { + handleFieldChange(e, field); + }} /> )}
@@ -219,8 +244,12 @@ export default function UserQualifications({ username }: { username: string }) { className="flex-1" type="number" min={0} + max={100} label={t("years_of_experience")} placeholder={t("years_of_experience_of_the_doctor")} + onChange={(e) => { + handleFieldChange(e, field); + }} /> { + handleFieldChange(e, field); + }} />
)} diff --git a/src/components/Users/UserResetPassword.tsx b/src/components/Users/UserResetPassword.tsx index 513c3e35eb3..50d808e13e0 100644 --- a/src/components/Users/UserResetPassword.tsx +++ b/src/components/Users/UserResetPassword.tsx @@ -1,7 +1,6 @@ -import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Submit } from "@/components/Common/ButtonV2"; +import Form from "@/components/Form/Form"; import TextFormField from "@/components/Form/FormFields/TextFormField"; import { validateRule } from "@/components/Users/UserAddEditForm"; import { UpdatePasswordForm, UserModel } from "@/components/Users/models"; @@ -10,6 +9,13 @@ import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; +interface PasswordForm { + username: string; + old_password: string; + new_password_1: string; + new_password_2: string; +} + export default function UserResetPassword({ userData, }: { @@ -17,25 +23,12 @@ export default function UserResetPassword({ }) { const { t } = useTranslation(); - const [changePasswordForm, setChangePasswordForm] = useState<{ - username: string; - old_password: string; - new_password_1: string; - new_password_2: string; - }>({ + const initForm: PasswordForm = { username: userData.username, old_password: "", new_password_1: "", new_password_2: "", - }); - - const [changePasswordErrors] = useState<{ - old_password: string; - password_confirmation: string; - }>({ - old_password: "", - password_confirmation: "", - }); + }; const validateNewPassword = (password: string) => { if ( @@ -49,145 +42,129 @@ export default function UserResetPassword({ return true; }; - const changePassword = async (e: any) => { - e.preventDefault(); - //validating form - if ( - changePasswordForm.new_password_1 !== changePasswordForm.new_password_2 - ) { - Notification.Error({ - msg: "Passwords are different in new password and confirmation password column.", - }); - } else if (!validateNewPassword(changePasswordForm.new_password_1)) { - Notification.Error({ - msg: "Entered New Password is not valid, please check!", - }); - } else if ( - changePasswordForm.new_password_1 === changePasswordForm.old_password - ) { - Notification.Error({ - msg: "New password is same as old password, Please enter a different new password.", - }); + const validateForm = (formData: PasswordForm) => { + const errors: Partial> = {}; + + if (!formData.old_password) { + errors.old_password = t("please_enter_current_password"); + } + + if (!formData.new_password_1) { + errors.new_password_1 = t("please_enter_new_password"); + } else if (!validateNewPassword(formData.new_password_1)) { + errors.new_password_1 = t("new_password_validation"); + } + + if (!formData.new_password_2) { + errors.new_password_2 = t("please_confirm_password"); + } else if (formData.new_password_1 !== formData.new_password_2) { + errors.new_password_2 = t("passwords_not_matching"); + } + + if (formData.new_password_1 === formData.old_password) { + errors.new_password_1 = t("new_password_same_as_old"); + } + + return errors; + }; + + const handleSubmit = async (formData: PasswordForm) => { + const form: UpdatePasswordForm = { + old_password: formData.old_password, + username: userData.username, + new_password: formData.new_password_1, + }; + + const { res, data, error } = await request(routes.updatePassword, { + body: form, + }); + + if (res?.ok) { + Notification.Success({ msg: data?.message }); } else { - const form: UpdatePasswordForm = { - old_password: changePasswordForm.old_password, - username: userData.username, - new_password: changePasswordForm.new_password_1, - }; - const { res, data, error } = await request(routes.updatePassword, { - body: form, - }); - if (res?.ok) { - Notification.Success({ msg: data?.message }); - } else { - Notification.Error({ - msg: - error?.message ?? - "There was some error. Please try again in some time.", - }); - } - setChangePasswordForm({ - ...changePasswordForm, - new_password_1: "", - new_password_2: "", - old_password: "", + Notification.Error({ + msg: error?.message ?? t("password_update_error"), }); } }; return ( - <> -
-
-
-
+
+ + defaults={initForm} + validate={validateForm} + onSubmit={handleSubmit} + resetFormVals + hideRestoreDraft + noPadding + > + {(field) => ( +
+ +
+ +
+ {validateRule( + field("new_password_1").value?.length >= 8, + t("password_length_validation"), + !field("new_password_1").value, + )} + {validateRule( + field("new_password_1").value !== + field("new_password_1").value?.toUpperCase(), + t("password_lowercase_validation"), + !field("new_password_1").value, + )} + {validateRule( + field("new_password_1").value !== + field("new_password_1").value?.toLowerCase(), + t("password_uppercase_validation"), + !field("new_password_1").value, + )} + {validateRule( + /\d/.test(field("new_password_1").value ?? ""), + t("password_number_validation"), + !field("new_password_1").value, + )} +
+
+
- setChangePasswordForm({ - ...changePasswordForm, - old_password: e.value, - }) - } - error={changePasswordErrors.old_password} required /> -
- { - setChangePasswordForm({ - ...changePasswordForm, - new_password_1: e.value, - }); - }} - required - /> + {field("new_password_2").value?.length > 0 && (
{validateRule( - changePasswordForm.new_password_1?.length >= 8, - "Password should be atleast 8 characters long", - !changePasswordForm.new_password_1, - )} - {validateRule( - changePasswordForm.new_password_1 !== - changePasswordForm.new_password_1.toUpperCase(), - "Password should contain at least 1 lowercase letter", - !changePasswordForm.new_password_1, - )} - {validateRule( - changePasswordForm.new_password_1 !== - changePasswordForm.new_password_1.toLowerCase(), - "Password should contain at least 1 uppercase letter", - !changePasswordForm.new_password_1, - )} - {validateRule( - /\d/.test(changePasswordForm.new_password_1), - "Password should contain at least 1 number", - !changePasswordForm.new_password_1, + field("new_password_1").value === + field("new_password_2").value, + t("passwords_not_matching"), + !field("new_password_2").value, )}
-
-
- { - setChangePasswordForm({ - ...changePasswordForm, - new_password_2: e.value, - }); - }} - /> - {changePasswordForm.new_password_2.length > 0 && ( -
- {validateRule( - changePasswordForm.new_password_1 === - changePasswordForm.new_password_2, - "Confirm password should match the new password", - !changePasswordForm.new_password_2, - )} -
- )} -
-
-
- + )}
- -
-
- +
+ )} + +
); } diff --git a/src/components/Users/UserSummary.tsx b/src/components/Users/UserSummary.tsx index c720f1177cd..938ec1cd61f 100644 --- a/src/components/Users/UserSummary.tsx +++ b/src/components/Users/UserSummary.tsx @@ -21,6 +21,7 @@ import request from "@/Utils/request/request"; export default function UserSummaryTab({ userData }: { userData?: UserModel }) { const { t } = useTranslation(); const [showDeleteDialog, setshowDeleteDialog] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); const authUser = useAuthUser(); if (!userData) { @@ -28,20 +29,23 @@ export default function UserSummaryTab({ userData }: { userData?: UserModel }) { } const handleSubmit = async () => { + setIsDeleting(true); const { res, error } = await request(routes.deleteUser, { pathParams: { username: userData.username }, }); + setIsDeleting(false); if (res?.status === 204) { Notification.Success({ msg: "User deleted successfully", }); + setshowDeleteDialog(!showDeleteDialog); + navigate("/users"); } else { Notification.Error({ msg: "Error while deleting User: " + (error || ""), }); + setshowDeleteDialog(!showDeleteDialog); } - setshowDeleteDialog(!showDeleteDialog); - navigate("/users"); }; const userColumnsData = { userData, username: userData.username }; @@ -75,10 +79,10 @@ export default function UserSummaryTab({ userData }: { userData?: UserModel }) { {deletePermitted && (
-

+

{t("delete_account")}

{t("delete_account_note")}

-

+
{t("delete_account_btn")} From 76a47be633c54b7a4dce82cd969e8d77c086abb3 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Tue, 12 Nov 2024 15:59:49 +0530 Subject: [PATCH 24/55] show district column only if data is available --- src/components/Users/UserListAndCard.tsx | 35 ++++++++++++++++-------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index 969212878b0..8a9df39f825 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -216,7 +216,11 @@ export const UserGrid = ({
); -const UserListHeader = () => { +const UserListHeader = ({ + showDistrictColumn, +}: { + showDistrictColumn: boolean; +}) => { const { t } = useTranslation(); return ( @@ -225,7 +229,9 @@ const UserListHeader = () => { {t("status")} {t("role")} {t("home_facility")} - {t("district")} + {showDistrictColumn && ( + {t("district")} + )} @@ -277,16 +283,21 @@ export const UserList = ({ users, }: { users?: UserModel[] | UserAssignedModel[]; -}) => ( -
- - - - {users?.map((user) => )} - -
-
-); +}) => { + const showDistrictColumn = users?.some( + (user) => "district_object" in user || "district" in user, + ); + return ( +
+ + + + {users?.map((user) => )} + +
+
+ ); +}; interface UserListViewProps { users: UserModel[] | UserAssignedModel[]; onSearch: (username: string) => void; From f0cc3624e90e410cab232e0a6b433851a3e90996 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Tue, 12 Nov 2024 23:43:29 +0530 Subject: [PATCH 25/55] RabbitAI comments --- public/locale/en.json | 6 +- src/components/Users/LinkedFacilitiesTab.tsx | 18 +- src/components/Users/UserAddEditForm.tsx | 208 +++++++++++-------- src/components/Users/UserFormValidations.tsx | 82 ++++++++ src/components/Users/UserListAndCard.tsx | 32 ++- src/components/Users/UserQualifications.tsx | 60 +++--- src/components/Users/UserResetPassword.tsx | 18 +- 7 files changed, 274 insertions(+), 150 deletions(-) create mode 100644 src/components/Users/UserFormValidations.tsx diff --git a/public/locale/en.json b/public/locale/en.json index e4bb87f5699..a9014516f24 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -594,8 +594,8 @@ "district": "District", "district_program_management_supporting_unit": "District Program Management Supporting Unit", "dob_format": "Please enter date in DD/MM/YYYY format", - "doctor_experience_error": "Doctor experience value should be at least 0 and less than 100.", - "doctor_experience_number_error": "Please enter a valid number", + "doctor_experience_error": "Please enter a valid number between 0 and 100.", + "doctor_experience_less_than_100_years": "Doctor experience should be less than 100 years.", "doctor_s_medical_council_registration": "Doctor's Medical Council Registration", "doctors_name": "Doctor's Name", "domestic_healthcare_support": "Domestic healthcare support", @@ -1346,6 +1346,8 @@ "uploading": "Uploading", "use_existing_abha_address": "Use Existing ABHA Address", "user_deleted_successfuly": "User Deleted Successfuly", + "user_details_update_error": "Error while updating user details", + "user_details_update_success": "User details updated successfully", "user_management": "User Management", "user_qualifications": "Qualifications", "user_qualifications_note": "Enter appropriate qualifications for this user", diff --git a/src/components/Users/LinkedFacilitiesTab.tsx b/src/components/Users/LinkedFacilitiesTab.tsx index 0f1cc848d8b..f3db269ade9 100644 --- a/src/components/Users/LinkedFacilitiesTab.tsx +++ b/src/components/Users/LinkedFacilitiesTab.tsx @@ -19,15 +19,13 @@ export default function LinkedFacilitiesTab(props: Props) { } return ( - <> -
- {userColumns( - t("linked_facilities"), - t("linked_facilities_note"), - LinkedFacilities, - props, - )} -
- +
+ {userColumns( + t("linked_facilities"), + t("linked_facilities_note"), + LinkedFacilities, + props, + )} +
); } diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index a8822c52d97..0d3c64347c6 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -21,6 +21,14 @@ import PhoneNumberFormField from "@/components/Form/FormFields/PhoneNumberFormFi import { SelectFormField } from "@/components/Form/FormFields/SelectFormField"; import TextFormField from "@/components/Form/FormFields/TextFormField"; import { FieldChangeEvent } from "@/components/Form/FormFields/Utils"; +import { + UserForm, + ValidateDoctorExperienceCommencedOn, + ValidateDoctorMedicalCouncilRegistration, + ValidateQualification, +} from "@/components/Users/UserFormValidations"; +import { GetUserTypes } from "@/components/Users/UserListAndCard"; +import { GenderType } from "@/components/Users/models"; import useAppHistory from "@/hooks/useAppHistory"; import useAuthUser from "@/hooks/useAuthUser"; @@ -48,9 +56,6 @@ import { scrollTo, } from "@/Utils/utils"; -import { GetUserTypes } from "./UserListAndCard"; -import { GenderType } from "./models"; - interface UserProps { username?: string; } @@ -60,31 +65,6 @@ interface StateObj { name: string; } -type UserForm = { - user_type?: string; - gender: GenderType; - password?: string; - c_password?: string; - facilities?: Array; - home_facility?: FacilityModel | null; - username?: string; - first_name: string; - last_name: string; - email: string; - phone_number: string; - alt_phone_number: string; - phone_number_is_whatsapp?: boolean; - date_of_birth: Date | null | string; - state?: number; - district?: number; - local_body?: number; - qualification?: string | undefined; - doctor_experience_commenced_on?: string; - doctor_medical_council_registration?: string; - video_connect_link?: string; - weekly_working_hours?: string | null; -}; - const initForm: UserForm = { user_type: "", gender: "Female", @@ -195,16 +175,20 @@ const UserAddEditForm = (props: UserProps) => { prefetch: editUser, onResponse: (result) => { if (!editUser || !result || !result.res || !result.data) return; + const userData = result.data; const formData: UserForm = { - first_name: result.data.first_name, - last_name: result.data.last_name, - date_of_birth: result.data.date_of_birth || null, - gender: result.data.gender || "Male", - email: result.data.email, - video_connect_link: result.data.video_connect_link, - phone_number: result.data.phone_number?.toString() || "", - alt_phone_number: result.data.alt_phone_number?.toString() || "", - weekly_working_hours: result.data.weekly_working_hours, + first_name: userData.first_name, + last_name: userData.last_name, + date_of_birth: userData.date_of_birth || null, + gender: userData.gender || "Male", + email: userData.email, + video_connect_link: userData.video_connect_link, + phone_number: userData.phone_number?.toString() || "", + alt_phone_number: userData.alt_phone_number?.toString() || "", + weekly_working_hours: userData.weekly_working_hours, + phone_number_is_whatsapp: + userData.phone_number?.toString() === + userData.alt_phone_number?.toString(), }; dispatch({ type: "set_form", @@ -255,12 +239,12 @@ const UserAddEditForm = (props: UserProps) => { }); if (res?.ok) { Notification.Success({ - msg: "Updated user details successfully", + msg: t("user_details_update_success"), }); await refetchUserData(); } else { Notification.Error({ - msg: error?.message ?? "Error while updating user details", + msg: error?.message ?? t("user_details_update_error"), }); } }; @@ -401,14 +385,34 @@ const UserAddEditForm = (props: UserProps) => { const changePhoneNumber = ( field: FormContextValue, fieldName: keyof UserForm, - phoneNumber: string, + value: string | boolean, ) => { field(fieldName).onChange({ name: field(fieldName).name, - value: phoneNumber, + value: value, }); }; + const updatePhoneNumber = ( + field: FormContextValue, + phoneNumber: string, + ) => { + changePhoneNumber(field, "phone_number", phoneNumber); + return { phone_number: phoneNumber }; + }; + + const updateAltPhoneNumber = ( + field: FormContextValue, + allowUpdate: boolean, + phoneNumber: string, + ) => { + if (allowUpdate) { + changePhoneNumber(field, "alt_phone_number", phoneNumber); + return { alt_phone_number: phoneNumber }; + } + return {}; + }; + const handlePhoneChange = ( event: FieldChangeEvent, field: FormContextValue, @@ -417,33 +421,42 @@ const UserAddEditForm = (props: UserProps) => { let phoneNumberVal = ""; switch (event.name) { case "phone_number": - phoneNumberVal = event.value as string; - changePhoneNumber(field, "phone_number", phoneNumberVal); - formData = { ...formData, phone_number: phoneNumberVal }; - if (state.form.phone_number_is_whatsapp) { - changePhoneNumber(field, "alt_phone_number", phoneNumberVal); - formData = { ...formData, alt_phone_number: phoneNumberVal }; - } + formData = { + ...formData, + ...updatePhoneNumber(field, event.value as string), + ...updateAltPhoneNumber( + field, + state.form.phone_number_is_whatsapp ?? true, + event.value as string, + ), + }; break; case "alt_phone_number": phoneNumberVal = event.value as string; - if (!state.form.phone_number_is_whatsapp) { - changePhoneNumber(field, "alt_phone_number", phoneNumberVal); - formData = { ...formData, alt_phone_number: phoneNumberVal }; - } + formData = { + ...formData, + ...updateAltPhoneNumber( + field, + !(state.form.phone_number_is_whatsapp ?? true), + phoneNumberVal, + ), + }; break; case "phone_number_is_whatsapp": - phoneNumberVal = state.form.phone_number; formData = { ...formData, - alt_phone_number: phoneNumberVal, + ...updateAltPhoneNumber( + field, + event.value as boolean, + state.form.phone_number, + ), phone_number_is_whatsapp: event.value as boolean, }; - changePhoneNumber(field, "alt_phone_number", phoneNumberVal); - field("phone_number_is_whatsapp").onChange({ - name: field("phone_number_is_whatsapp").name, - value: event.value, - }); + changePhoneNumber( + field, + "phone_number_is_whatsapp", + event.value as boolean, + ); break; } dispatch({ @@ -493,6 +506,7 @@ const UserAddEditForm = (props: UserProps) => { if (facilityError) { errors.facilities = facilityError; } + let currentError = null; Object.keys(formData).forEach((field) => { switch (field) { case "user_type": @@ -501,28 +515,21 @@ const UserAddEditForm = (props: UserProps) => { } break; case "qualification": - if ( - (formData.user_type === "Doctor" || - formData.user_type === "Nurse") && - !formData[field] - ) { - errors[field] = t("field_required"); + currentError = ValidateQualification(formData, t); + if (currentError) { + errors[field] = currentError; } break; case "doctor_experience_commenced_on": - if (formData.user_type === "Doctor") { - if (!formData[field]) { - errors[field] = t("field_required"); - } else if (!validateNumber(formData[field] ?? "")) { - errors[field] = t("doctor_experience_number_error"); - } else if (Number(formData.doctor_experience_commenced_on) > 100) { - errors[field] = t("doctor_experience_less_than_100_years"); - } + currentError = ValidateDoctorExperienceCommencedOn(formData, t); + if (currentError) { + errors[field] = currentError; } break; case "doctor_medical_council_registration": - if (formData.user_type === "Doctor" && !formData[field]) { - errors[field] = t("field_required"); + currentError = ValidateDoctorMedicalCouncilRegistration(formData, t); + if (currentError) { + errors[field] = currentError; } break; case "phone_number": @@ -748,11 +755,12 @@ const UserAddEditForm = (props: UserProps) => { {t("facilities")}
)} @@ -771,6 +779,7 @@ const UserAddEditForm = (props: UserProps) => { }} optionValue={(o) => o.id} className="flex-1" + aria-label={t("user_type")} /> { handleFieldChange(e, field); }} className="flex-1" + aria-label={t("home_facility")} />
)} @@ -796,6 +806,7 @@ const UserAddEditForm = (props: UserProps) => { handleFieldChange(e, field); }} className="flex-1" + aria-label={t("qualification")} /> )} {state.form.user_type === "Doctor" && ( @@ -803,14 +814,13 @@ const UserAddEditForm = (props: UserProps) => { { handleFieldChange(e, field); }} className="flex-1" + aria-label={t("years_of_experience")} /> { handleFieldChange(e, field); }} className="flex-1" + aria-label={t("medical_council_registration")} />
)} @@ -838,6 +849,7 @@ const UserAddEditForm = (props: UserProps) => { handlePhoneChange(e, field); }} className="" + aria-label={t("phone_number")} /> { handlePhoneChange(e, field); }} className="flex-1" + aria-label={t("whatsapp_number")} />
@@ -878,6 +891,7 @@ const UserAddEditForm = (props: UserProps) => { onBlur={() => { setUsernameInputInFocus(false); }} + aria-label={t("username")} /> )} {!editUser && usernameInputInFocus && ( @@ -921,28 +935,22 @@ const UserAddEditForm = (props: UserProps) => { )}
-
+
{validateRule( usernameInput.length >= 4 && usernameInput.length <= 16, "Username should be 4-16 characters long", !state.form.username, )} -
-
{validateRule( /^[a-z0-9._-]*$/.test(usernameInput), "Username can only contain lowercase letters, numbers, and . _ -", !state.form.username, )} -
-
{validateRule( /^[a-z0-9].*[a-z0-9]$/i.test(usernameInput), "Username must start and end with a letter or number", !state.form.username, )} -
-
{validateRule( !/(?:[._-]{2,})/.test(usernameInput), "Username can't contain consecutive special characters . _ -", @@ -969,9 +977,13 @@ const UserAddEditForm = (props: UserProps) => { onChange={(e) => { handleFieldChange(e, field); }} + aria-label={t("password")} /> {passwordInputInFocus && state.form.password && ( -
+
{validateRule( state.form.password.length >= 8, "Password should be atleast 8 characters long", @@ -1010,14 +1022,18 @@ const UserAddEditForm = (props: UserProps) => { onChange={(e) => { handleFieldChange(e, field); }} + aria-label={t("confirm_password")} /> {confirmPasswordInputInFocus && state.form.c_password && - state.form.c_password.length > 0 && - validateRule( - state.form.c_password === state.form.password, - "Confirm password should match the entered password", - !state.form.c_password, + state.form.c_password.length > 0 && ( +
+ {validateRule( + state.form.c_password === state.form.password, + "Confirm password should match the entered password", + !state.form.c_password, + )} +
)}
@@ -1032,6 +1048,7 @@ const UserAddEditForm = (props: UserProps) => { onChange={(e) => { handleFieldChange(e, field); }} + aria-label={t("first_name")} /> { onChange={(e) => { handleFieldChange(e, field); }} + aria-label={t("last_name")} />
{ onChange={(e) => { handleFieldChange(e, field); }} + aria-label={t("email")} />
{ }} disableFuture className="flex-1" + aria-label={t("date_of_birth")} /> { handleFieldChange(e, field); }} className="flex-1" + aria-label={t("gender")} />
@@ -1092,6 +1113,7 @@ const UserAddEditForm = (props: UserProps) => { onChange={(e) => { handleFieldChange(e, field); }} + aria-label={t("average_weekly_working_hours")} /> { onChange={(e) => { handleFieldChange(e, field); }} + aria-label={t("video_conference_link")} />
@@ -1123,6 +1146,7 @@ const UserAddEditForm = (props: UserProps) => { handleFieldChange(e, field); if (e) setSelectedStateId(e.value); }} + aria-label={t("state")} /> )} @@ -1141,6 +1165,7 @@ const UserAddEditForm = (props: UserProps) => { handleFieldChange(e, field); if (e) setSelectedDistrictId(e.value); }} + aria-label={t("district")} /> )} @@ -1160,6 +1185,7 @@ const UserAddEditForm = (props: UserProps) => { onChange={(e) => { handleFieldChange(e, field); }} + aria-label={t("local_body")} /> ))} diff --git a/src/components/Users/UserFormValidations.tsx b/src/components/Users/UserFormValidations.tsx new file mode 100644 index 00000000000..043226bf9a6 --- /dev/null +++ b/src/components/Users/UserFormValidations.tsx @@ -0,0 +1,82 @@ +import { TFunction } from "i18next"; + +import { validateNumber } from "@/common/validation"; + +import { FacilityModel } from "../Facility/models"; +import { GenderType } from "./models"; + +export type UserForm = { + user_type?: string; + gender: GenderType; + password?: string; + c_password?: string; + facilities?: Array; + home_facility?: FacilityModel | null; + username?: string; + first_name: string; + last_name: string; + email: string; + phone_number: string; + alt_phone_number: string; + phone_number_is_whatsapp?: boolean; + date_of_birth: Date | null | string; + state?: number; + district?: number; + local_body?: number; + qualification?: string | undefined; + doctor_experience_commenced_on?: string; + doctor_medical_council_registration?: string; + video_connect_link?: string; + weekly_working_hours?: string | null; +}; + +export type EditForm = { + user_type: string; + qualification?: string | null; + doctor_experience_commenced_on?: string | null; + doctor_medical_council_registration?: string | null; +}; + +export const ValidateQualification = ( + formData: UserForm | EditForm, + translator: TFunction, +) => { + if ( + (formData.user_type === "Doctor" || formData.user_type === "Nurse") && + !formData["qualification"] + ) { + return translator("field_required"); + } + return null; +}; + +export const ValidateDoctorExperienceCommencedOn = ( + formData: UserForm | EditForm, + translator: TFunction, +) => { + if (formData.user_type === "Doctor") { + if (!formData["doctor_experience_commenced_on"]) { + return translator("field_required"); + } else if ( + !validateNumber(formData["doctor_experience_commenced_on"] ?? "") + ) { + return translator("doctor_experience_error"); + } else if (Number(formData.doctor_experience_commenced_on) > 100) { + return translator("doctor_experience_less_than_100_years"); + } + } + return null; +}; + +export const ValidateDoctorMedicalCouncilRegistration = ( + formData: UserForm | EditForm, + translator: TFunction, +) => { + if ( + formData.user_type === "Doctor" && + !formData["doctor_medical_council_registration"] + ) { + return translator("field_required"); + } + return null; +}; diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index 8a9df39f825..f7b6317c0f5 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -238,7 +238,13 @@ const UserListHeader = ({ ); }; -const UserListRow = ({ user }: { user: UserModel | UserAssignedModel }) => { +const UserListRow = ({ + user, + showDistrictColumn, +}: { + user: UserModel | UserAssignedModel; + showDistrictColumn: boolean; +}) => { const { t } = useTranslation(); return ( { {user.home_facility_object?.name || t("no_home_facility")} - - {"district_object" in user && user.district_object - ? user.district_object?.name - : "district" in user && user.district - ? user.district - : ""} - + {showDistrictColumn && ( + + {"district_object" in user && user.district_object + ? user.district_object?.name + : "district" in user && user.district + ? user.district + : ""} + + )} {CanUserAccess(user) && GetDetailsButton(user.username)} @@ -292,7 +300,13 @@ export const UserList = ({ - {users?.map((user) => )} + {users?.map((user) => ( + + ))}
diff --git a/src/components/Users/UserQualifications.tsx b/src/components/Users/UserQualifications.tsx index 42c56ea8bed..133a39597fe 100644 --- a/src/components/Users/UserQualifications.tsx +++ b/src/components/Users/UserQualifications.tsx @@ -5,8 +5,12 @@ import Loading from "@/components/Common/Loading"; import { FieldError } from "@/components/Form/FieldValidators"; import Form from "@/components/Form/Form"; import TextFormField from "@/components/Form/FormFields/TextFormField"; - -import { validateNumber } from "@/common/validation"; +import { + EditForm, + ValidateDoctorExperienceCommencedOn, + ValidateDoctorMedicalCouncilRegistration, + ValidateQualification, +} from "@/components/Users/UserFormValidations"; import * as Notification from "@/Utils/Notifications"; import dayjs from "@/Utils/dayjs"; @@ -17,12 +21,6 @@ import useQuery from "@/Utils/request/useQuery"; import { FormContextValue } from "../Form/FormContext"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; -type EditForm = { - user_type: string; - qualification?: string | null; - doctor_experience_commenced_on?: string | null; - doctor_medical_council_registration?: string | null; -}; type ErrorForm = { user_type?: string; qualification?: string | null; @@ -104,38 +102,27 @@ export default function UserQualifications({ username }: { username: string }) { }, }); - const validateForm = () => { + const validateForm = (formData: EditForm) => { const errors: Partial> = {}; - Object.keys(states.form).forEach((field) => { + Object.keys(formData).forEach((field) => { + let currentError = null; switch (field) { case "doctor_experience_commenced_on": - if (states.form.user_type === "Doctor") { - if (states.form.doctor_experience_commenced_on === undefined) { - errors[field] = t("field_required"); - } else if ( - !validateNumber(states.form.doctor_experience_commenced_on ?? "") - ) { - errors[field] = t("doctor_experience_number_error"); - } else { - const years = Number(states.form.doctor_experience_commenced_on); - if (isNaN(years) || years < 0 || years > 100) { - errors[field] = t("doctor_experience_error"); - } - } + currentError = ValidateDoctorExperienceCommencedOn(formData, t); + if (currentError) { + errors[field] = currentError; } break; case "qualification": - if ( - (states.form.user_type === "Doctor" || - states.form.user_type === "Nurse") && - !states.form[field] - ) { - errors[field] = t("field_required"); + currentError = ValidateQualification(formData, t); + if (currentError) { + errors[field] = currentError; } break; case "doctor_medical_council_registration": - if (states.form.user_type === "Doctor" && !states.form[field]) { - errors[field] = t("field_required"); + currentError = ValidateDoctorMedicalCouncilRegistration(formData, t); + if (currentError) { + errors[field] = currentError; } break; } @@ -192,15 +179,19 @@ export default function UserQualifications({ username }: { username: string }) { ? (formData.doctor_medical_council_registration ?? undefined) : undefined, }; - const { res } = await request(routes.partialUpdateUser, { + const { res, error } = await request(routes.partialUpdateUser, { pathParams: { username: userData.username }, body: data, }); if (res?.ok) { Notification.Success({ - msg: "Details updated successfully", + msg: t("user_details_update_success"), }); await refetchUserData(); + } else { + Notification.Error({ + msg: error?.message ?? t("user_details_update_error"), + }); } }; @@ -242,9 +233,6 @@ export default function UserQualifications({ username }: { username: string }) { {...field("doctor_experience_commenced_on")} required className="flex-1" - type="number" - min={0} - max={100} label={t("years_of_experience")} placeholder={t("years_of_experience_of_the_doctor")} onChange={(e) => { diff --git a/src/components/Users/UserResetPassword.tsx b/src/components/Users/UserResetPassword.tsx index 50d808e13e0..4952d15847e 100644 --- a/src/components/Users/UserResetPassword.tsx +++ b/src/components/Users/UserResetPassword.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { useTranslation } from "react-i18next"; import Form from "@/components/Form/Form"; @@ -22,6 +23,7 @@ export default function UserResetPassword({ userData: UserModel; }) { const { t } = useTranslation(); + const [isSubmitting, setisSubmitting] = useState(false); const initForm: PasswordForm = { username: userData.username, @@ -69,6 +71,7 @@ export default function UserResetPassword({ }; const handleSubmit = async (formData: PasswordForm) => { + setisSubmitting(true); const form: UpdatePasswordForm = { old_password: formData.old_password, username: userData.username, @@ -86,6 +89,7 @@ export default function UserResetPassword({ msg: error?.message ?? t("password_update_error"), }); } + setisSubmitting(false); }; return ( @@ -97,6 +101,7 @@ export default function UserResetPassword({ resetFormVals hideRestoreDraft noPadding + disabled={isSubmitting} > {(field) => (
@@ -107,6 +112,7 @@ export default function UserResetPassword({ className="col-span-6 sm:col-span-3" type="password" required + aria-label={t("current_password")} />
-
+
{validateRule( field("new_password_1").value?.length >= 8, t("password_length_validation"), @@ -150,9 +160,13 @@ export default function UserResetPassword({ className="peer col-span-6 sm:col-span-3" type="password" required + aria-label={t("new_password_confirmation")} /> {field("new_password_2").value?.length > 0 && ( -
+
{validateRule( field("new_password_1").value === field("new_password_2").value, From 461cf75702efa932b07d6fc6479a4cde5ed86231 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Wed, 13 Nov 2024 02:03:34 +0530 Subject: [PATCH 26/55] Suggestions and error fixes --- public/locale/en.json | 4 +- src/components/Users/LinkedFacilities.tsx | 59 ++++++++++++-------- src/components/Users/LinkedSkills.tsx | 2 +- src/components/Users/UserAddEditForm.tsx | 5 +- src/components/Users/UserFormValidations.tsx | 6 +- src/components/Users/UserQualifications.tsx | 33 ++++++----- 6 files changed, 67 insertions(+), 42 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index a9014516f24..e47008b263d 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -595,7 +595,6 @@ "district_program_management_supporting_unit": "District Program Management Supporting Unit", "dob_format": "Please enter date in DD/MM/YYYY format", "doctor_experience_error": "Please enter a valid number between 0 and 100.", - "doctor_experience_less_than_100_years": "Doctor experience should be less than 100 years.", "doctor_s_medical_council_registration": "Doctor's Medical Council Registration", "doctors_name": "Doctor's Name", "domestic_healthcare_support": "Domestic healthcare support", @@ -765,6 +764,7 @@ "hide": "Hide", "home_facility": "Home Facility", "home_facility_cleared_success": "Home Facility cleared successfully", + "home_facility_updated_error": "Error while updating Home Facility", "home_facility_updated_success": "Home Facility updated successfully", "hubs": "Hub Facilities", "i_declare": "I hereby declare that:", @@ -1305,6 +1305,8 @@ "unlink_facility": "Unlink Facility", "unlink_facility_access": "The user will lose access to the facility", "unlink_facility_confirm": "Are you sure you want to unlink the facility", + "unlink_facility_error": "Error while unlinking facility. Try again later.", + "unlink_facility_success": "Facility unlinked successfully", "unlink_home_facility_error": "Error while unlinking home facility. Try again later.", "unlink_home_facility_success": "Home Facility cleared successfully", "unlink_skill": "Unlink Skill", diff --git a/src/components/Users/LinkedFacilities.tsx b/src/components/Users/LinkedFacilities.tsx index 61676818017..7e3fa2365b1 100644 --- a/src/components/Users/LinkedFacilities.tsx +++ b/src/components/Users/LinkedFacilities.tsx @@ -15,6 +15,9 @@ import { FacilityModel } from "@/components/Facility/models"; import ConfirmFacilityModal from "@/components/Users/ConfirmFacilityModal"; import { UserModel } from "@/components/Users/models"; +import { useIsAuthorized } from "@/hooks/useIsAuthorized"; + +import AuthorizeFor from "@/Utils/AuthorizeFor"; import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; @@ -47,6 +50,10 @@ export default function LinkedFacilities({ const [modalProps, setModalProps] = useState(initModalProps); const { t } = useTranslation(); + const authorizeForHomeFacility = useIsAuthorized( + AuthorizeFor(["DistrictAdmin", "StateAdmin"]), + ); + const { refetch: refetchUserFacilities } = useQuery(routes.userListFacility, { pathParams: { username: userData.username }, query: { limit: 36 }, @@ -111,7 +118,7 @@ export default function LinkedFacilities({ }); if (!res?.ok) { Notification.Error({ - msg: t("update_home_facility_error"), + msg: t("home_facility_updated_error"), }); } else { setHomeFacility(selectedFacility); @@ -149,11 +156,11 @@ export default function LinkedFacilities({ }); if (!res?.ok) { Notification.Error({ - msg: t("unlink_home_facility_error"), + msg: t("unlink_facility_error"), }); } else { Notification.Success({ - msg: t("unlink_home_facility_success"), + msg: t("unlink_facility_success"), }); } await refetchUserFacilities(); @@ -197,16 +204,20 @@ export default function LinkedFacilities({
- - handleOnClick( - homeFacility ? "replace_home_facility" : "set_home_facility", - facility, - ) - } - > - {t("set_home_facility")} - + {authorizeForHomeFacility && ( + + handleOnClick( + homeFacility + ? "replace_home_facility" + : "set_home_facility", + facility, + ) + } + > + {t("set_home_facility")} + + )} handleOnClick("unlink_facility", facility)} > @@ -226,15 +237,19 @@ export default function LinkedFacilities({ >
{homeFacility.name}
-
- -
+ {authorizeForHomeFacility && ( +
+ +
+ )}
); diff --git a/src/components/Users/LinkedSkills.tsx b/src/components/Users/LinkedSkills.tsx index 4afc230e633..2a034a47db1 100644 --- a/src/components/Users/LinkedSkills.tsx +++ b/src/components/Users/LinkedSkills.tsx @@ -146,7 +146,7 @@ export default function LinkedSkills({ username }: { username: string }) { tooltip={ !authorizeForAddSkill ? t("contact_your_admin_to_add_skills") - : t("add_skill") + : undefined } > {t("add_skill")} diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index 0d3c64347c6..1862c183a4d 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -372,14 +372,15 @@ const UserAddEditForm = (props: UserProps) => { event: FieldChangeEvent, field?: FormContextValue, ) => { + const fieldName = event.name as keyof UserForm; dispatch({ type: "set_form", form: { ...state.form, - [event.name]: event.value, + [fieldName]: event.value, }, }); - if (field) field(event.name as keyof UserForm).onChange(event); + field?.(fieldName).onChange(event); }; const changePhoneNumber = ( diff --git a/src/components/Users/UserFormValidations.tsx b/src/components/Users/UserFormValidations.tsx index 043226bf9a6..fb7b48c9254 100644 --- a/src/components/Users/UserFormValidations.tsx +++ b/src/components/Users/UserFormValidations.tsx @@ -58,11 +58,11 @@ export const ValidateDoctorExperienceCommencedOn = ( if (!formData["doctor_experience_commenced_on"]) { return translator("field_required"); } else if ( - !validateNumber(formData["doctor_experience_commenced_on"] ?? "") + !validateNumber(formData["doctor_experience_commenced_on"] ?? "") || + Number(formData["doctor_experience_commenced_on"]) < 0 || + Number(formData["doctor_experience_commenced_on"]) > 100 ) { return translator("doctor_experience_error"); - } else if (Number(formData.doctor_experience_commenced_on) > 100) { - return translator("doctor_experience_less_than_100_years"); } } return null; diff --git a/src/components/Users/UserQualifications.tsx b/src/components/Users/UserQualifications.tsx index 133a39597fe..7628204ce1a 100644 --- a/src/components/Users/UserQualifications.tsx +++ b/src/components/Users/UserQualifications.tsx @@ -1,4 +1,4 @@ -import { useReducer, useRef } from "react"; +import { useReducer, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import Loading from "@/components/Common/Loading"; @@ -23,9 +23,9 @@ import { FieldChangeEvent } from "../Form/FormFields/Utils"; type ErrorForm = { user_type?: string; - qualification?: string | null; - doctor_experience_commenced_on?: string | null; - doctor_medical_council_registration?: string | null; + qualification?: string; + doctor_experience_commenced_on?: string; + doctor_medical_council_registration?: string; }; type State = { form: EditForm; @@ -72,6 +72,7 @@ const editFormReducer = (state: State, action: Action) => { export default function UserQualifications({ username }: { username: string }) { const { t } = useTranslation(); const [states, dispatch] = useReducer(editFormReducer, initialState); + const [isSubmitting, setIsSubmitting] = useState(false); const formVals = useRef(initForm); const { @@ -84,15 +85,18 @@ export default function UserQualifications({ username }: { username: string }) { }, onResponse: (result) => { if (!result || !result.res || !result.data) return; + const userData = result.data; const formData: EditForm = { - user_type: result.data.user_type, - qualification: result.data.qualification, - doctor_experience_commenced_on: dayjs() - .diff(dayjs(result.data.doctor_experience_commenced_on), "years") - .toString(), + user_type: userData.user_type, + qualification: userData.qualification, + doctor_experience_commenced_on: userData.doctor_experience_commenced_on + ? dayjs() + .diff(dayjs(userData.doctor_experience_commenced_on), "years") + .toString() + : null, doctor_medical_council_registration: - result.data.doctor_medical_council_registration, + userData.doctor_medical_council_registration, }; dispatch({ type: "set_form", @@ -141,14 +145,15 @@ export default function UserQualifications({ username }: { username: string }) { event: FieldChangeEvent, field?: FormContextValue, ) => { + const fieldName = event.name as keyof EditForm; dispatch({ type: "set_form", form: { ...states.form, - [event.name]: event.value, + [fieldName]: event.value, }, }); - if (field) field(event.name as keyof EditForm).onChange(event); + field?.(fieldName).onChange(event); }; if (isLoading || !userData) { @@ -158,6 +163,7 @@ export default function UserQualifications({ username }: { username: string }) { if (!["Doctor", "Nurse"].includes(states.form.user_type ?? "")) return; const handleSubmit = async (formData: EditForm) => { + setIsSubmitting(true); const data = { qualification: formData.user_type === "Doctor" || formData.user_type === "Nurse" @@ -193,6 +199,7 @@ export default function UserQualifications({ username }: { username: string }) { msg: error?.message ?? t("user_details_update_error"), }); } + setIsSubmitting(false); }; return ( @@ -201,7 +208,7 @@ export default function UserQualifications({ username }: { username: string }) { {!isLoading && (
- disabled={isLoading} + disabled={isSubmitting || isLoading} defaults={userData ? states.form : initForm} validate={validateForm} onCancel={handleCancel} From 4f736a8f1fd7c1e58cd8c7b033aac96132eefe1d Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Wed, 13 Nov 2024 17:41:05 +0530 Subject: [PATCH 27/55] UsersManage tests - Modified UsersManage tests - Minor additions to components (Ids, names) to facilitate testing --- cypress/e2e/users_spec/UsersManage.cy.ts | 81 +++++++++++++------- cypress/pageobject/Users/ManageUserPage.ts | 88 +++++++++++++++++----- public/locale/en.json | 1 + src/components/Common/ConfirmDialog.tsx | 9 ++- src/components/Common/FacilitySelect.tsx | 3 + src/components/Users/LinkedFacilities.tsx | 15 +++- src/components/Users/LinkedSkills.tsx | 6 +- src/components/Users/UnlinkSkillDialog.tsx | 3 +- src/components/Users/UserAddEditForm.tsx | 1 + src/components/Users/UserBanner.tsx | 7 +- src/components/Users/UserHome.tsx | 2 +- src/components/Users/UserInformation.tsx | 4 +- src/components/Users/UserListAndCard.tsx | 7 +- 13 files changed, 168 insertions(+), 59 deletions(-) diff --git a/cypress/e2e/users_spec/UsersManage.cy.ts b/cypress/e2e/users_spec/UsersManage.cy.ts index 8899d951175..e0661b923c3 100644 --- a/cypress/e2e/users_spec/UsersManage.cy.ts +++ b/cypress/e2e/users_spec/UsersManage.cy.ts @@ -19,7 +19,7 @@ describe("Manage User", () => { const facilitytolinkusername = "Dummy Shifting Center"; const facilitytolinkskill = "Dummy Facility 40"; const workinghour = "23"; - const linkedskill = "General Medicine"; + const linkedskill = "Immunologist"; before(() => { loginPage.loginAsDistrictAdmin(); @@ -36,21 +36,33 @@ describe("Manage User", () => { // select the district user and select one skill link and verify its profile reflection userPage.typeInSearchInput(usernameforworkinghour); userPage.checkUsernameText(usernameforworkinghour); - manageUserPage.clicklinkedskillbutton(); + manageUserPage.clickMoreDetailsButton(); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickLinkedSkillTab(); + cy.wait(500); + manageUserPage.verifyLinkedSkillsTabPage(); manageUserPage.selectSkillFromDropdown(linkedskill); manageUserPage.clickAddSkillButton(); - manageUserPage.clickCloseSlideOver(); - cy.wait(5000); - manageUserPage.clicklinkedskillbutton(); + cy.wait(500); manageUserPage.assertSkillInAddedUserSkills(linkedskill); - manageUserPage.clickCloseSlideOver(); - cy.wait(5000); + cy.wait(500); manageUserPage.navigateToProfile(); userCreationPage.verifyElementContainsText( "username-profile-details", usernameforworkinghour, ); manageUserPage.assertSkillInAlreadyLinkedSkills(linkedskill); + // unlink the skill + manageUserPage.navigateToManageUser(); + userPage.typeInSearchInput(usernameforworkinghour); + userPage.checkUsernameText(usernameforworkinghour); + manageUserPage.clickMoreDetailsButton(); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickLinkedSkillTab(); + manageUserPage.assertSkillInAddedUserSkills(linkedskill); + manageUserPage.clickUnlinkSkill(); + manageUserPage.verifyUnlinkSkillModal(); + manageUserPage.clickConfirmUnlinkSkill(); }); it("linking skills for a doctor users and verify its reflection in doctor connect", () => { @@ -60,18 +72,20 @@ describe("Manage User", () => { userPage.typeInLastName(lastNameUserSkill); userPage.applyFilter(); userPage.checkUsernameText(usernameToLinkSkill); - manageUserPage.clicklinkedskillbutton(); + manageUserPage.clickMoreDetailsButton(); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickLinkedSkillTab(); + manageUserPage.verifyDoctorQualification(); + manageUserPage.verifyLinkedSkillsTabPage(); manageUserPage.selectSkillFromDropdown(linkedskill); manageUserPage.clickAddSkillButton(); - manageUserPage.clickCloseSlideOver(); - cy.wait(5000); // temporary hack to fix the failure - manageUserPage.clicklinkedskillbutton(); + cy.wait(500); // temporary hack to fix the failure manageUserPage.assertSkillInAddedUserSkills(linkedskill); manageUserPage.clickUnlinkSkill(); - manageUserPage.clickSubmit(); + manageUserPage.verifyUnlinkSkillModal(); + manageUserPage.clickConfirmUnlinkSkill(); manageUserPage.selectSkillFromDropdown(linkedskill); manageUserPage.clickAddSkillButton(); - manageUserPage.clickCloseSlideOver(); // verifying the doctor connect manageUserPage.navigateToFacility(); manageUserPage.typeFacilitySearch(facilitytolinkskill); @@ -85,13 +99,17 @@ describe("Manage User", () => { // verify mandatory field error and select working hour for a user userPage.typeInSearchInput(usernameforworkinghour); userPage.checkUsernameText(usernameforworkinghour); - manageUserPage.clicksetaveragehourbutton(); + manageUserPage.clickMoreDetailsButton(); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.verifyProfileTabPage(); manageUserPage.clearweeklyhourfield(); manageUserPage.clickSubmit(); - manageUserPage.verifyErrorText("Value should be between 0 and 168"); - // verify the data is reflected in user card and profile page + manageUserPage.verifyErrorText( + "Average weekly working hours must be a number between 0 and 168", + ); manageUserPage.typeInWeeklyWorkingHours(workinghour); manageUserPage.clickSubmit(); + // verify the data is reflected in user card and profile page manageUserPage.verifyWorkingHours(workinghour); manageUserPage.navigateToProfile(); manageUserPage.verifyProfileWorkingHours(workinghour); @@ -101,42 +119,51 @@ describe("Manage User", () => { // verify the user doesn't have any home facility userPage.typeInSearchInput(usernameToLinkFacilitydoc1); userPage.checkUsernameText(usernameToLinkFacilitydoc1); - manageUserPage.assertHomeFacility("No Home Facility"); + manageUserPage.assertHomeFacility("No home facility"); + manageUserPage.clickMoreDetailsButton(); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickLinkedFacilitiesTab(); + manageUserPage.verifyLinkedFacilitiesTabPage(); // Link a new facility and ensure it is under linked facility - doctor username (1) - manageUserPage.clickFacilitiesTab(); manageUserPage.selectFacilityFromDropdown(facilitytolinkusername); manageUserPage.clickLinkFacility(); manageUserPage.assertLinkedFacility(facilitytolinkusername); // Verify in the already linked facility are not present in droplist manageUserPage.assertFacilityNotInDropdown(facilitytolinkusername); - manageUserPage.clickCloseSlideOver(); + // Go back to manage user page + manageUserPage.navigateToManageUser(); // Link a new facility and ensure it is under home facility - doctor username (2) - userPage.clearSearchInput(); userPage.typeInSearchInput(usernameToLinkFacilitydoc2); userPage.checkUsernameText(usernameToLinkFacilitydoc2); - manageUserPage.clickFacilitiesTab(); + manageUserPage.clickMoreDetailsButton(); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickLinkedFacilitiesTab(); + manageUserPage.verifyLinkedFacilitiesTabPage(); manageUserPage.selectFacilityFromDropdown(facilitytolinkusername); manageUserPage.clickLinkFacility(); - manageUserPage.clickHomeFacilityIcon(); + manageUserPage.clickLinkedFacilitySettings(); + manageUserPage.clickSetHomeFacility(); manageUserPage.assertnotLinkedFacility(facilitytolinkusername); manageUserPage.assertHomeFacilitylink(facilitytolinkusername); - manageUserPage.clickCloseSlideOver(); // verify the home facility doctor id have reflection in user card - userPage.clearSearchInput(); + manageUserPage.navigateToManageUser(); userPage.typeInSearchInput(usernameToLinkFacilitydoc2); userPage.checkUsernameText(usernameToLinkFacilitydoc2); manageUserPage.assertHomeFacility(facilitytolinkusername); // Link a new facility and unlink the facility from the doctor username (3) - userPage.clearSearchInput(); + manageUserPage.navigateToManageUser(); userPage.typeInSearchInput(usernameToLinkFacilitydoc3); userPage.checkUsernameText(usernameToLinkFacilitydoc3); - manageUserPage.clickFacilitiesTab(); + manageUserPage.clickMoreDetailsButton(); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickLinkedFacilitiesTab(); + manageUserPage.verifyLinkedFacilitiesTabPage(); manageUserPage.selectFacilityFromDropdown(facilitytolinkusername); manageUserPage.clickLinkFacility(); + manageUserPage.clickLinkedFacilitySettings(); manageUserPage.clickUnlinkFacilityButton(); manageUserPage.clickSubmit(); manageUserPage.linkedfacilitylistnotvisible(); - manageUserPage.clickCloseSlideOver(); // Go to particular facility doctor connect and all user-id are reflected based on there access // Path will be facility page to patient page then doctor connect button manageUserPage.navigateToFacility(); diff --git a/cypress/pageobject/Users/ManageUserPage.ts b/cypress/pageobject/Users/ManageUserPage.ts index a3a6e72fbc3..50873119c1b 100644 --- a/cypress/pageobject/Users/ManageUserPage.ts +++ b/cypress/pageobject/Users/ManageUserPage.ts @@ -1,10 +1,6 @@ export class ManageUserPage { assertHomeFacility(expectedText: string) { - cy.get("#home_facility").should("contain.text", expectedText); - } - - clickFacilitiesTab() { - cy.get("#facilities").click(); + cy.get("#home-facility").should("contain.text", expectedText); } selectFacilityFromDropdown(facilityName: string) { @@ -15,10 +11,6 @@ export class ManageUserPage { cy.typeAndSelectOption("input[name='skill']", skill); } - clickLinkFacility() { - cy.get("#link-facility").click(); - } - assertLinkedFacility(facilityName: string) { cy.get("#linked-facility-list").should("contain.text", facilityName); } @@ -40,24 +32,28 @@ export class ManageUserPage { cy.get("[role='option']").should("not.exist"); } - clickCloseSlideOver() { - cy.get("#close-slide-over").click({ force: true }); + clickLinkedFacilitySettings() { + cy.get("#linked-facility-settings").click(); } - clickHomeFacilityIcon() { - cy.get("#home-facility-icon").click(); + clickSetHomeFacility() { + cy.get("#set-home-facility").click(); } clickUnlinkFacilityButton() { - cy.get("#unlink-facility-button").click(); + cy.get("#unlink-facility").click(); } - clickSubmit() { - cy.get("#submit").click(); + clickConfirmUnlinkSkill() { + cy.get("button[name='confirm-unlink-skill']").click(); } - clicksetaveragehourbutton() { - cy.get("#avg-workinghour").click(); + clickLinkFacility() { + cy.get("#link-facility").click(); + } + + clickSubmit() { + cy.get("#submit").click(); } clearweeklyhourfield() { @@ -80,7 +76,11 @@ export class ManageUserPage { } verifyWorkingHours(expectedHours: string) { - cy.get("#working-hours").should("contain", `${expectedHours} hours`); + cy.get("input[name='weekly_working_hours']").should("be.visible"); + cy.get("input[name='weekly_working_hours']").should( + "have.value", + expectedHours, + ); } verifyProfileWorkingHours(expectedHours: string) { @@ -94,6 +94,10 @@ export class ManageUserPage { cy.visit("/facility"); } + navigateToManageUser() { + cy.visit("/users"); + } + typeFacilitySearch(facilityName: string) { cy.get("#search").click().clear(); cy.get("#search").click().type(facilityName); @@ -116,10 +120,45 @@ export class ManageUserPage { cy.get("#facility-patients").click(); } - clicklinkedskillbutton() { + clickLinkedSkillTab() { cy.get("#skills").click(); } + clickLinkedFacilitiesTab() { + cy.get("#facilities").click(); + } + + clickMoreDetailsButton() { + cy.get("#more-details").click(); + } + + verifyMoreDetailsPage() { + cy.get("#users-name").should("be.visible"); + cy.get("#username").should("be.visible"); + cy.get("#role").should("be.visible"); + cy.get("#usermanagement_tab_nav").should("be.visible"); + cy.get("#profile").should("be.visible"); + cy.get("#facilities").should("be.visible"); + cy.get("#skills").should("be.visible"); + } + + verifyProfileTabPage() { + cy.get("#user-edit-form").should("be.visible"); + } + + verifyDoctorQualification() { + cy.get("input[name='qualification']").should("be.visible"); + } + + verifyLinkedSkillsTabPage() { + cy.get("#select-skill").scrollIntoView(); + cy.get("#select-skill").should("be.visible"); + } + + verifyLinkedFacilitiesTabPage() { + cy.get("#select-facility").should("be.visible"); + } + clickAddSkillButton() { cy.intercept("GET", "**/api/v1/skill/**").as("getSkills"); cy.get("#add-skill-button").click(); @@ -146,10 +185,19 @@ export class ManageUserPage { cy.get("#unlink-skill").click(); } + verifyUnlinkSkillModal() { + cy.get("#unlink-skill-modal-description").should("be.visible"); + cy.get("button[name='confirm-unlink-skill']").should("be.visible"); + } + assertSkillInAddedUserSkills(skillName: string) { cy.get("#added-user-skills").should("contain", skillName); } + assertSkillNotInAddedUserSkills(skillName: string) { + cy.get("#added-user-skills").should("not.contain", skillName); + } + assertDoctorConnectVisibility(realName: string) { cy.get('*[id="doctor-connect-home-doctor"]').should( "contain.text", diff --git a/public/locale/en.json b/public/locale/en.json index e47008b263d..6d1658078eb 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -447,6 +447,7 @@ "clear_home_facility_confirm": "Are you sure you want to clear the home facility", "clear_home_facility_error": "Error while clearing home facility. Try again later.", "clear_selection": "Clear selection", + "clear_skill": "Clear Skill", "close": "Close", "close_scanner": "Close Scanner", "collapse_sidebar": "Collapse Sidebar", diff --git a/src/components/Common/ConfirmDialog.tsx b/src/components/Common/ConfirmDialog.tsx index 0183fc1d651..5fff5d9f8fb 100644 --- a/src/components/Common/ConfirmDialog.tsx +++ b/src/components/Common/ConfirmDialog.tsx @@ -13,6 +13,7 @@ type ConfirmDialogProps = { onConfirm: () => void; children?: React.ReactNode; cancelLabel?: string; + name?: string; }; const ConfirmDialog = ({ @@ -22,6 +23,7 @@ const ConfirmDialog = ({ onConfirm, cancelLabel, children, + name, ...props }: ConfirmDialogProps) => { return ( @@ -29,7 +31,12 @@ const ConfirmDialog = ({ {children}
- + {action}
diff --git a/src/components/Common/FacilitySelect.tsx b/src/components/Common/FacilitySelect.tsx index e4397cbcb9c..4c0a6629bec 100644 --- a/src/components/Common/FacilitySelect.tsx +++ b/src/components/Common/FacilitySelect.tsx @@ -27,6 +27,7 @@ interface FacilitySelectProps { allowNone?: boolean; placeholder?: string; filter?: (facilities: FacilityModel) => boolean; + id?: string; } export const FacilitySelect = (props: FacilitySelectProps) => { @@ -50,6 +51,7 @@ export const FacilitySelect = (props: FacilitySelectProps) => { errors = "", placeholder, filter, + id, } = props; const facilitySearch = useCallback( @@ -88,6 +90,7 @@ export const FacilitySelect = (props: FacilitySelectProps) => { return (
{facility.name}
- +
@@ -206,6 +206,7 @@ export default function LinkedFacilities({ {authorizeForHomeFacility && ( handleOnClick( homeFacility @@ -219,6 +220,7 @@ export default function LinkedFacilities({ )} handleOnClick("unlink_facility", facility)} > {t("unlink_this_facility")} @@ -236,10 +238,13 @@ export default function LinkedFacilities({ key={`facility_${homeFacility.id}`} >
-
{homeFacility.name}
+
+ {homeFacility.name} +
{authorizeForHomeFacility && (
@@ -156,7 +157,10 @@ export default function LinkedSkills({ username }: { username: string }) {

{t("linked_skills")}

-
+
{skills?.results.map((skill: SkillModel) => { return renderSkillButtons(skill); })} diff --git a/src/components/Users/UnlinkSkillDialog.tsx b/src/components/Users/UnlinkSkillDialog.tsx index 14e15b6d4b3..6ddfcc6e569 100644 --- a/src/components/Users/UnlinkSkillDialog.tsx +++ b/src/components/Users/UnlinkSkillDialog.tsx @@ -22,6 +22,7 @@ export default function UnlinkSkillDialog(props: Props) { return ( - + {t("unlink_skill_confirm")} {props.skillName}{" "} {t("from_user")} {props.userName}?{" "} {t("unlink_skill_access")} diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index 1862c183a4d..dd432d7ac46 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -1106,6 +1106,7 @@ const UserAddEditForm = (props: UserProps) => {
-

+

{formatName(userData)}

@@ -34,7 +34,10 @@ export default function UserBanner({ userData }: { userData: UserModel }) {
- + {userData.username}
diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index 9c2b4616aaf..fd27016c4cb 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -116,7 +116,7 @@ export default function UserHome(props: UserHomeProps) { )} href={`/users/${username}/${p.toLocaleLowerCase()}`} > -
+
{t(`USERMANAGEMENT_TAB__${tabName}`)}
diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx index 9e64db79a1b..d5efa257f55 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserInformation.tsx @@ -110,7 +110,9 @@ export default function UserInformation({ username }: { username: string }) {
- +
+ +
); diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index f7b6317c0f5..3f0ae6efb05 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -61,6 +61,7 @@ const GetDetailsButton = (username: string) => { return (
- {user.username} + + {user.username} +
{showDetailsButton && @@ -171,7 +174,7 @@ const UserCard = ({ user }: { user: UserModel | UserAssignedModel }) => {
{t("home_facility")}
-
+
{user.home_facility_object?.name || t("no_home_facility")}
From 25cfc733255b7ee1a4fa5867c6ce44ab040ac8f5 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Wed, 13 Nov 2024 20:08:39 +0530 Subject: [PATCH 28/55] Fixed Usercreation tests --- cypress/e2e/users_spec/UsersCreation.cy.ts | 8 +----- cypress/pageobject/Users/UserSearch.ts | 2 +- public/locale/en.json | 2 +- src/components/Users/UserAddEditForm.tsx | 4 +-- src/components/Users/UserFormValidations.tsx | 3 +- src/components/Users/UserListAndCard.tsx | 30 ++++++++++++++------ 6 files changed, 27 insertions(+), 22 deletions(-) diff --git a/cypress/e2e/users_spec/UsersCreation.cy.ts b/cypress/e2e/users_spec/UsersCreation.cy.ts index f495a136d97..5502bdbbd9b 100644 --- a/cypress/e2e/users_spec/UsersCreation.cy.ts +++ b/cypress/e2e/users_spec/UsersCreation.cy.ts @@ -168,15 +168,9 @@ describe("User Creation", () => { userCreationPage.verifyElementContainsText("role", "Doctor"); userCreationPage.verifyElementContainsText("district", "Ernakulam"); userCreationPage.verifyElementContainsText( - "home_facility", + "home-facility", "Dummy Shifting Center", ); - userCreationPage.verifyElementContainsText("qualification", "MBBS"); - userCreationPage.verifyElementContainsText("doctor-experience", "2"); - userCreationPage.verifyElementContainsText( - "medical-council-registration", - "123456789", - ); }); it("create new user form throwing mandatory field error", () => { diff --git a/cypress/pageobject/Users/UserSearch.ts b/cypress/pageobject/Users/UserSearch.ts index 1292395fce3..880a3d55c30 100644 --- a/cypress/pageobject/Users/UserSearch.ts +++ b/cypress/pageobject/Users/UserSearch.ts @@ -102,7 +102,7 @@ export class UserPage { } verifyMultipleBadgesWithSameId(alreadylinkedusersviews: string[]) { - cy.get("#user-view-name").then(($elements) => { + cy.get("#name").then(($elements) => { const userViews = $elements .map((_, el) => Cypress.$(el).text().trim()) .get(); diff --git a/public/locale/en.json b/public/locale/en.json index 6d1658078eb..1f957ce0f16 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -780,7 +780,7 @@ "insurer_name_required": "Insurer Name is required", "international_mobile": "International Mobile", "invalid_asset_id_msg": "Oops! The asset ID you entered does not appear to be valid.", - "invalid_email": "Please Enter a Valid Email Address", + "invalid_email": "Please enter a valid email address", "invalid_ip_address": "Invalid IP Address", "invalid_link_msg": "It appears that the password reset link you have used is either invalid or expired. Please request a new password reset link.", "invalid_password": "Password doesn't meet the requirements", diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index dd432d7ac46..b9d11c94b91 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -67,7 +67,7 @@ interface StateObj { const initForm: UserForm = { user_type: "", - gender: "Female", + gender: "", password: "", c_password: "", facilities: [], @@ -756,7 +756,7 @@ const UserAddEditForm = (props: UserProps) => { {t("facilities")} ; diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index 3f0ae6efb05..0a31d40eb89 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -81,7 +81,9 @@ const getNameAndStatusCard = (
-

{formatName(user)}

+

+ {formatName(user)} +

{
{t("role")}
-
{user.user_type}
+
+ {user.user_type} +
{t("home_facility")}
@@ -181,7 +185,7 @@ const UserCard = ({ user }: { user: UserModel | UserAssignedModel }) => { {"district_object" in user && user.district_object && (
{t("district")}
-
+
{user.district_object.name}
@@ -189,7 +193,9 @@ const UserCard = ({ user }: { user: UserModel | UserAssignedModel }) => { {"district" in user && user.district && (
{t("district")}
-
{user.district}
+
+ {user.district} +
)}
@@ -263,20 +269,26 @@ const UserListRow = ({ className="h-10 w-10 text-lg" />
-

{formatName(user)}

- @{user.username} +

+ {formatName(user)} +

+ + @{user.username} +
- {user.user_type} - + + {user.user_type} + + {user.home_facility_object?.name || t("no_home_facility")} {showDistrictColumn && ( - + {"district_object" in user && user.district_object ? user.district_object?.name : "district" in user && user.district From 8d8d7cfc506716890fa5ae97d6455934aec38a31 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Thu, 14 Nov 2024 02:43:03 +0530 Subject: [PATCH 29/55] UsersHomepage tests, responsive design changes - And tab functionality change (bringing in state change upwards to ensure tab selections stays through search behavior) --- cypress/e2e/users_spec/UsersHomepage.cy.ts | 12 ++++++++ cypress/pageobject/Users/UserSearch.ts | 8 +++++ src/Utils/permissions.ts | 34 ++++++++++++++------ src/components/Common/Tabs.tsx | 3 +- src/components/Facility/FacilityUsers.tsx | 4 +++ src/components/Users/ManageUsers.tsx | 3 ++ src/components/Users/UserListAndCard.tsx | 36 ++++++++++++++-------- src/components/Users/UserSummary.tsx | 5 +-- 8 files changed, 81 insertions(+), 24 deletions(-) diff --git a/cypress/e2e/users_spec/UsersHomepage.cy.ts b/cypress/e2e/users_spec/UsersHomepage.cy.ts index 0dd03c74304..094ad00378b 100644 --- a/cypress/e2e/users_spec/UsersHomepage.cy.ts +++ b/cypress/e2e/users_spec/UsersHomepage.cy.ts @@ -87,6 +87,18 @@ describe("User Homepage", () => { userPage.verifyCurrentPageNumber(1); }); + it("Switch to list view, search by username and verify the results", () => { + userPage.switchToListView(); + userPage.verifyListView(); + userPage.checkSearchInputVisibility(); + userPage.typeInSearchInput(doctorUserName); + userPage.checkUrlForUsername(doctorUserName); + userPage.checkUsernameText(doctorUserName); + userPage.checkUsernameBadgeVisibility(true); + userPage.clearSearchInput(); + userPage.verifyListView(); + }); + afterEach(() => { cy.saveLocalStorage(); }); diff --git a/cypress/pageobject/Users/UserSearch.ts b/cypress/pageobject/Users/UserSearch.ts index 880a3d55c30..124391fc29f 100644 --- a/cypress/pageobject/Users/UserSearch.ts +++ b/cypress/pageobject/Users/UserSearch.ts @@ -122,4 +122,12 @@ export class UserPage { }); }); } + + switchToListView() { + cy.get("#user-list-view").click(); + } + + verifyListView() { + cy.get("#user-list-view").should("have.class", "text-white"); + } } diff --git a/src/Utils/permissions.ts b/src/Utils/permissions.ts index 35f12715eff..e813c6e420c 100644 --- a/src/Utils/permissions.ts +++ b/src/Utils/permissions.ts @@ -2,6 +2,18 @@ import { UserModel } from "@/components/Users/models"; import { USER_TYPES, UserRole } from "@/common/constants"; +const checkIfStateOrDistrictAdminInSameDistrict = ( + authUser: UserModel, + targetUser: UserModel, +) => { + return ( + (authUser.user_type === "StateAdmin" && + targetUser.state_object?.id === authUser.state) || + (authUser.user_type === "DistrictAdmin" && + targetUser.district_object?.id === authUser.district) + ); +}; + export const showUserDelete = (authUser: UserModel, targetUser: UserModel) => { // Auth user should be higher in hierarchy than target user if ( @@ -10,19 +22,23 @@ export const showUserDelete = (authUser: UserModel, targetUser: UserModel) => { ) return false; - if ( - authUser.user_type === "StateAdmin" && - targetUser.state_object?.id === authUser.state - ) - return true; + return checkIfStateOrDistrictAdminInSameDistrict(authUser, targetUser); +}; +export const showUserPasswordReset = ( + authUser: UserModel, + targetUser: UserModel, +) => { + if (authUser.username === targetUser.username) return true; + + // Auth user should be higher in hierarchy than target user if ( - authUser.user_type === "DistrictAdmin" && - targetUser.district_object?.id === authUser.district + USER_TYPES.indexOf(authUser.user_type) <= + USER_TYPES.indexOf(targetUser.user_type) ) - return true; + return false; - return false; + return checkIfStateOrDistrictAdminInSameDistrict(authUser, targetUser); }; export const CameraFeedPermittedUserTypes: UserRole[] = [ diff --git a/src/components/Common/Tabs.tsx b/src/components/Common/Tabs.tsx index 9a549cfabcf..9c375db618e 100644 --- a/src/components/Common/Tabs.tsx +++ b/src/components/Common/Tabs.tsx @@ -8,7 +8,7 @@ export default function Tabs(props: { className?: string; currentTab: string | number; onTabChange: (value: string | number) => void; - tabs: { text: ReactNode; value: string | number }[]; + tabs: { text: ReactNode; value: string | number; id?: string }[]; }) { const { className, currentTab, onTabChange, tabs } = props; const ref = useRef(null); @@ -60,6 +60,7 @@ export default function Tabs(props: { {tabs.map((tab, i) => (
diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index 0a31d40eb89..2dee44eeda6 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -1,5 +1,4 @@ import { navigate } from "raviger"; -import { useState } from "react"; import { useTranslation } from "react-i18next"; import Card from "@/CAREUI/display/Card"; @@ -59,7 +58,7 @@ export const CanUserAccess = (user: UserModel | UserAssignedModel) => { const GetDetailsButton = (username: string) => { const { t } = useTranslation(); return ( -
+
- {"district_object" in user && user.district_object && ( -
-
{t("district")}
-
- {user.district_object.name} -
-
- )} - {"district" in user && user.district && ( -
-
{t("district")}
-
- {user.district} -
-
- )} + {getDistrict(user, t)}
{t("average_weekly_working_hours")} @@ -276,10 +285,13 @@ const UserListRow = ({ className="h-10 w-10 text-lg" />
-

+

{formatName(user)}

- + @{user.username}
From 413bf75ee77b885e9da706d80bce594ad58f27c2 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Thu, 14 Nov 2024 16:06:27 +0530 Subject: [PATCH 33/55] minor additions --- cypress/e2e/users_spec/UsersManage.cy.ts | 1 + cypress/pageobject/Users/ManageUserPage.ts | 7 ++++++- src/components/Users/UserAddEditForm.tsx | 6 +++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/users_spec/UsersManage.cy.ts b/cypress/e2e/users_spec/UsersManage.cy.ts index 7fa5e0c77eb..f2bf98435ca 100644 --- a/cypress/e2e/users_spec/UsersManage.cy.ts +++ b/cypress/e2e/users_spec/UsersManage.cy.ts @@ -62,6 +62,7 @@ describe("Manage User", () => { manageUserPage.verifyMoreDetailsPage(); manageUserPage.clickLinkedSkillTab(); cy.wait(500); + manageUserPage.verifyDoctorQualificationDoesNotExist(); manageUserPage.verifyLinkedSkillsTabPage(); manageUserPage.selectSkillFromDropdown(linkedskill); manageUserPage.clickAddSkillButton(); diff --git a/cypress/pageobject/Users/ManageUserPage.ts b/cypress/pageobject/Users/ManageUserPage.ts index c080a282b20..f89f72a9435 100644 --- a/cypress/pageobject/Users/ManageUserPage.ts +++ b/cypress/pageobject/Users/ManageUserPage.ts @@ -165,7 +165,8 @@ export class ManageUserPage { } verifyMoreDetailsPage() { - cy.wait(2000); + cy.intercept("GET", "**/api/v1/users/get_user/**").as("getUserDetails"); + cy.wait("@getUserDetails"); cy.get("#username").should("be.visible"); cy.get("#role").should("be.visible"); cy.get("#usermanagement_tab_nav").should("be.visible"); @@ -182,6 +183,10 @@ export class ManageUserPage { cy.get("input[name='qualification']").should("be.visible"); } + verifyDoctorQualificationDoesNotExist() { + cy.get("input[name='qualification']").should("not.exist"); + } + verifyLinkedSkillsTabPage() { cy.get("#select-skill").scrollIntoView(); cy.get("#select-skill").should("be.visible"); diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index d687a9c404e..634fe55e827 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -107,7 +107,11 @@ const initialState = { errors: { ...initError }, }; -const user_create_reducer = (state = initialState, action: any) => { +type UserFormAction = + | { type: "set_form"; form: UserForm } + | { type: "set_state"; state?: typeof initialState }; + +const user_create_reducer = (state = initialState, action: UserFormAction) => { switch (action.type) { case "set_form": { return { From df56df05e0841010492693698391783c94f79e46 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Mon, 18 Nov 2024 10:15:09 +0530 Subject: [PATCH 34/55] switched to using userListview's get route --- src/Utils/request/api.tsx | 2 +- src/components/Users/UserAddEditForm.tsx | 6 +++--- src/components/Users/UserHome.tsx | 2 +- src/components/Users/UserInformation.tsx | 2 +- src/components/Users/UserQualifications.tsx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Utils/request/api.tsx b/src/Utils/request/api.tsx index 6a92fd293bc..3dd3c72ef3c 100644 --- a/src/Utils/request/api.tsx +++ b/src/Utils/request/api.tsx @@ -1032,7 +1032,7 @@ const routes = { }, getUserDetails: { - path: "/api/v1/users/get_user/", + path: "/api/v1/users/{username}", method: "GET", TRes: Type(), }, diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index 634fe55e827..05de69e084b 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -173,10 +173,10 @@ const UserAddEditForm = (props: UserProps) => { data: userData, refetch: refetchUserData, } = useQuery(routes.getUserDetails, { - query: { - username: username, + pathParams: { + username: username ?? "", }, - prefetch: editUser, + prefetch: editUser && !!username, onResponse: (result) => { if (!editUser || !result || !result.res || !result.data) return; const userData = result.data; diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index fd27016c4cb..f4d4e0d7670 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -34,7 +34,7 @@ export default function UserHome(props: UserHomeProps) { const { loading, refetch: refetchUserDetails } = useQuery( routes.getUserDetails, { - query: { + pathParams: { username: username, }, onResponse: ({ res, data, error }) => { diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx index 5ba17e2b2bb..2ba916a75e0 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserInformation.tsx @@ -30,7 +30,7 @@ export default function UserInformation({ username }: { username: string }) { loading: isLoading, refetch: refetchUserData, } = useQuery(routes.getUserDetails, { - query: { + pathParams: { username: username, }, }); diff --git a/src/components/Users/UserQualifications.tsx b/src/components/Users/UserQualifications.tsx index 7628204ce1a..64052fe84c0 100644 --- a/src/components/Users/UserQualifications.tsx +++ b/src/components/Users/UserQualifications.tsx @@ -80,7 +80,7 @@ export default function UserQualifications({ username }: { username: string }) { loading: isLoading, refetch: refetchUserData, } = useQuery(routes.getUserDetails, { - query: { + pathParams: { username: username, }, onResponse: (result) => { From 3df84447bd2fcb6997d3846e13f1d10f0529608c Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Mon, 18 Nov 2024 10:16:13 +0530 Subject: [PATCH 35/55] Added a profile view for read-only - Render for users who won't have permissions to edit the profile - Additional styling changes may be necessary - Hide qualifications/skills and linked facilities tab for users without permissions --- public/locale/en.json | 7 +- src/Utils/permissions.ts | 8 ++ src/components/Users/LinkedFacilitiesTab.tsx | 12 ++ src/components/Users/RoleAndSkillsTab.tsx | 14 ++- src/components/Users/UserHome.tsx | 61 ++++++---- src/components/Users/UserInformation.tsx | 74 ++++++------ src/components/Users/UserListAndCard.tsx | 10 +- src/components/Users/UserViewDetails.tsx | 113 +++++++++++++++++++ 8 files changed, 233 insertions(+), 66 deletions(-) create mode 100644 src/components/Users/UserViewDetails.tsx diff --git a/public/locale/en.json b/public/locale/en.json index 36f564dafac..7b164f5a7db 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -357,6 +357,7 @@ "back_to_consultation": "Go back to Consultation", "back_to_login": "Back to login", "base_dosage": "Dosage", + "basic_info": "Basic Information", "bed_capacity": "Bed Capacity", "bed_created_notification_one": "{{count}} Bed created successfully", "bed_created_notification_other": "{{count}} Beds created successfully", @@ -502,7 +503,7 @@ "consultation_not_filed_description": "Please file a consultation for this patient to continue.", "consultation_notes": "General Instructions (Advice)", "consultation_updates": "Consultation updates", - "contact_info": "Contact Info", + "contact_info": "Contact Information", "contact_number": "Contact Number", "contact_person": "Name of Contact Person at Facility", "contact_person_at_the_facility": "Contact person at the current facility", @@ -938,6 +939,7 @@ "no_log_update_delta": "No changes since previous log update", "no_notices_for_you": "No notices for you.", "no_patients_to_show": "No patients to show.", + "no_permission_to_view_page": "You do not have permissions to view this page", "no_policy_added": "No Insurance Policy Added", "no_policy_found": "No Insurance Policy Found for this Patient", "no_presets": "No Presets", @@ -1101,6 +1103,7 @@ "prn_prescriptions": "PRN Prescriptions", "procedure_suggestions": "Procedure Suggestions", "procedures_select_placeholder": "Select procedures to add details", + "professional_info": "Professional Information", "profile": "Profile", "provisional": "Provisional", "qualification": "Qualification", @@ -1350,6 +1353,7 @@ "uploading": "Uploading", "use_existing_abha_address": "Use Existing ABHA Address", "user_deleted_successfuly": "User Deleted Successfuly", + "user_details": "User Details", "user_details_update_error": "Error while updating user details", "user_details_update_success": "User details updated successfully", "user_management": "User Management", @@ -1360,6 +1364,7 @@ "username_already_exists": "This username already exists", "username_available": "Username is available", "username_not_available": "Username is not available", + "username_userdetails_not_found": "Unable to fetch details as username or user details not found", "users": "Users", "vacant": "Vacant", "vehicle_preference": "Vehicle preference", diff --git a/src/Utils/permissions.ts b/src/Utils/permissions.ts index e0997364f94..7409d4531fd 100644 --- a/src/Utils/permissions.ts +++ b/src/Utils/permissions.ts @@ -55,6 +55,14 @@ export const showAvatarEdit = (authUser: UserModel, targetUser: UserModel) => { return true; return false; }; +export const editUserPermissions = ( + authUser: UserModel, + targetUser: UserModel, +) => { + if (authUser.username === targetUser.username || authUser.is_superuser) + return true; + return checkIfStateOrDistrictAdminInSameLocation(authUser, targetUser); +}; export const CameraFeedPermittedUserTypes: UserRole[] = [ "DistrictAdmin", "StateAdmin", diff --git a/src/components/Users/LinkedFacilitiesTab.tsx b/src/components/Users/LinkedFacilitiesTab.tsx index f3db269ade9..50f20367512 100644 --- a/src/components/Users/LinkedFacilitiesTab.tsx +++ b/src/components/Users/LinkedFacilitiesTab.tsx @@ -1,9 +1,15 @@ +import { navigate } from "raviger"; import { useTranslation } from "react-i18next"; import userColumns from "@/components/Common/UserColumns"; import LinkedFacilities from "@/components/Users/LinkedFacilities"; import { UserModel } from "@/components/Users/models"; +import useAuthUser from "@/hooks/useAuthUser"; + +import * as Notification from "@/Utils/Notifications"; +import { editUserPermissions } from "@/Utils/permissions"; + type Props = { userData: UserModel; username: string; @@ -13,9 +19,15 @@ type Props = { export default function LinkedFacilitiesTab(props: Props) { const { userData } = props; const { t } = useTranslation(); + const authUser = useAuthUser(); + const editPermissions = editUserPermissions(authUser, userData); if (!userData) { + Notification.Error({ msg: t("username_userdetails_not_found") }); return <>; + } else if (!editPermissions) { + Notification.Error({ msg: t("no_permission_to_view_page") }); + navigate("/users"); } return ( diff --git a/src/components/Users/RoleAndSkillsTab.tsx b/src/components/Users/RoleAndSkillsTab.tsx index 3247329c627..de6d25e007b 100644 --- a/src/components/Users/RoleAndSkillsTab.tsx +++ b/src/components/Users/RoleAndSkillsTab.tsx @@ -1,3 +1,4 @@ +import { navigate } from "raviger"; import { useTranslation } from "react-i18next"; import userColumns from "@/components/Common/UserColumns"; @@ -5,6 +6,11 @@ import LinkedSkills from "@/components/Users/LinkedSkills"; import UserQualifications from "@/components/Users/UserQualifications"; import { UserModel } from "@/components/Users/models"; +import useAuthUser from "@/hooks/useAuthUser"; + +import * as Notification from "@/Utils/Notifications"; +import { editUserPermissions } from "@/Utils/permissions"; + type Props = { userData: UserModel; username: string; @@ -13,9 +19,15 @@ type Props = { export default function RoleAndSkillsTab(props: Props) { const { userData, username } = props; const { t } = useTranslation(); + const authUser = useAuthUser(); + const editPermissions = editUserPermissions(authUser, userData); if (!userData || !username) { - return; + Notification.Error({ msg: t("username_userdetails_not_found") }); + return <>; + } else if (!editPermissions) { + Notification.Error({ msg: t("no_permission_to_view_page") }); + navigate("/users"); } return ( diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index f4d4e0d7670..77c0b43f2ef 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -12,7 +12,10 @@ import UserBanner from "@/components/Users/UserBanner"; import UserSummaryTab from "@/components/Users/UserSummary"; import { UserModel } from "@/components/Users/models"; +import useAuthUser from "@/hooks/useAuthUser"; + import * as Notification from "@/Utils/Notifications"; +import { editUserPermissions } from "@/Utils/permissions"; import routes from "@/Utils/request/api"; import useQuery from "@/Utils/request/useQuery"; import { classNames, formatName, keysOf } from "@/Utils/utils"; @@ -24,12 +27,14 @@ export interface UserHomeProps { export interface tabChildProp { body: (childProps: userChildProps) => JSX.Element | undefined; name?: string; + hidden?: boolean; } export default function UserHome(props: UserHomeProps) { const { username, tab } = props; const [userData, setUserData] = useState(); const { t } = useTranslation(); + const authUser = useAuthUser(); const { loading, refetch: refetchUserDetails } = useQuery( routes.getUserDetails, @@ -51,11 +56,17 @@ export default function UserHome(props: UserHomeProps) { }, ); + if (loading || !userData) { + return ; + } + const roleInfoBeVisible = () => { if (["Doctor", "Nurse"].includes(userData?.user_type ?? "")) return true; return false; }; + const editPermissions = editUserPermissions(authUser, userData); + const TABS: { PROFILE: tabChildProp; SKILLS: tabChildProp; @@ -65,8 +76,12 @@ export default function UserHome(props: UserHomeProps) { SKILLS: { body: RoleAndSkillsTab, name: roleInfoBeVisible() ? "QUALIFICATIONS_SKILLS" : "SKILLS", + hidden: !editPermissions, + }, + FACILITIES: { + body: LinkedFacilitiesTab, + hidden: !editPermissions, }, - FACILITIES: { body: LinkedFacilitiesTab }, }; const normalizedTab = tab.toUpperCase(); @@ -78,10 +93,6 @@ export default function UserHome(props: UserHomeProps) { return ; } - if (loading || !userData) { - return ; - } - const SelectedTab = TABS[currentTab].body; return ( @@ -103,25 +114,27 @@ export default function UserHome(props: UserHomeProps) { className="flex space-x-6 overflow-x-auto" id="usermanagement_tab_nav" > - {keysOf(TABS).map((p) => { - const tabName = TABS[p]?.name ?? p; - return ( - -
- {t(`USERMANAGEMENT_TAB__${tabName}`)} -
- - ); - })} + {keysOf(TABS) + .filter((p) => !TABS[p].hidden) + .map((p) => { + const tabName = TABS[p]?.name ?? p; + return ( + +
+ {t(`USERMANAGEMENT_TAB__${tabName}`)} +
+ + ); + })}
diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx index 2ba916a75e0..3079855a5e7 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserInformation.tsx @@ -13,13 +13,15 @@ import useAuthUser from "@/hooks/useAuthUser"; import { LocalStorageKeys } from "@/common/constants"; import * as Notification from "@/Utils/Notifications"; -import { showAvatarEdit } from "@/Utils/permissions"; +import { editUserPermissions, showAvatarEdit } from "@/Utils/permissions"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; import uploadFile from "@/Utils/request/uploadFile"; import useQuery from "@/Utils/request/useQuery"; import { formatDisplayName, sleep } from "@/Utils/utils"; +import { UserViewDetails } from "./UserViewDetails"; + export default function UserInformation({ username }: { username: string }) { const { t } = useTranslation(); const [editAvatar, setEditAvatar] = useState(false); @@ -80,6 +82,9 @@ export default function UserInformation({ username }: { username: string }) { } }; + const avatarPermissions = showAvatarEdit(authUser, userData); + const editPermissions = editUserPermissions(authUser, userData); + return ( <> setEditAvatar(false)} /> -
-
-
- -
- setEditAvatar(!editAvatar)} - type="button" - id="edit-cancel-profile-button" - className="border border-gray-200 bg-gray-50 text-black hover:bg-gray-100" - shadow={false} - disabled={!showAvatarEdit(authUser, userData)} - tooltip={ - !showAvatarEdit(authUser, userData) - ? t("edit_avatar_permission_error") - : undefined - } - > - {t("change_avatar")} - -

- {t("change_avatar_note")} -

+ {editPermissions && ( +
+ {avatarPermissions && ( +
+
+ +
+ setEditAvatar(!editAvatar)} + type="button" + id="edit-cancel-profile-button" + className="border border-gray-200 bg-gray-50 text-black hover:bg-gray-100" + shadow={false} + disabled={!showAvatarEdit(authUser, userData)} + tooltip={ + !showAvatarEdit(authUser, userData) + ? t("edit_avatar_permission_error") + : undefined + } + > + {t("change_avatar")} + +

+ {t("change_avatar_note")} +

+
+
+ )} +
+
-
- -
-
+ )} + {!editPermissions && } ); } diff --git a/src/components/Users/UserListAndCard.tsx b/src/components/Users/UserListAndCard.tsx index 57eb935210d..0f33a8deacf 100644 --- a/src/components/Users/UserListAndCard.tsx +++ b/src/components/Users/UserListAndCard.tsx @@ -100,11 +100,7 @@ const getNameAndStatusCard = ( {user.username}
-
- {showDetailsButton && - CanUserAccess(user) && - GetDetailsButton(user.username)} -
+
{showDetailsButton && GetDetailsButton(user.username)}
); @@ -315,9 +311,7 @@ const UserListRow = ({ : ""} )} - - {CanUserAccess(user) && GetDetailsButton(user.username)} - + {GetDetailsButton(user.username)} ); }; diff --git a/src/components/Users/UserViewDetails.tsx b/src/components/Users/UserViewDetails.tsx new file mode 100644 index 00000000000..e05efb4d55c --- /dev/null +++ b/src/components/Users/UserViewDetails.tsx @@ -0,0 +1,113 @@ +import { useTranslation } from "react-i18next"; + +import { UserModel } from "./models"; + +interface UserViewDetailsProps { + user: UserModel; +} + +const LabelValue = ({ + label, + value, +}: { + label: string; + value?: string | null; +}) => ( +
+ {label} + {value || "-"} +
+); + +interface BadgeProps { + text: string; + bgColor?: string; + textColor?: string; + className?: string; +} + +export const Badge = ({ + text, + bgColor = "bg-green-600", + textColor = "text-white", + className = "", +}: BadgeProps) => { + return ( + + {text} + + ); +}; + +export const UserViewDetails = ({ user }: UserViewDetailsProps) => { + const { t } = useTranslation(); + + return ( +
+
+ +
+ + + + + + +
+
+ +
+ +
+ + + +
+
+ + {(user.user_type === "Doctor" || user.user_type === "Nurse") && ( +
+ +
+ + {user.user_type === "Doctor" && ( + <> + + + + )} + + +
+
+ )} +
+ ); +}; From a6e46687788efe5c23735bd17117566178bb87a3 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Tue, 19 Nov 2024 10:21:05 +0530 Subject: [PATCH 36/55] User View - Additional changes to facilitate user view - Added edit profile button - Remove cancel button from the edit form (submit only) - Removed UserQualifications - Instead using it in edit form - Additional changes include adjusting the tab names (for skills tab), moving additional form fields in add user form to their own functions --- public/locale/en.json | 4 +- src/components/Form/Form.tsx | 12 +- src/components/Users/RoleAndSkillsTab.tsx | 9 - src/components/Users/UserAddEditForm.tsx | 593 ++++++++++---------- src/components/Users/UserHome.tsx | 10 +- src/components/Users/UserInformation.tsx | 47 +- src/components/Users/UserQualifications.tsx | 269 --------- src/components/Users/UserResetPassword.tsx | 1 + src/components/Users/UserViewDetails.tsx | 28 +- 9 files changed, 376 insertions(+), 597 deletions(-) delete mode 100644 src/components/Users/UserQualifications.tsx diff --git a/public/locale/en.json b/public/locale/en.json index 7b164f5a7db..213dc1b5fc6 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -210,8 +210,7 @@ "URINATION_FREQUENCY__NORMAL": "Normal", "USERMANAGEMENT_TAB__FACILITIES": "Linked Facilities", "USERMANAGEMENT_TAB__PROFILE": "User Information", - "USERMANAGEMENT_TAB__QUALIFICATIONS_SKILLS": "Qualifications & Skills", - "USERMANAGEMENT_TAB__SKILLS": "Skills", + "USERMANAGEMENT_TAB__SKILLS": "Linked Skills", "VENTILATOR": "Detailed Update", "VENTILATOR_MODE__CMV": "Control Mechanical Ventilation (CMV)", "VENTILATOR_MODE__CMV_short": "CMV", @@ -1391,6 +1390,7 @@ "view_faciliy": "View Facility", "view_patients": "View Patients", "view_user": "View User", + "view_user_profile": "View Profile", "view_users": "View Users", "virtual_nursing_assistant": "Virtual Nursing Assistant", "vitals": "Vitals", diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index c8fc59a145d..33f62cc4ea4 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -34,11 +34,13 @@ type Props = { children: (props: FormContextValue) => React.ReactNode; hideRestoreDraft?: boolean; resetFormVals?: boolean; + hideCancelButton?: boolean; }; const Form = ({ asyncGetDefaults, validate, + hideCancelButton = false, ...props }: Props) => { const initial = { form: props.defaults, errors: {} }; @@ -130,10 +132,12 @@ const Form = ({ {props.children}
- + {!hideCancelButton && ( + + )}
- {userData.user_type && - ["Doctor", "Nurse"].includes(userData.user_type) && - userColumns( - t("user_qualifications"), - t("user_qualifications_note"), - UserQualifications, - props, - )} {userColumns( t("linked_skills"), t("linked_skills_note"), diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index 05de69e084b..bb4959f7ab8 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -58,6 +58,7 @@ import { interface UserProps { username?: string; + onSubmitSuccess?: () => void; } interface StateObj { @@ -193,6 +194,15 @@ const UserAddEditForm = (props: UserProps) => { phone_number_is_whatsapp: userData.phone_number?.toString() === userData.alt_phone_number?.toString(), + user_type: userData.user_type, + qualification: userData.qualification, + doctor_experience_commenced_on: userData.doctor_experience_commenced_on + ? dayjs() + .diff(dayjs(userData.doctor_experience_commenced_on), "years") + .toString() + : undefined, + doctor_medical_council_registration: + userData.doctor_medical_council_registration, }; dispatch({ type: "set_form", @@ -275,6 +285,7 @@ const UserAddEditForm = (props: UserProps) => { msg: error?.message ?? t("user_details_update_error"), }); } + props.onSubmitSuccess?.(); }; const [state, dispatch] = useAutoSaveReducer( @@ -811,88 +822,84 @@ const UserAddEditForm = (props: UserProps) => { const renderUsernameField = (field: FormContextValue) => { return ( <> - {!editUser && ( - <> - { - handleFieldChange(e, field); - setUsernameInput(e.value); - }} - onFocus={() => setUsernameInputInFocus(true)} - onBlur={() => { - setUsernameInputInFocus(false); - }} - aria-label={t("username")} - /> - {usernameInputInFocus && ( -
-
- {usernameExists !== userExistsEnums.idle && ( + { + handleFieldChange(e, field); + setUsernameInput(e.value); + }} + onFocus={() => setUsernameInputInFocus(true)} + onBlur={() => { + setUsernameInputInFocus(false); + }} + aria-label={t("username")} + /> + {usernameInputInFocus && ( +
+
+ {usernameExists !== userExistsEnums.idle && ( + <> + {usernameExists === userExistsEnums.checking ? ( + + {" "} + checking... + + ) : ( <> - {usernameExists === userExistsEnums.checking ? ( - - {" "} - checking... - + {usernameExists === userExistsEnums.exists ? ( +
+ {" "} + + {t("username_not_available")} + +
) : ( - <> - {usernameExists === userExistsEnums.exists ? ( -
- {" "} - - {t("username_not_available")} - -
- ) : ( -
- {" "} - - {t("username_available")} - -
- )} - +
+ {" "} + + {t("username_available")} + +
)} )} -
-
- {validateRule( - usernameInput.length >= 4 && usernameInput.length <= 16, - "Username should be 4-16 characters long", - !state.form.username, - )} - {validateRule( - /^[a-z0-9._-]*$/.test(usernameInput), - "Username can only contain lowercase letters, numbers, and . _ -", - !state.form.username, - )} - {validateRule( - /^[a-z0-9].*[a-z0-9]$/i.test(usernameInput), - "Username must start and end with a letter or number", - !state.form.username, - )} - {validateRule( - !/(?:[._-]{2,})/.test(usernameInput), - "Username can't contain consecutive special characters . _ -", - !state.form.username, - )} -
-
- )} - + + )} +
+
+ {validateRule( + usernameInput.length >= 4 && usernameInput.length <= 16, + "Username should be 4-16 characters long", + !state.form.username, + )} + {validateRule( + /^[a-z0-9._-]*$/.test(usernameInput), + "Username can only contain lowercase letters, numbers, and . _ -", + !state.form.username, + )} + {validateRule( + /^[a-z0-9].*[a-z0-9]$/i.test(usernameInput), + "Username must start and end with a letter or number", + !state.form.username, + )} + {validateRule( + !/(?:[._-]{2,})/.test(usernameInput), + "Username can't contain consecutive special characters . _ -", + !state.form.username, + )} +
+
)} ); @@ -901,80 +908,78 @@ const UserAddEditForm = (props: UserProps) => { const renderPasswordFields = (field: FormContextValue) => { return ( <> - {!editUser && ( -
-
- setPasswordInputInFocus(true)} - onBlur={() => setPasswordInputInFocus(false)} - onChange={(e) => { - handleFieldChange(e, field); - }} - aria-label={t("password")} - /> - {passwordInputInFocus && state.form.password && ( -
- {validateRule( - state.form.password.length >= 8, - "Password should be atleast 8 characters long", - !state.form.password, - )} - {validateRule( - state.form.password !== state.form.password.toUpperCase(), - "Password should contain at least 1 lowercase letter", - !state.form.password, - )} - {validateRule( - state.form.password !== state.form.password.toLowerCase(), - "Password should contain at least 1 uppercase letter", - !state.form.password, - )} +
+
+ setPasswordInputInFocus(true)} + onBlur={() => setPasswordInputInFocus(false)} + onChange={(e) => { + handleFieldChange(e, field); + }} + aria-label={t("password")} + /> + {passwordInputInFocus && state.form.password && ( +
+ {validateRule( + state.form.password.length >= 8, + "Password should be atleast 8 characters long", + !state.form.password, + )} + {validateRule( + state.form.password !== state.form.password.toUpperCase(), + "Password should contain at least 1 lowercase letter", + !state.form.password, + )} + {validateRule( + state.form.password !== state.form.password.toLowerCase(), + "Password should contain at least 1 uppercase letter", + !state.form.password, + )} + {validateRule( + /\d/.test(state.form.password), + "Password should contain at least 1 number", + !state.form.password, + )} +
+ )} +
+
+ setConfirmPasswordInputInFocus(true)} + onBlur={() => setConfirmPasswordInputInFocus(false)} + onChange={(e) => { + handleFieldChange(e, field); + }} + aria-label={t("confirm_password")} + /> + {confirmPasswordInputInFocus && + state.form.c_password && + state.form.c_password.length > 0 && ( +
{validateRule( - /\d/.test(state.form.password), - "Password should contain at least 1 number", - !state.form.password, + state.form.c_password === state.form.password, + "Confirm password should match the entered password", + !state.form.c_password, )}
)} -
-
- setConfirmPasswordInputInFocus(true)} - onBlur={() => setConfirmPasswordInputInFocus(false)} - onChange={(e) => { - handleFieldChange(e, field); - }} - aria-label={t("confirm_password")} - /> - {confirmPasswordInputInFocus && - state.form.c_password && - state.form.c_password.length > 0 && ( -
- {validateRule( - state.form.c_password === state.form.password, - "Confirm password should match the entered password", - !state.form.c_password, - )} -
- )} -
- )} +
); }; @@ -1046,6 +1051,154 @@ const UserAddEditForm = (props: UserProps) => { ); }; + const renderHoursAndConferenceLinkFields = ( + field: FormContextValue, + ) => { + return ( + <> +
+ { + handleFieldChange(e, field); + }} + aria-label={t("average_weekly_working_hours")} + /> + { + handleFieldChange(e, field); + }} + aria-label={t("video_conference_link")} + /> +
+ + ); + }; + + const renderStateDistrictLocalBodyFields = ( + field: FormContextValue, + ) => { + return ( + <> + {isStateLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e, field); + if (e) setSelectedStateId(e.value); + }} + aria-label={t("state")} + /> + )} + + {isDistrictLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e, field); + if (e) setSelectedDistrictId(e.value); + }} + aria-label={t("district")} + /> + )} + + {showLocalbody && + (isLocalbodyLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e, field); + }} + aria-label={t("local_body")} + /> + ))} + + ); + }; + + const renderFacilityUserTypeHomeFacilityFields = ( + field: FormContextValue, + ) => { + return ( + <> +
+ {t("facilities")} + +
+
+ o.role + (o.readOnly ? " (Read Only)" : "")} + onChange={(e) => { + handleFieldChange(e, field); + }} + optionValue={(o) => o.id} + className="flex-1" + aria-label={t("user_type")} + /> + option.name} + optionValue={(option) => option.id} + onChange={(e) => { + handleFieldChange(e, field); + }} + className="flex-1" + aria-label={t("home_facility")} + /> +
+ + ); + }; + return ( disabled={isLoading} @@ -1059,152 +1212,20 @@ const UserAddEditForm = (props: UserProps) => { hideRestoreDraft={editUser} noPadding resetFormVals + hideCancelButton={editUser} > {(field) => ( <>
- {!editUser && ( -
- {t("facilities")} - -
- )} - {!editUser && ( -
- - o.role + (o.readOnly ? " (Read Only)" : "") - } - onChange={(e) => { - handleFieldChange(e, field); - }} - optionValue={(o) => o.id} - className="flex-1" - aria-label={t("user_type")} - /> - option.name} - optionValue={(option) => option.id} - onChange={(e) => { - handleFieldChange(e, field); - }} - className="flex-1" - aria-label={t("home_facility")} - /> -
- )} - {renderDoctorOrNurseFields(field)} + {!editUser && renderFacilityUserTypeHomeFacilityFields(field)} + {!editUser && renderDoctorOrNurseFields(field)} {renderPhoneNumberFields(field)} - {renderUsernameField(field)} - {renderPasswordFields(field)} + {!editUser && renderUsernameField(field)} + {!editUser && renderPasswordFields(field)} {renderPersonalInfoFields(field)} - - {editUser && ( - <> -
- { - handleFieldChange(e, field); - }} - aria-label={t("average_weekly_working_hours")} - /> - { - handleFieldChange(e, field); - }} - aria-label={t("video_conference_link")} - /> -
- - )} - - {!editUser && ( - <> - {isStateLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - onChange={(e) => { - handleFieldChange(e, field); - if (e) setSelectedStateId(e.value); - }} - aria-label={t("state")} - /> - )} - - {isDistrictLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - onChange={(e) => { - handleFieldChange(e, field); - if (e) setSelectedDistrictId(e.value); - }} - aria-label={t("district")} - /> - )} - - {showLocalbody && - (isLocalbodyLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - onChange={(e) => { - handleFieldChange(e, field); - }} - aria-label={t("local_body")} - /> - ))} - - )} + {editUser && renderHoursAndConferenceLinkFields(field)} + {editUser && renderDoctorOrNurseFields(field)} + {!editUser && renderStateDistrictLocalBodyFields(field)}
)} diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index 77c0b43f2ef..a318b569c52 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -26,7 +26,6 @@ export interface UserHomeProps { } export interface tabChildProp { body: (childProps: userChildProps) => JSX.Element | undefined; - name?: string; hidden?: boolean; } @@ -60,11 +59,6 @@ export default function UserHome(props: UserHomeProps) { return ; } - const roleInfoBeVisible = () => { - if (["Doctor", "Nurse"].includes(userData?.user_type ?? "")) return true; - return false; - }; - const editPermissions = editUserPermissions(authUser, userData); const TABS: { @@ -75,7 +69,6 @@ export default function UserHome(props: UserHomeProps) { PROFILE: { body: UserSummaryTab }, SKILLS: { body: RoleAndSkillsTab, - name: roleInfoBeVisible() ? "QUALIFICATIONS_SKILLS" : "SKILLS", hidden: !editPermissions, }, FACILITIES: { @@ -117,7 +110,6 @@ export default function UserHome(props: UserHomeProps) { {keysOf(TABS) .filter((p) => !TABS[p].hidden) .map((p) => { - const tabName = TABS[p]?.name ?? p; return (
- {t(`USERMANAGEMENT_TAB__${tabName}`)} + {t(`USERMANAGEMENT_TAB__${p}`)}
); diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx index 3079855a5e7..77986952f65 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserInformation.tsx @@ -2,6 +2,8 @@ import careConfig from "@careConfig"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import CareIcon from "@/CAREUI/icons/CareIcon"; + import { Avatar } from "@/components/Common/Avatar"; import AvatarEditModal from "@/components/Common/AvatarEditModal"; import ButtonV2 from "@/components/Common/ButtonV2"; @@ -18,13 +20,14 @@ import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; import uploadFile from "@/Utils/request/uploadFile"; import useQuery from "@/Utils/request/useQuery"; -import { formatDisplayName, sleep } from "@/Utils/utils"; +import { classNames, formatDisplayName, sleep } from "@/Utils/utils"; import { UserViewDetails } from "./UserViewDetails"; export default function UserInformation({ username }: { username: string }) { const { t } = useTranslation(); const [editAvatar, setEditAvatar] = useState(false); + const [isEditing, setIsEditing] = useState(false); const authUser = useAuthUser(); const { @@ -85,6 +88,22 @@ export default function UserInformation({ username }: { username: string }) { const avatarPermissions = showAvatarEdit(authUser, userData); const editPermissions = editUserPermissions(authUser, userData); + const editButton = ( +
+ setIsEditing(!isEditing)} + type="button" + id="toggle-edit-mode-button" + className="flex items-center gap-2 rounded-sm border border-gray-100 bg-white px-3 py-1.5 text-sm text-[#009D48] shadow-sm hover:bg-gray-50" + shadow={false} + > + + + {isEditing ? t("view_user_profile") : t("edit_user_profile")} + +
+ ); + return ( <> setEditAvatar(false)} /> {editPermissions && ( -
+
{avatarPermissions && ( -
+
)} -
- +
+ {isEditing ? ( + <> + {editButton} + setIsEditing(false)} + /> + + ) : ( + <> + {editButton} + + + )}
)} diff --git a/src/components/Users/UserQualifications.tsx b/src/components/Users/UserQualifications.tsx deleted file mode 100644 index 64052fe84c0..00000000000 --- a/src/components/Users/UserQualifications.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import { useReducer, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import Loading from "@/components/Common/Loading"; -import { FieldError } from "@/components/Form/FieldValidators"; -import Form from "@/components/Form/Form"; -import TextFormField from "@/components/Form/FormFields/TextFormField"; -import { - EditForm, - ValidateDoctorExperienceCommencedOn, - ValidateDoctorMedicalCouncilRegistration, - ValidateQualification, -} from "@/components/Users/UserFormValidations"; - -import * as Notification from "@/Utils/Notifications"; -import dayjs from "@/Utils/dayjs"; -import routes from "@/Utils/request/api"; -import request from "@/Utils/request/request"; -import useQuery from "@/Utils/request/useQuery"; - -import { FormContextValue } from "../Form/FormContext"; -import { FieldChangeEvent } from "../Form/FormFields/Utils"; - -type ErrorForm = { - user_type?: string; - qualification?: string; - doctor_experience_commenced_on?: string; - doctor_medical_council_registration?: string; -}; -type State = { - form: EditForm; - errors: ErrorForm; -}; -type Action = - | { type: "set_form"; form: EditForm } - | { type: "set_error"; errors: ErrorForm }; - -const initForm: EditForm = { - user_type: "", - qualification: null, - doctor_experience_commenced_on: null, - doctor_medical_council_registration: null, -}; - -const initError: ErrorForm = Object.assign( - {}, - ...Object.keys(initForm).map((k) => ({ [k]: "" })), -); - -const initialState: State = { - form: { ...initForm }, - errors: { ...initError }, -}; - -const editFormReducer = (state: State, action: Action) => { - switch (action.type) { - case "set_form": { - return { - ...state, - form: action.form, - }; - } - case "set_error": { - return { - ...state, - errors: action.errors, - }; - } - } -}; - -export default function UserQualifications({ username }: { username: string }) { - const { t } = useTranslation(); - const [states, dispatch] = useReducer(editFormReducer, initialState); - const [isSubmitting, setIsSubmitting] = useState(false); - const formVals = useRef(initForm); - - const { - data: userData, - loading: isLoading, - refetch: refetchUserData, - } = useQuery(routes.getUserDetails, { - pathParams: { - username: username, - }, - onResponse: (result) => { - if (!result || !result.res || !result.data) return; - const userData = result.data; - - const formData: EditForm = { - user_type: userData.user_type, - qualification: userData.qualification, - doctor_experience_commenced_on: userData.doctor_experience_commenced_on - ? dayjs() - .diff(dayjs(userData.doctor_experience_commenced_on), "years") - .toString() - : null, - doctor_medical_council_registration: - userData.doctor_medical_council_registration, - }; - dispatch({ - type: "set_form", - form: formData, - }); - formVals.current = formData; - }, - }); - - const validateForm = (formData: EditForm) => { - const errors: Partial> = {}; - Object.keys(formData).forEach((field) => { - let currentError = null; - switch (field) { - case "doctor_experience_commenced_on": - currentError = ValidateDoctorExperienceCommencedOn(formData, t); - if (currentError) { - errors[field] = currentError; - } - break; - case "qualification": - currentError = ValidateQualification(formData, t); - if (currentError) { - errors[field] = currentError; - } - break; - case "doctor_medical_council_registration": - currentError = ValidateDoctorMedicalCouncilRegistration(formData, t); - if (currentError) { - errors[field] = currentError; - } - break; - } - }); - return errors; - }; - - const handleCancel = () => { - dispatch({ - type: "set_form", - form: formVals.current, - }); - }; - - const handleFieldChange = ( - event: FieldChangeEvent, - field?: FormContextValue, - ) => { - const fieldName = event.name as keyof EditForm; - dispatch({ - type: "set_form", - form: { - ...states.form, - [fieldName]: event.value, - }, - }); - field?.(fieldName).onChange(event); - }; - - if (isLoading || !userData) { - return ; - } - - if (!["Doctor", "Nurse"].includes(states.form.user_type ?? "")) return; - - const handleSubmit = async (formData: EditForm) => { - setIsSubmitting(true); - const data = { - qualification: - formData.user_type === "Doctor" || formData.user_type === "Nurse" - ? (formData.qualification ?? undefined) - : undefined, - doctor_experience_commenced_on: - formData.user_type === "Doctor" - ? dayjs() - .subtract( - parseInt( - (formData.doctor_experience_commenced_on as string) ?? "0", - ), - "years", - ) - .format("YYYY-MM-DD") - : undefined, - doctor_medical_council_registration: - formData.user_type === "Doctor" - ? (formData.doctor_medical_council_registration ?? undefined) - : undefined, - }; - const { res, error } = await request(routes.partialUpdateUser, { - pathParams: { username: userData.username }, - body: data, - }); - if (res?.ok) { - Notification.Success({ - msg: t("user_details_update_success"), - }); - await refetchUserData(); - } else { - Notification.Error({ - msg: error?.message ?? t("user_details_update_error"), - }); - } - setIsSubmitting(false); - }; - - return ( - <> -
- {!isLoading && ( -
- - disabled={isSubmitting || isLoading} - defaults={userData ? states.form : initForm} - validate={validateForm} - onCancel={handleCancel} - onSubmit={handleSubmit} - hideRestoreDraft - noPadding - resetFormVals - > - {(field) => ( - <> -
- {(states.form.user_type === "Doctor" || - states.form.user_type === "Nurse") && ( - { - handleFieldChange(e, field); - }} - /> - )} -
- {states.form.user_type === "Doctor" && ( -
- { - handleFieldChange(e, field); - }} - /> - { - handleFieldChange(e, field); - }} - /> -
- )} - - )} - -
- )} -
- - ); -} diff --git a/src/components/Users/UserResetPassword.tsx b/src/components/Users/UserResetPassword.tsx index 4952d15847e..370a7379f42 100644 --- a/src/components/Users/UserResetPassword.tsx +++ b/src/components/Users/UserResetPassword.tsx @@ -102,6 +102,7 @@ export default function UserResetPassword({ hideRestoreDraft noPadding disabled={isSubmitting} + hideCancelButton > {(field) => (
diff --git a/src/components/Users/UserViewDetails.tsx b/src/components/Users/UserViewDetails.tsx index e05efb4d55c..f3b104e2d4b 100644 --- a/src/components/Users/UserViewDetails.tsx +++ b/src/components/Users/UserViewDetails.tsx @@ -28,19 +28,21 @@ interface BadgeProps { export const Badge = ({ text, - bgColor = "bg-green-600", - textColor = "text-white", + textColor = "text-black", className = "", }: BadgeProps) => { return ( - - {text} - +
+
+ + {text} + +
); }; @@ -49,7 +51,7 @@ export const UserViewDetails = ({ user }: UserViewDetailsProps) => { return (
-
+
@@ -68,7 +70,7 @@ export const UserViewDetails = ({ user }: UserViewDetailsProps) => {
-
+
@@ -81,7 +83,7 @@ export const UserViewDetails = ({ user }: UserViewDetailsProps) => {
{(user.user_type === "Doctor" || user.user_type === "Nurse") && ( -
+
From 7c1b6a653237e819387b9b66edad7b365ec1132e Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Tue, 19 Nov 2024 13:26:08 +0530 Subject: [PATCH 37/55] UserInformation - Removed the edit button and replaced it with tabs - between edit and view for user information - Minor styling changes - Added a button to password reset form - Reveals/hides the form --- src/components/Users/UserInformation.tsx | 76 +++++++++++++--------- src/components/Users/UserResetPassword.tsx | 31 ++++++++- src/components/Users/UserViewDetails.tsx | 4 +- 3 files changed, 77 insertions(+), 34 deletions(-) diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx index 77986952f65..83c5cc9e19e 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserInformation.tsx @@ -2,8 +2,6 @@ import careConfig from "@careConfig"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import CareIcon from "@/CAREUI/icons/CareIcon"; - import { Avatar } from "@/components/Common/Avatar"; import AvatarEditModal from "@/components/Common/AvatarEditModal"; import ButtonV2 from "@/components/Common/ButtonV2"; @@ -20,7 +18,7 @@ import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; import uploadFile from "@/Utils/request/uploadFile"; import useQuery from "@/Utils/request/useQuery"; -import { classNames, formatDisplayName, sleep } from "@/Utils/utils"; +import { formatDisplayName, sleep } from "@/Utils/utils"; import { UserViewDetails } from "./UserViewDetails"; @@ -88,19 +86,40 @@ export default function UserInformation({ username }: { username: string }) { const avatarPermissions = showAvatarEdit(authUser, userData); const editPermissions = editUserPermissions(authUser, userData); - const editButton = ( -
- setIsEditing(!isEditing)} - type="button" - id="toggle-edit-mode-button" - className="flex items-center gap-2 rounded-sm border border-gray-100 bg-white px-3 py-1.5 text-sm text-[#009D48] shadow-sm hover:bg-gray-50" - shadow={false} + const ViewEditToggle = ({ + isEditing, + setIsEditing, + }: { + isEditing: boolean; + setIsEditing: (value: boolean) => void; + }) => ( +
+ +
); @@ -149,28 +168,25 @@ export default function UserInformation({ username }: { username: string }) { )}
+ {isEditing ? ( - <> - {editButton} - setIsEditing(false)} - /> - + setIsEditing(false)} + /> ) : ( - <> - {editButton} - - + )}
)} - {!editPermissions && } + {!editPermissions && ( +
+ +
+ )} ); } diff --git a/src/components/Users/UserResetPassword.tsx b/src/components/Users/UserResetPassword.tsx index 370a7379f42..7d6dad29c25 100644 --- a/src/components/Users/UserResetPassword.tsx +++ b/src/components/Users/UserResetPassword.tsx @@ -1,6 +1,8 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; +import CareIcon from "@/CAREUI/icons/CareIcon"; + import Form from "@/components/Form/Form"; import TextFormField from "@/components/Form/FormFields/TextFormField"; import { validateRule } from "@/components/Users/UserAddEditForm"; @@ -10,6 +12,8 @@ import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; +import ButtonV2 from "../Common/ButtonV2"; + interface PasswordForm { username: string; old_password: string; @@ -24,6 +28,7 @@ export default function UserResetPassword({ }) { const { t } = useTranslation(); const [isSubmitting, setisSubmitting] = useState(false); + const [isEditing, setIsEditing] = useState(false); const initForm: PasswordForm = { username: userData.username, @@ -92,8 +97,8 @@ export default function UserResetPassword({ setisSubmitting(false); }; - return ( -
+ const renderPasswordForm = () => { + return ( defaults={initForm} validate={validateForm} @@ -180,6 +185,28 @@ export default function UserResetPassword({
)} + ); + }; + + const editButton = () => ( +
+ setIsEditing(!isEditing)} + type="button" + id="change-edit-password-button" + className="flex items-center gap-2 rounded-sm border border-gray-100 bg-white px-3 py-1.5 text-sm text-[#009D48] shadow-sm hover:bg-gray-50" + shadow={false} + > + + {isEditing ? t("cancel") : t("change_password")} + +
+ ); + + return ( +
+ {editButton()} + {isEditing && renderPasswordForm()}
); } diff --git a/src/components/Users/UserViewDetails.tsx b/src/components/Users/UserViewDetails.tsx index f3b104e2d4b..3d8d1fa1bde 100644 --- a/src/components/Users/UserViewDetails.tsx +++ b/src/components/Users/UserViewDetails.tsx @@ -33,10 +33,10 @@ export const Badge = ({ }: BadgeProps) => { return (
-
+
From 07a4212b0fe3c4b438c0af6eb0d07591432f043f Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Wed, 20 Nov 2024 02:28:03 +0530 Subject: [PATCH 38/55] UserDetails - Separated out user details into 3 sections - Each with its own view/edit toggle button - Modified UserAddEdit form to support this - Takes in fields as a prop - Other minor changes to help with phasing out Profile component - Added language picker dropdown to user details page - Adjusted the sidebar notes based on if it's the user's personal page --- public/locale/en.json | 5 +- src/Utils/permissions.ts | 4 +- src/components/Common/LanguageSelector.tsx | 12 +- src/components/Users/UserAdd.tsx | 3 +- src/components/Users/UserAddEditForm.tsx | 813 ++++++++++--------- src/components/Users/UserEditDetails.tsx | 253 ++++++ src/components/Users/UserFormValidations.tsx | 63 ++ src/components/Users/UserHome.tsx | 9 +- src/components/Users/UserInformation.tsx | 67 +- src/components/Users/UserSummary.tsx | 16 +- src/components/Users/UserViewDetails.tsx | 86 +- 11 files changed, 840 insertions(+), 491 deletions(-) create mode 100644 src/components/Users/UserEditDetails.tsx diff --git a/public/locale/en.json b/public/locale/en.json index 213dc1b5fc6..584ba553598 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1039,7 +1039,7 @@ "patients": "Patients", "personal_information": "Personal Information", "personal_information_note": "Update User's Personal Information", - "personal_information_note_user": "Update Your Personal Information", + "personal_information_note_self": "Update Your Personal Information", "phone": "Phone", "phone_no": "Phone no.", "phone_number": "Phone Number", @@ -1152,7 +1152,8 @@ "resend_otp": "Resend OTP", "reset": "Reset", "reset_password": "Reset Password", - "reset_password_note": "Enter your current password, then create and confirm your new password", + "reset_password_note": "Enter user's current password, then create and confirm the new password", + "reset_password_note_self": "Enter your current password, then create and confirm your new password", "resource": "Resource", "resource_approving_facility": "Resource approving facility", "resource_origin_facility": "Origin Facility", diff --git a/src/Utils/permissions.ts b/src/Utils/permissions.ts index 7409d4531fd..e738d2a2c1f 100644 --- a/src/Utils/permissions.ts +++ b/src/Utils/permissions.ts @@ -25,9 +25,11 @@ const checkIfStateOrDistrictAdminInSameLocation = ( export const showUserDelete = (authUser: UserModel, targetUser: UserModel) => { // Auth user should be higher in hierarchy than target user + // User can't delete their own account if ( USER_TYPES.indexOf(authUser.user_type) <= - USER_TYPES.indexOf(targetUser.user_type) + USER_TYPES.indexOf(targetUser.user_type) || + authUser.username === targetUser.username ) return false; diff --git a/src/components/Common/LanguageSelector.tsx b/src/components/Common/LanguageSelector.tsx index 5904f2cf7c7..d84ac02296a 100644 --- a/src/components/Common/LanguageSelector.tsx +++ b/src/components/Common/LanguageSelector.tsx @@ -2,8 +2,6 @@ import careConfig from "@careConfig"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import CareIcon from "@/CAREUI/icons/CareIcon"; - import { classNames } from "@/Utils/utils"; import { LANGUAGES } from "@/i18n"; @@ -27,11 +25,11 @@ export const LanguageSelector = (props: any) => { ); return ( -
+
-
- -
); }; diff --git a/src/components/Users/UserAdd.tsx b/src/components/Users/UserAdd.tsx index 8f375700d2d..211b5bea0ec 100644 --- a/src/components/Users/UserAdd.tsx +++ b/src/components/Users/UserAdd.tsx @@ -5,6 +5,7 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import Page from "@/components/Common/Page"; import UserAddEditForm from "@/components/Users/UserAddEditForm"; +import { newUserFields } from "@/components/Users/UserFormValidations"; const UserAdd = () => { const { t } = useTranslation(); @@ -24,7 +25,7 @@ const UserAdd = () => { } backUrl="/users" > - + ); }; diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index bb4959f7ab8..5f071c77ec2 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -53,14 +53,8 @@ import { dateQueryString, isValidUrl, parsePhoneNumber, - scrollTo, } from "@/Utils/utils"; -interface UserProps { - username?: string; - onSubmitSuccess?: () => void; -} - interface StateObj { id: number; name: string; @@ -91,6 +85,12 @@ const initForm: UserForm = { video_connect_link: undefined, }; +interface UserProps { + username?: string; + includedFields?: Array; + onSubmitSuccess?: () => void; +} + const STAFF_OR_NURSE_USER = [ "Staff", "StaffReadOnly", @@ -164,7 +164,7 @@ export const validateRule = ( const UserAddEditForm = (props: UserProps) => { const { t } = useTranslation(); const { goBack } = useAppHistory(); - const { username } = props; + const { username, includedFields } = props; const editUser = username ? true : false; const formVals = useRef(initForm); const [facilityErrors, setFacilityErrors] = useState(""); @@ -542,12 +542,15 @@ const UserAddEditForm = (props: UserProps) => { const validateForm = (formData: UserForm) => { const errors: Partial> = {}; - const facilityError = validateFacility(formData, selectedFacility); + const fieldsToValidate = includedFields || Object.keys(formData); + const facilityError = fieldsToValidate.includes("facilities") + ? validateFacility(formData, selectedFacility) + : null; if (facilityError) { errors.facilities = facilityError; } let currentError = null; - Object.keys(formData).forEach((field) => { + fieldsToValidate.forEach((field) => { switch (field) { case "user_type": if (!formData[field]) { @@ -684,10 +687,6 @@ const UserAddEditForm = (props: UserProps) => { break; } }); - const firstError = Object.keys(errors).find((e) => e); - if (firstError) { - scrollTo(firstError); - } return errors; }; @@ -732,44 +731,50 @@ const UserAddEditForm = (props: UserProps) => { return ( <> {(state.form.user_type === "Doctor" || - state.form.user_type === "Nurse") && ( - { - handleFieldChange(e, field); - }} - className="flex-1" - aria-label={t("qualification")} - /> - )} - {state.form.user_type === "Doctor" && ( -
- { - handleFieldChange(e, field); - }} - className="flex-1" - aria-label={t("years_of_experience")} - /> - + state.form.user_type === "Nurse") && + includedFields?.includes("qualification") && ( { handleFieldChange(e, field); }} className="flex-1" - aria-label={t("medical_council_registration")} + aria-label={t("qualification")} /> + )} + {state.form.user_type === "Doctor" && ( +
+ {includedFields?.includes("doctor_experience_commenced_on") && ( + { + handleFieldChange(e, field); + }} + className="flex-1" + aria-label={t("years_of_experience")} + /> + )} + {includedFields?.includes( + "doctor_medical_council_registration", + ) && ( + { + handleFieldChange(e, field); + }} + className="flex-1" + aria-label={t("medical_council_registration")} + /> + )}
)} @@ -779,42 +784,44 @@ const UserAddEditForm = (props: UserProps) => { const renderPhoneNumberFields = (field: FormContextValue) => { return ( <> -
-
+ {includedFields?.includes("phone_number") && ( +
+
+ { + handlePhoneChange(e, field); + }} + className="" + aria-label={t("phone_number")} + /> + { + handlePhoneChange(e, field); + }} + label={t("is_phone_a_whatsapp_number")} + /> +
{ handlePhoneChange(e, field); }} - className="" - aria-label={t("phone_number")} - /> - { - handlePhoneChange(e, field); - }} - label={t("is_phone_a_whatsapp_number")} + className="flex-1" + aria-label={t("whatsapp_number")} />
- { - handlePhoneChange(e, field); - }} - className="flex-1" - aria-label={t("whatsapp_number")} - /> -
+ )} ); }; @@ -822,230 +829,248 @@ const UserAddEditForm = (props: UserProps) => { const renderUsernameField = (field: FormContextValue) => { return ( <> - { - handleFieldChange(e, field); - setUsernameInput(e.value); - }} - onFocus={() => setUsernameInputInFocus(true)} - onBlur={() => { - setUsernameInputInFocus(false); - }} - aria-label={t("username")} - /> - {usernameInputInFocus && ( -
-
- {usernameExists !== userExistsEnums.idle && ( - <> - {usernameExists === userExistsEnums.checking ? ( - - {" "} - checking... - - ) : ( + {includedFields?.includes("username") && ( + <> + { + handleFieldChange(e, field); + setUsernameInput(e.value); + }} + onFocus={() => setUsernameInputInFocus(true)} + onBlur={() => { + setUsernameInputInFocus(false); + }} + aria-label={t("username")} + /> + {usernameInputInFocus && ( +
+
+ {usernameExists !== userExistsEnums.idle && ( <> - {usernameExists === userExistsEnums.exists ? ( -
- {" "} - - {t("username_not_available")} - -
+ {usernameExists === userExistsEnums.checking ? ( + + {" "} + checking... + ) : ( -
- {" "} - - {t("username_available")} - -
+ <> + {usernameExists === userExistsEnums.exists ? ( +
+ {" "} + + {t("username_not_available")} + +
+ ) : ( +
+ {" "} + + {t("username_available")} + +
+ )} + )} )} - +
+
+ {validateRule( + usernameInput.length >= 4 && usernameInput.length <= 16, + "Username should be 4-16 characters long", + !state.form.username, + )} + {validateRule( + /^[a-z0-9._-]*$/.test(usernameInput), + "Username can only contain lowercase letters, numbers, and . _ -", + !state.form.username, + )} + {validateRule( + /^[a-z0-9].*[a-z0-9]$/i.test(usernameInput), + "Username must start and end with a letter or number", + !state.form.username, + )} + {validateRule( + !/(?:[._-]{2,})/.test(usernameInput), + "Username can't contain consecutive special characters . _ -", + !state.form.username, + )} +
+
+ )} + + )} + + ); + }; + + const renderPasswordFields = (field: FormContextValue) => { + return ( + <> +
+ {includedFields?.includes("password") && ( +
+ setPasswordInputInFocus(true)} + onBlur={() => setPasswordInputInFocus(false)} + onChange={(e) => { + handleFieldChange(e, field); + }} + aria-label={t("password")} + /> + {passwordInputInFocus && state.form.password && ( +
+ {validateRule( + state.form.password.length >= 8, + "Password should be atleast 8 characters long", + !state.form.password, + )} + {validateRule( + state.form.password !== state.form.password.toUpperCase(), + "Password should contain at least 1 lowercase letter", + !state.form.password, + )} + {validateRule( + state.form.password !== state.form.password.toLowerCase(), + "Password should contain at least 1 uppercase letter", + !state.form.password, + )} + {validateRule( + /\d/.test(state.form.password), + "Password should contain at least 1 number", + !state.form.password, + )} +
)}
-
- {validateRule( - usernameInput.length >= 4 && usernameInput.length <= 16, - "Username should be 4-16 characters long", - !state.form.username, - )} - {validateRule( - /^[a-z0-9._-]*$/.test(usernameInput), - "Username can only contain lowercase letters, numbers, and . _ -", - !state.form.username, - )} - {validateRule( - /^[a-z0-9].*[a-z0-9]$/i.test(usernameInput), - "Username must start and end with a letter or number", - !state.form.username, - )} - {validateRule( - !/(?:[._-]{2,})/.test(usernameInput), - "Username can't contain consecutive special characters . _ -", - !state.form.username, - )} + )} + {includedFields?.includes("c_password") && ( +
+ setConfirmPasswordInputInFocus(true)} + onBlur={() => setConfirmPasswordInputInFocus(false)} + onChange={(e) => { + handleFieldChange(e, field); + }} + aria-label={t("confirm_password")} + /> + {confirmPasswordInputInFocus && + state.form.c_password && + state.form.c_password.length > 0 && ( +
+ {validateRule( + state.form.c_password === state.form.password, + "Confirm password should match the entered password", + !state.form.c_password, + )} +
+ )}
-
- )} + )} +
); }; - const renderPasswordFields = (field: FormContextValue) => { + const renderPersonalInfoFields = (field: FormContextValue) => { return ( <>
-
+ {includedFields?.includes("first_name") && ( setPasswordInputInFocus(true)} - onBlur={() => setPasswordInputInFocus(false)} + label={t("first_name")} + className="flex-1" onChange={(e) => { handleFieldChange(e, field); }} - aria-label={t("password")} + aria-label={t("first_name")} /> - {passwordInputInFocus && state.form.password && ( -
- {validateRule( - state.form.password.length >= 8, - "Password should be atleast 8 characters long", - !state.form.password, - )} - {validateRule( - state.form.password !== state.form.password.toUpperCase(), - "Password should contain at least 1 lowercase letter", - !state.form.password, - )} - {validateRule( - state.form.password !== state.form.password.toLowerCase(), - "Password should contain at least 1 uppercase letter", - !state.form.password, - )} - {validateRule( - /\d/.test(state.form.password), - "Password should contain at least 1 number", - !state.form.password, - )} -
- )} -
-
+ )} + {includedFields?.includes("last_name") && ( setConfirmPasswordInputInFocus(true)} - onBlur={() => setConfirmPasswordInputInFocus(false)} + label={t("last_name")} + className="flex-1" onChange={(e) => { handleFieldChange(e, field); }} - aria-label={t("confirm_password")} + aria-label={t("last_name")} /> - {confirmPasswordInputInFocus && - state.form.c_password && - state.form.c_password.length > 0 && ( -
- {validateRule( - state.form.c_password === state.form.password, - "Confirm password should match the entered password", - !state.form.c_password, - )} -
- )} -
+ )}
- - ); - }; - - const renderPersonalInfoFields = (field: FormContextValue) => { - return ( - <> -
+ {includedFields?.includes("email") && ( { handleFieldChange(e, field); }} - aria-label={t("first_name")} + aria-label={t("email")} /> - { - handleFieldChange(e, field); - }} - aria-label={t("last_name")} - /> -
- { - handleFieldChange(e, field); - }} - aria-label={t("email")} - /> + )}
- { - handleDateChange(e, field); - }} - disableFuture - className="flex-1" - aria-label={t("date_of_birth")} - /> - o.text} - optionValue={(o) => o.text} - onChange={(e) => { - handleFieldChange(e, field); - }} - className="flex-1" - aria-label={t("gender")} - /> + {includedFields?.includes("date_of_birth") && ( + { + handleDateChange(e, field); + }} + disableFuture + className="flex-1" + aria-label={t("date_of_birth")} + /> + )} + {includedFields?.includes("gender") && ( + o.text} + optionValue={(o) => o.text} + onChange={(e) => { + handleFieldChange(e, field); + }} + className="flex-1" + aria-label={t("gender")} + /> + )}
); @@ -1057,29 +1082,33 @@ const UserAddEditForm = (props: UserProps) => { return ( <>
- { - handleFieldChange(e, field); - }} - aria-label={t("average_weekly_working_hours")} - /> - { - handleFieldChange(e, field); - }} - aria-label={t("video_conference_link")} - /> + {includedFields?.includes("weekly_working_hours") && ( + { + handleFieldChange(e, field); + }} + aria-label={t("average_weekly_working_hours")} + /> + )} + {includedFields?.includes("video_connect_link") && ( + { + handleFieldChange(e, field); + }} + aria-label={t("video_conference_link")} + /> + )}
); @@ -1090,63 +1119,72 @@ const UserAddEditForm = (props: UserProps) => { ) => { return ( <> - {isStateLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - onChange={(e) => { - handleFieldChange(e, field); - if (e) setSelectedStateId(e.value); - }} - aria-label={t("state")} - /> + {includedFields?.includes("state") && ( + <> + {isStateLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e, field); + if (e) setSelectedStateId(e.value); + }} + aria-label={t("state")} + /> + )} + )} - - {isDistrictLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - onChange={(e) => { - handleFieldChange(e, field); - if (e) setSelectedDistrictId(e.value); - }} - aria-label={t("district")} - /> + {includedFields?.includes("district") && ( + <> + {isDistrictLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e, field); + if (e) setSelectedDistrictId(e.value); + }} + aria-label={t("district")} + /> + )} + + )} + {includedFields?.includes("local_body") && ( + <> + {isLocalbodyLoading ? ( + + ) : ( + o.name} + optionValue={(o) => o.id} + onChange={(e) => { + handleFieldChange(e, field); + }} + aria-label={t("local_body")} + /> + )} + )} - - {showLocalbody && - (isLocalbodyLoading ? ( - - ) : ( - o.name} - optionValue={(o) => o.id} - onChange={(e) => { - handleFieldChange(e, field); - }} - aria-label={t("local_body")} - /> - ))} ); }; @@ -1156,44 +1194,50 @@ const UserAddEditForm = (props: UserProps) => { ) => { return ( <> -
- {t("facilities")} - -
+ {includedFields?.includes("facilities") && ( +
+ {t("facilities")} + +
+ )}
- o.role + (o.readOnly ? " (Read Only)" : "")} - onChange={(e) => { - handleFieldChange(e, field); - }} - optionValue={(o) => o.id} - className="flex-1" - aria-label={t("user_type")} - /> - option.name} - optionValue={(option) => option.id} - onChange={(e) => { - handleFieldChange(e, field); - }} - className="flex-1" - aria-label={t("home_facility")} - /> + {includedFields?.includes("user_type") && ( + o.role + (o.readOnly ? " (Read Only)" : "")} + onChange={(e) => { + handleFieldChange(e, field); + }} + optionValue={(o) => o.id} + className="flex-1" + aria-label={t("user_type")} + /> + )} + {includedFields?.includes("home_facility") && ( + option.name} + optionValue={(option) => option.id} + onChange={(e) => { + handleFieldChange(e, field); + }} + className="flex-1" + aria-label={t("home_facility")} + /> + )}
); @@ -1217,15 +1261,14 @@ const UserAddEditForm = (props: UserProps) => { {(field) => ( <>
- {!editUser && renderFacilityUserTypeHomeFacilityFields(field)} - {!editUser && renderDoctorOrNurseFields(field)} + {renderFacilityUserTypeHomeFacilityFields(field)} + {renderDoctorOrNurseFields(field)} {renderPhoneNumberFields(field)} - {!editUser && renderUsernameField(field)} - {!editUser && renderPasswordFields(field)} + {renderUsernameField(field)} + {renderPasswordFields(field)} {renderPersonalInfoFields(field)} - {editUser && renderHoursAndConferenceLinkFields(field)} - {editUser && renderDoctorOrNurseFields(field)} - {!editUser && renderStateDistrictLocalBodyFields(field)} + {renderHoursAndConferenceLinkFields(field)} + {renderStateDistrictLocalBodyFields(field)}
)} diff --git a/src/components/Users/UserEditDetails.tsx b/src/components/Users/UserEditDetails.tsx new file mode 100644 index 00000000000..89506ae56cf --- /dev/null +++ b/src/components/Users/UserEditDetails.tsx @@ -0,0 +1,253 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import UserAddEditForm from "@/components/Users/UserAddEditForm"; +import { + editBasicInfoFields, + editContactInfoFields, + editProfessionalInfoFields, +} from "@/components/Users/UserFormValidations"; +import { + BasicInfoDetails, + ContactInfoDetails, + ProfessionalInfoDetails, +} from "@/components/Users/UserViewDetails"; +import { UserModel } from "@/components/Users/models"; + +interface UserEditDetailsProps { + username: string; + userData: UserModel; + onSubmitSuccess: () => void; +} + +export function UserBasicInfoView({ + username, + userData, + onSubmitSuccess, +}: UserEditDetailsProps) { + const { t } = useTranslation(); + const [isEditing, setIsEditing] = useState(false); + + const ViewEditToggle = ({ + isEditing, + setIsEditing, + }: { + isEditing: boolean; + setIsEditing: (value: boolean) => void; + }) => ( +
+ + +
+ ); + + return ( +
+ + {isEditing ? ( + { + setIsEditing(false); + onSubmitSuccess(); + }} + /> + ) : ( + + )} +
+ ); +} + +export function UserContactInfoView({ + username, + userData, + onSubmitSuccess, +}: UserEditDetailsProps) { + const { t } = useTranslation(); + const [isEditing, setIsEditing] = useState(false); + + const ViewEditToggle = ({ + isEditing, + setIsEditing, + }: { + isEditing: boolean; + setIsEditing: (value: boolean) => void; + }) => ( +
+ + +
+ ); + + return ( +
+ + {isEditing ? ( + { + setIsEditing(false); + onSubmitSuccess(); + }} + /> + ) : ( + + )} +
+ ); +} + +export function UserProfessionalInfoView({ + username, + userData, + onSubmitSuccess, +}: UserEditDetailsProps) { + const { t } = useTranslation(); + const [isEditing, setIsEditing] = useState(false); + + // Only render if user is Doctor or Nurse + if (userData.user_type !== "Doctor" && userData.user_type !== "Nurse") { + return null; + } + + const ViewEditToggle = ({ + isEditing, + setIsEditing, + }: { + isEditing: boolean; + setIsEditing: (value: boolean) => void; + }) => ( +
+ + +
+ ); + + return ( +
+ + {isEditing ? ( + { + setIsEditing(false); + onSubmitSuccess(); + }} + /> + ) : ( + + )} +
+ ); +} + +export default function UserEditDetails({ + username, + userData, + onSubmitSuccess, +}: UserEditDetailsProps) { + return ( +
+ + + +
+ ); +} diff --git a/src/components/Users/UserFormValidations.tsx b/src/components/Users/UserFormValidations.tsx index 37454964009..46f607fb8f8 100644 --- a/src/components/Users/UserFormValidations.tsx +++ b/src/components/Users/UserFormValidations.tsx @@ -29,6 +29,69 @@ export type UserForm = { weekly_working_hours?: string | null; }; +export const newUserFields: Array = [ + "user_type", + "username", + "password", + "c_password", + "first_name", + "last_name", + "email", + "phone_number", + "alt_phone_number", + "phone_number_is_whatsapp", + "gender", + "date_of_birth", + "state", + "district", + "local_body", + "facilities", + "home_facility", + "qualification", + "doctor_experience_commenced_on", + "doctor_medical_council_registration", + "weekly_working_hours", + "video_connect_link", +]; + +export const editUserFields: Array = [ + "first_name", + "last_name", + "date_of_birth", + "gender", + "email", + "video_connect_link", + "phone_number", + "alt_phone_number", + "phone_number_is_whatsapp", + "qualification", + "doctor_experience_commenced_on", + "doctor_medical_council_registration", + "weekly_working_hours", +]; + +export const editBasicInfoFields: Array = [ + "first_name", + "last_name", + "date_of_birth", + "gender", +]; + +export const editContactInfoFields: Array = [ + "email", + "phone_number", + "alt_phone_number", + "phone_number_is_whatsapp", +]; + +export const editProfessionalInfoFields: Array = [ + "qualification", + "doctor_experience_commenced_on", + "doctor_medical_council_registration", + "weekly_working_hours", + "video_connect_link", +]; + export type EditForm = { user_type: string; qualification?: string | null; diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index a318b569c52..5d8e089a01f 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -21,7 +21,7 @@ import useQuery from "@/Utils/request/useQuery"; import { classNames, formatName, keysOf } from "@/Utils/utils"; export interface UserHomeProps { - username: string; + username?: string; tab: string; } export interface tabChildProp { @@ -30,10 +30,14 @@ export interface tabChildProp { } export default function UserHome(props: UserHomeProps) { - const { username, tab } = props; + const { tab } = props; + let { username } = props; const [userData, setUserData] = useState(); const { t } = useTranslation(); const authUser = useAuthUser(); + if (!username) { + username = authUser.username; + } const { loading, refetch: refetchUserDetails } = useQuery( routes.getUserDetails, @@ -133,6 +137,7 @@ export default function UserHome(props: UserHomeProps) {
diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserInformation.tsx index 83c5cc9e19e..a5a10d2e796 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserInformation.tsx @@ -6,7 +6,7 @@ import { Avatar } from "@/components/Common/Avatar"; import AvatarEditModal from "@/components/Common/AvatarEditModal"; import ButtonV2 from "@/components/Common/ButtonV2"; import Loading from "@/components/Common/Loading"; -import UserAddEditForm from "@/components/Users/UserAddEditForm"; +import { UserViewDetails } from "@/components/Users/UserViewDetails"; import useAuthUser from "@/hooks/useAuthUser"; @@ -20,12 +20,11 @@ import uploadFile from "@/Utils/request/uploadFile"; import useQuery from "@/Utils/request/useQuery"; import { formatDisplayName, sleep } from "@/Utils/utils"; -import { UserViewDetails } from "./UserViewDetails"; +import UserEditDetails from "./UserEditDetails"; export default function UserInformation({ username }: { username: string }) { const { t } = useTranslation(); const [editAvatar, setEditAvatar] = useState(false); - const [isEditing, setIsEditing] = useState(false); const authUser = useAuthUser(); const { @@ -86,43 +85,6 @@ export default function UserInformation({ username }: { username: string }) { const avatarPermissions = showAvatarEdit(authUser, userData); const editPermissions = editUserPermissions(authUser, userData); - const ViewEditToggle = ({ - isEditing, - setIsEditing, - }: { - isEditing: boolean; - setIsEditing: (value: boolean) => void; - }) => ( -
- - -
- ); - return ( <>
)} -
- - {isEditing ? ( - setIsEditing(false)} - /> - ) : ( - - )} -
-
- )} - {!editPermissions && ( -
- +
)} + {!editPermissions && } ); } diff --git a/src/components/Users/UserSummary.tsx b/src/components/Users/UserSummary.tsx index a1945d3956c..f23e5774fcc 100644 --- a/src/components/Users/UserSummary.tsx +++ b/src/components/Users/UserSummary.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import ButtonV2 from "@/components/Common/ButtonV2"; +import LanguageSelector from "@/components/Common/LanguageSelector"; import userColumns from "@/components/Common/UserColumns"; import UserDeleteDialog from "@/components/Users/UserDeleteDialog"; import UserInformation from "@/components/Users/UserInformation"; @@ -66,17 +67,28 @@ export default function UserSummaryTab({ userData }: { userData?: UserModel }) {
{userColumns( t("personal_information"), - t("personal_information_note"), + authUser.username === userData.username + ? t("personal_information_note_self") + : t("personal_information_note"), UserInformation, userColumnsData, )} {passwordResetPermitted && userColumns( t("reset_password"), - t("reset_password_note"), + authUser.username === userData.username + ? t("reset_password_note_self") + : t("reset_password_note"), UserResetPassword, userColumnsData, )} + {authUser.username === userData.username && + userColumns( + t("language_selection"), + t("set_your_local_language"), + LanguageSelector, + userColumnsData, + )} {deletePermitted && (
diff --git a/src/components/Users/UserViewDetails.tsx b/src/components/Users/UserViewDetails.tsx index 3d8d1fa1bde..e874177fc56 100644 --- a/src/components/Users/UserViewDetails.tsx +++ b/src/components/Users/UserViewDetails.tsx @@ -46,42 +46,54 @@ export const Badge = ({ ); }; -export const UserViewDetails = ({ user }: UserViewDetailsProps) => { +export const BasicInfoDetails = ({ user }: UserViewDetailsProps) => { const { t } = useTranslation(); return ( -
-
- -
- - - - - - -
+
+ +
+ + + + + +
+
+ ); +}; -
- -
- - - -
+export const ContactInfoDetails = ({ user }: UserViewDetailsProps) => { + const { t } = useTranslation(); + + return ( +
+ +
+ + +
+
+ ); +}; +export const ProfessionalInfoDetails = ({ user }: UserViewDetailsProps) => { + const { t } = useTranslation(); + + return ( + <> {(user.user_type === "Doctor" || user.user_type === "Nurse") && (
@@ -110,6 +122,22 @@ export const UserViewDetails = ({ user }: UserViewDetailsProps) => {
)} + + ); +}; + +export const UserViewDetails = ({ user }: UserViewDetailsProps) => { + return ( +
+
+ +
+
+ +
+
+ +
); }; From dd40aeea0389a7f85e69ebe227aef2ecfc759469 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Wed, 20 Nov 2024 12:58:31 +0530 Subject: [PATCH 39/55] UserSummary Tab Changes - moving out the form/view components into UserSummary - allowing for separate heading/note sections for each --- public/locale/en.json | 10 +- .../{UserInformation.tsx => UserAvatar.tsx} | 78 +++++------- src/components/Users/UserEditDetails.tsx | 36 +----- src/components/Users/UserSummary.tsx | 116 +++++++++++++++++- src/components/Users/UserViewDetails.tsx | 16 --- 5 files changed, 154 insertions(+), 102 deletions(-) rename src/components/Users/{UserInformation.tsx => UserAvatar.tsx} (55%) diff --git a/public/locale/en.json b/public/locale/en.json index 584ba553598..e874e56bfd5 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -503,6 +503,8 @@ "consultation_notes": "General Instructions (Advice)", "consultation_updates": "Consultation updates", "contact_info": "Contact Information", + "contact_info_note": "View or update user's contact information", + "contact_info_note_self": "View or update your contact information", "contact_number": "Contact Number", "contact_person": "Name of Contact Person at Facility", "contact_person_at_the_facility": "Contact person at the current facility", @@ -615,6 +617,8 @@ "duplicate_patient_record_rejection": "I confirm that the suspect / patient I want to create is not on the list.", "edit": "Edit", "edit_avatar": "Edit Avatar", + "edit_avatar_note": "Change the avatar of the user", + "edit_avatar_note_self": "Change your avatar", "edit_avatar_permission_error": "You do not have permissions to edit the avatar of this user", "edit_caution_note": "A new prescription will be added to the consultation with the edited details and the current prescription will be discontinued.", "edit_cover_photo": "Edit Cover Photo", @@ -1038,8 +1042,8 @@ "patient_transfer_birth_match_note": "Note: Year of birth must match the patient to process the transfer request.", "patients": "Patients", "personal_information": "Personal Information", - "personal_information_note": "Update User's Personal Information", - "personal_information_note_self": "Update Your Personal Information", + "personal_information_note": "View or update user's personal information", + "personal_information_note_self": "View or update your personal information", "phone": "Phone", "phone_no": "Phone no.", "phone_number": "Phone Number", @@ -1103,6 +1107,8 @@ "procedure_suggestions": "Procedure Suggestions", "procedures_select_placeholder": "Select procedures to add details", "professional_info": "Professional Information", + "professional_info_note": "View or update user's professional information", + "professional_info_note_self": "View or update your professional information", "profile": "Profile", "provisional": "Provisional", "qualification": "Qualification", diff --git a/src/components/Users/UserInformation.tsx b/src/components/Users/UserAvatar.tsx similarity index 55% rename from src/components/Users/UserInformation.tsx rename to src/components/Users/UserAvatar.tsx index a5a10d2e796..26443811c0a 100644 --- a/src/components/Users/UserInformation.tsx +++ b/src/components/Users/UserAvatar.tsx @@ -6,23 +6,20 @@ import { Avatar } from "@/components/Common/Avatar"; import AvatarEditModal from "@/components/Common/AvatarEditModal"; import ButtonV2 from "@/components/Common/ButtonV2"; import Loading from "@/components/Common/Loading"; -import { UserViewDetails } from "@/components/Users/UserViewDetails"; import useAuthUser from "@/hooks/useAuthUser"; import { LocalStorageKeys } from "@/common/constants"; import * as Notification from "@/Utils/Notifications"; -import { editUserPermissions, showAvatarEdit } from "@/Utils/permissions"; +import { showAvatarEdit } from "@/Utils/permissions"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; import uploadFile from "@/Utils/request/uploadFile"; import useQuery from "@/Utils/request/useQuery"; import { formatDisplayName, sleep } from "@/Utils/utils"; -import UserEditDetails from "./UserEditDetails"; - -export default function UserInformation({ username }: { username: string }) { +export default function UserAvatar({ username }: { username: string }) { const { t } = useTranslation(); const [editAvatar, setEditAvatar] = useState(false); const authUser = useAuthUser(); @@ -82,9 +79,6 @@ export default function UserInformation({ username }: { username: string }) { } }; - const avatarPermissions = showAvatarEdit(authUser, userData); - const editPermissions = editUserPermissions(authUser, userData); - return ( <> setEditAvatar(false)} /> - {editPermissions && ( -
- {avatarPermissions && ( -
-
- -
- setEditAvatar(!editAvatar)} - type="button" - id="edit-cancel-profile-button" - className="border border-gray-200 bg-gray-50 text-black hover:bg-gray-100" - shadow={false} - disabled={!showAvatarEdit(authUser, userData)} - tooltip={ - !showAvatarEdit(authUser, userData) - ? t("edit_avatar_permission_error") - : undefined - } - > - {t("change_avatar")} - -

- {t("change_avatar_note")} -

-
-
+
+
+
+ +
+ setEditAvatar(!editAvatar)} + type="button" + id="edit-cancel-profile-button" + className="border border-gray-200 bg-gray-50 text-black hover:bg-gray-100" + shadow={false} + disabled={!showAvatarEdit(authUser, userData)} + tooltip={ + !showAvatarEdit(authUser, userData) + ? t("edit_avatar_permission_error") + : undefined + } + > + {t("change_avatar")} + +

+ {t("change_avatar_note")} +

- )} - +
- )} - {!editPermissions && } +
); } diff --git a/src/components/Users/UserEditDetails.tsx b/src/components/Users/UserEditDetails.tsx index 89506ae56cf..b80031212c6 100644 --- a/src/components/Users/UserEditDetails.tsx +++ b/src/components/Users/UserEditDetails.tsx @@ -17,7 +17,7 @@ import { UserModel } from "@/components/Users/models"; interface UserEditDetailsProps { username: string; userData: UserModel; - onSubmitSuccess: () => void; + onSubmitSuccess?: () => void; } export function UserBasicInfoView({ @@ -35,7 +35,7 @@ export function UserBasicInfoView({ isEditing: boolean; setIsEditing: (value: boolean) => void; }) => ( -
+
); } - -export default function UserEditDetails({ - username, - userData, - onSubmitSuccess, -}: UserEditDetailsProps) { - return ( -
- - - -
- ); -} diff --git a/src/components/Users/UserSummary.tsx b/src/components/Users/UserSummary.tsx index f23e5774fcc..de28b9a127f 100644 --- a/src/components/Users/UserSummary.tsx +++ b/src/components/Users/UserSummary.tsx @@ -7,19 +7,40 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import ButtonV2 from "@/components/Common/ButtonV2"; import LanguageSelector from "@/components/Common/LanguageSelector"; import userColumns from "@/components/Common/UserColumns"; +import UserAvatar from "@/components/Users/UserAvatar"; import UserDeleteDialog from "@/components/Users/UserDeleteDialog"; -import UserInformation from "@/components/Users/UserInformation"; +import { + UserBasicInfoView, + UserContactInfoView, + UserProfessionalInfoView, +} from "@/components/Users/UserEditDetails"; import UserResetPassword from "@/components/Users/UserResetPassword"; +import { + BasicInfoDetails, + ContactInfoDetails, + ProfessionalInfoDetails, +} from "@/components/Users/UserViewDetails"; import { UserModel } from "@/components/Users/models"; import useAuthUser from "@/hooks/useAuthUser"; import * as Notification from "@/Utils/Notifications"; -import { showUserDelete, showUserPasswordReset } from "@/Utils/permissions"; +import { + editUserPermissions, + showAvatarEdit, + showUserDelete, + showUserPasswordReset, +} from "@/Utils/permissions"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; -export default function UserSummaryTab({ userData }: { userData?: UserModel }) { +export default function UserSummaryTab({ + userData, + refetchUserData, +}: { + userData?: UserModel; + refetchUserData?: () => void; +}) { const { t } = useTranslation(); const [showDeleteDialog, setshowDeleteDialog] = useState(false); const [isDeleting, setIsDeleting] = useState(false); @@ -49,9 +70,66 @@ export default function UserSummaryTab({ userData }: { userData?: UserModel }) { } }; - const userColumnsData = { userData, username: userData.username }; + const userColumnsData = { + userData, + username: userData.username, + refetchUserData, + }; const deletePermitted = showUserDelete(authUser, userData); const passwordResetPermitted = showUserPasswordReset(authUser, userData); + const avatarPermitted = showAvatarEdit(authUser, userData); + const editPermissions = editUserPermissions(authUser, userData); + + const renderBasicInformation = () => { + if (editPermissions) { + return ( + + ); + } + return ( +
+ +
+ ); + }; + + const renderContactInformation = () => { + if (editPermissions) { + return ( + + ); + } + return ( +
+ +
+ ); + }; + + const renderProfessionalInformation = () => { + if (editPermissions) { + return ( + + ); + } + return ( +
+ +
+ ); + }; return ( <> @@ -64,15 +142,41 @@ export default function UserSummaryTab({ userData }: { userData?: UserModel }) { }} /> )} -
+
+ {avatarPermitted && + userColumns( + t("edit_avatar"), + authUser.username === userData.username + ? t("edit_avatar_note_self") + : t("edit_avatar_note"), + UserAvatar, + userColumnsData, + )} {userColumns( t("personal_information"), authUser.username === userData.username ? t("personal_information_note_self") : t("personal_information_note"), - UserInformation, + renderBasicInformation, + userColumnsData, + )} + {userColumns( + t("contact_info"), + authUser.username === userData.username + ? t("contact_info_note_self") + : t("contact_info_note"), + renderContactInformation, userColumnsData, )} + {(userData.user_type === "Doctor" || userData.user_type === "Nurse") && + userColumns( + t("professional_info"), + authUser.username === userData.username + ? t("professional_info_note_self") + : t("professional_info_note"), + renderProfessionalInformation, + userColumnsData, + )} {passwordResetPermitted && userColumns( t("reset_password"), diff --git a/src/components/Users/UserViewDetails.tsx b/src/components/Users/UserViewDetails.tsx index e874177fc56..4a5307af5e3 100644 --- a/src/components/Users/UserViewDetails.tsx +++ b/src/components/Users/UserViewDetails.tsx @@ -125,19 +125,3 @@ export const ProfessionalInfoDetails = ({ user }: UserViewDetailsProps) => { ); }; - -export const UserViewDetails = ({ user }: UserViewDetailsProps) => { - return ( -
-
- -
-
- -
-
- -
-
- ); -}; From 0ec3c06b6afe967a3f7830e4b67a7c6b828dc144 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Thu, 21 Nov 2024 00:09:25 +0530 Subject: [PATCH 40/55] Tests and minor fixes - Ensure weekly working hours and video conference link is rendered for everyone - Minor refactoring - Moving out toggle button as common element for user detail components - Added and/or modified tests for user detail components --- cypress/e2e/users_spec/UsersManage.cy.ts | 172 ++++++++++++-- cypress/pageobject/Login/LoginPage.ts | 6 +- cypress/pageobject/Users/ManageUserPage.ts | 227 ++++++++++++++++--- public/locale/en.json | 5 +- src/components/Users/UserAvatar.tsx | 2 +- src/components/Users/UserEditDetails.tsx | 146 ++++-------- src/components/Users/UserFormValidations.tsx | 14 +- src/components/Users/UserSummary.tsx | 17 +- src/components/Users/UserViewDetails.tsx | 96 +++++--- 9 files changed, 488 insertions(+), 197 deletions(-) diff --git a/cypress/e2e/users_spec/UsersManage.cy.ts b/cypress/e2e/users_spec/UsersManage.cy.ts index 6a9c48e6e0e..a27c3b585aa 100644 --- a/cypress/e2e/users_spec/UsersManage.cy.ts +++ b/cypress/e2e/users_spec/UsersManage.cy.ts @@ -17,6 +17,8 @@ describe("Manage User", () => { const lastNameUserSkill = "Doctor"; const userCreationPage = new UserCreationPage(); const usernameforworkinghour = "devdistrictadmin"; + const nurseUsername = "dummynurse1"; + const doctorUsername = "devdoctor"; const usernamerealname = "Dummy Doctor"; const facilitytolinkusername = "Dummy Shifting Center"; const facilitytolinkskill = "Dummy Facility 40"; @@ -34,28 +36,155 @@ describe("Manage User", () => { cy.awaitUrl("/users"); }); - it("edit a user and verify its reflection in profile", () => { - userPage.typeInSearchInput(usernameforworkinghour); - userPage.checkUsernameText(usernameforworkinghour); - manageUserPage.clickMoreDetailsButton(usernameforworkinghour); + // To Do: Add avatar upload + /* it("District Admin can change their own avatar", () => { + userPage.typeInSearchInput(nurseUsername); + userPage.checkUsernameText(nurseUsername); + manageUserPage.clickMoreDetailsButton(nurseUsername); manageUserPage.verifyMoreDetailsPage(); - manageUserPage.editUserDetails( - "Devo", - "Districto", - "dev@test.com", - "01081999", - "Female", - ); + manageUserPage.verifyChangeAvatarButtonVisible(); + manageUserPage.clickChangeAvatarButton(); + }); */ + + it("edit a nurse user's basic information and verify its reflection", () => { + userPage.typeInSearchInput(nurseUsername); + userPage.checkUsernameText(nurseUsername); + manageUserPage.clickMoreDetailsButton(nurseUsername); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickBasicInfoViewButton(); + manageUserPage.clickBasicInfoEditButton(); + manageUserPage.clearUserBasicInfo(); manageUserPage.clickSubmit(); + manageUserPage.verifyErrorText("First Name is required"); + manageUserPage.verifyErrorText("Last Name is required"); + manageUserPage.editUserBasicInfo("Devo", "Districto", "11081999", "Female"); + manageUserPage.clickSubmit(); + manageUserPage.clickBasicInfoViewButton(); manageUserPage.verifyEditUserDetails( "Devo", "Districto", - "dev@test.com", - "01/08/1999", + "8/11/1999", "Female", ); }); + it("edit a nurse user's contact information and verify its reflection", () => { + userPage.typeInSearchInput(nurseUsername); + userPage.checkUsernameText(nurseUsername); + manageUserPage.clickMoreDetailsButton(nurseUsername); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickContactInfoViewButton(); + manageUserPage.clickContactInfoEditButton(); + manageUserPage.clearUserContactInfo(); + manageUserPage.clickSubmit(); + manageUserPage.verifyErrorText("Please enter a valid email address"); + manageUserPage.verifyErrorText("Please enter a valid phone number"); + manageUserPage.editUserContactInfo("dev@gmail.com", "6234343435"); + manageUserPage.clickSubmit(); + manageUserPage.clickContactInfoViewButton(); + manageUserPage.verifyEditUserContactInfo("dev@gmail.com", "6234343435"); + }); + + it("edit a nurse user's professional information and verify its reflection", () => { + userPage.typeInSearchInput(nurseUsername); + userPage.checkUsernameText(nurseUsername); + manageUserPage.clickMoreDetailsButton(nurseUsername); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickProfessionalInfoViewButton(); + manageUserPage.clickProfessionalInfoEditButton(); + // Should have qualification field + // Should not have years of experience and medical council registration fields + manageUserPage.verifyQualificationExist(); + manageUserPage.verifyYoeAndCouncilRegistrationDoesntExist(); + manageUserPage.clearDoctorOrNurseProfessionalInfo(false); + manageUserPage.clickSubmit(); + manageUserPage.verifyErrorText("Qualification is required"); + manageUserPage.editUserProfessionalInfo("Msc"); + manageUserPage.clickSubmit(); + manageUserPage.clickProfessionalInfoViewButton(); + manageUserPage.verifyEditUserProfessionalInfo("Msc"); + }); + + it("edit a doctor user's professional information and verify its reflection", () => { + // Should have qualification, years of experience and medical council registration + userPage.typeInSearchInput(usernameToLinkFacilitydoc1); + userPage.checkUsernameText(usernameToLinkFacilitydoc1); + manageUserPage.clickMoreDetailsButton(usernameToLinkFacilitydoc1); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickProfessionalInfoViewButton(); + manageUserPage.verifyQualificationExist(); + manageUserPage.verifyYoeAndCouncilRegistrationExist(); + manageUserPage.clickProfessionalInfoEditButton(); + manageUserPage.clearDoctorOrNurseProfessionalInfo(true); + manageUserPage.clickSubmit(); + manageUserPage.verifyErrorText("Qualification is required"); + manageUserPage.verifyErrorText("Years of experience is required"); + manageUserPage.verifyErrorText("Medical Council Registration is required"); + manageUserPage.editUserProfessionalInfo("Msc", "120", "1234567890"); + manageUserPage.clickSubmit(); + manageUserPage.verifyErrorText( + "Please enter a valid number between 0 and 100.", + ); + manageUserPage.clearDoctorOrNurseProfessionalInfo(true); + manageUserPage.editUserProfessionalInfo("Msc", "10", "1234567890"); + manageUserPage.clickSubmit(); + manageUserPage.clickProfessionalInfoViewButton(); + const experienceCommencedOn = new Date(Date.now()); + experienceCommencedOn.setFullYear(experienceCommencedOn.getFullYear() - 10); + const formattedDate = experienceCommencedOn.toISOString().split("T")[0]; + manageUserPage.verifyEditUserProfessionalInfo( + "Msc", + formattedDate, + "1234567890", + ); + }); + + it("Nurse user doesn't have edit options (users above them in hierarchy) or password change option (for other users)", () => { + loginPage.ensureLoggedIn(); + loginPage.clickSignOutBtn(); + loginPage.loginManuallyAsNurse(); + loginPage.ensureLoggedIn(); + cy.visit("/users"); + userPage.typeInSearchInput(doctorUsername); + userPage.checkUsernameText(doctorUsername); + manageUserPage.clickMoreDetailsButton(doctorUsername); + manageUserPage.verifyMoreDetailsPage(false); + manageUserPage.verifyUsername(doctorUsername); + manageUserPage.verifyBasicInfoEditButtonNotExist(); + manageUserPage.verifyContactInfoEditButtonNotExist(); + manageUserPage.verifyProfessionalInfoEditButtonNotExist(); + manageUserPage.verifyPasswordEditButtonNotExist(); + loginPage.ensureLoggedIn(); + loginPage.clickSignOutBtn(); + loginPage.loginManuallyAsDistrictAdmin(); + loginPage.ensureLoggedIn(); + }); + + it("District Admin can change a user's password", () => { + userPage.typeInSearchInput(nurseUsername); + userPage.checkUsernameText(nurseUsername); + manageUserPage.clickMoreDetailsButton(nurseUsername); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickPasswordEditButton(); + manageUserPage.changePassword("Coronasafe@123", "Coronasafe@1233"); + manageUserPage.clickSubmit(); + loginPage.ensureLoggedIn(); + loginPage.clickSignOutBtn(); + loginPage.loginManuallyAsNurse("Coronasafe@1233"); + loginPage.ensureLoggedIn(); + loginPage.clickSignOutBtn(); + loginPage.loginManuallyAsDistrictAdmin(); + loginPage.ensureLoggedIn(); + cy.visit("/users"); + userPage.typeInSearchInput(nurseUsername); + userPage.checkUsernameText(nurseUsername); + manageUserPage.clickMoreDetailsButton(nurseUsername); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.clickPasswordEditButton(); + manageUserPage.changePassword("Coronasafe@1233", "Coronasafe@123"); + manageUserPage.clickSubmit(); + }); + it("linking skills for users and verify its reflection in profile", () => { // select the district user and select one skill link and verify its profile reflection userPage.typeInSearchInput(usernameforworkinghour); @@ -64,7 +193,6 @@ describe("Manage User", () => { manageUserPage.verifyMoreDetailsPage(); manageUserPage.clickLinkedSkillTab(); cy.wait(500); - manageUserPage.verifyDoctorQualificationDoesNotExist(); manageUserPage.verifyLinkedSkillsTabPage(); manageUserPage.selectSkillFromDropdown(linkedskill); manageUserPage.clickAddSkillButton(); @@ -101,14 +229,12 @@ describe("Manage User", () => { manageUserPage.clickMoreDetailsButton(usernameToLinkSkill); manageUserPage.verifyMoreDetailsPage(); manageUserPage.clickLinkedSkillTab(); - manageUserPage.verifyDoctorQualification(); manageUserPage.verifyLinkedSkillsTabPage(); manageUserPage.selectSkillFromDropdown(linkedskill); manageUserPage.clickAddSkillButton(); cy.verifyNotification("Skill added successfully"); cy.closeNotification(); manageUserPage.assertSkillInAddedUserSkills(linkedskill); - manageUserPage.clickCloseSlideOver(); // verifying the doctor connect manageUserPage.navigateToFacility(); manageUserPage.typeFacilitySearch(facilitytolinkskill); @@ -119,22 +245,28 @@ describe("Manage User", () => { }); it("add working hour for a user and verify its reflection in card and user profile", () => { - // verify mandatory field error and select working hour for a user + // verify qualification and yoe and council registration fields are not present + // verify field error and add working hour userPage.typeInSearchInput(usernameforworkinghour); userPage.checkUsernameText(usernameforworkinghour); manageUserPage.clickMoreDetailsButton(usernameforworkinghour); manageUserPage.verifyMoreDetailsPage(); manageUserPage.verifyProfileTabPage(); - manageUserPage.clearweeklyhourfield(); + manageUserPage.clickProfessionalInfoViewButton(); + manageUserPage.verifyQualificationDoesntExist(); + manageUserPage.verifyYoeAndCouncilRegistrationDoesntExist(); + manageUserPage.clickProfessionalInfoEditButton(); + manageUserPage.clearProfessionalInfo(); + manageUserPage.typeInWeeklyWorkingHours("200"); manageUserPage.clickSubmit(); manageUserPage.verifyErrorText( "Average weekly working hours must be a number between 0 and 168", ); + manageUserPage.clearProfessionalInfo(); manageUserPage.typeInWeeklyWorkingHours(workinghour); manageUserPage.clickSubmit(); - // verify the data is reflected in user card and profile page + // verify the data is reflected in the page manageUserPage.verifyWorkingHours(workinghour); - manageUserPage.navigateToProfile(); manageUserPage.verifyProfileWorkingHours(workinghour); }); diff --git a/cypress/pageobject/Login/LoginPage.ts b/cypress/pageobject/Login/LoginPage.ts index 714004023f7..7840d3b3914 100644 --- a/cypress/pageobject/Login/LoginPage.ts +++ b/cypress/pageobject/Login/LoginPage.ts @@ -23,9 +23,11 @@ class LoginPage { cy.clickSubmitButton("Login"); } - loginManuallyAsNurse(): void { + loginManuallyAsNurse(password?: string): void { cy.get("input[id='username']").click().type("dummynurse1"); - cy.get("input[id='password']").click().type("Coronasafe@123"); + cy.get("input[id='password']") + .click() + .type(password || "Coronasafe@123"); cy.clickSubmitButton("Login"); } diff --git a/cypress/pageobject/Users/ManageUserPage.ts b/cypress/pageobject/Users/ManageUserPage.ts index f89f72a9435..80ee9bbcab1 100644 --- a/cypress/pageobject/Users/ManageUserPage.ts +++ b/cypress/pageobject/Users/ManageUserPage.ts @@ -56,27 +56,31 @@ export class ManageUserPage { cy.get("#submit").click(); } - clearweeklyhourfield() { - cy.get("#weekly_working_hours").click().clear(); + verifyErrorText(expectedError: string) { + cy.get(".error-text").first().scrollIntoView(); + cy.get(".error-text") + .should("be.visible") + .then(($elements) => { + const errorTextArray = Array.from($elements).map( + (el) => el.textContent, + ); + expect(errorTextArray).to.include(expectedError); + }); } - verifyErrorText(expectedError: string) { - cy.get(".error-text").should("contain", expectedError).and("be.visible"); + clearUserBasicInfo() { + cy.get("input[name='first_name']").click().clear(); + cy.get("input[name='last_name']").click().clear(); } - editUserDetails( + editUserBasicInfo( fName: string, lName: string, - email: string, dateOfBirth: string, gender: string, ) { - cy.get("#first_name").click().clear(); - cy.get("#first_name").click().type(fName); - cy.get("#last_name").click().clear(); - cy.get("#last_name").click().type(lName); - cy.get("#email").click().clear(); - cy.get("#email").click().type(email); + cy.get("input[name='first_name']").click().type(fName); + cy.get("input[name='last_name']").click().type(lName); cy.clickAndTypeDate("#date_of_birth", dateOfBirth); cy.get("#gender").click(); cy.get("[role='option']").contains(gender).click(); @@ -85,19 +89,98 @@ export class ManageUserPage { verifyEditUserDetails( fName: string, lName: string, - email: string, dateOfBirth: string, gender: string, ) { - cy.get("#first_name").should("have.value", fName); - cy.get("#last_name").should("have.value", lName); - cy.get("#email").should("have.value", email); - cy.get("#date_of_birth").should("have.value", dateOfBirth); - cy.get("#gender").should("contain.text", gender); + cy.get("#view-first_name").should("contain.text", fName); + cy.get("#view-last_name").should("contain.text", lName); + cy.get("#view-date_of_birth").should("contain.text", dateOfBirth); + cy.get("#view-gender").should("contain.text", gender); + } + + clearUserContactInfo() { + cy.get("input[name='email']").click().clear(); + cy.get("input[name='phone_number']").click().clear(); + cy.get("input[name='phone_number_is_whatsapp']").should("be.checked"); + } + + editUserContactInfo(email: string, phoneNumber: string) { + cy.get("input[name='email']").click().type(email); + cy.get("input[name='phone_number']").click().type(phoneNumber); + cy.get("input[name='phone_number_is_whatsapp']").should("be.checked"); + } + + verifyEditUserContactInfo(email: string, phoneNumber: string) { + cy.get("#view-email").should("contain.text", email); + cy.get("#view-phone_number").should("contain.text", phoneNumber); + cy.get("#view-whatsapp_number").should("contain.text", phoneNumber); + } + + clearDoctorOrNurseProfessionalInfo(yoeAndCouncilRegistration: boolean) { + cy.get("input[name='qualification']").click().clear(); + if (yoeAndCouncilRegistration) { + cy.get("input[name='doctor_experience_commenced_on']").click().clear(); + cy.get("input[name='doctor_medical_council_registration']") + .click() + .clear(); + } + } + + clearProfessionalInfo() { + cy.get("input[name='weekly_working_hours']").click().clear(); + cy.get("input[name='video_connect_link']").click().clear(); + } + + editUserProfessionalInfo( + qualification: string, + yearsOfExperience?: string, + medicalCouncilRegistration?: string, + ) { + cy.get("input[name='qualification']").click().type(qualification); + if (yearsOfExperience) { + cy.get("input[name='doctor_experience_commenced_on']") + .click() + .type(yearsOfExperience); + } + if (medicalCouncilRegistration) { + cy.get("input[name='doctor_medical_council_registration']") + .click() + .type(medicalCouncilRegistration); + } + } + + verifyEditUserProfessionalInfo( + qualification: string, + yearsOfExperience?: string, + medicalCouncilRegistration?: string, + ) { + cy.get("#view-qualification").should("contain.text", qualification); + if (yearsOfExperience) { + cy.get("#view-years_of_experience").should( + "contain.text", + yearsOfExperience, + ); + } + if (medicalCouncilRegistration) { + cy.get("#view-doctor_medical_council_registration").should( + "contain.text", + medicalCouncilRegistration, + ); + } + } + + verifyPasswordEditButtonNotExist() { + cy.get("#change-edit-password-button").should("not.exist"); + } + + changePassword(oldPassword: string, newPassword: string) { + cy.get("input[name='old_password']").click().type(oldPassword); + cy.get("input[name='new_password_1']").click().type(newPassword); + cy.get("input[name='new_password_2']").click().type(newPassword); } typeInWeeklyWorkingHours(hours: string) { - cy.get("#weekly_working_hours").click().type(hours); + cy.get("input[name='weekly_working_hours']").click().type(hours); } navigateToProfile() { @@ -116,8 +199,8 @@ export class ManageUserPage { } verifyProfileWorkingHours(expectedHours: string) { - cy.get("#averageworkinghour-profile-details").should( - "contain", + cy.get("#view-average_weekly_working_hours").should( + "contain.text", expectedHours, ); } @@ -161,18 +244,106 @@ export class ManageUserPage { } clickMoreDetailsButton(username: string) { + cy.intercept("GET", "**/api/v1/users/**").as("getUserDetails"); cy.get(`#more-details-${username}`).click(); + cy.wait("@getUserDetails"); } - verifyMoreDetailsPage() { - cy.intercept("GET", "**/api/v1/users/get_user/**").as("getUserDetails"); - cy.wait("@getUserDetails"); + verifyMoreDetailsPage(hasPermissions = true) { cy.get("#username").should("be.visible"); cy.get("#role").should("be.visible"); cy.get("#usermanagement_tab_nav").should("be.visible"); cy.get("#profile").should("be.visible"); - cy.get("#facilities").should("be.visible"); - cy.get("#skills").should("be.visible"); + if (hasPermissions) { + cy.get("#facilities").should("be.visible"); + cy.get("#skills").should("be.visible"); + } + cy.get("#view-username").scrollIntoView(); + cy.get("#view-username").should("be.visible"); + } + + verifyChangeAvatarButtonVisible() { + cy.get("#change-avatar").should("be.visible"); + } + + clickChangeAvatarButton() { + cy.get("#change-avatar").click(); + } + + clickBasicInfoViewButton() { + cy.get("#basic-info-view-button").scrollIntoView(); + cy.get("#basic-info-view-button").should("be.visible"); + cy.get("#basic-info-view-button").click(); + } + + clickBasicInfoEditButton() { + cy.get("#basic-info-edit-button").scrollIntoView(); + cy.get("#basic-info-edit-button").should("be.visible"); + cy.get("#basic-info-edit-button").click(); + } + + clickContactInfoViewButton() { + cy.get("#contact-info-view-button").scrollIntoView(); + cy.get("#contact-info-view-button").should("be.visible"); + cy.get("#contact-info-view-button").click(); + } + + clickContactInfoEditButton() { + cy.get("#contact-info-edit-button").scrollIntoView(); + cy.get("#contact-info-edit-button").should("be.visible"); + cy.get("#contact-info-edit-button").click(); + } + + clickProfessionalInfoViewButton() { + cy.get("#professional-info-view-button").scrollIntoView(); + cy.get("#professional-info-view-button").should("be.visible"); + cy.get("#professional-info-view-button").click(); + } + + clickProfessionalInfoEditButton() { + cy.get("#professional-info-edit-button").scrollIntoView(); + cy.get("#professional-info-edit-button").should("be.visible"); + cy.get("#professional-info-edit-button").click(); + } + + clickPasswordEditButton() { + cy.get("#change-edit-password-button").scrollIntoView(); + cy.get("#change-edit-password-button").should("be.visible"); + cy.get("#change-edit-password-button").click(); + } + + verifyQualificationDoesntExist() { + cy.get("input[name='qualification']").should("not.exist"); + } + + verifyQualificationExist() { + cy.get("#view-qualification").should("be.visible"); + } + + verifyYoeAndCouncilRegistrationDoesntExist() { + cy.get("#view-years_of_experience").should("not.exist"); + cy.get("#view-doctor_medical_council_registration").should("not.exist"); + } + + verifyYoeAndCouncilRegistrationExist() { + cy.get("#view-years_of_experience").should("be.visible"); + cy.get("#view-doctor_medical_council_registration").should("be.visible"); + } + + verifyUsername(username: string) { + cy.get("#view-username").should("contain", username); + } + + verifyBasicInfoEditButtonNotExist() { + cy.get("#basic-info-edit-button").should("not.exist"); + } + + verifyContactInfoEditButtonNotExist() { + cy.get("#contact-info-edit-button").should("not.exist"); + } + + verifyProfessionalInfoEditButtonNotExist() { + cy.get("#professional-info-edit-button").should("not.exist"); } verifyProfileTabPage() { @@ -180,11 +351,11 @@ export class ManageUserPage { } verifyDoctorQualification() { - cy.get("input[name='qualification']").should("be.visible"); + cy.get("#view-qualification").should("be.visible"); } verifyDoctorQualificationDoesNotExist() { - cy.get("input[name='qualification']").should("not.exist"); + cy.get("#view-qualification").should("not.exist"); } verifyLinkedSkillsTabPage() { diff --git a/public/locale/en.json b/public/locale/en.json index 83307a91de5..f35242ec4cf 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -607,6 +607,7 @@ "district_program_management_supporting_unit": "District Program Management Supporting Unit", "dob_format": "Please enter date in DD/MM/YYYY format", "doctor_experience_error": "Please enter a valid number between 0 and 100.", + "doctor_experience_required": "Years of experience is required", "doctor_s_medical_council_registration": "Doctor's Medical Council Registration", "doctors_name": "Doctor's Name", "domestic_healthcare_support": "Domestic healthcare support", @@ -801,7 +802,7 @@ "invalid_link_msg": "It appears that the password reset link you have used is either invalid or expired. Please request a new password reset link.", "invalid_password": "Password doesn't meet the requirements", "invalid_password_reset_link": "Invalid password reset link", - "invalid_phone": "Please enter valid phone number", + "invalid_phone": "Please enter a valid phone number", "invalid_phone_number": "Invalid Phone Number", "invalid_pincode": "Invalid Pincode", "invalid_reset": "Invalid Reset", @@ -898,6 +899,7 @@ "measured_after": "Measured after", "measured_before": "Measured before", "medical_council_registration": "Medical Council Registration", + "medical_council_registration_required": "Medical Council Registration is required", "medical_worker": "Medical Worker", "medicine": "Medicine", "medicine_administration_history": "Medicine Administration History", @@ -1129,6 +1131,7 @@ "profile": "Profile", "provisional": "Provisional", "qualification": "Qualification", + "qualification_required": "Qualification is required", "raise_consent_request": "Raise a consent request to fetch patient records over ABDM", "ration_card__APL": "APL", "ration_card__BPL": "BPL", diff --git a/src/components/Users/UserAvatar.tsx b/src/components/Users/UserAvatar.tsx index 26443811c0a..bfe236cb90c 100644 --- a/src/components/Users/UserAvatar.tsx +++ b/src/components/Users/UserAvatar.tsx @@ -101,7 +101,7 @@ export default function UserAvatar({ username }: { username: string }) { setEditAvatar(!editAvatar)} type="button" - id="edit-cancel-profile-button" + id="change-avatar" className="border border-gray-200 bg-gray-50 text-black hover:bg-gray-100" shadow={false} disabled={!showAvatarEdit(authUser, userData)} diff --git a/src/components/Users/UserEditDetails.tsx b/src/components/Users/UserEditDetails.tsx index b80031212c6..66b9d0b22fd 100644 --- a/src/components/Users/UserEditDetails.tsx +++ b/src/components/Users/UserEditDetails.tsx @@ -1,3 +1,4 @@ +import { TFunction } from "i18next"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -6,6 +7,7 @@ import { editBasicInfoFields, editContactInfoFields, editProfessionalInfoFields, + editProfessionalInfoFieldsForNurseDoctor, } from "@/components/Users/UserFormValidations"; import { BasicInfoDetails, @@ -20,21 +22,18 @@ interface UserEditDetailsProps { onSubmitSuccess?: () => void; } -export function UserBasicInfoView({ - username, - userData, - onSubmitSuccess, -}: UserEditDetailsProps) { - const { t } = useTranslation(); - const [isEditing, setIsEditing] = useState(false); - - const ViewEditToggle = ({ - isEditing, - setIsEditing, - }: { - isEditing: boolean; - setIsEditing: (value: boolean) => void; - }) => ( +const ViewEditToggle = ({ + isEditing, + setIsEditing, + id, + t, +}: { + isEditing: boolean; + setIsEditing: (value: boolean) => void; + id: string; + t: TFunction; +}) => { + return (
@@ -59,18 +59,33 @@ export function UserBasicInfoView({ : "text-gray-500 hover:text-gray-700" } `} + id={`${id}-edit-button`} > {t("edit")}
); +}; + +export function UserBasicInfoView({ + username, + userData, + onSubmitSuccess, +}: UserEditDetailsProps) { + const { t } = useTranslation(); + const [isEditing, setIsEditing] = useState(false); return (
- + {isEditing ? ( void; - }) => ( -
- - -
- ); - return (
- + {isEditing ? ( void; - }) => ( -
- - -
- ); + const editFields = + userData.user_type === "Doctor" || userData.user_type === "Nurse" + ? editProfessionalInfoFieldsForNurseDoctor + : editProfessionalInfoFields; return (
- + {isEditing ? ( { setIsEditing(false); onSubmitSuccess?.(); diff --git a/src/components/Users/UserFormValidations.tsx b/src/components/Users/UserFormValidations.tsx index 46f607fb8f8..1693995ad46 100644 --- a/src/components/Users/UserFormValidations.tsx +++ b/src/components/Users/UserFormValidations.tsx @@ -85,11 +85,15 @@ export const editContactInfoFields: Array = [ ]; export const editProfessionalInfoFields: Array = [ + "weekly_working_hours", + "video_connect_link", +]; + +export const editProfessionalInfoFieldsForNurseDoctor: Array = [ "qualification", "doctor_experience_commenced_on", "doctor_medical_council_registration", - "weekly_working_hours", - "video_connect_link", + ...editProfessionalInfoFields, ]; export type EditForm = { @@ -107,7 +111,7 @@ export const ValidateQualification = ( (formData.user_type === "Doctor" || formData.user_type === "Nurse") && !formData["qualification"] ) { - return translator("field_required"); + return translator("qualification_required"); } return null; }; @@ -118,7 +122,7 @@ export const ValidateDoctorExperienceCommencedOn = ( ) => { if (formData.user_type === "Doctor") { if (!formData["doctor_experience_commenced_on"]) { - return translator("field_required"); + return translator("doctor_experience_required"); } else if ( !validateNumber(formData["doctor_experience_commenced_on"] ?? "") || Number(formData["doctor_experience_commenced_on"]) < 0 || @@ -138,7 +142,7 @@ export const ValidateDoctorMedicalCouncilRegistration = ( formData.user_type === "Doctor" && !formData["doctor_medical_council_registration"] ) { - return translator("field_required"); + return translator("medical_council_registration_required"); } return null; }; diff --git a/src/components/Users/UserSummary.tsx b/src/components/Users/UserSummary.tsx index de28b9a127f..27d54d4740d 100644 --- a/src/components/Users/UserSummary.tsx +++ b/src/components/Users/UserSummary.tsx @@ -168,15 +168,14 @@ export default function UserSummaryTab({ renderContactInformation, userColumnsData, )} - {(userData.user_type === "Doctor" || userData.user_type === "Nurse") && - userColumns( - t("professional_info"), - authUser.username === userData.username - ? t("professional_info_note_self") - : t("professional_info_note"), - renderProfessionalInformation, - userColumnsData, - )} + {userColumns( + t("professional_info"), + authUser.username === userData.username + ? t("professional_info_note_self") + : t("professional_info_note"), + renderProfessionalInformation, + userColumnsData, + )} {passwordResetPermitted && userColumns( t("reset_password"), diff --git a/src/components/Users/UserViewDetails.tsx b/src/components/Users/UserViewDetails.tsx index 4a5307af5e3..d348d14fe9b 100644 --- a/src/components/Users/UserViewDetails.tsx +++ b/src/components/Users/UserViewDetails.tsx @@ -9,13 +9,17 @@ interface UserViewDetailsProps { const LabelValue = ({ label, value, + id, }: { label: string; value?: string | null; + id?: string; }) => (
{label} - {value || "-"} + + {value || "-"} +
); @@ -53,12 +57,25 @@ export const BasicInfoDetails = ({ user }: UserViewDetailsProps) => {
- - - - - + + + + + {
- - + + @@ -94,34 +116,42 @@ export const ProfessionalInfoDetails = ({ user }: UserViewDetailsProps) => { return ( <> - {(user.user_type === "Doctor" || user.user_type === "Nurse") && ( -
- -
- - {user.user_type === "Doctor" && ( - <> - - - - )} +
+ +
+ {(user.user_type === "Doctor" || user.user_type === "Nurse") && ( - -
+ )} + {user.user_type === "Doctor" && ( + <> + + + + )} + +
- )} +
); }; From d6255c5aa62afba110d7b6542a789c7b56f6f0fb Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Thu, 21 Nov 2024 00:41:11 +0530 Subject: [PATCH 41/55] rabbit suggestions --- cypress/e2e/users_spec/UsersCreation.cy.ts | 2 +- public/locale/en.json | 1 + src/components/Users/UserAddEditForm.tsx | 15 ++++------ src/components/Users/UserAvatar.tsx | 2 +- src/components/Users/UserFormValidations.tsx | 30 +++++++++++++------- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/cypress/e2e/users_spec/UsersCreation.cy.ts b/cypress/e2e/users_spec/UsersCreation.cy.ts index 0c278065232..2bd04ff88df 100644 --- a/cypress/e2e/users_spec/UsersCreation.cy.ts +++ b/cypress/e2e/users_spec/UsersCreation.cy.ts @@ -34,7 +34,7 @@ describe("User Creation", () => { ]; const EXPECTED_ERROR_MESSAGES = [ "Please select the User Type", - "Please enter valid phone number", + "Please enter a valid phone number", "Please enter the username", "Please enter date in DD/MM/YYYY format", "Please enter the password", diff --git a/public/locale/en.json b/public/locale/en.json index 2d4225955f8..c162784bdde 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -360,6 +360,7 @@ "autofilled_fields": "Autofilled Fields", "available_features": "Available Features", "available_in": "Available in", + "avatar_updated_success": "Avatar updated successfully", "average_weekly_working_hours": "Average weekly working hours", "awaiting_destination_approval": "AWAITING DESTINATION APPROVAL", "back": "Back", diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index 5f071c77ec2..17f19fcac52 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -26,6 +26,7 @@ import { ValidateDoctorExperienceCommencedOn, ValidateDoctorMedicalCouncilRegistration, ValidateQualification, + ValidateVideoLink, } from "@/components/Users/UserFormValidations"; import { GetUserTypes } from "@/components/Users/UserListAndCard"; import { GenderType, UserModel } from "@/components/Users/models"; @@ -48,12 +49,7 @@ import dayjs from "@/Utils/dayjs"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; import useQuery from "@/Utils/request/useQuery"; -import { - classNames, - dateQueryString, - isValidUrl, - parsePhoneNumber, -} from "@/Utils/utils"; +import { classNames, dateQueryString, parsePhoneNumber } from "@/Utils/utils"; interface StateObj { id: number; @@ -677,10 +673,9 @@ const UserAddEditForm = (props: UserProps) => { } break; case "video_connect_link": - if (formData[field]) { - if (isValidUrl(formData[field]) === false) { - errors[field] = t("invalid_url"); - } + currentError = ValidateVideoLink(formData, t); + if (currentError) { + errors[field] = currentError; } break; default: diff --git a/src/components/Users/UserAvatar.tsx b/src/components/Users/UserAvatar.tsx index bfe236cb90c..c186039951e 100644 --- a/src/components/Users/UserAvatar.tsx +++ b/src/components/Users/UserAvatar.tsx @@ -55,7 +55,7 @@ export default function UserAvatar({ username }: { username: string }) { if (xhr.status === 200) { await sleep(1000); refetchUserData(); - Notification.Success({ msg: "Profile picture updated." }); + Notification.Success({ msg: t("avatar_updated_success") }); setEditAvatar(false); } }, diff --git a/src/components/Users/UserFormValidations.tsx b/src/components/Users/UserFormValidations.tsx index 1693995ad46..64125736f08 100644 --- a/src/components/Users/UserFormValidations.tsx +++ b/src/components/Users/UserFormValidations.tsx @@ -96,15 +96,8 @@ export const editProfessionalInfoFieldsForNurseDoctor: Array = [ ...editProfessionalInfoFields, ]; -export type EditForm = { - user_type: string; - qualification?: string | null; - doctor_experience_commenced_on?: string | null; - doctor_medical_council_registration?: string | null; -}; - export const ValidateQualification = ( - formData: UserForm | EditForm, + formData: UserForm, translator: TFunction, ) => { if ( @@ -117,7 +110,7 @@ export const ValidateQualification = ( }; export const ValidateDoctorExperienceCommencedOn = ( - formData: UserForm | EditForm, + formData: UserForm, translator: TFunction, ) => { if (formData.user_type === "Doctor") { @@ -135,7 +128,7 @@ export const ValidateDoctorExperienceCommencedOn = ( }; export const ValidateDoctorMedicalCouncilRegistration = ( - formData: UserForm | EditForm, + formData: UserForm, translator: TFunction, ) => { if ( @@ -146,3 +139,20 @@ export const ValidateDoctorMedicalCouncilRegistration = ( } return null; }; + +export const ValidateVideoLink = ( + formData: UserForm, + translator: TFunction, +) => { + if (!formData["video_connect_link"]) return null; + + try { + const parsed = new URL(formData["video_connect_link"]); + if (!["https:", "http:"].includes(parsed.protocol)) { + return translator("invalid_url"); + } + } catch { + return translator("invalid_url"); + } + return null; +}; From 3d7266cc1578d72b4772fe4f618e8321dafb826f Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Fri, 22 Nov 2024 16:11:16 +0530 Subject: [PATCH 42/55] Additional coderabbit suggestions --- public/locale/en.json | 13 ++--- src/components/Auth/ResetPassword.tsx | 10 ++-- src/components/Users/UserAddEditForm.tsx | 12 ++-- src/components/Users/UserResetPassword.tsx | 4 +- src/components/Users/UserViewDetails.tsx | 68 +++++++++++----------- 5 files changed, 52 insertions(+), 55 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index c162784bdde..246ea3c209e 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1017,17 +1017,16 @@ "pain_chart_description": "Mark region and intensity of pain", "passport_number": "Passport Number", "password": "Password", - "password_length_validation": "Password should be atleast 8 characters long", - "password_lowercase_validation": "Password should contain at least 1 lowercase letter", - "password_mismatch": "Password and confirm password must be same.", - "password_number_validation": "Password should contain at least 1 number", + "password_length_validation": "Password must be at least 8 characters long", + "password_lowercase_validation": "Password must contain at least one lowercase letter (a-z)", + "password_mismatch": "New password and confirm password must be the same.", + "password_number_validation": "Password must contain at least one number (0-9)", "password_reset_failure": "Password Reset Failed", "password_reset_success": "Password Reset successfully", "password_sent": "Password Reset Email Sent", "password_update_error": "Error while updating password. Try again later.", - "password_uppercase_validation": "Password should contain at least 1 uppercase letter", - "password_validation": "Password should have 1 lowercase letter, 1 uppercase letter, 1 number, and be at least 8 characters long", - "passwords_not_matching": "Passwords do not match.", + "password_uppercase_validation": "Password must contain at least one uppercase letter (A-Z)", + "password_validation": "Password must contain at least: 8 characters, 1 uppercase letter (A-Z), 1 lowercase letter (a-z), and 1 number (0-9)", "patient": "Patient", "patient_address": "Patient Address", "patient_body": "Patient Body", diff --git a/src/components/Auth/ResetPassword.tsx b/src/components/Auth/ResetPassword.tsx index 035d58e6dd8..f993b621062 100644 --- a/src/components/Auth/ResetPassword.tsx +++ b/src/components/Auth/ResetPassword.tsx @@ -134,22 +134,22 @@ const ResetPassword = (props: ResetPasswordProps) => {
{validateRule( form.password?.length >= 8, - "Password should be atleast 8 characters long", + t("password_length_validation"), !form.password, )} {validateRule( form.password !== form.password.toUpperCase(), - "Password should contain at least 1 lowercase letter", + t("password_lowercase_validation"), !form.password, )} {validateRule( form.password !== form.password.toLowerCase(), - "Password should contain at least 1 uppercase letter", + t("password_uppercase_validation"), !form.password, )} {validateRule( /\d/.test(form.password), - "Password should contain at least 1 number", + t("password_number_validation"), !form.password, )}
@@ -168,7 +168,7 @@ const ResetPassword = (props: ResetPasswordProps) => { form.password.length > 0 && validateRule( form.confirm === form.password, - "Confirm password should match the entered password", + t("password_mismatch"), !form.password && form.password.length > 0, )}
diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index 17f19fcac52..09e2404ae2d 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -605,7 +605,7 @@ const UserAddEditForm = (props: UserProps) => { if (!formData.password) { errors.c_password = t("confirm_password_required"); } else if (formData.password !== formData.c_password) { - errors.c_password = t("passwords_not_matching"); + errors.c_password = t("password_mismatch"); } break; case "first_name": @@ -938,22 +938,22 @@ const UserAddEditForm = (props: UserProps) => { > {validateRule( state.form.password.length >= 8, - "Password should be atleast 8 characters long", + t("password_length_validation"), !state.form.password, )} {validateRule( state.form.password !== state.form.password.toUpperCase(), - "Password should contain at least 1 lowercase letter", + t("password_lowercase_validation"), !state.form.password, )} {validateRule( state.form.password !== state.form.password.toLowerCase(), - "Password should contain at least 1 uppercase letter", + t("password_uppercase_validation"), !state.form.password, )} {validateRule( /\d/.test(state.form.password), - "Password should contain at least 1 number", + t("password_number_validation"), !state.form.password, )}
@@ -982,7 +982,7 @@ const UserAddEditForm = (props: UserProps) => {
{validateRule( state.form.c_password === state.form.password, - "Confirm password should match the entered password", + t("password_mismatch"), !state.form.c_password, )}
diff --git a/src/components/Users/UserResetPassword.tsx b/src/components/Users/UserResetPassword.tsx index 7d6dad29c25..dd0ea1aaf33 100644 --- a/src/components/Users/UserResetPassword.tsx +++ b/src/components/Users/UserResetPassword.tsx @@ -65,7 +65,7 @@ export default function UserResetPassword({ if (!formData.new_password_2) { errors.new_password_2 = t("please_confirm_password"); } else if (formData.new_password_1 !== formData.new_password_2) { - errors.new_password_2 = t("passwords_not_matching"); + errors.new_password_2 = t("password_mismatch"); } if (formData.new_password_1 === formData.old_password) { @@ -176,7 +176,7 @@ export default function UserResetPassword({ {validateRule( field("new_password_1").value === field("new_password_2").value, - t("passwords_not_matching"), + t("password_mismatch"), !field("new_password_2").value, )}
diff --git a/src/components/Users/UserViewDetails.tsx b/src/components/Users/UserViewDetails.tsx index d348d14fe9b..8b6fb40a1d3 100644 --- a/src/components/Users/UserViewDetails.tsx +++ b/src/components/Users/UserViewDetails.tsx @@ -115,43 +115,41 @@ export const ProfessionalInfoDetails = ({ user }: UserViewDetailsProps) => { const { t } = useTranslation(); return ( - <> -
- -
- {(user.user_type === "Doctor" || user.user_type === "Nurse") && ( - - )} - {user.user_type === "Doctor" && ( - <> - - - - )} - +
+ +
+ {(user.user_type === "Doctor" || user.user_type === "Nurse") && ( -
+ )} + {user.user_type === "Doctor" && ( + <> + + + + )} + +
- +
); }; From fd6150cf8c32bf7ebb73bbe7b4de102f3d11b062 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Fri, 22 Nov 2024 16:58:54 +0530 Subject: [PATCH 43/55] more coderabbit suggestions --- public/locale/en.json | 7 ++++++- src/components/Users/UserAddEditForm.tsx | 4 ++-- src/components/Users/UserFormValidations.tsx | 5 ++++- src/components/Users/UserSummary.tsx | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 246ea3c209e..99264b47ad3 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -812,6 +812,8 @@ "invalid_pincode": "Invalid Pincode", "invalid_reset": "Invalid Reset", "invalid_url": "Please enter a valid url", + "invalid_url_http_https": "URL should start with http:// or https://", + "invalid_url_javascript": "URL should not include javascript, please enter a valid URL.", "invalid_username": "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", "invalid_username_format": "Please enter a 4-16 characters long username with lowercase letters, digits and . _ - only and it should not start or end with . _ -", "inventory_management": "Inventory Management", @@ -1392,7 +1394,10 @@ "upload_headings__supporting_info": "Upload Supporting Info", "uploading": "Uploading", "use_existing_abha_address": "Use Existing ABHA Address", - "user_deleted_successfuly": "User Deleted Successfuly", + "user_add_error": "Error while adding User", + "user_added_successfully": "User Added successfully", + "user_delete_error": "Error while deleting User", + "user_deleted_successfully": "User Deleted Successfuly", "user_details": "User Details", "user_details_update_error": "Error while updating user details", "user_details_update_success": "User details updated successfully", diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index 09e2404ae2d..a33234e36a0 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -695,12 +695,12 @@ const UserAddEditForm = (props: UserProps) => { if (res?.ok) { dispatch({ type: "set_form", form: initForm }); Notification.Success({ - msg: "User added successfully", + msg: t("user_added_successfully"), }); navigate("/users"); } else { Notification.Error({ - msg: error?.message ?? "Error while adding user", + msg: error?.message ?? t("user_add_error"), }); } setIsLoading(false); diff --git a/src/components/Users/UserFormValidations.tsx b/src/components/Users/UserFormValidations.tsx index 64125736f08..a5af4549803 100644 --- a/src/components/Users/UserFormValidations.tsx +++ b/src/components/Users/UserFormValidations.tsx @@ -149,7 +149,10 @@ export const ValidateVideoLink = ( try { const parsed = new URL(formData["video_connect_link"]); if (!["https:", "http:"].includes(parsed.protocol)) { - return translator("invalid_url"); + return translator("invalid_url_http_https"); + } + if (parsed.href.toLowerCase().includes("javascript:")) { + return translator("invalid_url_javascript"); } } catch { return translator("invalid_url"); diff --git a/src/components/Users/UserSummary.tsx b/src/components/Users/UserSummary.tsx index 27d54d4740d..29fb20b29f3 100644 --- a/src/components/Users/UserSummary.tsx +++ b/src/components/Users/UserSummary.tsx @@ -58,13 +58,13 @@ export default function UserSummaryTab({ setIsDeleting(false); if (res?.status === 204) { Notification.Success({ - msg: "User deleted successfully", + msg: t("user_deleted_successfully"), }); setshowDeleteDialog(!showDeleteDialog); navigate("/users"); } else { Notification.Error({ - msg: "Error while deleting User: " + (error || ""), + msg: t("user_delete_error") + ": " + (error || ""), }); setshowDeleteDialog(!showDeleteDialog); } From fa8e51abcf38cf9b98232aa0e1544c5ee66b7f15 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Fri, 22 Nov 2024 19:32:30 +0530 Subject: [PATCH 44/55] added user delete tests --- cypress/e2e/users_spec/UsersManage.cy.ts | 34 +++++++++++++++++++++- cypress/pageobject/Users/ManageUserPage.ts | 13 +++++++++ cypress/pageobject/Users/UserSearch.ts | 4 +++ public/locale/en.json | 2 +- 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/users_spec/UsersManage.cy.ts b/cypress/e2e/users_spec/UsersManage.cy.ts index a27c3b585aa..d50cef5fc26 100644 --- a/cypress/e2e/users_spec/UsersManage.cy.ts +++ b/cypress/e2e/users_spec/UsersManage.cy.ts @@ -19,6 +19,7 @@ describe("Manage User", () => { const usernameforworkinghour = "devdistrictadmin"; const nurseUsername = "dummynurse1"; const doctorUsername = "devdoctor"; + const doctorToDelete = "dummydoctor12"; const usernamerealname = "Dummy Doctor"; const facilitytolinkusername = "Dummy Shifting Center"; const facilitytolinkskill = "Dummy Facility 40"; @@ -91,11 +92,11 @@ describe("Manage User", () => { manageUserPage.clickMoreDetailsButton(nurseUsername); manageUserPage.verifyMoreDetailsPage(); manageUserPage.clickProfessionalInfoViewButton(); - manageUserPage.clickProfessionalInfoEditButton(); // Should have qualification field // Should not have years of experience and medical council registration fields manageUserPage.verifyQualificationExist(); manageUserPage.verifyYoeAndCouncilRegistrationDoesntExist(); + manageUserPage.clickProfessionalInfoEditButton(); manageUserPage.clearDoctorOrNurseProfessionalInfo(false); manageUserPage.clickSubmit(); manageUserPage.verifyErrorText("Qualification is required"); @@ -160,6 +161,23 @@ describe("Manage User", () => { loginPage.ensureLoggedIn(); }); + it("Nurse user doesn't have delete option for other users", () => { + loginPage.ensureLoggedIn(); + loginPage.clickSignOutBtn(); + loginPage.loginManuallyAsNurse(); + loginPage.ensureLoggedIn(); + cy.visit("/users"); + userPage.typeInSearchInput(doctorUsername); + userPage.checkUsernameText(doctorUsername); + manageUserPage.clickMoreDetailsButton(doctorUsername); + manageUserPage.verifyMoreDetailsPage(false); + manageUserPage.verifyDeleteButtonNotExist(); + loginPage.ensureLoggedIn(); + loginPage.clickSignOutBtn(); + loginPage.loginManuallyAsDistrictAdmin(); + loginPage.ensureLoggedIn(); + }); + it("District Admin can change a user's password", () => { userPage.typeInSearchInput(nurseUsername); userPage.checkUsernameText(nurseUsername); @@ -185,6 +203,20 @@ describe("Manage User", () => { manageUserPage.clickSubmit(); }); + it("District Admin can delete a user", () => { + userPage.typeInSearchInput(doctorToDelete); + userPage.checkUsernameText(doctorToDelete); + manageUserPage.clickMoreDetailsButton(doctorToDelete); + manageUserPage.verifyMoreDetailsPage(); + manageUserPage.verifyDeleteButtonVisible(); + manageUserPage.clickDeleteButton(); + manageUserPage.clickSubmit(); + cy.verifyNotification("User Deleted Successfully"); + cy.closeNotification(); + userPage.typeInSearchInput(doctorToDelete); + userPage.checkUsernameTextDoesNotExist(doctorToDelete); + }); + it("linking skills for users and verify its reflection in profile", () => { // select the district user and select one skill link and verify its profile reflection userPage.typeInSearchInput(usernameforworkinghour); diff --git a/cypress/pageobject/Users/ManageUserPage.ts b/cypress/pageobject/Users/ManageUserPage.ts index 80ee9bbcab1..abe265643e2 100644 --- a/cypress/pageobject/Users/ManageUserPage.ts +++ b/cypress/pageobject/Users/ManageUserPage.ts @@ -367,6 +367,19 @@ export class ManageUserPage { cy.get("#select-facility").should("be.visible"); } + verifyDeleteButtonNotExist() { + cy.get("[data-testid='user-delete-button']").should("not.exist"); + } + + verifyDeleteButtonVisible() { + cy.get("[data-testid='user-delete-button']").scrollIntoView(); + cy.get("[data-testid='user-delete-button']").should("be.visible"); + } + + clickDeleteButton() { + cy.get("[data-testid='user-delete-button']").click(); + } + clickAddSkillButton() { cy.intercept("GET", "**/api/v1/skill/**").as("getSkills"); cy.get("#add-skill-button").click(); diff --git a/cypress/pageobject/Users/UserSearch.ts b/cypress/pageobject/Users/UserSearch.ts index c31e6789fe2..4278c50d5ba 100644 --- a/cypress/pageobject/Users/UserSearch.ts +++ b/cypress/pageobject/Users/UserSearch.ts @@ -26,6 +26,10 @@ export class UserPage { cy.get(`${this.usernameText}-${username}`).should("contain.text", username); } + checkUsernameTextDoesNotExist(username: string) { + cy.get(`${this.usernameText}-${username}`).should("not.exist"); + } + checkUsernameBadgeVisibility(shouldBeVisible: boolean) { const assertion = shouldBeVisible ? "be.visible" : "not.be.visible"; cy.get(this.usernameBadge).should(assertion); diff --git a/public/locale/en.json b/public/locale/en.json index 99264b47ad3..8c07007c10f 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1397,7 +1397,7 @@ "user_add_error": "Error while adding User", "user_added_successfully": "User Added successfully", "user_delete_error": "Error while deleting User", - "user_deleted_successfully": "User Deleted Successfuly", + "user_deleted_successfully": "User Deleted Successfully", "user_details": "User Details", "user_details_update_error": "Error while updating user details", "user_details_update_success": "User details updated successfully", From 759e897e5b1090e4d1b7c21d90f62148d622fa36 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Fri, 22 Nov 2024 21:56:03 +0530 Subject: [PATCH 45/55] fixes and suggestions --- cypress/e2e/users_spec/UsersCreation.cy.ts | 2 +- cypress/e2e/users_spec/UsersManage.cy.ts | 2 +- cypress/pageobject/Users/UserSearch.ts | 2 +- ...leAndSkillsTab.tsx => LinkedSkillsTab.tsx} | 2 +- src/components/Users/UserAddEditForm.tsx | 23 +++++++++++++++---- src/components/Users/UserHome.tsx | 4 ++-- src/components/Users/UserSummary.tsx | 2 +- 7 files changed, 26 insertions(+), 11 deletions(-) rename src/components/Users/{RoleAndSkillsTab.tsx => LinkedSkillsTab.tsx} (95%) diff --git a/cypress/e2e/users_spec/UsersCreation.cy.ts b/cypress/e2e/users_spec/UsersCreation.cy.ts index 2bd04ff88df..15d6e1e1cff 100644 --- a/cypress/e2e/users_spec/UsersCreation.cy.ts +++ b/cypress/e2e/users_spec/UsersCreation.cy.ts @@ -161,7 +161,7 @@ describe("User Creation", () => { userCreationPage.selectDropdownOption("state", "Kerala"); userCreationPage.selectDropdownOption("district", "Ernakulam"); userCreationPage.clickElementById("submit"); - userCreationPage.verifyNotification("User added successfully"); + userCreationPage.verifyNotification("User Added successfully"); userPage.typeInSearchInput(username); userPage.checkUsernameText(username); userCreationPage.verifyElementContainsText( diff --git a/cypress/e2e/users_spec/UsersManage.cy.ts b/cypress/e2e/users_spec/UsersManage.cy.ts index d50cef5fc26..722898e15d2 100644 --- a/cypress/e2e/users_spec/UsersManage.cy.ts +++ b/cypress/e2e/users_spec/UsersManage.cy.ts @@ -140,7 +140,7 @@ describe("Manage User", () => { ); }); - it("Nurse user doesn't have edit options (users above them in hierarchy) or password change option (for other users)", () => { + it("Nurse user doesn't have edit options or password change option (for other users)", () => { loginPage.ensureLoggedIn(); loginPage.clickSignOutBtn(); loginPage.loginManuallyAsNurse(); diff --git a/cypress/pageobject/Users/UserSearch.ts b/cypress/pageobject/Users/UserSearch.ts index 4278c50d5ba..f485fb4f331 100644 --- a/cypress/pageobject/Users/UserSearch.ts +++ b/cypress/pageobject/Users/UserSearch.ts @@ -81,7 +81,7 @@ export class UserPage { } verifyMultipleBadgesWithSameId(alreadylinkedusersviews: string[]) { - alreadylinkedusersviews.forEach((username) => { + cy.wrap(alreadylinkedusersviews).each((username) => { cy.get(`#name-${username}`).scrollIntoView().should("be.visible"); }); } diff --git a/src/components/Users/RoleAndSkillsTab.tsx b/src/components/Users/LinkedSkillsTab.tsx similarity index 95% rename from src/components/Users/RoleAndSkillsTab.tsx rename to src/components/Users/LinkedSkillsTab.tsx index 324e249bc1d..8d58909077c 100644 --- a/src/components/Users/RoleAndSkillsTab.tsx +++ b/src/components/Users/LinkedSkillsTab.tsx @@ -15,7 +15,7 @@ type Props = { username: string; }; -export default function RoleAndSkillsTab(props: Props) { +export default function LinkedSkillsTab(props: Props) { const { userData, username } = props; const { t } = useTranslation(); const authUser = useAuthUser(); diff --git a/src/components/Users/UserAddEditForm.tsx b/src/components/Users/UserAddEditForm.tsx index a33234e36a0..42771314ebe 100644 --- a/src/components/Users/UserAddEditForm.tsx +++ b/src/components/Users/UserAddEditForm.tsx @@ -208,12 +208,18 @@ const UserAddEditForm = (props: UserProps) => { }, }); - const prepData = (formData: UserForm, isCreate: boolean = false) => { + const prepData = ( + formData: UserForm, + isCreate: boolean = false, + ): Partial => { + const fields = includedFields ?? Object.keys(formData); + let baseData: Partial = {}; const phoneNumber = parsePhoneNumber(formData.phone_number) ?? ""; const altPhoneNumber = formData.phone_number_is_whatsapp ? phoneNumber : (parsePhoneNumber(formData.alt_phone_number) ?? ""); - let baseData: Partial = { + + let fieldMappings: Partial = { first_name: formData.first_name, last_name: formData.last_name, email: formData.email, @@ -248,8 +254,8 @@ const UserAddEditForm = (props: UserProps) => { }; if (isCreate) { - baseData = { - ...baseData, + fieldMappings = { + ...fieldMappings, user_type: formData.user_type, password: formData.password, facilities: formData.facilities ? formData.facilities : undefined, @@ -261,6 +267,15 @@ const UserAddEditForm = (props: UserProps) => { }; } + for (const field of fields) { + if (field in fieldMappings) { + baseData = { + ...baseData, + [field as keyof UserForm]: fieldMappings[field as keyof UserForm], + }; + } + } + return baseData; }; diff --git a/src/components/Users/UserHome.tsx b/src/components/Users/UserHome.tsx index 5d8e089a01f..dea75598ec9 100644 --- a/src/components/Users/UserHome.tsx +++ b/src/components/Users/UserHome.tsx @@ -7,7 +7,7 @@ import Page from "@/components/Common/Page"; import { userChildProps } from "@/components/Common/UserColumns"; import Error404 from "@/components/ErrorPages/404"; import LinkedFacilitiesTab from "@/components/Users/LinkedFacilitiesTab"; -import RoleAndSkillsTab from "@/components/Users/RoleAndSkillsTab"; +import LinkedSkillsTab from "@/components/Users/LinkedSkillsTab"; import UserBanner from "@/components/Users/UserBanner"; import UserSummaryTab from "@/components/Users/UserSummary"; import { UserModel } from "@/components/Users/models"; @@ -72,7 +72,7 @@ export default function UserHome(props: UserHomeProps) { } = { PROFILE: { body: UserSummaryTab }, SKILLS: { - body: RoleAndSkillsTab, + body: LinkedSkillsTab, hidden: !editPermissions, }, FACILITIES: { diff --git a/src/components/Users/UserSummary.tsx b/src/components/Users/UserSummary.tsx index 29fb20b29f3..0bcafbfef70 100644 --- a/src/components/Users/UserSummary.tsx +++ b/src/components/Users/UserSummary.tsx @@ -47,7 +47,7 @@ export default function UserSummaryTab({ const authUser = useAuthUser(); if (!userData) { - return; + return <>; } const handleSubmit = async () => { From 871c845a989444bed98bdd06aa168c5cc265ae73 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Mon, 25 Nov 2024 10:49:31 +0530 Subject: [PATCH 46/55] updated notes for user details view --- public/locale/en.json | 3 +++ src/components/Users/UserSummary.tsx | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 8c07007c10f..e42a457f8f2 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -517,6 +517,7 @@ "contact_info": "Contact Information", "contact_info_note": "View or update user's contact information", "contact_info_note_self": "View or update your contact information", + "contact_info_note_view": "View user's contact information", "contact_number": "Contact Number", "contact_person": "Name of Contact Person at Facility", "contact_person_at_the_facility": "Contact person at the current facility", @@ -1068,6 +1069,7 @@ "personal_information": "Personal Information", "personal_information_note": "View or update user's personal information", "personal_information_note_self": "View or update your personal information", + "personal_information_note_view": "View user's personal information", "phone": "Phone", "phone_no": "Phone no.", "phone_number": "Phone Number", @@ -1134,6 +1136,7 @@ "professional_info": "Professional Information", "professional_info_note": "View or update user's professional information", "professional_info_note_self": "View or update your professional information", + "professional_info_note_view": "View user's professional information", "profile": "Profile", "provisional": "Provisional", "qualification": "Qualification", diff --git a/src/components/Users/UserSummary.tsx b/src/components/Users/UserSummary.tsx index 0bcafbfef70..74e362fd937 100644 --- a/src/components/Users/UserSummary.tsx +++ b/src/components/Users/UserSummary.tsx @@ -156,7 +156,9 @@ export default function UserSummaryTab({ t("personal_information"), authUser.username === userData.username ? t("personal_information_note_self") - : t("personal_information_note"), + : editPermissions + ? t("personal_information_note") + : t("personal_information_note_view"), renderBasicInformation, userColumnsData, )} @@ -164,7 +166,9 @@ export default function UserSummaryTab({ t("contact_info"), authUser.username === userData.username ? t("contact_info_note_self") - : t("contact_info_note"), + : editPermissions + ? t("contact_info_note") + : t("contact_info_note_view"), renderContactInformation, userColumnsData, )} @@ -172,7 +176,9 @@ export default function UserSummaryTab({ t("professional_info"), authUser.username === userData.username ? t("professional_info_note_self") - : t("professional_info_note"), + : editPermissions + ? t("professional_info_note") + : t("professional_info_note_view"), renderProfessionalInformation, userColumnsData, )} @@ -181,7 +187,9 @@ export default function UserSummaryTab({ t("reset_password"), authUser.username === userData.username ? t("reset_password_note_self") - : t("reset_password_note"), + : editPermissions + ? t("reset_password_note") + : t("reset_password_note_view"), UserResetPassword, userColumnsData, )} From d131ef7e04dfc5a5ae4a39aff88047ab99694fa7 Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Mon, 25 Nov 2024 22:13:33 +0530 Subject: [PATCH 47/55] changed password perms to reflect changes in BE --- cypress/e2e/users_spec/UsersManage.cy.ts | 14 ++++++++++---- src/Utils/permissions.ts | 15 ++------------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/cypress/e2e/users_spec/UsersManage.cy.ts b/cypress/e2e/users_spec/UsersManage.cy.ts index 722898e15d2..44c4a64dbf2 100644 --- a/cypress/e2e/users_spec/UsersManage.cy.ts +++ b/cypress/e2e/users_spec/UsersManage.cy.ts @@ -178,7 +178,12 @@ describe("Manage User", () => { loginPage.ensureLoggedIn(); }); - it("District Admin can change a user's password", () => { + it("Nurse user can change their own password", () => { + loginPage.ensureLoggedIn(); + loginPage.clickSignOutBtn(); + loginPage.loginManuallyAsNurse(); + loginPage.ensureLoggedIn(); + cy.visit("/users"); userPage.typeInSearchInput(nurseUsername); userPage.checkUsernameText(nurseUsername); manageUserPage.clickMoreDetailsButton(nurseUsername); @@ -190,9 +195,6 @@ describe("Manage User", () => { loginPage.clickSignOutBtn(); loginPage.loginManuallyAsNurse("Coronasafe@1233"); loginPage.ensureLoggedIn(); - loginPage.clickSignOutBtn(); - loginPage.loginManuallyAsDistrictAdmin(); - loginPage.ensureLoggedIn(); cy.visit("/users"); userPage.typeInSearchInput(nurseUsername); userPage.checkUsernameText(nurseUsername); @@ -201,6 +203,10 @@ describe("Manage User", () => { manageUserPage.clickPasswordEditButton(); manageUserPage.changePassword("Coronasafe@1233", "Coronasafe@123"); manageUserPage.clickSubmit(); + loginPage.ensureLoggedIn(); + loginPage.clickSignOutBtn(); + loginPage.loginManuallyAsDistrictAdmin(); + loginPage.ensureLoggedIn(); }); it("District Admin can delete a user", () => { diff --git a/src/Utils/permissions.ts b/src/Utils/permissions.ts index e738d2a2c1f..913d9eb2b75 100644 --- a/src/Utils/permissions.ts +++ b/src/Utils/permissions.ts @@ -40,22 +40,11 @@ export const showUserPasswordReset = ( authUser: UserModel, targetUser: UserModel, ) => { - if (authUser.username === targetUser.username) return true; - - // Auth user should be higher in hierarchy than target user - if ( - USER_TYPES.indexOf(authUser.user_type) <= - USER_TYPES.indexOf(targetUser.user_type) - ) - return false; - - return checkIfStateOrDistrictAdminInSameLocation(authUser, targetUser); + return authUser.username === targetUser.username || authUser.is_superuser; }; export const showAvatarEdit = (authUser: UserModel, targetUser: UserModel) => { - if (authUser.username === targetUser.username || authUser.is_superuser) - return true; - return false; + return authUser.username === targetUser.username || authUser.is_superuser; }; export const editUserPermissions = ( authUser: UserModel, From 093f875f2db40342080696d8d67186d73f940d0d Mon Sep 17 00:00:00 2001 From: Jacobjohnjeevan Date: Wed, 27 Nov 2024 18:55:04 +0530 Subject: [PATCH 48/55] perms changes, form reset, sticky header on list --- public/locale/en.json | 1 + src/components/Form/Form.tsx | 7 +++-- src/components/Users/LinkedFacilities.tsx | 13 +++++++- src/components/Users/ManageUsers.tsx | 36 ++++++++-------------- src/components/Users/UserAddEditForm.tsx | 2 +- src/components/Users/UserBanner.tsx | 4 +-- src/components/Users/UserListAndCard.tsx | 24 +++++++++++---- src/components/Users/UserResetPassword.tsx | 3 +- 8 files changed, 53 insertions(+), 37 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index c895e5d907f..de28d93498a 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -525,6 +525,7 @@ "contact_phone": "Contact Person Number", "contact_with_confirmed_carrier": "Contact with confirmed carrier", "contact_with_suspected_carrier": "Contact with suspected carrier", + "contact_your_admin_to_add_facilities": "Contact your admin to add facilities", "contact_your_admin_to_add_skills": "Contact your admin to add skills", "continue": "Continue", "continue_watching": "Continue watching", diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index 33f62cc4ea4..11985351a0c 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -33,7 +33,8 @@ type Props = { onDraftRestore?: (newState: FormState) => void; children: (props: FormContextValue) => React.ReactNode; hideRestoreDraft?: boolean; - resetFormVals?: boolean; + resetFormValsOnCancel?: boolean; + resetFormValsOnSubmit?: boolean; hideCancelButton?: boolean; }; @@ -79,11 +80,13 @@ const Form = ({ type: "set_errors", errors: { ...state.errors, ...errors }, }); + } else if (props.resetFormValsOnSubmit) { + dispatch({ type: "set_form", form: formVals.current }); } }; const handleCancel = () => { - if (props.resetFormVals) { + if (props.resetFormValsOnCancel) { dispatch({ type: "set_form", form: formVals.current }); } props.onCancel?.(); diff --git a/src/components/Users/LinkedFacilities.tsx b/src/components/Users/LinkedFacilities.tsx index b416be42c63..18d520abca3 100644 --- a/src/components/Users/LinkedFacilities.tsx +++ b/src/components/Users/LinkedFacilities.tsx @@ -15,6 +15,7 @@ import { FacilityModel } from "@/components/Facility/models"; import ConfirmFacilityModal from "@/components/Users/ConfirmFacilityModal"; import { UserModel } from "@/components/Users/models"; +import useAuthUser from "@/hooks/useAuthUser"; import { useIsAuthorized } from "@/hooks/useIsAuthorized"; import AuthorizeFor from "@/Utils/AuthorizeFor"; @@ -49,11 +50,14 @@ export default function LinkedFacilities({ const [homeFacility, setHomeFacility] = useState(); const [modalProps, setModalProps] = useState(initModalProps); const { t } = useTranslation(); + const authUser = useAuthUser(); const authorizeForHomeFacility = useIsAuthorized( AuthorizeFor(["DistrictAdmin", "StateAdmin"]), ); + const isCurrentUser = userData.username === authUser.username; + const { refetch: refetchUserFacilities } = useQuery(routes.userListFacility, { pathParams: { username: userData.username }, query: { limit: 36 }, @@ -241,7 +245,7 @@ export default function LinkedFacilities({
{homeFacility.name}
- {authorizeForHomeFacility && ( + {(authorizeForHomeFacility || isCurrentUser) && (