From afdf9f00240f9a57c55977e448ee609f05708dd1 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 18 May 2026 12:42:32 +0530 Subject: [PATCH 01/17] set encounter while raising consent request --- src/apis/index.ts | 1 + src/components/CreateConsentRequestForm.tsx | 50 ++++++++++--------- .../pluggables/EncounterActions.tsx | 1 + 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/apis/index.ts b/src/apis/index.ts index a7f4ff3..f3100e5 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -31,6 +31,7 @@ export const apis = { create: async (body: { patient_abha: string; + encounter: string; hi_types: ConsentHIType[]; purpose: ConsentPurpose; from_time: Date | string; diff --git a/src/components/CreateConsentRequestForm.tsx b/src/components/CreateConsentRequestForm.tsx index a405317..d551d38 100644 --- a/src/components/CreateConsentRequestForm.tsx +++ b/src/components/CreateConsentRequestForm.tsx @@ -1,11 +1,8 @@ -import dayjs from "@/lib/dayjs"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation } from "@tanstack/react-query"; -import { FC } from "react"; -import { useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { z } from "zod"; +import { + CONSENT_HI_TYPES, + CONSENT_PURPOSES, + ConsentRequest, +} from "@/types/consent"; import { Form, FormControl, @@ -14,16 +11,6 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { toast } from "@/lib/utils"; -import { apis } from "@/apis"; -import { AbhaNumber } from "@/types/abhaNumber"; -import { - CONSENT_HI_TYPES, - CONSENT_PURPOSES, - ConsentRequest, -} from "@/types/consent"; import { Select, SelectContent, @@ -31,13 +18,28 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { DatePickerWithRange } from "./ui/date-range-picker"; + +import { AbhaNumber } from "@/types/abhaNumber"; +import { Button } from "@/components/ui/button"; import { DatePicker } from "./ui/date-picker"; -import { MultiSelect } from "./ui/multi-select"; +import { DatePickerWithRange } from "./ui/date-range-picker"; +import { Encounter } from "@/types/encounter"; +import { FC } from "react"; import { I18NNAMESPACE } from "@/lib/constants"; +import { Input } from "@/components/ui/input"; +import { MultiSelect } from "./ui/multi-select"; +import { apis } from "@/apis"; +import dayjs from "@/lib/dayjs"; +import { toast } from "@/lib/utils"; +import { useForm } from "react-hook-form"; +import { useMutation } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; type CreateConsentRequestFormProps = { abhaNumber?: AbhaNumber; + encounter: Encounter; onSuccess?: (consentRequest: ConsentRequest) => void; }; @@ -58,6 +60,7 @@ type CreateConsentRequestFormValues = z.infer< const CreateConsentRequestForm: FC = ({ abhaNumber, + encounter, onSuccess, }) => { const { t } = useTranslation(I18NNAMESPACE); @@ -76,7 +79,7 @@ const CreateConsentRequestForm: FC = ({ }, }); - const createConsentRequestOtpMutation = useMutation({ + const createConsentRequestMutation = useMutation({ mutationFn: apis.consent.create, onSuccess: (data) => { toast.success(t("consent_requested_successfully")); @@ -85,9 +88,10 @@ const CreateConsentRequestForm: FC = ({ }); function onSubmit(values: CreateConsentRequestFormValues) { - createConsentRequestOtpMutation.mutate({ + createConsentRequestMutation.mutate({ ...values, patient_abha: form.getValues("patient_abha"), + encounter: encounter.id, from_time: values.time_range.from, to_time: values.time_range.to, }); @@ -193,7 +197,7 @@ const CreateConsentRequestForm: FC = ({ diff --git a/src/components/pluggables/EncounterActions.tsx b/src/components/pluggables/EncounterActions.tsx index 5d5048c..e4de2a0 100644 --- a/src/components/pluggables/EncounterActions.tsx +++ b/src/components/pluggables/EncounterActions.tsx @@ -72,6 +72,7 @@ const EncounterActions: FC> = ({
{ queryClient.invalidateQueries({ queryKey: ["consents", encounter.patient.id], From 4c7224f90b26865af95f786b995eb752e0dbb7ac Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 18 May 2026 13:20:53 +0530 Subject: [PATCH 02/17] filter consents by encounter --- src/apis/index.ts | 1 + src/components/encounter-tabs/Abdm.tsx | 27 +++++++++++++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/apis/index.ts b/src/apis/index.ts index f3100e5..3ff20eb 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -22,6 +22,7 @@ export const apis = { list: async (query?: { facility?: string; patient?: string; + encounter?: string; ordering?: string; }) => { return await request>( diff --git a/src/components/encounter-tabs/Abdm.tsx b/src/components/encounter-tabs/Abdm.tsx index f30cc36..5eb95b5 100644 --- a/src/components/encounter-tabs/Abdm.tsx +++ b/src/components/encounter-tabs/Abdm.tsx @@ -1,18 +1,22 @@ -import { FC } from "react"; -import { EncounterTabProps } from "."; -import { apis } from "@/apis"; -import { useTranslation } from "react-i18next"; -import { useMutation, useQuery } from "@tanstack/react-query"; import { ConsentArtefact, ConsentRequest } from "@/types/consent"; -import { toast } from "@/lib/utils"; -import dayjs from "@/lib/dayjs"; -import { Button } from "../ui/button"; import { Loader2Icon, RefreshCcwIcon } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { Link } from "raviger"; +import { useMutation, useQuery } from "@tanstack/react-query"; + +import { Button } from "../ui/button"; +import { EncounterTabProps } from "."; +import { FC } from "react"; import { I18NNAMESPACE } from "@/lib/constants"; +import { Link } from "raviger"; +import { apis } from "@/apis"; +import { cn } from "@/lib/utils"; +import dayjs from "@/lib/dayjs"; +import { toast } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; -export const AbdmEncounterTab: FC = ({ patient }) => { +export const AbdmEncounterTab: FC = ({ + patient, + encounter, +}) => { const { t } = useTranslation(I18NNAMESPACE); const { data, isLoading } = useQuery({ @@ -20,6 +24,7 @@ export const AbdmEncounterTab: FC = ({ patient }) => { queryFn: () => apis.consent.list({ patient: patient.id, + encounter: encounter.id, ordering: "-created_date", }), enabled: !!patient.id, From 3a30545988879f2994ce67ad7c6f894d55f53d80 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 18 May 2026 20:17:15 +0530 Subject: [PATCH 03/17] fixed facility home actions button style to stay consistent with core --- src/components/pluggables/FacilityHomeActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pluggables/FacilityHomeActions.tsx b/src/components/pluggables/FacilityHomeActions.tsx index 54928ac..d0d6854 100644 --- a/src/components/pluggables/FacilityHomeActions.tsx +++ b/src/components/pluggables/FacilityHomeActions.tsx @@ -42,7 +42,7 @@ const FacilityHomeActions: FC> = ({ @@ -719,6 +756,92 @@ const VerifyId: FC = ({ memory, setMemory, onSuccess }) => { ); }; +type ChooseAbhaAccountProps = InjectedStepProps & { + onSuccess: (abhaNumber: AbhaNumber) => void; +}; + +const ChooseAbhaAccount: FC = ({ + memory, + onSuccess, +}) => { + const { t } = useTranslation(I18NNAMESPACE); + + const accounts = memory?.loginAccounts ?? []; + + const verifyUserMutation = useMutation({ + mutationFn: apis.healthId.abhaLoginVerifyUser, + onSuccess: (data) => { + if (data) { + toast.success(t("otp_verified_successfully")); + onSuccess(data.abha_number); + } + }, + }); + + return ( +
+
+

+ {t("choose_abha_account")} +

+

+ {t("choose_abha_account_description")} +

+
+ +
+ {accounts.map((account) => ( + + ))} +
+
+ ); +}; + type VerifyAadhaarWithOtpProps = InjectedStepProps; const verifyAadhaarWithOtpFormSchema = z.object({ diff --git a/src/types/abhaNumber.ts b/src/types/abhaNumber.ts index fdd0af7..e9688cb 100644 --- a/src/types/abhaNumber.ts +++ b/src/types/abhaNumber.ts @@ -22,3 +22,13 @@ export type AbhaNumber = { patient: string | null; patient_object: unknown | null; // FIXME: Patient type }; + +export type AbhaLoginAccount = { + id: number; + abha_number: string | null; + preferred_abha_address: string | null; + name: string | null; + gender: "F" | "M" | "O" | null; + date_of_birth: string | null; + profile_photo: string | null; +}; From 7a4096ac0cdc7dd1548e6f2bde2b62d884e6ac59 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 23 Jun 2026 19:35:51 +0530 Subject: [PATCH 05/17] fix calendar ui --- .../pluggables/EncounterActions.tsx | 4 +- src/components/ui/calendar.tsx | 75 +++++++++---------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/src/components/pluggables/EncounterActions.tsx b/src/components/pluggables/EncounterActions.tsx index e4de2a0..512b88f 100644 --- a/src/components/pluggables/EncounterActions.tsx +++ b/src/components/pluggables/EncounterActions.tsx @@ -56,13 +56,13 @@ const EncounterActions: FC> = ({ variant="ghost" className={cn( "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-gray-100 focus:text-gray-900 data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0 dark:focus:bg-gray-800 dark:focus:text-gray-50", - className + className, )} > {t("hi__fetch_records")} - + {t("hi__fetch_records")} diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index 640dc2c..e722484 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -18,56 +18,55 @@ function Calendar({ showOutsideDays={showOutsideDays} className={cn("p-3", className)} classNames={{ - months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", - month: "space-y-4", - caption: "flex justify-center pt-1 relative items-center", + months: "relative flex flex-col sm:flex-row gap-4", + month: "flex flex-col gap-4", + month_caption: "flex h-9 items-center justify-center relative", caption_label: "text-sm font-medium", - nav: "space-x-1 flex items-center", - nav_button: cn( + nav: "absolute inset-x-0 top-0 flex items-center justify-between z-10", + button_previous: cn( buttonVariants({ variant: "outline" }), - "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" + "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", ), - nav_button_previous: "absolute left-1", - nav_button_next: "absolute right-1", - table: "w-full border-collapse space-y-1", - head_row: "flex", - head_cell: - "text-gray-500 rounded-md w-8 font-normal text-[0.8rem] dark:text-gray-400", - row: "flex w-full mt-2", - cell: cn( - "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-gray-100 [&:has([aria-selected].day-outside)]:bg-gray-100/50 [&:has([aria-selected].day-range-end)]:rounded-r-md dark:[&:has([aria-selected])]:bg-gray-800 dark:[&:has([aria-selected].day-outside)]:bg-gray-800/50", - props.mode === "range" - ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" - : "[&:has([aria-selected])]:rounded-md" + button_next: cn( + buttonVariants({ variant: "outline" }), + "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", ), - day: cn( + month_grid: "w-full border-collapse", + weekdays: "flex", + weekday: + "text-gray-500 rounded-md w-9 font-normal text-[0.8rem] dark:text-gray-400", + week: "flex w-full mt-2", + day: "relative size-9 p-0 text-center text-sm focus-within:relative focus-within:z-20", + day_button: cn( buttonVariants({ variant: "ghost" }), - "h-8 w-8 p-0 font-normal aria-selected:opacity-100" + "size-9 p-0 font-normal aria-selected:opacity-100", ), - day_range_start: "day-range-start", - day_range_end: "day-range-end", - day_selected: - "bg-gray-900 text-gray-50 hover:bg-gray-900 hover:text-gray-50 focus:bg-gray-900 focus:text-gray-50 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50 dark:hover:text-gray-900 dark:focus:bg-gray-50 dark:focus:text-gray-900", - day_today: "bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-50", - day_outside: - "day-outside text-gray-500 aria-selected:bg-gray-100/50 aria-selected:text-gray-500 dark:text-gray-400 dark:aria-selected:bg-gray-800/50 dark:aria-selected:text-gray-400", - day_disabled: "text-gray-500 opacity-50 dark:text-gray-400", - day_range_middle: - "aria-selected:bg-gray-100 aria-selected:text-gray-900 dark:aria-selected:bg-gray-800 dark:aria-selected:text-gray-50", - day_hidden: "invisible", + range_start: + "day-range-start rounded-l-md bg-gray-100 dark:bg-gray-800", + range_end: "day-range-end rounded-r-md bg-gray-100 dark:bg-gray-800", + range_middle: + "rounded-none bg-gray-100 dark:bg-gray-800 [&>button]:!bg-transparent [&>button]:!text-gray-900 [&>button]:!rounded-none [&>button]:hover:!bg-transparent dark:[&>button]:!text-gray-50", + selected: + "[&>button]:bg-primary-500 [&>button]:text-gray-50 [&>button]:hover:bg-primary-500 [&>button]:hover:text-gray-50 [&>button]:focus:bg-primary-500 [&>button]:focus:text-gray-50 dark:[&>button]:bg-primary-900 dark:[&>button]:text-gray-50", + today: + "[&>button]:border [&>button]:border-primary-500 [&>button]:text-primary-600 dark:[&>button]:border-primary-400 dark:[&>button]:text-primary-400", + outside: + "day-outside text-gray-500 aria-selected:text-gray-500 dark:text-gray-400", + disabled: "text-gray-500 opacity-50 dark:text-gray-400", + hidden: "invisible", ...classNames, }} components={{ - IconLeft: ({ className, ...props }) => ( - - ), - IconRight: ({ className, ...props }) => ( - - ), + Chevron: ({ className, orientation, ...props }) => + orientation === "left" ? ( + + ) : ( + + ), }} {...props} /> - ) + ); } Calendar.displayName = "Calendar" From 9d106eafa4586addcbf4a1982afedc47dbc8cdd5 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 25 Jun 2026 07:30:48 +0530 Subject: [PATCH 06/17] fix styling issue showing hi type badges --- src/components/encounter-tabs/Abdm.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/encounter-tabs/Abdm.tsx b/src/components/encounter-tabs/Abdm.tsx index 5eb95b5..bddf02c 100644 --- a/src/components/encounter-tabs/Abdm.tsx +++ b/src/components/encounter-tabs/Abdm.tsx @@ -32,7 +32,7 @@ export const AbdmEncounterTab: FC = ({ if (isLoading) { return ( -
+

{t("loading_consent_requests")} @@ -43,7 +43,7 @@ export const AbdmEncounterTab: FC = ({ if (!data?.results.length) { return ( -

+

{t("no_records_found")}

@@ -55,7 +55,7 @@ export const AbdmEncounterTab: FC = ({ } return ( -
+
{data?.results.map((record) => { return ; })} From 8dd8619229404e34005e37fa72735b35b79cd2a8 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 25 Jun 2026 20:59:52 +0530 Subject: [PATCH 07/17] use same date and date range components as core --- src/components/CreateConsentRequestForm.tsx | 2 +- src/components/ui/calendar.tsx | 559 ++++++++++++++++++-- src/components/ui/date-picker.tsx | 71 ++- src/components/ui/date-range-picker.tsx | 196 +++++-- src/components/ui/popover.tsx | 22 +- src/components/ui/select.tsx | 40 +- 6 files changed, 752 insertions(+), 138 deletions(-) diff --git a/src/components/CreateConsentRequestForm.tsx b/src/components/CreateConsentRequestForm.tsx index d551d38..051d76e 100644 --- a/src/components/CreateConsentRequestForm.tsx +++ b/src/components/CreateConsentRequestForm.tsx @@ -188,7 +188,7 @@ const CreateConsentRequestForm: FC = ({ render={({ field }) => ( {t("consent_request__expiry")} - + )} diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index e722484..b7bb9ac 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -1,73 +1,530 @@ -import * as React from "react" -import { ChevronLeft, ChevronRight } from "lucide-react" -import { DayPicker } from "react-day-picker" +import { differenceInCalendarDays } from "date-fns"; +import { ChevronDownIcon, ChevronLeft, ChevronRight } from "lucide-react"; +import * as React from "react"; +import { + DateRange, + DayPicker, + type DayPickerProps, + labelNext, + labelPrevious, + useDayPicker, +} from "react-day-picker"; -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +import { cn } from "@/lib/utils"; -export type CalendarProps = React.ComponentProps +import { Button, buttonVariants } from "@/components/ui/button"; +export type CalendarProps = DayPickerProps & { + /** + * In the year view, the number of years to display at once. + * @default 12 + */ + yearRange?: number; + + /** + * Wether to show the year switcher in the caption. + * @default true + */ + showYearSwitcher?: boolean; + + monthsClassName?: string; + monthCaptionClassName?: string; + weekdaysClassName?: string; + weekdayClassName?: string; + monthClassName?: string; + captionClassName?: string; + captionLabelClassName?: string; + buttonNextClassName?: string; + buttonPreviousClassName?: string; + navClassName?: string; + monthGridClassName?: string; + weekClassName?: string; + dayClassName?: string; + dayButtonClassName?: string; + rangeStartClassName?: string; + rangeEndClassName?: string; + selectedClassName?: string; + todayClassName?: string; + outsideClassName?: string; + disabledClassName?: string; + rangeMiddleClassName?: string; + hiddenClassName?: string; +}; + +type NavView = "days" | "years"; + +/** + * A custom calendar component built on top of react-day-picker. + * @param props The props for the calendar. + * @default yearRange 12 + * @returns + */ function Calendar({ className, - classNames, showOutsideDays = true, + showYearSwitcher = true, + yearRange = 12, + numberOfMonths, ...props }: CalendarProps) { + const [navView, setNavView] = React.useState("days"); + const [displayYears, setDisplayYears] = React.useState<{ + from: number; + to: number; + }>( + React.useMemo(() => { + const currentYear = new Date().getFullYear(); + return { + from: currentYear - Math.floor(yearRange / 2 - 1), + to: currentYear + Math.ceil(yearRange / 2), + }; + }, [yearRange]), + ); + + const { onPrevClick, startMonth, endMonth } = props; + + const columnsDisplayed = navView === "years" ? 1 : numberOfMonths; + + const _monthsClassName = cn("relative flex", props.monthsClassName); + const _monthCaptionClassName = cn( + "relative mx-10 flex h-7 items-center justify-center", + props.monthCaptionClassName, + ); + const _weekdaysClassName = cn("flex flex-row", props.weekdaysClassName); + const _weekdayClassName = cn( + "w-8 text-sm font-normal text-gray-700", + props.weekdayClassName, + ); + const _monthClassName = cn("w-full", props.monthClassName); + const _captionClassName = cn( + "relative flex items-center justify-center pt-1", + props.captionClassName, + ); + const _captionLabelClassName = cn( + "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:size-3.5 border border-gray-400", + props.captionLabelClassName, + ); + const buttonNavClassName = buttonVariants({ + variant: "outline", + className: + "absolute size-7 bg-transparent p-0 opacity-50 hover:opacity-100", + }); + const _buttonNextClassName = cn( + buttonNavClassName, + "right-0", + props.buttonNextClassName, + ); + const _buttonPreviousClassName = cn( + buttonNavClassName, + "left-0", + props.buttonPreviousClassName, + ); + const _navClassName = cn("flex items-start", props.navClassName); + const _monthGridClassName = cn("mx-auto mt-4", props.monthGridClassName); + const _weekClassName = cn("mt-2 flex w-max items-start", props.weekClassName); + const _dayClassName = cn( + "flex size-8 flex-1 items-center justify-center p-0 text-sm", + props.dayClassName, + ); + const _dayButtonClassName = cn( + buttonVariants({ variant: "ghost" }), + "size-8 rounded-md p-0 font-normal transition-none aria-selected:opacity-100", + props.dayButtonClassName, + ); + const buttonRangeClassName = + "bg-accent [&>button]:bg-primary [&>button]:text-primary-foreground hover:[&>button]:bg-primary hover:[&>button]:text-primary-foreground"; + const _rangeStartClassName = cn( + buttonRangeClassName, + "day-range-start rounded-s-md", + props.rangeStartClassName, + ); + const _rangeEndClassName = cn( + buttonRangeClassName, + "day-range-end rounded-e-md", + props.rangeEndClassName, + ); + const _rangeMiddleClassName = cn( + "bg-accent text-foreground! [&>button]:bg-transparent [&>button]:text-foreground! hover:[&>button]:bg-transparent hover:[&>button]:text-foreground!", + props.rangeMiddleClassName, + ); + const _selectedClassName = cn( + "[&>button]:bg-primary [&>button]:text-primary-foreground hover:[&>button]:bg-primary hover:[&>button]:text-primary-foreground", + props.selectedClassName, + ); + const _todayClassName = cn( + "[&>button]:bg-gray-100 [&>button]:rounded-md", + props.todayClassName, + ); + const _outsideClassName = cn( + "day-outside text-gray-500 opacity-50 aria-selected:bg-accent/50 aria-selected:text-gray-700 aria-selected:opacity-30", + props.outsideClassName, + ); + const _disabledClassName = cn( + "text-gray-500 opacity-50", + props.disabledClassName, + ); + const _hiddenClassName = cn("invisible flex-1", props.hiddenClassName); + return ( + date.toLocaleString("default", { month: "short" }), + }} classNames={{ - months: "relative flex flex-col sm:flex-row gap-4", - month: "flex flex-col gap-4", - month_caption: "flex h-9 items-center justify-center relative", - caption_label: "text-sm font-medium", - nav: "absolute inset-x-0 top-0 flex items-center justify-between z-10", - button_previous: cn( - buttonVariants({ variant: "outline" }), - "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", + months: _monthsClassName, + month_caption: _monthCaptionClassName, + weekdays: _weekdaysClassName, + weekday: _weekdayClassName, + month: _monthClassName, + caption: _captionClassName, + caption_label: _captionLabelClassName, + button_next: _buttonNextClassName, + button_previous: _buttonPreviousClassName, + nav: _navClassName, + month_grid: _monthGridClassName, + week: _weekClassName, + day: _dayClassName, + day_button: _dayButtonClassName, + range_start: _rangeStartClassName, + range_middle: _rangeMiddleClassName, + range_end: _rangeEndClassName, + selected: _selectedClassName, + today: _todayClassName, + outside: _outsideClassName, + disabled: _disabledClassName, + hidden: _hiddenClassName, + }} + components={{ + Chevron: ({ orientation }) => { + if (orientation === "left") { + return ; + } + if (orientation === "right") { + return ; + } + return ; + }, + Nav: ({ className }) => ( +
); } From d73cee41c66d35106a221523bb1499ba2f03baf6 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 26 Jun 2026 08:39:09 +0530 Subject: [PATCH 08/17] fix calendar cliping in date range picker --- src/components/ui/date-range-picker.tsx | 85 ++++++++++++++++++++----- src/components/ui/input.tsx | 15 ++++- src/components/ui/popover.tsx | 2 +- 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/components/ui/date-range-picker.tsx b/src/components/ui/date-range-picker.tsx index 5754132..4a5b7c2 100644 --- a/src/components/ui/date-range-picker.tsx +++ b/src/components/ui/date-range-picker.tsx @@ -1,6 +1,6 @@ import { format, isBefore, isSameDay } from "date-fns"; -import { CalendarIcon } from "lucide-react"; -import { useEffect, useState } from "react"; +import { CalendarIcon, ChevronDown } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; import { DateRange } from "react-day-picker"; import { useTranslation } from "react-i18next"; @@ -47,14 +47,22 @@ export function DatePickerWithRange({ }: DatePickerWithRangeProps) { const { t } = useTranslation(); const [open, setOpen] = useState(false); + const [showManualInputs, setShowManualInputs] = useState(false); const [dateFrom, setDateFrom] = useState(value?.from); const [dateTo, setDateTo] = useState(value?.to); + const fromInputRef = useRef(null); useEffect(() => { setDateFrom(value?.from); setDateTo(value?.to); }, [value]); + useEffect(() => { + if (showManualInputs) { + fromInputRef.current?.focus(); + } + }, [showManualInputs]); + const handleDateChange = (date: DateRange | undefined) => { setDateFrom(date?.from); setDateTo(date?.to); @@ -64,7 +72,16 @@ export function DatePickerWithRange({ const formattedRange = formatDateRange(value); return ( - + { + setOpen(nextOpen); + if (!nextOpen) { + setShowManualInputs(false); + } + }} + modal={!showManualInputs} + >
+ + + + ); diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 267829b..3c6e837 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -3,15 +3,26 @@ import * as React from "react" import { cn } from "@/lib/utils" const Input = React.forwardRef>( - ({ className, type, ...props }, ref) => { + ({ className, type, onClick, onFocus, onWheel, ...props }, ref) => { return ( { + if (type === "date") { + e.currentTarget.showPicker(); + } + onClick?.(e); + }} + onFocus={onFocus} + onWheel={(e) => { + e.currentTarget.blur(); + onWheel?.(e); + }} {...props} /> ) diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index eeb8367..4d0ee0e 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -14,7 +14,7 @@ const PopoverContent = React.forwardRef< React.ComponentPropsWithoutRef >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( -
+
Date: Fri, 26 Jun 2026 08:45:38 +0530 Subject: [PATCH 09/17] fix ABDM health records date range to use current time when "to" is today --- src/components/ui/date-range-picker.tsx | 28 ++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/ui/date-range-picker.tsx b/src/components/ui/date-range-picker.tsx index 4a5b7c2..79d853e 100644 --- a/src/components/ui/date-range-picker.tsx +++ b/src/components/ui/date-range-picker.tsx @@ -1,4 +1,4 @@ -import { format, isBefore, isSameDay } from "date-fns"; +import { format, isBefore, isSameDay, isToday } from "date-fns"; import { CalendarIcon, ChevronDown } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { DateRange } from "react-day-picker"; @@ -24,6 +24,19 @@ interface DatePickerWithRangeProps { disablePicker?: boolean; } +function normalizeToDate(date: Date | undefined): Date | undefined { + if (!date) return undefined; + return isToday(date) ? new Date() : date; +} + +function normalizeDateRange(date: DateRange | undefined): DateRange | undefined { + if (!date) return date; + return { + ...date, + to: normalizeToDate(date.to), + }; +} + function formatDateRange(value?: DateRange) { if (!value?.from) return null; @@ -64,9 +77,10 @@ export function DatePickerWithRange({ }, [showManualInputs]); const handleDateChange = (date: DateRange | undefined) => { - setDateFrom(date?.from); - setDateTo(date?.to); - onChange?.(date); + const normalized = normalizeDateRange(date); + setDateFrom(normalized?.from); + setDateTo(normalized?.to); + onChange?.(normalized); }; const formattedRange = formatDateRange(value); @@ -186,9 +200,9 @@ export function DatePickerWithRange({ type="date" value={dateTo ? format(dateTo, "yyyy-MM-dd") : ""} onChange={(e) => { - const nextTo = e.target.value - ? new Date(e.target.value) - : undefined; + const nextTo = normalizeToDate( + e.target.value ? new Date(e.target.value) : undefined, + ); setDateTo(nextTo); onChange?.({ from: dateFrom, to: nextTo }); }} From 3ae41402062a3edbb8325868eb8a252478625ebb Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 26 Jun 2026 09:17:38 +0530 Subject: [PATCH 10/17] update health information route and add back button --- public/locale/en.json | 1 + src/components/encounter-tabs/Abdm.tsx | 45 ++++++++++++-- src/components/pages/HealthInformation.tsx | 69 ++++++++++++++++------ src/lib/paths.ts | 17 ++++++ src/routes.tsx | 12 +++- 5 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 src/lib/paths.ts diff --git a/public/locale/en.json b/public/locale/en.json index 129eb34..1ab0606 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -117,6 +117,7 @@ "checking_consent_status": "Consent request status is being checked!", "async_operation_warning": "This operation may take some time. Please check back later.", "loading_health_information": "Loading Health Information", + "back": "Back", "configure_health_facility": "Configure Health Facility", "configure_health_facility_description": "Configure the health facility, link your ABDM health facility with care and register it as a service in bridge", "generate_scan_and_share_qr": "Generate Scan and Share QR", diff --git a/src/components/encounter-tabs/Abdm.tsx b/src/components/encounter-tabs/Abdm.tsx index bddf02c..772d5d9 100644 --- a/src/components/encounter-tabs/Abdm.tsx +++ b/src/components/encounter-tabs/Abdm.tsx @@ -4,12 +4,15 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { Button } from "../ui/button"; import { EncounterTabProps } from "."; +import { Encounter } from "@/types/encounter"; import { FC } from "react"; import { I18NNAMESPACE } from "@/lib/constants"; import { Link } from "raviger"; +import { Patient } from "@/types/patient"; import { apis } from "@/apis"; import { cn } from "@/lib/utils"; import dayjs from "@/lib/dayjs"; +import { healthInformationPath } from "@/lib/paths"; import { toast } from "@/lib/utils"; import { useTranslation } from "react-i18next"; @@ -57,7 +60,14 @@ export const AbdmEncounterTab: FC = ({ return (
{data?.results.map((record) => { - return ; + return ( + + ); })}
); @@ -65,14 +75,27 @@ export const AbdmEncounterTab: FC = ({ interface IConsentArtefactCardProps { artefact: ConsentArtefact; + facilityId: string; + patientId: string; + encounterId: string; } -function ConsentArtefactCard({ artefact }: IConsentArtefactCardProps) { +function ConsentArtefactCard({ + artefact, + facilityId, + patientId, + encounterId, +}: IConsentArtefactCardProps) { const { t } = useTranslation(I18NNAMESPACE); return (
@@ -115,9 +138,15 @@ function ConsentArtefactCard({ artefact }: IConsentArtefactCardProps) { interface IConsentRequestCardProps { consent: ConsentRequest; + encounter: Encounter; + patient: Patient; } -function ConsentRequestCard({ consent }: IConsentRequestCardProps) { +function ConsentRequestCard({ + consent, + encounter, + patient, +}: IConsentRequestCardProps) { const { t } = useTranslation(I18NNAMESPACE); const checkStatusMutation = useMutation({ @@ -170,7 +199,13 @@ function ConsentRequestCard({ consent }: IConsentRequestCardProps) { {consent.consent_artefacts?.length ? (
{consent.consent_artefacts?.map((artefact) => ( - + ))}
) : ( diff --git a/src/components/pages/HealthInformation.tsx b/src/components/pages/HealthInformation.tsx index b6fa4e7..0e7d56c 100644 --- a/src/components/pages/HealthInformation.tsx +++ b/src/components/pages/HealthInformation.tsx @@ -1,18 +1,31 @@ import { FC } from "react"; import { HIProfile } from "hi-profiles"; -import { I18NNAMESPACE } from "@/lib/constants"; -import { Loader2Icon } from "lucide-react"; -import Page from "@/components/ui/page"; -import { apis } from "@/apis"; +import { ArrowLeftIcon, Loader2Icon } from "lucide-react"; +import { Link } from "raviger"; import { useQuery } from "@tanstack/react-query"; import { useTranslation } from "react-i18next"; +import { apis } from "@/apis"; +import { Button } from "@/components/ui/button"; +import Page from "@/components/ui/page"; +import { I18NNAMESPACE } from "@/lib/constants"; +import { encounterPath } from "@/lib/paths"; + interface HealthInformationProps { artefactId: string; + facilityId: string; + patientId: string; + encounterId: string; } -const HealthInformation: FC = ({ artefactId }) => { +const HealthInformation: FC = ({ + artefactId, + facilityId, + patientId, + encounterId, +}) => { const { t } = useTranslation(I18NNAMESPACE); + const encounterUrl = encounterPath(facilityId, patientId, encounterId, "abdm"); const { data, @@ -26,17 +39,6 @@ const HealthInformation: FC = ({ artefactId }) => { const error: any = errorT; // eslint-disable-line @typescript-eslint/no-explicit-any Intentionally typecasting to any - if (isLoading) { - return ( -
- -

- {t("loading_health_information")} -

-
- ); - } - const parseData = (data: string) => { try { return JSON.parse(data); @@ -47,8 +49,41 @@ const HealthInformation: FC = ({ artefactId }) => { } }; + const pageHeader = ( +
+ +

+ {t("hi__page_title")} +

+
+ ); + + if (isLoading) { + return ( + + {pageHeader} +
+ +

+ {t("loading_health_information")} +

+
+
+ ); + } + return ( - + + {pageHeader}
{!!error?.is_archived && ( <> diff --git a/src/lib/paths.ts b/src/lib/paths.ts new file mode 100644 index 0000000..9bce59e --- /dev/null +++ b/src/lib/paths.ts @@ -0,0 +1,17 @@ +export function healthInformationPath( + facilityId: string, + patientId: string, + encounterId: string, + artefactId: string, +) { + return `/facility/${facilityId}/patient/${patientId}/encounter/${encounterId}/healthInformation/${artefactId}`; +} + +export function encounterPath( + facilityId: string, + patientId: string, + encounterId: string, + tab = "updates", +) { + return `/facility/${facilityId}/patient/${patientId}/encounter/${encounterId}/${tab}`; +} diff --git a/src/routes.tsx b/src/routes.tsx index 52f1cdf..7a60246 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,9 +1,15 @@ import HealthInformation from "@/components/pages/HealthInformation"; const routes = { - "/abdm/health-information/:id": ({ id }: { id: string }) => ( - - ), + "/facility/:facilityId/patient/:patientId/encounter/:encounterId/healthInformation/:id": + ({ facilityId, patientId, encounterId, id }: { facilityId: string, patientId: string, encounterId: string, id: string }) => ( + + ), }; export default routes; From 7b2850bcb3a3a4628771e3f0401a74da47cd014f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 26 Jun 2026 12:50:16 +0530 Subject: [PATCH 11/17] fix disabled submit button even after auto filling all required fields --- .../pluggables/PatientRegistrationForm.tsx | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/src/components/pluggables/PatientRegistrationForm.tsx b/src/components/pluggables/PatientRegistrationForm.tsx index 0c499ca..a5d547b 100644 --- a/src/components/pluggables/PatientRegistrationForm.tsx +++ b/src/components/pluggables/PatientRegistrationForm.tsx @@ -18,6 +18,8 @@ type PatientRegistrationFormProps = { patientId?: string; }; +const setFormValueOpts = { shouldDirty: true } as const; + const PatientRegistrationForm: FC> = ({ form, facilityId, @@ -35,9 +37,9 @@ const PatientRegistrationForm: FC> = ({ useEffect(() => { if (abhaNumber) { - form.setValue("abha_id", abhaNumber.external_id); - form.setValue("abha_number", abhaNumber.abha_number); - form.setValue("abha_address", abhaNumber.health_id); + form.setValue("abha_id", abhaNumber.external_id, setFormValueOpts); + form.setValue("abha_number", abhaNumber.abha_number, setFormValueOpts); + form.setValue("abha_address", abhaNumber.health_id, setFormValueOpts); } }, [abhaNumber]); @@ -84,8 +86,8 @@ const PatientRegistrationForm: FC> = ({ return; } - form.setValue("_selected_levels", data.results); - form.setValue("geo_organization", data.results[0].id); + form.setValue("_selected_levels", data.results, setFormValueOpts); + form.setValue("geo_organization", data.results[0].id, setFormValueOpts); }, }); @@ -104,11 +106,11 @@ const PatientRegistrationForm: FC> = ({ variant="outline" className="text-primary border-primary-400" onSuccess={(abhaNumber) => { - form.setValue("abha", abhaNumber); + form.setValue("abha", abhaNumber, setFormValueOpts); - form.setValue("abha_id", abhaNumber.external_id); - form.setValue("abha_number", abhaNumber.abha_number); - form.setValue("abha_address", abhaNumber.health_id); + form.setValue("abha_id", abhaNumber.external_id, setFormValueOpts); + form.setValue("abha_number", abhaNumber.abha_number, setFormValueOpts); + form.setValue("abha_address", abhaNumber.health_id, setFormValueOpts); if (patientId) { linkAbhaNumberAndPatientMutation.mutate({ @@ -118,24 +120,35 @@ const PatientRegistrationForm: FC> = ({ return; } - form.setValue("name", abhaNumber.name); + form.setValue("name", abhaNumber.name, setFormValueOpts); form.setValue( "phone_number", - "+91" + abhaNumber.mobile?.replace("+91", "") + "+91" + abhaNumber.mobile?.replace("+91", ""), + setFormValueOpts + ); + form.setValue("age_or_dob", "dob", setFormValueOpts); + form.setValue( + "date_of_birth", + abhaNumber.date_of_birth, + setFormValueOpts ); - form.setValue("yob_or_dob", "dob"); - form.setValue("date_of_birth", abhaNumber.date_of_birth); - form.setValue("blood_group", "unknown"); + form.setValue("blood_group", "unknown", setFormValueOpts); form.setValue( "gender", { M: "male", F: "female", O: "transgender" }[abhaNumber.gender] ?? - "transgender" + "transgender", + setFormValueOpts + ); + form.setValue("address", abhaNumber.address, setFormValueOpts); + form.setValue( + "permanent_address", + abhaNumber.address, + setFormValueOpts ); - form.setValue("address", abhaNumber.address); - form.setValue("permanent_address", abhaNumber.address); form.setValue( "pincode", - abhaNumber.pincode && Number(abhaNumber.pincode) + abhaNumber.pincode && Number(abhaNumber.pincode), + setFormValueOpts ); if (abhaNumber.district) { From d8cab843953bbd69449232031f39673f106cc643 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 26 Jun 2026 12:59:28 +0530 Subject: [PATCH 12/17] handle an edge case where abha number linking was not reflecting in patient form immediately after linking and reopening --- .../pluggables/PatientHomeActions.tsx | 8 ++++--- .../pluggables/PatientRegistrationForm.tsx | 23 ++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/components/pluggables/PatientHomeActions.tsx b/src/components/pluggables/PatientHomeActions.tsx index 637a9fb..c0d99fc 100644 --- a/src/components/pluggables/PatientHomeActions.tsx +++ b/src/components/pluggables/PatientHomeActions.tsx @@ -1,4 +1,4 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { FC } from "react"; import { I18NNAMESPACE } from "@/lib/constants"; @@ -20,11 +20,13 @@ const PatientHomeActions: FC> = ({ facilityId, className, }) => { + const queryClient = useQueryClient(); const { t } = useTranslation(I18NNAMESPACE); - const { data: abhaNumber, refetch } = useQuery({ + const { data: abhaNumber } = useQuery({ queryKey: ["abhaNumber", patient.id], queryFn: () => apis.abhaNumber.get(patient.id), enabled: !!patient.id, + retry: false, }); const linkAbhaNumberAndPatientMutation = useMutation({ @@ -32,7 +34,7 @@ const PatientHomeActions: FC> = ({ onSuccess: (data) => { if (data) { toast.success(data.detail || t("abha_number_linked_successfully")); - refetch(); + queryClient.invalidateQueries({ queryKey: ["abhaNumber", patient.id] }); } }, onError: (error) => { diff --git a/src/components/pluggables/PatientRegistrationForm.tsx b/src/components/pluggables/PatientRegistrationForm.tsx index a5d547b..ea616ef 100644 --- a/src/components/pluggables/PatientRegistrationForm.tsx +++ b/src/components/pluggables/PatientRegistrationForm.tsx @@ -29,26 +29,29 @@ const PatientRegistrationForm: FC> = ({ const queryClient = useQueryClient(); const { t } = useTranslation(I18NNAMESPACE); - const { data: abhaNumber, refetch } = useQuery({ + const { data: abhaNumber } = useQuery({ queryKey: ["abhaNumber", patientId], queryFn: () => apis.abhaNumber.get(patientId!), enabled: !!patientId, + retry: false, }); + const patientName = form.watch("name"); + useEffect(() => { if (abhaNumber) { form.setValue("abha_id", abhaNumber.external_id, setFormValueOpts); form.setValue("abha_number", abhaNumber.abha_number, setFormValueOpts); form.setValue("abha_address", abhaNumber.health_id, setFormValueOpts); } - }, [abhaNumber]); + }, [abhaNumber, patientName, form]); const linkAbhaNumberAndPatientMutation = useMutation({ mutationFn: apis.healthId.linkAbhaNumberAndPatient, onSuccess: (data) => { if (data) { toast.success(data.detail || t("abha_number_linked_successfully")); - refetch(); + queryClient.invalidateQueries({ queryKey: ["abhaNumber", patientId] }); } }, onError: (error) => { @@ -91,7 +94,9 @@ const PatientRegistrationForm: FC> = ({ }, }); - if (!form.watch("abha_id")) { + const abhaId = form.watch("abha_id"); + + if (!abhaId && !abhaNumber) { return (
> = ({
- +
- +
{abhaProfile && } From e3ab8e615c217a28082d26d333fb8cf50dfe308c Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 26 Jun 2026 15:41:34 +0530 Subject: [PATCH 13/17] update the ui of consent cards --- public/locale/en.json | 9 + src/components/encounter-tabs/Abdm.tsx | 289 +++++++++++++++---------- 2 files changed, 181 insertions(+), 117 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 1ab0606..44c4a33 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -35,6 +35,15 @@ "consent__status__GRANTED": "Granted", "consent__status__REQUESTED": "Requested", "consent__status__REVOKED": "Revoked", + "valid_till": "Valid till {{date}}", + "hi_types": "HI Types", + "artefact_status__waiting": "Waiting", + "artefact_status__fetched": "Fetched", + "unknown_facility": "Unknown Facility", + "view": "View", + "status": "Status", + "expand": "Expand", + "collapse": "Collapse", "consent_request__date_range": "Health Records Date Range", "consent_request__expiry": "Consent Expiry Date", "consent_request__hi_types": "Health Information Types", diff --git a/src/components/encounter-tabs/Abdm.tsx b/src/components/encounter-tabs/Abdm.tsx index 772d5d9..3c9d60e 100644 --- a/src/components/encounter-tabs/Abdm.tsx +++ b/src/components/encounter-tabs/Abdm.tsx @@ -1,11 +1,13 @@ import { ConsentArtefact, ConsentRequest } from "@/types/consent"; -import { Loader2Icon, RefreshCcwIcon } from "lucide-react"; +import { ChevronDownIcon, Loader2Icon, RefreshCcwIcon } from "lucide-react"; import { useMutation, useQuery } from "@tanstack/react-query"; +import { APIError } from "@/apis/request"; +import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { EncounterTabProps } from "."; import { Encounter } from "@/types/encounter"; -import { FC } from "react"; +import { FC, useState } from "react"; import { I18NNAMESPACE } from "@/lib/constants"; import { Link } from "raviger"; import { Patient } from "@/types/patient"; @@ -16,6 +18,22 @@ import { healthInformationPath } from "@/lib/paths"; import { toast } from "@/lib/utils"; import { useTranslation } from "react-i18next"; +function getConsentStatusBadgeClass(status: ConsentRequest["status"]) { + switch (status) { + case "GRANTED": + return "border-transparent bg-green-100 text-green-800"; + case "REQUESTED": + return "border-transparent bg-amber-100 text-amber-800"; + case "EXPIRED": + return "border-transparent bg-secondary-100 text-secondary-700"; + case "DENIED": + case "REVOKED": + return "border-transparent bg-red-100 text-red-800"; + default: + return "border-transparent bg-secondary-100 text-secondary-700"; + } +} + export const AbdmEncounterTab: FC = ({ patient, encounter, @@ -36,7 +54,7 @@ export const AbdmEncounterTab: FC = ({ if (isLoading) { return (
- +

{t("loading_consent_requests")}

@@ -88,51 +106,47 @@ function ConsentArtefactCard({ }: IConsentArtefactCardProps) { const { t } = useTranslation(I18NNAMESPACE); + const { isLoading, error } = useQuery({ + queryKey: ["healthInformation", "status", artefact.id], + queryFn: () => apis.healthInformation.get(artefact.id), + retry: false, + staleTime: 60_000, + }); + + const isWaiting = + !isLoading && (error as APIError | null)?.status === 404; + const recordStatus = isWaiting + ? t("artefact_status__waiting") + : t("artefact_status__fetched"); + return ( - -
-
-
- {artefact.hip} -
-

- {t("created_on")} {dayjs(artefact.created_date).fromNow()} -

-
-
-
- {artefact.status} -
-
- {dayjs(artefact.from_time).format("MMM DD YYYY")} -{" "} - {dayjs(artefact.to_time).format("MMM DD YYYY")} -
-

- {t("expires_on")} {dayjs(artefact.expiry).fromNow()} -

-
-
-
- {artefact.hi_types.map((hiType) => { - return ( -
- {t(`consent__hi_type__${hiType}`)} -
- ); - })} +
+
+
+ {artefact.hip ?? t("unknown_facility")} +
+

+ {t("status")}:{" "} + {isLoading ? ( + + ) : ( + {recordStatus} + )} +

- + +
); } @@ -148,6 +162,7 @@ function ConsentRequestCard({ patient, }: IConsentRequestCardProps) { const { t } = useTranslation(I18NNAMESPACE); + const [expanded, setExpanded] = useState(false); const checkStatusMutation = useMutation({ mutationFn: apis.consent.checkStatus, @@ -158,82 +173,122 @@ function ConsentRequestCard({ }); return ( -
-
-
-
- {t(`consent__purpose__${consent.purpose}`)} -
-
- {[consent.requester.first_name, consent.requester.last_name] - .filter(Boolean) - .join(" ")} -
-
-
-
- {dayjs(consent.from_time).format("MMM DD YYYY")} -{" "} - {dayjs(consent.to_time).format("MMM DD YYYY")} -
-

- {t("expires_on")} {dayjs(consent.expiry).fromNow()} -

-
-
- -

- {t("created_on")} {dayjs(consent.created_date).fromNow()} -

-

- {t("modified_on")} {dayjs(consent.modified_date).fromNow()} -

+
+
+ + {t(`consent__purpose__${consent.purpose}`)} + + + {t(`consent__status__${consent.status}`, { + defaultValue: consent.status, + })} + +
+

+ {t("valid_till", { + date: dayjs(consent.expiry).format("MMM DD, YYYY"), + })} +

+
+ +
+

+ {t("hi_types")}: +

+
+ {consent.hi_types.map((hiType) => ( + + {t(`consent__hi_type__${hiType}`)} + + ))} +
+
+ +
+ + {t("created_on")}:{" "} + {dayjs(consent.created_date).format("MMM DD, YYYY HH:mm")} + + + {t("modified_on")}:{" "} + {dayjs(consent.modified_date).format("MMM DD, YYYY HH:mm")} + +
+ + +
+ + +
- {consent.consent_artefacts?.length ? ( -
- {consent.consent_artefacts?.map((artefact) => ( - - ))} -
- ) : ( -
-

- {consent.status === "REQUESTED" - ? t("consent_request_waiting_approval") - : t("consent_request_rejected")} -

+ + {expanded && ( +
+ {consent.consent_artefacts?.length ? ( +
+ {consent.consent_artefacts.map((artefact) => ( + + ))} +
+ ) : ( +

+ {consent.status === "REQUESTED" + ? t("consent_request_waiting_approval") + : t("consent_request_rejected")} +

+ )}
)} -
- {consent.hi_types.map((hiType) => { - return ( -
- {t(`consent__hi_type__${hiType}`)} -
- ); - })} -
); } From e0f9afe1fc69322142039736b85e761cdae248b0 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 26 Jun 2026 15:47:31 +0530 Subject: [PATCH 14/17] Add date range display to consent request cards --- public/locale/en.json | 3 ++- src/components/encounter-tabs/Abdm.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 44c4a33..cbba47e 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -35,7 +35,8 @@ "consent__status__GRANTED": "Granted", "consent__status__REQUESTED": "Requested", "consent__status__REVOKED": "Revoked", - "valid_till": "Valid till {{date}}", + "valid_till": "Valid until {{date}}", + "consent_records_date_range": "Records requested from {{from}} to {{to}}", "hi_types": "HI Types", "artefact_status__waiting": "Waiting", "artefact_status__fetched": "Fetched", diff --git a/src/components/encounter-tabs/Abdm.tsx b/src/components/encounter-tabs/Abdm.tsx index 3c9d60e..f706fa0 100644 --- a/src/components/encounter-tabs/Abdm.tsx +++ b/src/components/encounter-tabs/Abdm.tsx @@ -200,7 +200,13 @@ function ConsentRequestCard({

{t("valid_till", { - date: dayjs(consent.expiry).format("MMM DD, YYYY"), + date: dayjs(consent.expiry).format("MMM D, YYYY"), + })} +

+

+ {t("consent_records_date_range", { + from: dayjs(consent.from_time).format("MMM D, YYYY"), + to: dayjs(consent.to_time).format("MMM D, YYYY"), })}

From 7aa83310416ca46cf681fdbb05e9204adb6f4fd7 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 26 Jun 2026 16:01:56 +0530 Subject: [PATCH 15/17] filter prescription from different facilities --- src/components/pages/HealthInformation.tsx | 49 +++++++++++- src/lib/patches/filterHiBundleByConsentHip.ts | 77 +++++++++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/lib/patches/filterHiBundleByConsentHip.ts diff --git a/src/components/pages/HealthInformation.tsx b/src/components/pages/HealthInformation.tsx index 0e7d56c..e4ce579 100644 --- a/src/components/pages/HealthInformation.tsx +++ b/src/components/pages/HealthInformation.tsx @@ -1,4 +1,4 @@ -import { FC } from "react"; +import { FC, useMemo } from "react"; import { HIProfile } from "hi-profiles"; import { ArrowLeftIcon, Loader2Icon } from "lucide-react"; import { Link } from "raviger"; @@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button"; import Page from "@/components/ui/page"; import { I18NNAMESPACE } from "@/lib/constants"; import { encounterPath } from "@/lib/paths"; +// TEMPORARY PATCH: Remove when HIP matching is handled server-side. +import { shouldIncludeHiBundle } from "@/lib/patches/filterHiBundleByConsentHip"; interface HealthInformationProps { artefactId: string; @@ -37,6 +39,34 @@ const HealthInformation: FC = ({ enabled: !!artefactId, }); + // TEMPORARY PATCH: Remove when MedicationRequest is changed from date based to prescription based. + const { data: consentsData } = useQuery({ + queryKey: ["consents", patientId, encounterId], + queryFn: () => + apis.consent.list({ + patient: patientId, + encounter: encounterId, + }), + enabled: !!patientId && !!encounterId, + }); + + const consentHip = useMemo(() => { + if (!consentsData) { + return undefined; + } + + for (const consent of consentsData.results) { + const artefact = consent.consent_artefacts?.find( + (item) => item.id === artefactId, + ); + if (artefact) { + return artefact.hip; + } + } + + return null; + }, [consentsData, artefactId]); + const error: any = errorT; // eslint-disable-line @typescript-eslint/no-explicit-any Intentionally typecasting to any const parseData = (data: string) => { @@ -49,6 +79,21 @@ const HealthInformation: FC = ({ } }; + // TEMPORARY PATCH: Remove when MedicationRequest is changed from date based to prescription based. + const filteredItems = useMemo(() => { + if (!data?.data) { + return []; + } + + if (consentHip === undefined) { + return data.data; + } + + return data.data.filter((item) => + shouldIncludeHiBundle(parseData(item.content), consentHip), + ); + }, [consentHip, data?.data]); + const pageHeader = (