diff --git a/crowdin.yml b/crowdin.yml index fdcb6bcd32d..04cbe1a8d50 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,6 +1,5 @@ files: - - source: /public/locale/{{lang}}.json - translation: /public/locale/%two_letters_code%/%original_file_name% + - source: public/locale/en.json + translation: /public/locale/%two_letters_code%.json bundles: - 2 - diff --git a/cypress/e2e/patient_spec/patient_search.cy.ts b/cypress/e2e/patient_spec/patient_search.cy.ts index 96ba5d94a47..b63e456be9b 100644 --- a/cypress/e2e/patient_spec/patient_search.cy.ts +++ b/cypress/e2e/patient_spec/patient_search.cy.ts @@ -17,7 +17,7 @@ describe("Patient Search", () => { it("search patient with phone number and verifies details", () => { patientSearch - .selectFacility("PHC Kakkanad -1") + .selectFacility("Arike") .clickSearchPatients() .searchPatient(TEST_PHONE) .verifySearchResults(PATIENT_DETAILS); diff --git a/public/locale/en.json b/public/locale/en.json index eb18efd6829..01f19412c66 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -359,6 +359,7 @@ "all_changes_have_been_saved": "All changes have been saved", "all_details": "All Details", "all_patients": "All Patients", + "allergen": "Allergen", "allergies": "Allergies", "allow_transfer": "Allow Transfer", "allowed_formats_are": "Allowed formats are", @@ -650,6 +651,7 @@ "created_by": "Created By", "created_date": "Created Date", "created_on": "Created On", + "criticality": "Criticality", "csv_file_in_the_specified_format": "Select a CSV file in the specified format", "current_address": "Current Address", "current_password": "Current Password", @@ -830,6 +832,7 @@ "encounter_discharge_disposition__snf": "Skilled nursing facility", "encounter_duration_confirmation": "The duration of this encounter would be", "encounter_id": "Encounter ID", + "encounter_marked_as_complete": "Encounter Completed", "encounter_notes__all_discussions": "All Discussions", "encounter_notes__be_first_to_send": "Be the first to send a message", "encounter_notes__choose_template": "Choose a template or enter a custom title", @@ -900,6 +903,7 @@ "error_deleting_shifting": "Error while deleting Shifting record", "error_fetching_slots_data": "Error while fetching slots data", "error_sending_otp": "Error while sending OTP, Please try again later", + "error_updating_encounter": "Error to Updating Encounter", "error_verifying_otp": "Error while verifying OTP, Please request a new OTP", "error_while_deleting_record": "Error while deleting record", "escape": "Escape", @@ -1165,6 +1169,7 @@ "manufacturer": "Manufacturer", "map_acronym": "M.A.P.", "mark_all_as_read": "Mark all as Read", + "mark_as_complete": "Mark as Complete", "mark_as_entered_in_error": "Mark as entered in error", "mark_as_fulfilled": "Mark as Fullfilled", "mark_as_noshow": "Mark as no-show", diff --git a/src/Providers/AuthUserProvider.tsx b/src/Providers/AuthUserProvider.tsx index 2f657b67fc6..7b94ba73e4a 100644 --- a/src/Providers/AuthUserProvider.tsx +++ b/src/Providers/AuthUserProvider.tsx @@ -1,6 +1,5 @@ import careConfig from "@careConfig"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import dayjs from "dayjs"; import { navigate } from "raviger"; import { useCallback, useEffect, useState } from "react"; @@ -13,6 +12,7 @@ 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; @@ -29,8 +29,10 @@ export default function AuthUserProvider({ const [accessToken, setAccessToken] = useState( localStorage.getItem(LocalStorageKeys.accessToken), ); - const [patientToken, setPatientToken] = useState( - JSON.parse(localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}"), + const [patientToken, setPatientToken] = useState( + JSON.parse( + localStorage.getItem(LocalStorageKeys.patientTokenKey) || "null", + ), ); const { data: user, isLoading } = useQuery({ @@ -40,16 +42,6 @@ export default function AuthUserProvider({ enabled: !!localStorage.getItem(LocalStorageKeys.accessToken), }); - useEffect(() => { - if ( - patientToken.token && - Object.keys(patientToken).length > 0 && - dayjs(patientToken.createdAt).isAfter(dayjs().subtract(14, "minutes")) - ) { - navigate("/patient/home"); - } - }, [patientToken]); - useEffect(() => { if (!user) { return; @@ -83,20 +75,20 @@ export default function AuthUserProvider({ [queryClient], ); - const patientLogin = useCallback(() => { - setPatientToken( - JSON.parse( - localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", - ), + const patientLogin = (tokenData: TokenData, redirectUrl: string) => { + setPatientToken(tokenData); + localStorage.setItem( + LocalStorageKeys.patientTokenKey, + JSON.stringify(tokenData), ); - navigate("/patient/home"); - }, []); + navigate(redirectUrl); + }; const signOut = useCallback(async () => { localStorage.removeItem(LocalStorageKeys.accessToken); localStorage.removeItem(LocalStorageKeys.refreshToken); localStorage.removeItem(LocalStorageKeys.patientTokenKey); - setPatientToken({}); + setPatientToken(null); await queryClient.resetQueries({ queryKey: ["currentUser"] }); @@ -134,7 +126,7 @@ export default function AuthUserProvider({ const SelectedRouter = () => { if (user) { return children; - } else if (patientToken.token) { + } else if (patientToken?.token) { return otpAuthorized; } else { return unauthorized; @@ -148,6 +140,7 @@ export default function AuthUserProvider({ signOut, user, patientLogin, + patientToken, }} > diff --git a/src/Providers/PatientUserProvider.tsx b/src/Providers/PatientUserProvider.tsx index 3ec0c5f038a..1ae9a9af8f1 100644 --- a/src/Providers/PatientUserProvider.tsx +++ b/src/Providers/PatientUserProvider.tsx @@ -1,20 +1,14 @@ import { useQuery } from "@tanstack/react-query"; +import { navigate } from "raviger"; import { createContext, useEffect, useState } from "react"; -import { SidebarProvider } from "@/components/ui/sidebar"; -import { AppSidebar } from "@/components/ui/sidebar/app-sidebar"; - -import { LocalStorageKeys } from "@/common/constants"; +import { useAuthContext } from "@/hooks/useAuthUser"; import routes from "@/Utils/request/api"; import query from "@/Utils/request/query"; import { AppointmentPatient } from "@/pages/Patient/Utils"; import { TokenData } from "@/types/auth/otpToken"; -const tokenData: TokenData = JSON.parse( - localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", -); - export type PatientUserContextType = { patients?: AppointmentPatient[]; selectedPatient: AppointmentPatient | null; @@ -22,12 +16,9 @@ export type PatientUserContextType = { tokenData: TokenData; }; -export const PatientUserContext = createContext({ - patients: undefined, - selectedPatient: null, - setSelectedPatient: () => {}, - tokenData: tokenData, -}); +export const PatientUserContext = createContext( + null, +); interface Props { children: React.ReactNode; @@ -38,14 +29,16 @@ export default function PatientUserProvider({ children }: Props) { const [selectedPatient, setSelectedPatient] = useState(null); + const { patientToken: tokenData } = useAuthContext(); + const { data: userData } = useQuery({ - queryKey: ["patients", tokenData.phoneNumber], + queryKey: ["patients", tokenData], queryFn: query(routes.otp.getPatient, { headers: { - Authorization: `Bearer ${tokenData.token}`, + Authorization: `Bearer ${tokenData?.token}`, }, }), - enabled: !!tokenData.token, + enabled: !!tokenData?.token, }); useEffect(() => { @@ -62,22 +55,21 @@ export default function PatientUserProvider({ children }: Props) { } }, [userData]); - const patientUserContext: PatientUserContextType = { - patients, - selectedPatient, - setSelectedPatient, - tokenData, - }; + if (!tokenData) { + navigate("/"); + return null; + } return ( - - - - {children} - + + {children} ); } diff --git a/src/Routers/PatientRouter.tsx b/src/Routers/PatientRouter.tsx index ded69ce375d..bf16c8c3e30 100644 --- a/src/Routers/PatientRouter.tsx +++ b/src/Routers/PatientRouter.tsx @@ -1,7 +1,8 @@ import careConfig from "@careConfig"; import { useRoutes } from "raviger"; -import { SidebarTrigger } from "@/components/ui/sidebar"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { AppSidebar, SidebarFor } from "@/components/ui/sidebar/app-sidebar"; import ErrorBoundary from "@/components/Common/ErrorBoundary"; import ErrorPage from "@/components/ErrorPages/DefaultErrorPage"; @@ -9,13 +10,16 @@ import { patientTabs } from "@/components/Patient/PatientDetailsTab"; import { PatientHome } from "@/components/Patient/PatientHome"; import PatientUserProvider from "@/Providers/PatientUserProvider"; +import { PatientRegistration } from "@/pages/Appoinments/PatientRegistration"; +import PatientSelect from "@/pages/Appoinments/PatientSelect"; +import { ScheduleAppointment } from "@/pages/Appoinments/Schedule"; import { AppointmentSuccess } from "@/pages/Appoinments/Success"; import { FacilitiesPage } from "@/pages/Facility/FacilitiesPage"; import PatientIndex from "@/pages/Patient/index"; import PublicRouter from "./PublicRouter"; -const PatientRoutes = { +const DashboardRoutes = { "/nearby_facilities": () => , "/facility/:facilityId/appointments/:appointmentId/success": ({ appointmentId, @@ -35,43 +39,75 @@ const PatientRoutes = { }) => , }; +const AppointmentRoutes = { + "/facility/:facilityId/appointments/:staffId/book-appointment": ({ + facilityId, + staffId, + }: { + facilityId: string; + staffId: string; + }) => , + "/facility/:facilityId/appointments/:staffId/patient-select": ({ + facilityId, + staffId, + }: { + facilityId: string; + staffId: string; + }) => , + "/facility/:facilityId/appointments/:staffId/patient-registration": ({ + facilityId, + staffId, + }: { + facilityId: string; + staffId: string; + }) => , +}; + export default function PatientRouter() { - const pages = useRoutes(PatientRoutes); + const pages = useRoutes(DashboardRoutes); + + const appointmentPages = useRoutes(AppointmentRoutes); if (!pages) { + if (appointmentPages) { + return {appointmentPages}; + } return ; } return ( -
-
-
- + + +
+
+
+ +
+ + care logo +
- - care logo - -
-
- }> - {pages} - -
-
+ }> + {pages} + + + +
); } diff --git a/src/Routers/PublicRouter.tsx b/src/Routers/PublicRouter.tsx index 62c67eb8cbf..47739e6ff37 100644 --- a/src/Routers/PublicRouter.tsx +++ b/src/Routers/PublicRouter.tsx @@ -6,9 +6,6 @@ import ResetPassword from "@/components/Auth/ResetPassword"; import InvalidReset from "@/components/ErrorPages/InvalidReset"; import SessionExpired from "@/components/ErrorPages/SessionExpired"; -import { PatientRegistration } from "@/pages/Appoinments/PatientRegistration"; -import PatientSelect from "@/pages/Appoinments/PatientSelect"; -import { ScheduleAppointment } from "@/pages/Appoinments/Schedule"; import PatientLogin from "@/pages/Appoinments/auth/PatientLogin"; import { FacilitiesPage } from "@/pages/Facility/FacilitiesPage"; import { FacilityDetailsPage } from "@/pages/Facility/FacilityDetailsPage"; @@ -29,27 +26,6 @@ export const routes = { staffId: string; page: string; }) => , - "/facility/:facilityId/appointments/:staffId/book-appointment": ({ - facilityId, - staffId, - }: { - facilityId: string; - staffId: string; - }) => , - "/facility/:facilityId/appointments/:staffId/patient-select": ({ - facilityId, - staffId, - }: { - facilityId: string; - staffId: string; - }) => , - "/facility/:facilityId/appointments/:staffId/patient-registration": ({ - facilityId, - staffId, - }: { - facilityId: string; - staffId: string; - }) => , "/login": () => , "/forgot-password": () => , "/password_reset/:token": ({ token }: { token: string }) => ( diff --git a/src/Routers/routes/ConsultationRoutes.tsx b/src/Routers/routes/ConsultationRoutes.tsx index a55b00b3513..96812b5ea40 100644 --- a/src/Routers/routes/ConsultationRoutes.tsx +++ b/src/Routers/routes/ConsultationRoutes.tsx @@ -42,26 +42,6 @@ const consultationRoutes: AppRoutes = { patientId={patientId} /> ), - "/facility/:facilityId/patient/:patientId/consultation/:id/consent-records": - ({ facilityId, patientId, id }) => ( - - ), - "/facility/:facilityId/patient/:patientId/encounterId/:id/files/": ({ - facilityId, - patientId, - id, - }) => ( - - ), "/facility/:facilityId/patient/:patientId/questionnaire": ({ facilityId, patientId, @@ -80,10 +60,6 @@ const consultationRoutes: AppRoutes = { patientId={patientId} /> ), - "/facility/:facilityId/patient/:patientId/encounter/:encounterId/questionnaire_response/:id": - ({ patientId, id }) => ( - - ), "/facility/:facilityId/patient/:patientId/encounter/:encounterId/questionnaire/:slug": ({ facilityId, encounterId, slug, patientId }) => ( ), + "/facility/:facilityId/patient/:patientId/encounter/:encounterId/questionnaire_response/:id": + ({ patientId, id }) => ( + + ), + "/facility/:facilityId/patient/:patientId/consultation/:id/consent-records": + ({ facilityId, patientId, id }) => ( + + ), + "/facility/:facilityId/patient/:patientId/encounterId/:id/files/": ({ + facilityId, + patientId, + id, + }) => ( + + ), }; export default consultationRoutes; diff --git a/src/components/Auth/Login.tsx b/src/components/Auth/Login.tsx index d1756c79624..8858d33be0e 100644 --- a/src/components/Auth/Login.tsx +++ b/src/components/Auth/Login.tsx @@ -28,8 +28,6 @@ import BrowserWarning from "@/components/ErrorPages/BrowserWarning"; import { useAuthContext } from "@/hooks/useAuthUser"; -import { LocalStorageKeys } from "@/common/constants"; - import FiltersCache from "@/Utils/FiltersCache"; import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; @@ -152,11 +150,7 @@ const Login = (props: LoginProps) => { phoneNumber: `+91${phone}`, createdAt: new Date().toISOString(), }; - localStorage.setItem( - LocalStorageKeys.patientTokenKey, - JSON.stringify(tokenData), - ); - patientLogin(); + patientLogin(tokenData, `/patient/home`); } }, onError: (error: any) => { diff --git a/src/components/Patient/PatientInfoCard.tsx b/src/components/Patient/PatientInfoCard.tsx index 8c0c8d29014..2c716095400 100644 --- a/src/components/Patient/PatientInfoCard.tsx +++ b/src/components/Patient/PatientInfoCard.tsx @@ -1,3 +1,4 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { BedSingle, Building, @@ -9,8 +10,18 @@ import { } from "lucide-react"; import { Link } from "raviger"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Popover, PopoverContent, @@ -19,13 +30,29 @@ import { import { Avatar } from "@/components/Common/Avatar"; +import routes from "@/Utils/request/api"; +import mutate from "@/Utils/request/mutate"; import { formatDateTime, formatPatientAge } from "@/Utils/utils"; import { Encounter, completedEncounterStatus } from "@/types/emr/encounter"; import { Patient } from "@/types/emr/newPatient"; -import { Button } from "../ui/button"; import ManageEncounterOrganizations from "./ManageEncounterOrganizations"; +const QUESTIONNAIRE_OPTIONS = [ + { + slug: "encounter", + title: "Update Encounter", + }, + { + slug: "community-nurse", + title: "Community Nurse Form", + }, + { + slug: "recommend_discharge_v2", + title: "Recommend Discharge", + }, +] as const; + export interface PatientInfoCardProps { patient: Patient; encounter: Encounter; @@ -35,6 +62,36 @@ export interface PatientInfoCardProps { export default function PatientInfoCard(props: PatientInfoCardProps) { const { patient, encounter } = props; const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const { mutate: updateEncounter } = useMutation({ + mutationFn: mutate(routes.encounter.update, { + pathParams: { id: encounter.id }, + }), + onSuccess: () => { + toast.success(t("encounter_marked_as_complete")); + queryClient.invalidateQueries({ queryKey: ["encounter", encounter.id] }); + }, + onError: () => { + toast.error(t("error_updating_encounter")); + }, + }); + + const handleMarkAsComplete = () => { + updateEncounter({ + ...encounter, + status: "completed", + organizations: encounter.organizations.map((org) => org.id), + patient: encounter.patient.id, + encounter_class: encounter.encounter_class, + period: encounter.period, + hospitalization: encounter.hospitalization, + priority: encounter.priority, + external_identifier: encounter.external_identifier, + facility: encounter.facility.id, + }); + }; + return ( <>
@@ -262,13 +319,31 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { > {!completedEncounterStatus.includes(encounter.status) && (
- + + + + + + {QUESTIONNAIRE_OPTIONS.map((option) => ( + + + {t(option.title)} + + + ))} + + {t("actions")} + + {t("mark_as_complete")} + + +
)} diff --git a/src/components/Patient/allergy/list.tsx b/src/components/Patient/allergy/list.tsx index 284b7885a70..456f61c759e 100644 --- a/src/components/Patient/allergy/list.tsx +++ b/src/components/Patient/allergy/list.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { t } from "i18next"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -85,22 +86,22 @@ export function AllergyList({ patientId, encounterId }: AllergyListProps) { return ( - Allergies + {t("allergies")} - Allergen - Category - Status - Criticality - Created By + {t("allergen")} + {t("category")} + {t("status")} + {t("criticality")} + {t("created_by")} {allergies.results.map((allergy: AllergyIntolerance) => ( - + {allergy.code.display} diff --git a/src/components/ui/sidebar/app-sidebar.tsx b/src/components/ui/sidebar/app-sidebar.tsx index ab750aff9a6..f88eb77e771 100644 --- a/src/components/ui/sidebar/app-sidebar.tsx +++ b/src/components/ui/sidebar/app-sidebar.tsx @@ -1,8 +1,6 @@ import { DashboardIcon } from "@radix-ui/react-icons"; -import { TFunction } from "i18next"; import { Link, usePathParams } from "raviger"; import * as React from "react"; -import { useTranslation } from "react-i18next"; import { Sidebar, @@ -14,121 +12,32 @@ import { SidebarMenuItem, SidebarRail, } from "@/components/ui/sidebar"; +import { FacilityNav } from "@/components/ui/sidebar/facility-nav"; import { FacilitySwitcher } from "@/components/ui/sidebar/facility-switcher"; -import { NavMain } from "@/components/ui/sidebar/nav-main"; import { FacilityNavUser, PatientNavUser, } from "@/components/ui/sidebar/nav-user"; +import { OrgNav } from "@/components/ui/sidebar/org-nav"; import { OrganizationSwitcher } from "@/components/ui/sidebar/organization-switcher"; +import { PatientNav } from "@/components/ui/sidebar/patient-nav"; import { UserFacilityModel, UserModel } from "@/components/Users/models"; -import { PatientUserContextType } from "@/Providers/PatientUserProvider"; -import { AppointmentPatient } from "@/pages/Patient/Utils"; -import { Organization } from "@/types/organization/organization"; - -import { PatientSwitcher } from "./patient-switcher"; - -interface NavigationLink { - name: string; - url: string; - icon?: string; -} - interface AppSidebarProps extends React.ComponentProps { user?: UserModel; facilitySidebar?: boolean; - patientUserContext?: PatientUserContextType; + sidebarFor?: SidebarFor; } -function generateFacilityLinks( - selectedFacility: UserFacilityModel | null, - t: TFunction, - // TODO: switch to UserBase once getcurrentuser serializer is updated - user?: UserModel, -) { - if (!selectedFacility) return []; - - const baseUrl = `/facility/${selectedFacility.id}`; - const links: NavigationLink[] = [ - { name: t("facility"), url: baseUrl, icon: "d-hospital" }, - { - name: t("appointments"), - url: `${baseUrl}/appointments`, - icon: "d-calendar", - }, - { - name: t("search_patients"), - url: `${baseUrl}/patients`, - icon: "d-patient", - }, - { name: t("encounters"), url: `${baseUrl}/encounters`, icon: "d-patient" }, - // { name: t("assets"), url: `${baseUrl}/assets`, icon: "d-folder" }, - { name: t("resource"), url: "/resource", icon: "d-book-open" }, - { name: t("users"), url: `${baseUrl}/users`, icon: "d-people" }, - { - name: t("organization"), - url: `${baseUrl}/organization`, - icon: "d-book-open", - }, - ]; - - if (user) { - links.push({ - name: t("my_schedules"), - url: `${baseUrl}/users/${user.username}/availability`, - icon: "d-calendar", - }); - } - - return links; -} - -function generateOrganizationLinks( - organizations: Organization[], -): NavigationLink[] { - return organizations.map((org) => ({ - name: org.name, - url: `/organization/${org.id}`, - })); -} - -function generatePatientLinks( - selectedUser: AppointmentPatient | null, - t: TFunction, -): NavigationLink[] { - if (!selectedUser) return []; - - const { geo_organization } = selectedUser; - let parentOrganization = geo_organization?.parent; - while (parentOrganization?.parent) { - if (parentOrganization.level_cache === 1) { - break; - } - parentOrganization = parentOrganization.parent; - } - - const queryParams = new URLSearchParams(); - - if (parentOrganization) { - queryParams.set("organization", String(parentOrganization?.id)); - } - - return [ - { name: t("appointments"), url: "/patient/home", icon: "d-patient" }, - { - name: t("nearby_facilities"), - url: `/nearby_facilities/?${queryParams.toString()}`, - icon: "d-patient", - }, - ]; +export enum SidebarFor { + FACILITY = "facility", + PATIENT = "patient", } export function AppSidebar({ user, - facilitySidebar = true, - patientUserContext, + sidebarFor = SidebarFor.FACILITY, ...props }: AppSidebarProps) { const exactMatch = usePathParams("/facility/:facilityId"); @@ -139,11 +48,12 @@ export function AppSidebar({ const orgSubpathMatch = usePathParams("/organization/:id/*"); const organizationId = orgMatch?.id || orgSubpathMatch?.id; + const facilitySidebar = sidebarFor === SidebarFor.FACILITY; + const patientSidebar = sidebarFor === SidebarFor.PATIENT; + const [selectedFacility, setSelectedFacility] = React.useState(null); - const { t } = useTranslation(); - const selectedOrganization = React.useMemo(() => { if (!user?.organizations || !organizationId) return undefined; return user.organizations.find((org) => org.id === organizationId); @@ -207,34 +117,17 @@ export function AppSidebar({ {facilitySidebar && !selectedOrganization && ( - + )} {selectedOrganization && ( - - )} - {patientUserContext && ( - <> - - - + )} + {patientSidebar && } {(facilitySidebar || selectedOrganization) && } - {patientUserContext && ( - - )} + {patientSidebar && } diff --git a/src/components/ui/sidebar/facility-nav.tsx b/src/components/ui/sidebar/facility-nav.tsx new file mode 100644 index 00000000000..1a8a4cd6e35 --- /dev/null +++ b/src/components/ui/sidebar/facility-nav.tsx @@ -0,0 +1,63 @@ +import { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; + +import { NavMain } from "@/components/ui/sidebar/nav-main"; + +import { UserFacilityModel, UserModel } from "@/components/Users/models"; + +interface NavigationLink { + name: string; + url: string; + icon?: string; +} + +interface FacilityNavProps { + selectedFacility: UserFacilityModel | null; + user?: UserModel; +} + +function generateFacilityLinks( + selectedFacility: UserFacilityModel | null, + t: TFunction, + user?: UserModel, +) { + if (!selectedFacility) return []; + + const baseUrl = `/facility/${selectedFacility.id}`; + const links: NavigationLink[] = [ + { name: t("facility"), url: baseUrl, icon: "d-hospital" }, + { + name: t("appointments"), + url: `${baseUrl}/appointments`, + icon: "d-calendar", + }, + { + name: t("search_patients"), + url: `${baseUrl}/patients`, + icon: "d-patient", + }, + { name: t("encounters"), url: `${baseUrl}/encounters`, icon: "d-patient" }, + { name: t("resource"), url: "/resource", icon: "d-book-open" }, + { name: t("users"), url: `${baseUrl}/users`, icon: "d-people" }, + { + name: t("organization"), + url: `${baseUrl}/organization`, + icon: "d-book-open", + }, + ]; + + if (user) { + links.push({ + name: t("my_schedules"), + url: `${baseUrl}/users/${user.username}/availability`, + icon: "d-calendar", + }); + } + + return links; +} + +export function FacilityNav({ selectedFacility, user }: FacilityNavProps) { + const { t } = useTranslation(); + return ; +} diff --git a/src/components/ui/sidebar/nav-user.tsx b/src/components/ui/sidebar/nav-user.tsx index 56857862ad4..9a36b79a8ae 100644 --- a/src/components/ui/sidebar/nav-user.tsx +++ b/src/components/ui/sidebar/nav-user.tsx @@ -25,8 +25,7 @@ import { Avatar } from "@/components/Common/Avatar"; import useAuthUser, { useAuthContext } from "@/hooks/useAuthUser"; import { usePatientSignOut } from "@/hooks/usePatientSignOut"; - -import { AppointmentPatient } from "@/pages/Patient/Utils"; +import { usePatientContext } from "@/hooks/usePatientUser"; export function FacilityNavUser() { const { t } = useTranslation(); @@ -117,16 +116,14 @@ export function FacilityNavUser() { ); } -export function PatientNavUser({ - patient, - phoneNumber, -}: { - patient: AppointmentPatient | null; - phoneNumber: string; -}) { +export function PatientNavUser() { const { t } = useTranslation(); const { isMobile, open } = useSidebar(); const signOut = usePatientSignOut(); + const patientUserContext = usePatientContext(); + + const patient = patientUserContext?.selectedPatient; + const phoneNumber = patientUserContext?.tokenData.phoneNumber; return ( diff --git a/src/components/ui/sidebar/org-nav.tsx b/src/components/ui/sidebar/org-nav.tsx new file mode 100644 index 00000000000..cb4e3990554 --- /dev/null +++ b/src/components/ui/sidebar/org-nav.tsx @@ -0,0 +1,26 @@ +import { NavMain } from "@/components/ui/sidebar/nav-main"; + +import { Organization } from "@/types/organization/organization"; + +interface NavigationLink { + name: string; + url: string; + icon?: string; +} + +interface OrgNavProps { + organizations: Organization[]; +} + +function generateOrganizationLinks( + organizations: Organization[], +): NavigationLink[] { + return organizations.map((org) => ({ + name: org.name, + url: `/organization/${org.id}`, + })); +} + +export function OrgNav({ organizations }: OrgNavProps) { + return ; +} diff --git a/src/components/ui/sidebar/patient-nav.tsx b/src/components/ui/sidebar/patient-nav.tsx new file mode 100644 index 00000000000..b52cb5b4481 --- /dev/null +++ b/src/components/ui/sidebar/patient-nav.tsx @@ -0,0 +1,59 @@ +import { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; + +import { NavMain } from "@/components/ui/sidebar/nav-main"; +import { PatientSwitcher } from "@/components/ui/sidebar/patient-switcher"; + +import { usePatientContext } from "@/hooks/usePatientUser"; + +import { AppointmentPatient } from "@/pages/Patient/Utils"; + +interface NavigationLink { + name: string; + url: string; + icon?: string; +} + +function generatePatientLinks( + selectedUser: AppointmentPatient | null, + t: TFunction, +): NavigationLink[] { + if (!selectedUser) return []; + + const { geo_organization } = selectedUser; + let parentOrganization = geo_organization?.parent; + while (parentOrganization?.parent) { + if (parentOrganization.level_cache === 1) { + break; + } + parentOrganization = parentOrganization.parent; + } + + const queryParams = new URLSearchParams(); + + if (parentOrganization) { + queryParams.set("organization", String(parentOrganization?.id)); + } + + return [ + { name: t("appointments"), url: "/patient/home", icon: "d-patient" }, + { + name: t("nearby_facilities"), + url: `/nearby_facilities/?${queryParams.toString()}`, + icon: "d-patient", + }, + ]; +} + +export function PatientNav() { + const { t } = useTranslation(); + const patientUserContext = usePatientContext(); + const selectedPatient = patientUserContext?.selectedPatient; + + return ( + <> + + + + ); +} diff --git a/src/components/ui/sidebar/patient-switcher.tsx b/src/components/ui/sidebar/patient-switcher.tsx index 96749b2e033..fe2b0aa02d6 100644 --- a/src/components/ui/sidebar/patient-switcher.tsx +++ b/src/components/ui/sidebar/patient-switcher.tsx @@ -10,23 +10,26 @@ import { import { Avatar } from "@/components/Common/Avatar"; -import { PatientUserContextType } from "@/Providers/PatientUserProvider"; +import { usePatientContext } from "@/hooks/usePatientUser"; + import { classNames } from "@/Utils/utils"; import { useSidebar } from "../sidebar"; interface PatientSwitcherProps { - patientUserContext: PatientUserContextType; className?: string; } -export function PatientSwitcher({ - patientUserContext, - className, -}: PatientSwitcherProps) { +export function PatientSwitcher({ className }: PatientSwitcherProps) { const { t } = useTranslation(); const { open } = useSidebar(); + const patientUserContext = usePatientContext(); + + if (!patientUserContext) { + return null; + } + return (
; @@ -11,7 +12,8 @@ interface AuthContextType { user: UserModel | undefined; signIn: (creds: LoginCredentials) => Promise; signOut: () => Promise; - patientLogin: () => void; + patientLogin: (tokenData: TokenData, redirectUrl: string) => void; + patientToken: TokenData | null; } export const AuthUserContext = createContext(null); diff --git a/src/hooks/usePatientUser.ts b/src/hooks/usePatientUser.ts new file mode 100644 index 00000000000..06137cfd93b --- /dev/null +++ b/src/hooks/usePatientUser.ts @@ -0,0 +1,13 @@ +import { useContext } from "react"; + +import { PatientUserContext } from "@/Providers/PatientUserProvider"; + +export function usePatientContext() { + const ctx = useContext(PatientUserContext); + if (!ctx) { + throw new Error( + "'usePatientContext' must be used within 'PatientUserProvider' only", + ); + } + return ctx; +} diff --git a/src/pages/Appoinments/PatientRegistration.tsx b/src/pages/Appoinments/PatientRegistration.tsx index d0c2cc09269..0f3d9554b06 100644 --- a/src/pages/Appoinments/PatientRegistration.tsx +++ b/src/pages/Appoinments/PatientRegistration.tsx @@ -24,7 +24,9 @@ import { Textarea } from "@/components/ui/textarea"; import DateFormField from "@/components/Form/FormFields/DateFormField"; -import { GENDER_TYPES, LocalStorageKeys } from "@/common/constants"; +import { usePatientContext } from "@/hooks/usePatientUser"; + +import { GENDER_TYPES } from "@/common/constants"; import { validateName, validatePincode } from "@/common/validation"; import * as Notification from "@/Utils/Notifications"; @@ -37,7 +39,6 @@ import { AppointmentPatient, AppointmentPatientRegister, } from "@/pages/Patient/Utils"; -import { TokenData } from "@/types/auth/otpToken"; import PublicAppointmentApi from "@/types/scheduling/PublicAppointmentApi"; import { Appointment, @@ -72,9 +73,6 @@ export function PatientRegistration(props: PatientRegistrationProps) { localStorage.getItem("selectedSlot") ?? "", ) as TokenSlot; const reason = localStorage.getItem("reason"); - const tokenData: TokenData = JSON.parse( - localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", - ); const { t } = useTranslation(); @@ -82,6 +80,9 @@ export function PatientRegistration(props: PatientRegistrationProps) { const { publish } = usePubSub(); + const patientUserContext = usePatientContext(); + const tokenData = patientUserContext?.tokenData; + const patientSchema = z .object({ name: z diff --git a/src/pages/Appoinments/PatientSelect.tsx b/src/pages/Appoinments/PatientSelect.tsx index 6b4a3a4b468..258c2c63dc9 100644 --- a/src/pages/Appoinments/PatientSelect.tsx +++ b/src/pages/Appoinments/PatientSelect.tsx @@ -10,7 +10,7 @@ import { Button } from "@/components/ui/button"; import Loading from "@/components/Common/Loading"; -import { LocalStorageKeys } from "@/common/constants"; +import { usePatientContext } from "@/hooks/usePatientUser"; import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; @@ -18,7 +18,6 @@ import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import { PaginatedResponse } from "@/Utils/request/types"; import { AppointmentPatient } from "@/pages/Patient/Utils"; -import { TokenData } from "@/types/auth/otpToken"; import PublicAppointmentApi from "@/types/scheduling/PublicAppointmentApi"; import { Appointment, @@ -38,11 +37,11 @@ export default function PatientSelect({ localStorage.getItem("selectedSlot") ?? "", ) as TokenSlot; const reason = localStorage.getItem("reason"); - const tokenData: TokenData = JSON.parse( - localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", - ); const [selectedPatient, setSelectedPatient] = useState(null); + const patientUserContext = usePatientContext(); + const tokenData = patientUserContext?.tokenData; + const queryClient = useQueryClient(); if (!staffId) { diff --git a/src/pages/Appoinments/Schedule.tsx b/src/pages/Appoinments/Schedule.tsx index 494b19b4c69..faed7fab9a6 100644 --- a/src/pages/Appoinments/Schedule.tsx +++ b/src/pages/Appoinments/Schedule.tsx @@ -19,7 +19,7 @@ import Loading from "@/components/Common/Loading"; import { FacilityModel } from "@/components/Facility/models"; import { groupSlotsByAvailability } from "@/components/Schedule/Appointments/utils"; -import { LocalStorageKeys } from "@/common/constants"; +import { usePatientContext } from "@/hooks/usePatientUser"; import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; @@ -27,7 +27,6 @@ import query from "@/Utils/request/query"; import request from "@/Utils/request/request"; import { RequestResult } from "@/Utils/request/types"; import { dateQueryString } from "@/Utils/utils"; -import { TokenData } from "@/types/auth/otpToken"; import PublicAppointmentApi from "@/types/scheduling/PublicAppointmentApi"; import { TokenSlot } from "@/types/scheduling/schedule"; @@ -44,9 +43,8 @@ export function ScheduleAppointment(props: AppointmentsProps) { const [selectedSlot, setSelectedSlot] = useState(); const [reason, setReason] = useState(""); - const tokenData: TokenData = JSON.parse( - localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", - ); + const patientUserContext = usePatientContext(); + const tokenData = patientUserContext?.tokenData; if (!staffId) { Notification.Error({ msg: "Staff username not found" }); diff --git a/src/pages/Appoinments/Success.tsx b/src/pages/Appoinments/Success.tsx index 14ce2cb209a..7a7ae0399fc 100644 --- a/src/pages/Appoinments/Success.tsx +++ b/src/pages/Appoinments/Success.tsx @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { format } from "date-fns"; +import dayjs from "dayjs"; import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; @@ -7,20 +8,20 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import Loading from "@/components/Common/Loading"; import { UserModel } from "@/components/Users/models"; -import { LocalStorageKeys } from "@/common/constants"; +import { usePatientContext } from "@/hooks/usePatientUser"; import * as Notification from "@/Utils/Notifications"; import query from "@/Utils/request/query"; import { formatName } from "@/Utils/utils"; -import { TokenData } from "@/types/auth/otpToken"; import PublicAppointmentApi from "@/types/scheduling/PublicAppointmentApi"; export function AppointmentSuccess(props: { appointmentId: string }) { const { appointmentId } = props; const { t } = useTranslation(); - const tokenData: TokenData = JSON.parse( - localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", - ); + + const patientUserContext = usePatientContext(); + const tokenData = patientUserContext?.tokenData; + const userData: UserModel = JSON.parse(localStorage.getItem("user") ?? "{}"); const { data, isLoading, error } = useQuery({ @@ -45,6 +46,13 @@ export function AppointmentSuccess(props: { appointmentId: string }) { return ; } + const appointmentTime = dayjs(appointmentData?.token_slot.start_datetime) + .add(-5, "hours") + .add(-30, "minutes") + .toISOString(); + const appointmentDate = format(new Date(appointmentTime), "do MMMM"); + const appointmentTimeSlot = format(new Date(appointmentTime), "hh:mm a"); + return (
@@ -76,28 +84,14 @@ export function AppointmentSuccess(props: { appointmentId: string }) {

{t("date")}:

-

- {appointmentData?.token_slot.start_datetime - ? format( - new Date(appointmentData?.token_slot.start_datetime), - "do MMMM", - ) - : ""} -

+

{appointmentDate}

{t("time")}:

-

- {appointmentData?.token_slot.start_datetime - ? format( - new Date(appointmentData?.token_slot.start_datetime), - "hh:mm a", - ) - : ""} -

+

{appointmentTimeSlot}

diff --git a/src/pages/Appoinments/auth/PatientLogin.tsx b/src/pages/Appoinments/auth/PatientLogin.tsx index e8d22387090..c43d8331fda 100644 --- a/src/pages/Appoinments/auth/PatientLogin.tsx +++ b/src/pages/Appoinments/auth/PatientLogin.tsx @@ -24,12 +24,12 @@ import { InputOTPSlot, } from "@/components/ui/input-otp"; +import CircularProgress from "@/components/Common/CircularProgress"; import { PhoneNumberValidator } from "@/components/Form/FieldValidators"; import PhoneNumberFormField from "@/components/Form/FormFields/PhoneNumberFormField"; import useAppHistory from "@/hooks/useAppHistory"; - -import { LocalStorageKeys } from "@/common/constants"; +import { useAuthContext } from "@/hooks/useAuthUser"; import * as Notification from "@/Utils/Notifications"; import routes from "@/Utils/request/api"; @@ -64,12 +64,11 @@ export default function PatientLogin({ pin: "", }, }); - - const tokenData: TokenData = JSON.parse( - localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", - ); + const { patientLogin } = useAuthContext(); + const { patientToken: tokenData } = useAuthContext(); if ( + tokenData && Object.keys(tokenData).length > 0 && dayjs(tokenData.createdAt).isAfter(dayjs().subtract(14, "minutes")) ) { @@ -90,7 +89,7 @@ export default function PatientLogin({ return errors; }; - const { mutate: sendOTP } = useMutation({ + const { mutate: sendOTP, isPending: isSendOTPLoading } = useMutation({ mutationFn: (phoneNumber: string) => request(routes.otp.sendOtp, { body: { @@ -100,23 +99,7 @@ export default function PatientLogin({ }), onSuccess: () => { if (page === "send") { - const tokenData: TokenData = JSON.parse( - localStorage.getItem(LocalStorageKeys.patientTokenKey) || "{}", - ); - if ( - Object.keys(tokenData).length > 0 && - tokenData.phoneNumber === phoneNumber && - dayjs(tokenData.createdAt).isAfter(dayjs().subtract(14, "minutes")) - ) { - Notification.Success({ msg: t("valid_otp_found") }); - navigate( - `/facility/${facilityId}/appointments/${staffId}/book-appointment`, - ); - } else { - navigate( - `/facility/${facilityId}/appointments/${staffId}/otp/verify`, - ); - } + navigate(`/facility/${facilityId}/appointments/${staffId}/otp/verify`); } }, onError: () => { @@ -136,7 +119,7 @@ export default function PatientLogin({ sendOTP(phoneNumber); }; - const { mutate: verifyOTP } = useMutation({ + const { mutate: verifyOTP, isPending: isVerifyOTPLoading } = useMutation({ mutationFn: async ({ phone_number, otp, @@ -160,11 +143,8 @@ export default function PatientLogin({ phoneNumber: phoneNumber, createdAt: new Date().toISOString(), }; - localStorage.setItem( - LocalStorageKeys.patientTokenKey, - JSON.stringify(tokenData), - ); - navigate( + patientLogin( + tokenData, `/facility/${facilityId}/appointments/${staffId}/book-appointment`, ); } @@ -208,9 +188,14 @@ export default function PatientLogin({ variant="primary" type="submit" className="w-full h-12 text-lg" + disabled={isSendOTPLoading} > - {t("send_otp")} + {isSendOTPLoading ? ( + + ) : ( + t("send_otp") + )}
@@ -268,8 +253,13 @@ export default function PatientLogin({ variant="primary_gradient" type="submit" className="w-full h-12 text-lg" + disabled={isVerifyOTPLoading} > - {t("verify_otp")} + {isVerifyOTPLoading ? ( + + ) : ( + t("verify_otp") + )} { if ( + tokenData && Object.keys(tokenData).length > 0 && dayjs(tokenData.createdAt).isAfter(dayjs().subtract(14, "minutes")) ) { diff --git a/src/pages/Patient/index.tsx b/src/pages/Patient/index.tsx index 35fbe019adc..8ea025168af 100644 --- a/src/pages/Patient/index.tsx +++ b/src/pages/Patient/index.tsx @@ -20,7 +20,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import Loading from "@/components/Common/Loading"; import { formatAppointmentSlotTime } from "@/components/Schedule/Appointments/utils"; -import { usePatientContext } from "@/hooks/useAuthOrPatientUser"; +import { usePatientContext } from "@/hooks/usePatientUser"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query";