diff --git a/src/Common/hooks/useAuthUser.ts b/src/Common/hooks/useAuthUser.ts index 78bf93fa8f8..ae181f62ca8 100644 --- a/src/Common/hooks/useAuthUser.ts +++ b/src/Common/hooks/useAuthUser.ts @@ -9,6 +9,7 @@ type AuthContextType = { user: UserModel | undefined; signIn: (creds: LoginCredentials) => Promise; signOut: () => Promise; + refetchUser: () => Promise>; }; export const AuthUserContext = createContext(null); diff --git a/src/Components/Common/Avatar.tsx b/src/Components/Common/Avatar.tsx index e4d42dd72ae..c945611b5dc 100644 --- a/src/Components/Common/Avatar.tsx +++ b/src/Components/Common/Avatar.tsx @@ -1,5 +1,7 @@ +import CareIcon from "@/CAREUI/icons/CareIcon"; import { cn } from "@/lib/utils"; -import React, { useEffect, useRef, useState } from "react"; +import React from "react"; +import { useTranslation } from "react-i18next"; const colors: string[] = [ "#E6F3FF", // Light Blue @@ -43,57 +45,109 @@ const initials = (name: string): string => { }; interface AvatarProps { + id?: string; colors?: [string, string]; name: string; imageUrl?: string; className?: string; } +interface EditableAvatarProps extends AvatarProps { + editable?: boolean; + onClick?: () => void; +} + const Avatar: React.FC = ({ + id, colors: propColors, name, imageUrl, className, }) => { const [bgColor] = propColors || toColor(name); - const [width, setWidth] = useState(0); - const avatarRef = useRef(null); - - useEffect(() => { - const updateWidth = () => { - const avatarRect = avatarRef.current?.getBoundingClientRect(); - const width = avatarRect?.width || 0; - setWidth(width); - }; - updateWidth(); - document.addEventListener("resize", updateWidth); - return () => document.removeEventListener("resize", updateWidth); - }, []); - return (
{imageUrl ? ( {name} ) : ( -
{initials(name)}
+ + + {initials(name)} + + + )} +
+ ); +}; + +const EditableAvatar: React.FC = ({ + id, + colors: propColors, + name, + imageUrl, + className, + editable = true, + onClick, +}) => { + const { t } = useTranslation(); + return ( +
+ + + {editable && ( +
+ + {t(imageUrl ? "edit" : "upload")} +
)}
); }; -export { Avatar }; +export { Avatar, EditableAvatar }; diff --git a/src/Components/Common/AvatarEditModal.tsx b/src/Components/Common/AvatarEditModal.tsx new file mode 100644 index 00000000000..f83b76e4a2e --- /dev/null +++ b/src/Components/Common/AvatarEditModal.tsx @@ -0,0 +1,391 @@ +import React, { + useState, + ChangeEventHandler, + useCallback, + useEffect, + useRef, +} from "react"; +import { Warn } from "@/Utils/Notifications"; +import useDragAndDrop from "@/Utils/useDragAndDrop"; +import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2"; +import Webcam from "react-webcam"; +import CareIcon from "@/CAREUI/icons/CareIcon"; +import { useTranslation } from "react-i18next"; +import DialogModal from "../Common/Dialog"; + +interface Props { + title: string; + open: boolean; + imageUrl?: string; + handleUpload: (file: File, onError: () => void) => Promise; + handleDelete: (onError: () => void) => Promise; + onClose?: () => void; +} + +const VideoConstraints = { + user: { + width: 1280, + height: 720, + facingMode: "user", + }, + environment: { + width: 1280, + height: 720, + facingMode: { exact: "environment" }, + }, +} as const; + +type IVideoConstraint = + (typeof VideoConstraints)[keyof typeof VideoConstraints]; + +const AvatarEditModal = ({ + title, + open, + imageUrl, + handleUpload, + handleDelete, + onClose, +}: Props) => { + const [isProcessing, setIsProcessing] = useState(false); + const [selectedFile, setSelectedFile] = useState(); + const [preview, setPreview] = useState(); + const [isCameraOpen, setIsCameraOpen] = useState(false); + const webRef = useRef(null); + const [previewImage, setPreviewImage] = useState(null); + const [isCaptureImgBeingUploaded, setIsCaptureImgBeingUploaded] = + useState(false); + const [constraint, setConstraint] = useState( + VideoConstraints.user, + ); + const { t } = useTranslation(); + const [isDragging, setIsDragging] = useState(false); + + const handleSwitchCamera = useCallback(() => { + setConstraint( + constraint.facingMode === "user" + ? VideoConstraints.environment + : VideoConstraints.user, + ); + }, []); + + const captureImage = () => { + setPreviewImage(webRef.current.getScreenshot()); + const canvas = webRef.current.getCanvas(); + canvas?.toBlob((blob: Blob) => { + const myFile = new File([blob], "image.png", { + type: blob.type, + }); + setSelectedFile(myFile); + }); + }; + + const closeModal = () => { + setPreview(undefined); + setIsProcessing(false); + setSelectedFile(undefined); + onClose?.(); + }; + + useEffect(() => { + if (selectedFile) { + const objectUrl = URL.createObjectURL(selectedFile); + setPreview(objectUrl); + return () => URL.revokeObjectURL(objectUrl); + } + }, [selectedFile]); + + const onSelectFile: ChangeEventHandler = (e) => { + if (!e.target.files || e.target.files.length === 0) { + setSelectedFile(undefined); + return; + } + if (e.target.files[0]?.type.split("/")[0] !== "image") { + Warn({ msg: "Please upload an image file!" }); + return; + } + setSelectedFile(e.target.files[0]); + }; + + const uploadAvatar = async () => { + if (!selectedFile) { + closeModal(); + return; + } + + setIsProcessing(true); + setIsCaptureImgBeingUploaded(true); + + await handleUpload(selectedFile, () => { + setIsCaptureImgBeingUploaded(false); + setIsProcessing(false); + }); + }; + + const deleteAvatar = async () => { + setIsProcessing(true); + await handleDelete(() => { + setIsProcessing(false); + }); + }; + + const dragProps = useDragAndDrop(); + const onDrop = (e: React.DragEvent) => { + e.preventDefault(); + dragProps.setDragOver(false); + setIsDragging(false); + const droppedFile = e?.dataTransfer?.files[0]; + if (droppedFile.type.split("/")[0] !== "image") + return dragProps.setFileDropError("Please drop an image file to upload!"); + setSelectedFile(droppedFile); + }; + + const onDragOver = (e: React.DragEvent) => { + e.preventDefault(); + dragProps.onDragOver(e); + setIsDragging(true); + }; + + const onDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + dragProps.onDragLeave(); + setIsDragging(false); + }; + + const commonHint = ( + <> + {t("max_size_for_image_uploaded_should_be")} 1mb. +
+ {t("allowed_formats_are")} jpg,png,jpeg.{" "} + {t("recommended_aspect_ratio_for")} the image is 1:1 + + ); + + return ( + +
+
+ {!isCameraOpen ? ( + <> + {preview || imageUrl ? ( + <> +
+ cover-photo +
+

+ {commonHint} +

+ + ) : ( +
+ +

+ {dragProps.fileDropError !== "" + ? dragProps.fileDropError + : `${t("drag_drop_image_to_upload")}`} +

+

+ {t("no_image_found")}. {commonHint} +

+
+ )} + +
+
+ +
+ { + setConstraint(() => VideoConstraints.user); + setIsCameraOpen(true); + }} + > + {`${t("open")} ${t("camera")}`} + +
+ { + e.stopPropagation(); + closeModal(); + dragProps.setFileDropError(""); + }} + disabled={isProcessing} + /> + {imageUrl && ( + + {t("delete")} + + )} + + {isProcessing ? ( + + ) : ( + + )} + + {isProcessing ? `${t("uploading")}...` : `${t("save")}`} + + +
+ + ) : ( + <> +
+ + {t("capture_cover_photo")} + +
+
+ {!previewImage ? ( + <> + { + setIsCameraOpen(false); + Warn({ msg: t("camera_permission_denied") }); + }} + /> + + ) : ( + <> + + + )} +
+ {/* buttons for mobile screens */} +
+ {!previewImage ? ( + <> + + + {`${t("switch")} ${t("camera")}`} + + { + captureImage(); + }} + > + + {t("capture")} + + + ) : ( + <> + { + setPreviewImage(null); + }} + > + {t("retake")} + + + {isCaptureImgBeingUploaded ? ( + <> + + {`${t("submitting")}...`} + + ) : ( + <> {t("submit")} + )} + + + )} +
+ { + setPreviewImage(null); + setIsCameraOpen(false); + webRef.current.stopCamera(); + }} + label={t("close")} + disabled={isProcessing} + /> +
+ + )} +
+
+ + ); +}; + +export default AvatarEditModal; diff --git a/src/Components/Common/LanguageSelector.tsx b/src/Components/Common/LanguageSelector.tsx index 38fc8024a01..a015d6fd609 100644 --- a/src/Components/Common/LanguageSelector.tsx +++ b/src/Components/Common/LanguageSelector.tsx @@ -29,7 +29,7 @@ export const LanguageSelector = (props: any) => { - -
-
- { - setConstraint(() => VideoConstraints.user); - setIsCameraOpen(true); - }} - > - {`${t("open")} ${t("camera")}`} - - { - e.stopPropagation(); - closeModal(); - dragProps.setFileDropError(""); - }} - disabled={isProcessing} - /> - {facility.read_cover_image_url && ( - - {t("delete")} - - )} - - {isProcessing ? ( - - ) : ( - - )} - - {isProcessing ? `${t("uploading")}...` : `${t("save")}`} - - -
- - ) : ( -
-
- - {t("capture_cover_photo")} - -
-
- {!previewImage ? ( - <> - { - setIsCameraOpen(false); - Warn({ msg: t("camera_permission_denied") }); - }} - /> - - ) : ( - <> - - - )} -
- {/* buttons for mobile screens */} -
-
- {!previewImage ? ( - - {t("switch")} - - ) : ( - <> - )} -
-
- {!previewImage ? ( - <> -
- { - captureImage(); - }} - className="my-2 w-full" - > - {t("capture")} - -
- - ) : ( - <> -
- { - setPreviewImage(null); - }} - className="my-2 w-full" - disabled={isProcessing} - > - {t("retake")} - - - {isCaptureImgBeingUploaded && ( - - )} - {t("submit")} - -
- - )} -
-
- { - setPreviewImage(null); - setIsCameraOpen(false); - webRef.current.stopCamera(); - }} - className="my-2 w-full" - > - {t("close")} - -
-
- {/* buttons for laptop screens */} -
-
- - - {`${t("switch")} ${t("camera")}`} - -
- -
-
- {!previewImage ? ( - <> -
- { - captureImage(); - }} - > - - {t("capture")} - -
- - ) : ( - <> -
- { - setPreviewImage(null); - }} - > - {t("retake")} - - - {isCaptureImgBeingUploaded ? ( - <> - - {`${t("submitting")}...`} - - ) : ( - <> {t("submit")} - )} - -
- - )} -
-
- { - setPreviewImage(null); - setIsCameraOpen(false); - webRef.current.stopCamera(); - }} - > - {`${t("close")} ${t("camera")}`} - -
-
-
- )} -
-
- ); -}; - -export default CoverImageEditModal; diff --git a/src/Components/Facility/FacilityHome.tsx b/src/Components/Facility/FacilityHome.tsx index 56919e6fde4..5fc5c7d8f2a 100644 --- a/src/Components/Facility/FacilityHome.tsx +++ b/src/Components/Facility/FacilityHome.tsx @@ -1,8 +1,11 @@ import * as Notification from "../../Utils/Notifications.js"; import { NonReadOnlyUsers } from "../../Utils/AuthorizeFor"; -import { FacilityModel } from "./models"; -import { FACILITY_FEATURE_TYPES, USER_TYPES } from "../../Common/constants"; +import { + FACILITY_FEATURE_TYPES, + LocalStorageKeys, + USER_TYPES, +} from "../../Common/constants"; import DropdownMenu, { DropdownItem } from "../Common/components/Menu"; import { useState } from "react"; @@ -11,7 +14,6 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import Chip from "../../CAREUI/display/Chip"; import ConfirmDialog from "../Common/ConfirmDialog"; import ContactLink from "../Common/components/ContactLink"; -import CoverImageEditModal from "./CoverImageEditModal"; import Page from "../Common/components/Page"; import RecordMeta from "../../CAREUI/display/RecordMeta"; @@ -37,13 +39,16 @@ import { LocationSelect } from "../Common/LocationSelect.js"; import { CameraFeedPermittedUserTypes } from "../../Utils/permissions.js"; import { FacilityStaffList } from "./FacilityStaffList.js"; import FacilityBlock from "./FacilityBlock.js"; +import Loading from "@/Components/Common/Loading"; +import { EditableAvatar } from "@/Components/Common/Avatar"; +import AvatarEditModal from "@/Components/Common/AvatarEditModal"; +import careConfig from "@careConfig"; +import uploadFile from "@/Utils/request/uploadFile"; +import { sleep } from "@/Utils/utils"; type Props = { facilityId: string; }; - -import Loading from "@/Components/Common/Loading"; -import { Avatar } from "@/Components/Common/Avatar.js"; export const getFacilityFeatureIcon = (featureId: number) => { const feature = FACILITY_FEATURE_TYPES.find((f) => f.id === featureId); if (!feature?.icon) return null; @@ -107,12 +112,51 @@ export const FacilityHome = ({ facilityId }: Props) => { }); }; + const handleCoverImageUpload = async (file: File, onError: () => void) => { + const formData = new FormData(); + formData.append("cover_image", file); + const url = `${careConfig.apiUrl}/api/v1/facility/${facilityId}/cover_image/`; + + uploadFile( + url, + formData, + "POST", + { + Authorization: + "Bearer " + localStorage.getItem(LocalStorageKeys.accessToken), + }, + async (xhr: XMLHttpRequest) => { + if (xhr.status === 200) { + await sleep(1000); + facilityFetch(); + Notification.Success({ msg: "Cover image updated." }); + setEditCoverImage(false); + } + }, + null, + () => { + onError(); + }, + ); + }; + + const handleCoverImageDelete = async (onError: () => void) => { + const { res } = await request(routes.deleteFacilityCoverImage, { + pathParams: { id: facilityId }, + }); + if (res?.ok) { + Notification.Success({ msg: "Cover image deleted" }); + facilityFetch(); + setEditCoverImage(false); + } else { + onError(); + } + }; + if (isLoading) { return ; } - const hasCoverImage = !!facilityData?.read_cover_image_url; - const StaffUserTypeIndex = USER_TYPES.findIndex((type) => type === "Staff"); const hasPermissionToEditCoverImage = !(authUser.user_type as string).includes("ReadOnly") && @@ -123,19 +167,6 @@ export const FacilityHome = ({ facilityId }: Props) => { authUser.user_type === "DistrictAdmin" || authUser.user_type === "StateAdmin"; - const editCoverImageTooltip = hasPermissionToEditCoverImage && ( -
setEditCoverImage(true)} - > - - {t(hasCoverImage ? "edit" : "upload")} -
- ); - return ( { onClose={handleDeleteClose} onConfirm={handleDeleteSubmit} /> - facilityFetch()} + imageUrl={facilityData?.read_cover_image_url} + handleUpload={handleCoverImageUpload} + handleDelete={handleCoverImageDelete} onClose={() => setEditCoverImage(false)} - onDelete={() => facilityFetch()} - facility={facilityData ?? ({} as FacilityModel)} /> -
hasPermissionToEditCoverImage && setEditCoverImage(true)} - > - - {editCoverImageTooltip} -
-
+
-
- +
+ setEditCoverImage(true)} + className="md:mr-2 lg:mr-6 lg:h-80 lg:w-80" + />
diff --git a/src/Components/Users/UserProfile.tsx b/src/Components/Users/UserProfile.tsx index 22f2a0e2607..a05c7f04e81 100644 --- a/src/Components/Users/UserProfile.tsx +++ b/src/Components/Users/UserProfile.tsx @@ -1,5 +1,5 @@ import { useState, useReducer, FormEvent } from "react"; -import { GENDER_TYPES } from "../../Common/constants"; +import { GENDER_TYPES, LocalStorageKeys } from "../../Common/constants"; import { validateEmailAddress } from "../../Common/validation"; import * as Notification from "../../Utils/Notifications.js"; import LanguageSelector from "../../Components/Common/LanguageSelector"; @@ -9,9 +9,11 @@ import { classNames, dateQueryString, formatDate, + formatDisplayName, isValidUrl, parsePhoneNumber, -} from "../../Utils/utils"; + sleep, +} from "@/Utils/utils"; import CareIcon from "../../CAREUI/icons/CareIcon"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; @@ -27,7 +29,13 @@ import request from "../../Utils/request/request"; import DateFormField from "../Form/FormFields/DateFormField"; import { validateRule } from "./UserAdd"; import { useTranslation } from "react-i18next"; +import { EditableAvatar } from "@/Components/Common/Avatar"; +import Page from "@/Components/Common/components/Page"; import Loading from "@/Components/Common/Loading"; +import AvatarEditModal from "@/Components/Common/AvatarEditModal"; +import uploadFile from "@/Utils/request/uploadFile"; +import careConfig from "@careConfig"; + type EditForm = { firstName: string; lastName: string; @@ -111,8 +119,9 @@ const editFormReducer = (state: State, action: Action) => { export default function UserProfile() { const { t } = useTranslation(); - const { signOut } = useAuthContext(); + const { signOut, refetchUser } = useAuthContext(); const [states, dispatch] = useReducer(editFormReducer, initialState); + const [editAvatar, setEditAvatar] = useState(false); const [updateStatus, setUpdateStatus] = useState({ isChecking: false, isUpdateAvailable: false, @@ -462,490 +471,546 @@ export default function UserProfile() { }); } }; + + 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); + refetchUser(); + 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 refetchUser(); + setEditAvatar(false); + } else { + onError(); + } + }; + return ( -
-
-
-
-
-

- {t("personal_information")} + + setEditAvatar(false)} + /> +
+
+

+ {t("local_body")}, {t("district")}, {t("state")}{" "} + {t("are_non_editable_fields")}. +

+
+ setEditAvatar(!editAvatar)} + className="h-20 w-20" + /> +
+

+ {authUser?.first_name} {authUser?.last_name}

-

- {t("local_body")}, {t("district")}, {t("state")}{" "} - {t("are_non_editable_fields")}. +

+ @{authUser?.username}

-
- setShowEdit(!showEdit)} - type="button" - id="edit-cancel-profile-button" - > - {showEdit ? t("cancel") : t("edit_user_profile")} - - - - {t("sign_out")} - -
-
- {!showEdit && !isLoading && ( -
-
-
-
- {t("username")} -
-
- {userData?.username || "-"} -
-
-
-
- {t("phone_number")} -
-
- {userData?.phone_number || "-"} -
-
+
+ setShowEdit(!showEdit)} + type="button" + id="edit-cancel-profile-button" + > + {showEdit ? t("cancel") : t("edit_user_profile")} + + + + {t("sign_out")} + +
+
+
+ {!showEdit && !isLoading && ( +
+
+
+
+ {t("username")} +
+
+ {userData?.username || "-"} +
+
+
+
+ {t("phone_number")} +
+
+ {userData?.phone_number || "-"} +
+
-
-
- {t("whatsapp_number")} -
-
- {userData?.alt_phone_number || "-"} -
-
-
-
- {t("email")} -
-
- {userData?.email || "-"} -
-
-
-
- {t("first_name")} -
-
- {userData?.first_name || "-"} -
-
-
-
- {t("last_name")} -
-
- {userData?.last_name || "-"} -
-
-
-
- {t("date_of_birth")} -
-
- {userData?.date_of_birth - ? formatDate(userData?.date_of_birth) +
+
+ {t("whatsapp_number")} +
+
+ {userData?.alt_phone_number || "-"} +
+
+
+
+ {t("email")} +
+
+ {userData?.email || "-"} +
+
+
+
+ {t("first_name")} +
+
+ {userData?.first_name || "-"} +
+
+
+
+ {t("last_name")} +
+
+ {userData?.last_name || "-"} +
+
+
+
+ {t("date_of_birth")} +
+
+ {userData?.date_of_birth + ? formatDate(userData?.date_of_birth) + : "-"} +
+
+
+
+ {t("access_level")} +
+
+ + {userData?.user_type || "-"} +
+
+
+
+ {t("gender")} +
+
+ {userData?.gender || "-"} +
+
+
+
+ {t("local_body")} +
+
+ {userData?.local_body_object?.name || "-"} +
+
+
+
+ {t("district")} +
+
+ {userData?.district_object?.name || "-"} +
+
+
+
+ {t("state")} +
+
+ {userData?.state_object?.name || "-"} +
+
+
+
+ {t("skills")} +
+
+
+ {skillsView?.results?.length + ? skillsView.results?.map((skill: SkillModel) => { + return ( + +

+ {skill.skill_object.name} +

+
+ ); + }) : "-"} -
-
-
-
- {t("access_level")} -
-
- {" "} - {userData?.user_type || "-"} -
-
-
-
- {t("gender")} -
-
- {userData?.gender || "-"} -
-
-
-
- {t("local_body")} -
-
- {userData?.local_body_object?.name || "-"} -
-
-
-
- {t("district")} -
-
- {userData?.district_object?.name || "-"} -
-
-
-
- {t("state")} -
-
- {userData?.state_object?.name || "-"} -
-
-
-
- {t("skills")} -
-
-
+
+
+
+
+ {t("average_weekly_working_hours")} +
+
+ {userData?.weekly_working_hours ?? "-"} +
+
+ - -
-
-
- {t("average_weekly_working_hours")} -
-
- {userData?.weekly_working_hours ?? "-"} -
-
-
- -
- )} - {showEdit && ( -
-
-
-
-
- + {userData?.video_connect_link} + + ) : ( + "-" + )} + +
+ +
+ )} + {showEdit && ( +
+ +
+
+
+ + + + o.text} + optionValue={(o) => o.text} + options={GENDER_TYPES} + /> + + + + {(states.form.user_type === "Doctor" || + states.form.user_type === "Nurse") && ( - - o.text} - optionValue={(o) => o.text} - options={GENDER_TYPES} - /> - - - - {(states.form.user_type === "Doctor" || - states.form.user_type === "Nurse") && ( + )} + {states.form.user_type === "Doctor" && ( + <> - )} - {states.form.user_type === "Doctor" && ( - <> - - - - )} - - -
-
-
- + + + )} + +
- -
-
-
-
+
+ +
+
+ +
+
+
+
+ + setChangePasswordForm({ + ...changePasswordForm, + old_password: e.value, + }) + } + error={changePasswordErrors.old_password} + required + /> +
+ value={changePasswordForm.new_password_1} + className="peer col-span-6 sm:col-span-3" + onChange={(e) => { setChangePasswordForm({ ...changePasswordForm, - old_password: e.value, - }) - } - error={changePasswordErrors.old_password} + new_password_1: e.value, + }); + }} 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?.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", + changePasswordForm.new_password_1 === + changePasswordForm.new_password_2, + "Confirm password should match the new password", )}
-
-
- { - 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", - )} -
- )} -
+ )}
-
- -
- -
- )} -
+
+ +
+
+ +
+ )}
+
-
-
-
-

- {t("language_selection")} -

-

- {t("set_your_local_language")} -

-
-
-
- +
+
+
+

+ {t("language_selection")} +

+

+ {t("set_your_local_language")} +

-
-
-
-

- {t("software_update")} -

-

- {t("check_for_available_update")} -

-
+
+ +
+
+
+
+
+

+ {t("software_update")} +

+

+ {t("check_for_available_update")} +

- {updateStatus.isUpdateAvailable && ( - - -
- - {t("update_available")} -
-
-
+
+ {updateStatus.isUpdateAvailable && ( + + +
+ + {t("update_available")} +
+
+
+ )} +
+ {!updateStatus.isUpdateAvailable && ( + + {" "} +
+ + {updateStatus.isChecking + ? t("checking_for_update") + : t("check_for_update")} +
+
)} -
- {!updateStatus.isUpdateAvailable && ( - - {" "} -
- - {updateStatus.isChecking - ? t("checking_for_update") - : t("check_for_update")} -
-
- )} -
-
+ ); } diff --git a/src/Components/Users/models.tsx b/src/Components/Users/models.tsx index 5d5c092dd02..cb41779c7ed 100644 --- a/src/Components/Users/models.tsx +++ b/src/Components/Users/models.tsx @@ -33,6 +33,7 @@ export type UserModel = UserBareMinimum & { phone_number?: string; alt_phone_number?: string; gender?: GenderType; + read_profile_picture_url?: string; date_of_birth: Date | null | string; is_superuser?: boolean; verified?: boolean; diff --git a/src/Locale/en.json b/src/Locale/en.json index e516ec61733..cc8a4ad1524 100644 --- a/src/Locale/en.json +++ b/src/Locale/en.json @@ -569,6 +569,7 @@ "duplicate_patient_record_confirmation": "Admit the patient record to your facility by adding the year of birth", "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_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", "edit_history": "Edit History", @@ -843,12 +844,12 @@ "no_changes": "No changes", "no_changes_made": "No changes made", "no_consultation_updates": "No consultation updates", - "no_cover_photo_uploaded_for_this_facility": "No cover photo uploaded for this facility", "no_data_found": "No data found", "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_image_found": "No image found", "no_investigation": "No investigation Reports found", "no_investigation_suggestions": "No Investigation Suggestions", "no_linked_facilities": "No Linked Facilities", diff --git a/src/Locale/hi.json b/src/Locale/hi.json index e235c558978..20a822323bf 100644 --- a/src/Locale/hi.json +++ b/src/Locale/hi.json @@ -506,7 +506,6 @@ "no_changes": "कोई परिवर्तन नहीं", "no_changes_made": "कोई परिवर्तन नहीं किया गया", "no_consultation_updates": "कोई परामर्श अपडेट नहीं", - "no_cover_photo_uploaded_for_this_facility": "इस सुविधा के लिए कोई कवर फ़ोटो अपलोड नहीं किया गया", "no_data_found": "डाटा प्राप्त नहीं हुआ", "no_duplicate_facility": "आपको डुप्लिकेट सुविधाएं नहीं बनानी चाहिए", "no_facilities": "कोई सुविधा नहीं मिली", diff --git a/src/Locale/kn.json b/src/Locale/kn.json index dc46e49394f..0fa96427b85 100644 --- a/src/Locale/kn.json +++ b/src/Locale/kn.json @@ -507,7 +507,6 @@ "no_changes": "ಯಾವುದೇ ಬದಲಾವಣೆಗಳಿಲ್ಲ", "no_changes_made": "ಯಾವುದೇ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡಲಾಗಿಲ್ಲ", "no_consultation_updates": "ಸಮಾಲೋಚನೆಯ ನವೀಕರಣಗಳಿಲ್ಲ", - "no_cover_photo_uploaded_for_this_facility": "ಈ ಸೌಲಭ್ಯಕ್ಕಾಗಿ ಯಾವುದೇ ಕವರ್ ಫೋಟೋ ಅಪ್‌ಲೋಡ್ ಮಾಡಲಾಗಿಲ್ಲ", "no_data_found": "ಯಾವುದೇ ಡೇಟಾ ಕಂಡುಬಂದಿಲ್ಲ", "no_duplicate_facility": "ನೀವು ನಕಲಿ ಸೌಲಭ್ಯಗಳನ್ನು ರಚಿಸಬಾರದು", "no_facilities": "ಯಾವುದೇ ಸೌಲಭ್ಯಗಳು ಕಂಡುಬಂದಿಲ್ಲ", diff --git a/src/Locale/ml.json b/src/Locale/ml.json index d830d3a46ec..b9a45ca73fb 100644 --- a/src/Locale/ml.json +++ b/src/Locale/ml.json @@ -506,7 +506,6 @@ "no_changes": "മാറ്റങ്ങളൊന്നുമില്ല", "no_changes_made": "മാറ്റങ്ങളൊന്നും വരുത്തിയിട്ടില്ല", "no_consultation_updates": "കൺസൾട്ടേഷൻ അപ്‌ഡേറ്റുകളൊന്നുമില്ല", - "no_cover_photo_uploaded_for_this_facility": "ഈ സൗകര്യത്തിനായി കവർ ഫോട്ടോ അപ്‌ലോഡ് ചെയ്‌തിട്ടില്ല", "no_data_found": "വിവരങ്ങളൊന്നും കണ്ടെത്തിയില്ല", "no_duplicate_facility": "അനധികൃതമായി ഫെസിലിറ്റികള്‍ സൃഷ്ടിക്കരുത്", "no_facilities": "ഫെസിലിറ്റികളൊന്നും കണ്ടെത്തുവാനായില്ല", diff --git a/src/Locale/ta.json b/src/Locale/ta.json index 042f5a068c4..fb5dd0a72c8 100644 --- a/src/Locale/ta.json +++ b/src/Locale/ta.json @@ -506,7 +506,6 @@ "no_changes": "மாற்றங்கள் இல்லை", "no_changes_made": "எந்த மாற்றமும் செய்யப்படவில்லை", "no_consultation_updates": "ஆலோசனை அறிவிப்புகள் இல்லை", - "no_cover_photo_uploaded_for_this_facility": "இந்த வசதிக்காக அட்டைப் புகைப்படம் பதிவேற்றப்படவில்லை", "no_data_found": "தரவு எதுவும் கிடைக்கவில்லை", "no_duplicate_facility": "நீங்கள் நகல் வசதிகளை உருவாக்கக்கூடாது", "no_facilities": "வசதிகள் எதுவும் கிடைக்கவில்லை", diff --git a/src/Providers/AuthUserProvider.tsx b/src/Providers/AuthUserProvider.tsx index df2997abe2e..cda5fbfe671 100644 --- a/src/Providers/AuthUserProvider.tsx +++ b/src/Providers/AuthUserProvider.tsx @@ -87,7 +87,14 @@ export default function AuthUserProvider({ children, unauthorized }: Props) { } return ( - + {!res.ok || !user ? unauthorized : children} ); diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index d562908a140..a10a0a4dfa3 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -288,6 +288,19 @@ const routes = { TBody: Type>(), }, + updateProfilePicture: { + path: "/api/v1/users/{username}/profile_picture/", + method: "PATCH", + TRes: Type(), + TBody: Type<{ profile_picture_url: string }>(), + }, + + deleteProfilePicture: { + path: "/api/v1/users/{username}/profile_picture/", + method: "DELETE", + TRes: Type(), + }, + deleteUser: { path: "/api/v1/users/{username}/", method: "DELETE",