Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues/8607/review missed #9259

Closed
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 143 additions & 115 deletions src/components/Patient/ManagePatients.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
import dayjs from "dayjs";
import { Link, navigate } from "raviger";
import { ReactNode, useCallback, useEffect, useState } from "react";
import { ReactNode, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";

import Chip from "@/CAREUI/display/Chip";
import CountBlock from "@/CAREUI/display/Count";
import FilterBadge from "@/CAREUI/display/FilterBadge";
import RecordMeta from "@/CAREUI/display/RecordMeta";
import CareIcon from "@/CAREUI/icons/CareIcon";
import { AdvancedFilterButton } from "@/CAREUI/interactive/FiltersSlideover";

import { Badge } from "@/components/ui/badge";

import { Avatar } from "@/components/Common/Avatar";
import ButtonV2 from "@/components/Common/ButtonV2";
import { ExportMenu } from "@/components/Common/Export";
import Loading from "@/components/Common/Loading";
import Page from "@/components/Common/Page";
import SearchByMultipleFields from "@/components/Common/SearchByMultipleFields";
import SortDropdownMenu from "@/components/Common/SortDropdown";
import Tabs from "@/components/Common/Tabs";
import { ICD11DiagnosisModel } from "@/components/Diagnosis/types";
import { getDiagnosesByIds } from "@/components/Diagnosis/utils";
import FacilitiesSelectDialogue from "@/components/ExternalResult/FacilitiesSelectDialogue";
import DoctorVideoSlideover from "@/components/Facility/DoctorVideoSlideover";
import { FacilityModel, PatientCategory } from "@/components/Facility/models";
import { PhoneNumberValidator } from "@/components/Form/FieldValidators";
import PhoneNumberFormField from "@/components/Form/FormFields/PhoneNumberFormField";
import { FieldChangeEvent } from "@/components/Form/FormFields/Utils";
import SearchInput from "@/components/Form/SearchInput";
import {
DIAGNOSES_FILTER_LABELS,
DiagnosesFilterKey,
FILTER_BY_DIAGNOSES_KEYS,
} from "@/components/Patient/DiagnosesFilter";
import PatientFilter from "@/components/Patient/PatientFilter";
import { isPatientMandatoryDataFilled } from "@/components/Patient/Utils";

import useAuthUser from "@/hooks/useAuthUser";
import useFilters from "@/hooks/useFilters";
Expand All @@ -27,36 +51,17 @@ import {
} from "@/common/constants";
import { parseOptionId } from "@/common/utils";

import { triggerGoal } from "@/Integrations/Plausible";
import * as Notification from "@/Utils/Notifications";
import routes from "@/Utils/request/api";

import Chip from "../../CAREUI/display/Chip";
import CountBlock from "../../CAREUI/display/Count";
import FilterBadge from "../../CAREUI/display/FilterBadge";
import RecordMeta from "../../CAREUI/display/RecordMeta";
import CareIcon from "../../CAREUI/icons/CareIcon";
import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover";
import { triggerGoal } from "../../Integrations/Plausible";
import * as Notification from "../../Utils/Notifications";
import request from "../../Utils/request/request";
import useQuery from "../../Utils/request/useQuery";
import request from "@/Utils/request/request";
import useQuery from "@/Utils/request/useQuery";
import {
formatPatientAge,
humanizeStrings,
isAntenatal,
parsePhoneNumber,
} from "../../Utils/utils";
import { ICD11DiagnosisModel } from "../Diagnosis/types";
import { getDiagnosesByIds } from "../Diagnosis/utils";
import FacilitiesSelectDialogue from "../ExternalResult/FacilitiesSelectDialogue";
import DoctorVideoSlideover from "../Facility/DoctorVideoSlideover";
import { FacilityModel, PatientCategory } from "../Facility/models";
import {
DIAGNOSES_FILTER_LABELS,
DiagnosesFilterKey,
FILTER_BY_DIAGNOSES_KEYS,
} from "./DiagnosesFilter";
import PatientFilter from "./PatientFilter";
import { isPatientMandatoryDataFilled } from "./Utils";
} from "@/Utils/utils";

interface TabPanelProps {
children?: ReactNode;
Expand Down Expand Up @@ -90,7 +95,6 @@ export const PatientManager = () => {
Pagination,
FilterBadges,
resultsPerPage,
clearSearch,
} = useFilters({
limit: 12,
cacheBlacklist: [
Expand All @@ -107,6 +111,30 @@ export const PatientManager = () => {
const [diagnoses, setDiagnoses] = useState<ICD11DiagnosisModel[]>([]);
const [showDialog, setShowDialog] = useState<"create" | "list-discharged">();
const [showDoctors, setShowDoctors] = useState(false);
const [phoneNumber, _setPhoneNumber] = useState("");
const [emergencyPhoneNumber, _setEmergencyPhoneNumber] = useState("");

const setPhoneNumber = (value: string) => {
_setPhoneNumber(value);
const error = PhoneNumberValidator()(value);
if (!error) {
updateQuery({ phone_number: value });
}
if ((value === "+91" || value === "") && qParams.phone_number) {
updateQuery({ phone_number: null });
}
};

const setEmergencyPhoneNumber = (value: string) => {
_setEmergencyPhoneNumber(value);
const error = PhoneNumberValidator()(value);
if (!error) {
updateQuery({ emergency_phone_number: value });
}
if ((value === "+91" || value === "") && qParams.emergency_phone_number) {
updateQuery({ emergency_phone_number: null });
}
};

const tabValue =
qParams.last_consultation__new_discharge_reason ||
Expand Down Expand Up @@ -293,6 +321,14 @@ export const PatientManager = () => {

const { loading: isLoading, data } = useQuery(routes.patientList, {
query: params,
onResponse: () => {
if (!params.phone_number) {
_setPhoneNumber("+91");
}
if (!params.emergency_phone_number) {
_setEmergencyPhoneNumber("+91");
}
},
});

const getTheCategoryFromId = () => {
Expand Down Expand Up @@ -596,15 +632,26 @@ export const PatientManager = () => {
)}
{patient.review_time &&
!patient.last_consultation?.discharge_date &&
Number(patient.last_consultation?.review_interval) > 0 &&
dayjs().isAfter(patient.review_time) && (
<Chip
size="small"
variant="danger"
startIcon="l-clock"
text="Review Missed"
/>
Number(patient.last_consultation?.review_interval) > 0 && (
<Badge
variant={
dayjs().isAfter(patient.review_time)
? "purple"
: "destructive"
}
className="flex items-center gap-1"
>
<i className="icon-class l-clock"></i>
{dayjs().isAfter(patient.review_time)
? `Review Missed ${Math.abs(
dayjs().diff(dayjs(patient.review_time), "days"),
)} days ago`
: `Review Due in ${Math.abs(
dayjs(patient.review_time).diff(dayjs(), "days"),
)} days`}
</Badge>
)}

{patient.last_consultation?.is_readmission && (
<Chip
size="small"
Expand Down Expand Up @@ -745,74 +792,22 @@ export const PatientManager = () => {
);
}

const queryField = <T,>(name: string, defaultValue?: T) => {
return {
name,
value: qParams[name] || defaultValue,
onChange: (e: FieldChangeEvent<T>) => updateQuery({ [e.name]: e.value }),
};
};

const onlyAccessibleFacility =
permittedFacilities?.count === 1 ? permittedFacilities.results[0] : null;

const searchOptions = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge error; Part of SearchByMultipleFields, and as such should be included.

{
key: "phone_number",
label: "Phone Number",
type: "phone" as const,
placeholder: "Search_by_phone_number",
value: qParams.phone_number || "",
shortcutKey: "p",
},
{
key: "name",
label: "Name",
type: "text" as const,
placeholder: "search_by_patient_name",
value: qParams.name || "",
shortcutKey: "n",
},
{
key: "patient_no",
label: "IP/OP No",
type: "text" as const,
placeholder: "search_by_patient_no",
value: qParams.patient_no || "",
shortcutKey: "u",
},
{
key: "emergency_contact_number",
label: "Emergency Contact Phone Number",
type: "phone" as const,
placeholder: "search_by_emergency_phone_number",
value: qParams.emergency_phone_number || "",
shortcutKey: "e",
},
];

const handleSearch = useCallback(
(key: string, value: string) => {
const updatedQuery = {
phone_number:
key === "phone_number"
? value.length >= 13 || value === ""
? value
: undefined
: undefined,
name: key === "name" ? value : undefined,
patient_no: key === "patient_no" ? value : undefined,
emergency_phone_number:
key === "emergency_contact_number"
? value.length >= 13 || value === ""
? value
: undefined
: undefined,
};

updateQuery(updatedQuery);
},
[updateQuery],
);

return (
<Page
title={t("patients")}
hideBack={true}
breadcrumbs={false}
className="px-4 md:px-6"
options={
<div className="flex w-full flex-col items-center justify-between lg:flex-row">
<div className="mb-2 flex w-full flex-col items-center lg:mb-0 lg:w-fit lg:flex-row lg:gap-5">
Expand Down Expand Up @@ -991,23 +986,59 @@ export const PatientManager = () => {
}}
/>

<div className="mt-4 gap-4 lg:gap-16 flex flex-col lg:flex-row lg:items-center">
<div id="total-patientcount">
<CountBlock
text={t("total_patients")}
count={data?.count || 0}
loading={isLoading}
icon="d-patient"
/>
<div className="manualGrid my-4 mb-[-12px] mt-5 grid-cols-1 gap-3 px-2 sm:grid-cols-4 md:px-0">
<div className="mt-2 flex h-full flex-col gap-3 xl:flex-row">
<div className="flex-1" id="total-patientcount">
<CountBlock
text="Total Patients"
count={data?.count || 0}
loading={isLoading}
icon="l-user-injured"
className="pb-12"
/>
</div>
</div>
<div className="col-span-3 w-full">
<div className="mt-2">
<div className="mb-4 mt-1 md:flex md:gap-4">
<SearchInput
label="Search by Patient"
placeholder="Enter patient name"
{...queryField("name")}
className="w-full grow"
/>
<SearchInput
label="Search by IP/OP Number"
placeholder="Enter IP/OP Number"
secondary
{...queryField("patient_no")}
className="w-full grow"
/>
</div>
<div className="mb-4 md:flex md:gap-4">
<PhoneNumberFormField
label="Search by Primary Number"
{...queryField("phone_number", "+91")}
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.value)}
types={["mobile", "landline"]}
className="w-full grow"
error={((phoneNumber || "+91") === "+91" && "") || undefined}
/>
<PhoneNumberFormField
label="Search by Emergency Number"
{...queryField("emergency_phone_number", "+91")}
value={emergencyPhoneNumber}
onChange={(e) => setEmergencyPhoneNumber(e.value)}
types={["mobile", "landline"]}
className="w-full"
error={
((emergencyPhoneNumber || "+91") === "+91" && "") || undefined
}
/>
</div>
</div>
</div>

<SearchByMultipleFields
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge error here; SearchByMultipleFields should be included instead of the individual search fields.

id="patient-search"
options={searchOptions}
onSearch={handleSearch}
clearSearch={clearSearch}
className="w-full"
/>
</div>
<div className="col-span-3 flex flex-wrap">
<FilterBadges
Expand Down Expand Up @@ -1125,10 +1156,7 @@ export const PatientManager = () => {
/>
</div>
<div>
<PatientFilter
{...advancedFilter}
key={JSON.stringify(advancedFilter.filter)}
/>
<PatientFilter {...advancedFilter} key={window.location.search} />
<TabPanel value={tabValue} index={0}>
<div className="mb-4">{managePatients}</div>
</TabPanel>
Expand Down
38 changes: 38 additions & 0 deletions src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";

import { cn } from "@/lib/utils";

const badgeVariants = cva(
"inline-flex items-center rounded-md border border-gray-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2 dark:border-gray-800 dark:focus:ring-gray-300",
{
variants: {
variant: {
default:
"border-transparent bg-gray-900 text-gray-50 shadow hover:bg-gray-900/80 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/80",
secondary:
"border-transparent bg-gray-100 text-gray-900 hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80",
destructive:
"border-transparent bg-red-500 text-gray-50 shadow hover:bg-red-500/80 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/80",
outline: "text-gray-950 dark:text-gray-50",
purple:
"border-transparent bg-purple-200 text-purple-800 shadow hover:bg-purple-300 dark:bg-purple-900 dark:text-purple-200 dark:hover:bg-purple-800",
},
},
defaultVariants: {
variant: "default",
},
},
);

export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}

export { Badge, badgeVariants };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance accessibility and documentation.

The component implementation is clean, but could benefit from improved accessibility and documentation:

+/**
+ * Badge component for displaying status indicators.
+ * @param variant - The visual style variant of the badge
+ * @param className - Additional CSS classes to apply
+ */
 export interface BadgeProps
   extends React.HTMLAttributes<HTMLDivElement>,
     VariantProps<typeof badgeVariants> {}

 function Badge({ className, variant, ...props }: BadgeProps) {
   return (
     <div
+      role="status"
+      aria-label={props['aria-label'] || props.children?.toString()}
       className={cn(badgeVariants({ variant }), className)}
       {...props}
     />
   );
 }

This change:

  1. Adds TypeScript documentation for better developer experience
  2. Adds proper ARIA role for accessibility
  3. Adds aria-label with fallback to child content
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };
/**
* Badge component for displaying status indicators.
* @param variant - The visual style variant of the badge
* @param className - Additional CSS classes to apply
*/
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div
role="status"
aria-label={props['aria-label'] || props.children?.toString()}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };

Loading