diff --git a/package-lock.json b/package-lock.json index aae20921b8a..edfd7250ef1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,10 +59,10 @@ "events": "^3.3.0", "hi-profiles": "^1.1.0", "html2canvas": "^1.4.1", - "i18next": "^24.2.0", + "i18next": "^24.2.1", "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", - "input-otp": "^1.4.1", + "input-otp": "^1.4.2", "lodash-es": "^4.17.21", "lucide-react": "^0.469.0", "markdown-it": "^14.1.0", @@ -11778,9 +11778,9 @@ } }, "node_modules/i18next": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.0.tgz", - "integrity": "sha512-ArJJTS1lV6lgKH7yEf4EpgNZ7+THl7bsGxxougPYiXRTJ/Fe1j08/TBpV9QsXCIYVfdE/HWG/xLezJ5DOlfBOA==", + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.1.tgz", + "integrity": "sha512-Q2wC1TjWcSikn1VAJg13UGIjc+okpFxQTxjVAymOnSA3RpttBQNMPf2ovcgoFVsV4QNxTfNZMAxorXZXsk4fBA==", "funding": [ { "type": "individual", @@ -11986,9 +11986,9 @@ } }, "node_modules/input-otp": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz", - "integrity": "sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", "license": "MIT", "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", diff --git a/package.json b/package.json index e66c7f8a6aa..cd0f1948a36 100644 --- a/package.json +++ b/package.json @@ -97,10 +97,10 @@ "events": "^3.3.0", "hi-profiles": "^1.1.0", "html2canvas": "^1.4.1", - "i18next": "^24.2.0", + "i18next": "^24.2.1", "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", - "input-otp": "^1.4.1", + "input-otp": "^1.4.2", "lodash-es": "^4.17.21", "lucide-react": "^0.469.0", "markdown-it": "^14.1.0", diff --git a/public/locale/en.json b/public/locale/en.json index 5c87e6a2971..044a9ac2e2d 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -734,6 +734,7 @@ "domestic_international_travel": "Domestic/international Travel (within last 28 days)", "done": "Done", "dosage": "Dosage", + "dosage_instructions": "Dosage Instructions", "down": "Down", "download": "Download", "download_discharge_summary": "Download discharge summary", diff --git a/src/Providers/AuthUserProvider.tsx b/src/Providers/AuthUserProvider.tsx index 5cfc64ca1f2..2f657b67fc6 100644 --- a/src/Providers/AuthUserProvider.tsx +++ b/src/Providers/AuthUserProvider.tsx @@ -13,7 +13,6 @@ import { LocalStorageKeys } from "@/common/constants"; import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; import request from "@/Utils/request/request"; -import { TokenData } from "@/types/auth/otpToken"; interface Props { children: React.ReactNode; @@ -27,29 +26,29 @@ export default function AuthUserProvider({ otpAuthorized, }: Props) { const queryClient = useQueryClient(); + const [accessToken, setAccessToken] = useState( + localStorage.getItem(LocalStorageKeys.accessToken), + ); + const [patientToken, setPatientToken] = useState( + JSON.parse(localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}"), + ); const { data: user, isLoading } = useQuery({ - queryKey: ["currentUser"], + queryKey: ["currentUser", accessToken], queryFn: query(routes.currentUser, { silent: true }), retry: false, enabled: !!localStorage.getItem(LocalStorageKeys.accessToken), }); - const [isOTPAuthorized, setIsOTPAuthorized] = useState(false); - - const tokenData: TokenData = JSON.parse( - localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", - ); - useEffect(() => { if ( - tokenData.token && - Object.keys(tokenData).length > 0 && - dayjs(tokenData.createdAt).isAfter(dayjs().subtract(14, "minutes")) + patientToken.token && + Object.keys(patientToken).length > 0 && + dayjs(patientToken.createdAt).isAfter(dayjs().subtract(14, "minutes")) ) { - setIsOTPAuthorized(true); + navigate("/patient/home"); } - }, [tokenData]); + }, [patientToken]); useEffect(() => { if (!user) { @@ -68,6 +67,7 @@ export default function AuthUserProvider({ const query = await request(routes.login, { body: creds }); if (query.res?.ok && query.data) { + setAccessToken(query.data.access); localStorage.setItem(LocalStorageKeys.accessToken, query.data.access); localStorage.setItem(LocalStorageKeys.refreshToken, query.data.refresh); @@ -83,10 +83,20 @@ export default function AuthUserProvider({ [queryClient], ); + const patientLogin = useCallback(() => { + setPatientToken( + JSON.parse( + localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", + ), + ); + navigate("/patient/home"); + }, []); + const signOut = useCallback(async () => { localStorage.removeItem(LocalStorageKeys.accessToken); localStorage.removeItem(LocalStorageKeys.refreshToken); localStorage.removeItem(LocalStorageKeys.patientTokenKey); + setPatientToken({}); await queryClient.resetQueries({ queryKey: ["currentUser"] }); @@ -124,7 +134,7 @@ export default function AuthUserProvider({ const SelectedRouter = () => { if (user) { return children; - } else if (isOTPAuthorized) { + } else if (patientToken.token) { return otpAuthorized; } else { return unauthorized; @@ -132,7 +142,14 @@ export default function AuthUserProvider({ }; return ( - + ); diff --git a/src/components/Auth/Login.tsx b/src/components/Auth/Login.tsx index 487f7bc2fb0..2af8ebe4663 100644 --- a/src/components/Auth/Login.tsx +++ b/src/components/Auth/Login.tsx @@ -1,7 +1,7 @@ import careConfig from "@careConfig"; import { useMutation } from "@tanstack/react-query"; import { Link, useQueryParams } from "raviger"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import ReCaptcha from "react-google-recaptcha"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; @@ -29,14 +29,13 @@ import BrowserWarning from "@/components/ErrorPages/BrowserWarning"; import { useAuthContext } from "@/hooks/useAuthUser"; -import { CarePatientTokenKey } from "@/common/constants"; +import { LocalStorageKeys } from "@/common/constants"; import FiltersCache from "@/Utils/FiltersCache"; import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; import request from "@/Utils/request/request"; -import { HTTPError } from "@/Utils/request/types"; import { TokenData } from "@/types/auth/otpToken"; interface LoginFormData { @@ -44,10 +43,9 @@ interface LoginFormData { password: string; } -type LoginMode = "staff" | "patient"; - -interface LoginProps { - forgot?: boolean; +interface OtpLoginData { + phone_number: string; + otp: string; } interface OtpError { @@ -61,14 +59,19 @@ interface OtpError { url: string; } -// Update interface for OTP data -interface OtpLoginData { - phone_number: string; - otp: string; +interface OtpValidationError { + otp?: string; + [key: string]: string | undefined; +} + +type LoginMode = "staff" | "patient"; + +interface LoginProps { + forgot?: boolean; } const Login = (props: LoginProps) => { - const { signIn } = useAuthContext(); + const { signIn, patientLogin } = useAuthContext(); const { reCaptchaSiteKey, urls, stateLogo, customLogo, customLogoAlt } = careConfig; const customDescriptionHtml = __CUSTOM_DESCRIPTION_HTML__; @@ -84,8 +87,6 @@ const Login = (props: LoginProps) => { const [errors, setErrors] = useState(initErr); const [isCaptchaEnabled, setCaptcha] = useState(false); const { t } = useTranslation(); - // display spinner while login is under progress - const [loading, setLoading] = useState(false); const [forgotPassword, setForgotPassword] = useState(forgot); const [loginMode, setLoginMode] = useState( mode === "patient" ? "patient" : "staff", @@ -104,15 +105,6 @@ const Login = (props: LoginProps) => { }, onSuccess: ({ res }) => { setCaptcha(res?.status === 429); - window.location.href = "/"; - }, - }); - - // Forgot Password Mutation - const { mutate: submitForgetPassword } = useMutation({ - mutationFn: mutate(routes.forgotPassword), - onSuccess: () => { - toast.success(t("password_sent")); }, }); @@ -128,7 +120,7 @@ const Login = (props: LoginProps) => { onSuccess: () => { setIsOtpSent(true); setOtpError(""); - Notification.Success({ msg: t("send_otp_success") }); + toast.success(t("send_otp_success")); }, onError: (error: any) => { const errors = error?.data || []; @@ -161,20 +153,23 @@ const Login = (props: LoginProps) => { phoneNumber: `+91${phone}`, createdAt: new Date().toISOString(), }; - localStorage.setItem(CarePatientTokenKey, JSON.stringify(tokenData)); - window.location.href = "/patient/home"; + localStorage.setItem( + LocalStorageKeys.patientTokenKey, + JSON.stringify(tokenData), + ); + patientLogin(); } }, - - //Invalid OTP error handling - onError: (error: HTTPError) => { + onError: (error: any) => { let errorMessage = t("invalid_otp"); if ( error.cause && Array.isArray(error.cause.errors) && error.cause.errors.length > 0 ) { - const otpError = error.cause.errors.find((e) => e.otp); + const otpError = error.cause.errors.find( + (e: OtpValidationError) => e.otp, + ); if (otpError && otpError.otp) { errorMessage = otpError.otp; } @@ -186,14 +181,20 @@ const Login = (props: LoginProps) => { }, }); + // Forgot Password Mutation + const { mutate: submitForgetPassword } = useMutation({ + mutationFn: mutate(routes.forgotPassword), + onSuccess: () => { + toast.success(t("password_sent")); + }, + }); + // Format phone number to include +91 const formatPhoneNumber = (value: string) => { // Remove any non-digit characters const digits = value.replace(/\D/g, ""); - // Limit to 10 digits const truncated = digits.slice(0, 10); - return truncated; }; @@ -205,7 +206,6 @@ const Login = (props: LoginProps) => { }; // Login form validation - const handleChange = (e: any) => { const { value, name } = e.target; const fieldValue = Object.assign({}, form); @@ -244,17 +244,9 @@ const Login = (props: LoginProps) => { setErrors(err); return false; } - return form; }; - // set loading to false when component is unmounted - useEffect(() => { - return () => { - setLoading(false); - }; - }, []); - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const validated = validateData(); @@ -305,10 +297,19 @@ const Login = (props: LoginProps) => { const handlePatientLogin = async (e: React.FormEvent) => { e.preventDefault(); - if (!isOtpSent) { - sendOtp(phone); - } else { - verifyOtp({ phone_number: `+91${phone}`, otp }); + try { + if (!isOtpSent) { + await sendOtp(phone); + setIsOtpSent(true); + } else { + await verifyOtp({ phone_number: `+91${phone}`, otp }); + } + } catch (error: any) { + if (!isOtpSent) { + setOtpError(error.message); + } else { + setOtpValidationError(error.message); + } } }; @@ -316,6 +317,8 @@ const Login = (props: LoginProps) => { setIsOtpSent(false); setPhone(""); setOtp(""); + setOtpError(""); + setOtpValidationError(""); }; // Loading state derived from mutations @@ -695,13 +698,13 @@ const Login = (props: LoginProps) => { className="w-full" variant="primary" disabled={ - loading || + isLoading || !phone || phone.length !== 10 || (isOtpSent && otp.length !== 5) } > - {loading ? ( + {isLoading ? ( ) : isOtpSent ? ( t("verify_otp") diff --git a/src/components/Patient/PatientHome.tsx b/src/components/Patient/PatientHome.tsx index 713031169e7..1d4f942f900 100644 --- a/src/components/Patient/PatientHome.tsx +++ b/src/components/Patient/PatientHome.tsx @@ -165,8 +165,8 @@ export const PatientHome = (props: {
{t("last_updated_by")}{" "} - {patientData.updated_by.first_name}{" "} - {patientData.updated_by.last_name} + {patientData.updated_by?.first_name}{" "} + {patientData.updated_by?.last_name}
@@ -187,8 +187,8 @@ export const PatientHome = (props: {
{t("patient_profile_created_by")}{" "} - {patientData.created_by.first_name}{" "} - {patientData.created_by.last_name} + {patientData.created_by?.first_name}{" "} + {patientData.created_by?.last_name}
diff --git a/src/components/Questionnaire/QuestionLabel.tsx b/src/components/Questionnaire/QuestionLabel.tsx new file mode 100644 index 00000000000..17855845b0f --- /dev/null +++ b/src/components/Questionnaire/QuestionLabel.tsx @@ -0,0 +1,27 @@ +import { Label } from "@/components/ui/label"; + +import type { Question } from "@/types/questionnaire/question"; + +interface QuestionLabelProps { + question: Question; + className?: string; +} + +export function QuestionLabel({ + question, + className = "text-base font-medium block", +}: QuestionLabelProps) { + return ( + + ); +} diff --git a/src/components/Questionnaire/QuestionTypes/BooleanQuestion.tsx b/src/components/Questionnaire/QuestionTypes/BooleanQuestion.tsx index 905a50122da..b2b094790b4 100644 --- a/src/components/Questionnaire/QuestionTypes/BooleanQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/BooleanQuestion.tsx @@ -20,48 +20,42 @@ export function BooleanQuestion({ clearError, }: BooleanQuestionProps) { return ( -
- - { - clearError(); - updateQuestionnaireResponseCB({ - ...questionnaireResponse, - values: [ - { - type: "boolean", - value: value === "true", - }, - ], - }); - }} - disabled={disabled} - > -
-
- - -
-
- - -
+ { + clearError(); + updateQuestionnaireResponseCB({ + ...questionnaireResponse, + values: [ + { + type: "boolean", + value: value === "true", + }, + ], + }); + }} + disabled={disabled} + > +
+
+ +
- -
+
+ + +
+
+
); } diff --git a/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx b/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx index 39f3b66697e..4c0f7259bef 100644 --- a/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/ChoiceQuestion.tsx @@ -1,6 +1,5 @@ import { memo } from "react"; -import { Label } from "@/components/ui/label"; import { Select, SelectContent, @@ -30,7 +29,6 @@ export const ChoiceQuestion = memo(function ChoiceQuestion({ questionnaireResponse, updateQuestionnaireResponseCB, disabled = false, - withLabel = true, clearError, index = 0, }: ChoiceQuestionProps) { @@ -52,32 +50,24 @@ export const ChoiceQuestion = memo(function ChoiceQuestion({ }; return ( -
- {withLabel && ( - - )} - -
+ ); }); diff --git a/src/components/Questionnaire/QuestionTypes/DateTimeQuestion.tsx b/src/components/Questionnaire/QuestionTypes/DateTimeQuestion.tsx index b83e593452c..b20629e171c 100644 --- a/src/components/Questionnaire/QuestionTypes/DateTimeQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/DateTimeQuestion.tsx @@ -7,7 +7,6 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, @@ -15,10 +14,8 @@ import { } from "@/components/ui/popover"; import type { QuestionnaireResponse } from "@/types/questionnaire/form"; -import type { Question } from "@/types/questionnaire/question"; interface DateTimeQuestionProps { - question: Question; questionnaireResponse: QuestionnaireResponse; updateQuestionnaireResponseCB: (response: QuestionnaireResponse) => void; disabled?: boolean; @@ -27,7 +24,6 @@ interface DateTimeQuestionProps { } export function DateTimeQuestion({ - question, questionnaireResponse, updateQuestionnaireResponseCB, disabled, @@ -39,39 +35,39 @@ export function DateTimeQuestion({ : undefined; const handleSelect = (date: Date | undefined) => { - if (!date) { - handleUpdate(undefined); - return; - } + if (!date) return; - // Preserve the time if it exists, otherwise set to current time + clearError(); if (currentValue) { date.setHours(currentValue.getHours()); date.setMinutes(currentValue.getMinutes()); } - handleUpdate(date); - }; - const handleTimeChange = (e: React.ChangeEvent) => { - const timeString = e.target.value; - if (!timeString) return; + updateQuestionnaireResponseCB({ + ...questionnaireResponse, + values: [ + { + type: "dateTime", + value: date.toISOString(), + }, + ], + }); + }; - const [hours, minutes] = timeString.split(":").map(Number); - const newDate = currentValue ? new Date(currentValue) : new Date(); - newDate.setHours(hours); - newDate.setMinutes(minutes); + const handleTimeChange = (event: React.ChangeEvent) => { + const [hours, minutes] = event.target.value.split(":").map(Number); + if (isNaN(hours) || isNaN(minutes)) return; - handleUpdate(newDate); - }; + const date = currentValue || new Date(); + date.setHours(hours); + date.setMinutes(minutes); - const handleUpdate = (date: Date | undefined) => { - clearError(); updateQuestionnaireResponseCB({ ...questionnaireResponse, values: [ { type: "dateTime", - value: date?.toISOString() || "", + value: date.toISOString(), }, ], }); @@ -86,44 +82,38 @@ export function DateTimeQuestion({ }; return ( -
- -
- - - - - - - - - -
+
+ + + + + + + + +
); } diff --git a/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx b/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx index c1ad524503b..af85d52b152 100644 --- a/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/DiagnosisQuestion.tsx @@ -32,10 +32,8 @@ import { } from "@/types/emr/diagnosis/diagnosis"; import { Code } from "@/types/questionnaire/code"; import { QuestionnaireResponse } from "@/types/questionnaire/form"; -import { Question } from "@/types/questionnaire/question"; interface DiagnosisQuestionProps { - question: Question; questionnaireResponse: QuestionnaireResponse; updateQuestionnaireResponseCB: (response: QuestionnaireResponse) => void; disabled?: boolean; @@ -49,7 +47,6 @@ const DIAGNOSIS_INITIAL_VALUE: Partial = { }; export function DiagnosisQuestion({ - question, questionnaireResponse, updateQuestionnaireResponseCB, disabled, @@ -90,11 +87,7 @@ export function DiagnosisQuestion({ }; return ( -
- + <> {diagnoses.length > 0 && (
@@ -123,7 +116,7 @@ export function DiagnosisQuestion({ onSelect={handleAddDiagnosis} disabled={disabled} /> -
+ ); } diff --git a/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx b/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx index dd9cb4322aa..ab52140001f 100644 --- a/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/MedicationRequestQuestion.tsx @@ -25,10 +25,8 @@ import { } from "@/types/emr/medicationRequest"; import { Code } from "@/types/questionnaire/code"; import { QuestionnaireResponse } from "@/types/questionnaire/form"; -import { Question } from "@/types/questionnaire/question"; interface MedicationRequestQuestionProps { - question: Question; questionnaireResponse: QuestionnaireResponse; updateQuestionnaireResponseCB: (response: QuestionnaireResponse) => void; disabled?: boolean; @@ -46,7 +44,6 @@ const MEDICATION_REQUEST_INITIAL_VALUE: MedicationRequest = { }; export function MedicationRequestQuestion({ - question, questionnaireResponse, updateQuestionnaireResponseCB, disabled, @@ -102,11 +99,7 @@ export function MedicationRequestQuestion({ }; return ( -
- + <> {medications.length > 0 && (
    @@ -133,7 +126,7 @@ export function MedicationRequestQuestion({ disabled={disabled} searchPostFix=" clinical drug" /> -
+ ); } diff --git a/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx b/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx index 7c237b35f5d..e4b63b125bf 100644 --- a/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx +++ b/src/components/Questionnaire/QuestionTypes/MedicationStatementQuestion.tsx @@ -243,7 +243,7 @@ const MedicationStatementItem: React.FC<{
void; disabled?: boolean; - classes?: string; } export function NumberQuestion({ @@ -19,7 +15,6 @@ export function NumberQuestion({ questionnaireResponse, updateQuestionnaireResponseCB, disabled, - classes, }: NumberQuestionProps) { const handleChange = (value: string) => { const numericValue = @@ -37,19 +32,12 @@ export function NumberQuestion({ }; return ( -
- - handleChange(e.target.value)} - step={question.type === "decimal" ? "0.01" : "1"} - disabled={disabled} - /> -
+ handleChange(e.target.value)} + step={question.type === "decimal" ? "0.01" : "1"} + disabled={disabled} + /> ); } diff --git a/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx b/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx index e9f6f2a62e2..8176dbaf294 100644 --- a/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx +++ b/src/components/Questionnaire/QuestionTypes/QuestionGroup.tsx @@ -118,7 +118,7 @@ export const QuestionGroup = memo(function QuestionGroup({ > {question.text && (
-