Skip to content

Commit

Permalink
Add seach by multiple fileds
Browse files Browse the repository at this point in the history
  • Loading branch information
bodhish committed Oct 19, 2024
1 parent c55d12e commit 6b0daf2
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 45 deletions.
220 changes: 220 additions & 0 deletions src/Components/Common/SearchByMultipleFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import React, {
useState,
useCallback,
useRef,
useEffect,
useMemo,
} from "react";
import { useTranslation } from "react-i18next";
import { Input } from "@/Components/ui/input";
import { Button } from "@/Components/ui/button";
import { cn } from "@/lib/utils";
import CareIcon from "@/CAREUI/icons/CareIcon";
import PhoneNumberFormField from "@/Components/Form/FormFields/PhoneNumberFormField";
import {
Command,
CommandGroup,
CommandItem,
CommandList,
} from "@/Components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/Components/ui/popover";

interface SearchOption {
key: string;
label: string;
type: "text" | "phone";
placeholder: string;
value: string;
shortcut_key: string;
component?: React.ComponentType<any>;
}

interface SearchByMultipleFieldsProps {
options: SearchOption[];
onSearch: (key: string, value: string) => void;
initialOption?: SearchOption;
className?: string;
inputClassName?: string;
buttonClassName?: string;
}

const SearchByMultipleFields: React.FC<SearchByMultipleFieldsProps> = ({
options,
onSearch,
initialOption,
className,
inputClassName,
buttonClassName,
}) => {
const { t } = useTranslation();
const [selectedOption, setSelectedOption] = useState<SearchOption>(
initialOption || options[0],
);
const [searchValue, setSearchValue] = useState(selectedOption.value || "");
const [open, setOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "/" && document.activeElement !== inputRef.current) {
e.preventDefault();
setOpen(true);
}

if (open) {
if (e.key === "Escape") {
setOpen(false);
}
}

options.forEach((option) => {
if (e.key.toLowerCase() === option.shortcut_key.toLowerCase()) {
e.preventDefault();
handleOptionChange(option);
}
});
};

document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);

const handleOptionChange = useCallback(
(option: SearchOption) => {
setSelectedOption(option);
setSearchValue(option.value || "");
setOpen(false);
inputRef.current?.focus();
onSearch(option.key, option.value);
},
[onSearch],
);

const handleSearchChange = useCallback(
(value: string) => {
setSearchValue(value);
onSearch(selectedOption.key, value);
},
[selectedOption, onSearch],
);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
console.log(e.key);
if (e.key === "Escape") {
setSearchValue("");
onSearch(selectedOption.key, "");
}
if (e.key === "/") {
e.preventDefault();
setOpen(true);
}
},
[selectedOption, onSearch],
);

const renderSearchInput = useMemo(() => {
const commonProps = {
ref: inputRef,
value: searchValue,
onChange: (e: any) =>
handleSearchChange(e.target ? e.target.value : e.value),
onKeyDown: handleKeyDown,
className: cn(
"flex-grow border-none shadow-none focus-visible:ring-0 h-10",
inputClassName,
),
};

switch (selectedOption.type) {
case "phone":
return (
<PhoneNumberFormField
name={selectedOption.key}
placeholder={t(selectedOption.placeholder)}
types={["mobile", "landline"]}
{...commonProps}
/>
);
default:
return (
<Input
type="text"
placeholder={t(selectedOption.placeholder)}
{...commonProps}
/>
);
}
}, [
selectedOption,
searchValue,
handleSearchChange,
handleKeyDown,
t,
inputClassName,
]);

return (
<div className={className}>
<div className="flex items-center rounded-t-lg border-x border-t border-gray-200 bg-white">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
className="focus:ring-0"
size="sm"
onClick={() => setOpen(true)}
>
<CareIcon icon="l-search" className="mr-2 h-4 w-4" />/
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandList>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.key}
onSelect={() => handleOptionChange(option)}
>
<CareIcon icon="l-search" className="mr-2 h-4 w-4" />
<span className="flex-1">{t(option.label)}</span>
<kbd className="ml-auto text-xs text-gray-400">
{option.label.charAt(0).toUpperCase()}
</kbd>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{renderSearchInput}
</div>
<div className="flex flex-wrap gap-2 rounded-b-lg border-x border-b border-gray-200 bg-gray-50 p-2 shadow">
{options.map((option) => (
<Button
key={option.key}
onClick={() => handleOptionChange(option)}
variant="outline"
size="xs"
className={cn(
selectedOption.key === option.key
? "bg-primary-100 text-primary-700"
: "bg-gray-100 text-gray-700 hover:bg-gray-200",
buttonClassName,
)}
>
{t(option.label)}
</Button>
))}
</div>
</div>
);
};

export default SearchByMultipleFields;
4 changes: 3 additions & 1 deletion src/Components/Form/FormFields/FormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ const FormField = ({
)}
</div>
<div className={field?.className}>{props.children}</div>
<FieldErrorText error={field?.error} className={field?.errorClassName} />
{field?.error && (
<FieldErrorText error={field.error} className={field?.errorClassName} />
)}
</div>
);
};
Expand Down
14 changes: 6 additions & 8 deletions src/Components/Form/FormFields/PhoneNumberFormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,17 @@ export default function PhoneNumberFormField(props: Props) {
field={{
...field,
error: field.error || error,
labelSuffix: field.labelSuffix || (
<PhoneNumberTypesHelp types={props.types} />
),
labelSuffix: field.labelSuffix || "",
}}
>
<div className="relative rounded-md shadow-sm">
<div className="relative rounded-md">
<Popover>
{({ open }: { open: boolean }) => {
return (
<>
<PopoverButton className="absolute h-full">
<div className="absolute inset-y-0 left-0 m-0.5 flex w-[4.5rem] cursor-pointer items-center justify-around bg-slate-100">
<span className="rounded-md pl-4">
<div className="hover:border-1 absolute inset-y-0 left-0 flex cursor-pointer items-center justify-around border-gray-200 hover:border hover:bg-gray-50">
<span className="rounded-md pl-2">
{country?.flag ?? "🇮🇳"}
</span>
<CareIcon
Expand All @@ -128,7 +126,7 @@ export default function PhoneNumberFormField(props: Props) {
name={field.name}
autoComplete={props.autoComplete ?? "tel"}
className={classNames(
"cui-input-base h-full pl-20 tracking-widest sm:leading-6",
"cui-input-base h-full pl-14 tracking-widest sm:leading-6",
field.error && "border-danger-500",
field.className,
)}
Expand Down Expand Up @@ -204,7 +202,7 @@ const CountryCodesList = ({
const [searchValue, setSearchValue] = useState<string>("");

return (
<div className="absolute z-10 w-full rounded-md border border-secondary-300 bg-white shadow-lg transition-all duration-300">
<div className="absolute z-10 w-full rounded-md border border-gray-300 bg-white shadow-lg transition-all duration-300">
<div className="relative m-2">
<CareIcon
icon="l-search"
Expand Down
91 changes: 55 additions & 36 deletions src/Components/Patient/ManagePatients.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "../../Common/constants";
import { FacilityModel, PatientCategory } from "../Facility/models";
import { Link, navigate } from "raviger";
import { ReactNode, useEffect, useState } from "react";
import { ReactNode, useEffect, useState, useCallback } from "react";
import { parseOptionId } from "../../Common/utils";

import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover";
Expand Down Expand Up @@ -56,6 +56,8 @@ import request from "../../Utils/request/request.js";
import { Avatar } from "../Common/Avatar.js";

import Loading from "@/Components/Common/Loading";
import SearchByMultipleFields from "@/Components/Common/SearchByMultipleFields";

interface TabPanelProps {
children?: ReactNode;
dir?: string;
Expand Down Expand Up @@ -783,6 +785,54 @@ export const PatientManager = () => {
const onlyAccessibleFacility =
permittedFacilities?.count === 1 ? permittedFacilities.results[0] : null;

const searchOptions = [
{
key: "phone_number",
label: "Phone Number",
type: "phone" as const,
placeholder: "Search by phone number",
value: qParams.phone_number || "",
shortcut_key: "p",
},
{
key: "name",
label: "Name",
type: "text" as const,
placeholder: "Search by patient name",
value: qParams.name || "",
shortcut_key: "n",
},
{
key: "patient_no",
label: "UHID",
type: "text" as const,
placeholder: "Search by UHID",
value: qParams.patient_no || "",
shortcut_key: "u",
},
{
key: "emergency_contact_phone_number",
label: "Emergency Contact Phone Number",
type: "phone" as const,
placeholder: "Search by emergency contact phone number",
value: qParams.emergency_contact_phone_number || "",
shortcut_key: "e",
},
];

const handleSearch = useCallback(
(key: string, value: string) => {
if (key === "phone_number" || key === "emergency_contact_phone_number") {
if (value.length >= 13 || value === "+91" || value === "") {
updateQuery({ [key]: value });
}
} else {
updateQuery({ [key]: value });
}
},
[updateQuery],
);

return (
<Page
title={t("patients")}
Expand Down Expand Up @@ -979,41 +1029,10 @@ export const PatientManager = () => {
</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={phone_number}
onChange={(e) => setPhoneNum(e.value)}
error={phoneNumberError}
types={["mobile", "landline"]}
className="w-full grow"
/>
<PhoneNumberFormField
label="Search by Emergency Number"
{...queryField("emergency_phone_number", "+91")}
value={emergency_phone_number}
onChange={(e) => setEmergencyPhoneNum(e.value)}
error={emergencyPhoneNumberError}
types={["mobile", "landline"]}
className="w-full"
/>
</div>
<SearchByMultipleFields
options={searchOptions}
onSearch={handleSearch}
/>
</div>
</div>
</div>
Expand Down

0 comments on commit 6b0daf2

Please sign in to comment.