diff --git a/public/locale/en.json b/public/locale/en.json index 1390570659a..99c3885db86 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -545,6 +545,7 @@ "choose_file": "Upload From Device", "choose_localbody": "Choose Local Body", "choose_location": "Choose Location", + "choose_other_search_type": "Choose other search types", "choose_state": "Choose State", "claim__add_item": "Add Item", "claim__create_claim": "Create Claim", @@ -1824,6 +1825,7 @@ "scribe__reviewing_field": "Reviewing field {{currentField}} / {{totalFields}}", "scribe_error": "Could not autofill fields", "search": "Search", + "search_by": "Search by", "search_by_emergency_contact_phone_number": "Search by Emergency Contact Phone Number", "search_by_emergency_phone_number": "Search by Emergency Phone Number", "search_by_patient_name": "Search by Patient Name", diff --git a/src/components/Common/SearchByMultipleFields.tsx b/src/components/Common/SearchByMultipleFields.tsx index 7aec153408d..0d4aee275fc 100644 --- a/src/components/Common/SearchByMultipleFields.tsx +++ b/src/components/Common/SearchByMultipleFields.tsx @@ -28,12 +28,13 @@ import { import { FieldError } from "@/components/Form/FieldValidators"; import PhoneNumberFormField from "@/components/Form/FormFields/PhoneNumberFormField"; +import { isAppleDevice } from "@/Utils/utils"; + interface SearchOption { key: string; type: "text" | "phone"; placeholder: string; value: string; - shortcutKey: string; component?: React.ComponentType; } @@ -41,7 +42,7 @@ interface SearchByMultipleFieldsProps { id: string; options: SearchOption[]; onSearch: (key: string, value: string) => void; - initialOptionIndex?: number; + initialOptionIndex: number; className?: string; inputClassName?: string; buttonClassName?: string; @@ -52,6 +53,36 @@ interface SearchByMultipleFieldsProps { type EventType = React.ChangeEvent | { value: string }; +const KeyboardShortcutHint = ({ open }: { open: boolean }) => { + return ( +
+ {open ? ( + + Esc + + ) : isAppleDevice ? ( +
+ + + + + K + +
+ ) : ( +
+ + Ctrl + + + K + +
+ )} +
+ ); +}; + const SearchByMultipleFields: React.FC = ({ id, options, @@ -65,28 +96,19 @@ const SearchByMultipleFields: React.FC = ({ enableOptionButtons = true, }) => { const { t } = useTranslation(); - const [selectedOptionIndex, setSelectedOptionIndex] = useState( - initialOptionIndex || 0, - ); + const [selectedOptionIndex, setSelectedOptionIndex] = + useState(initialOptionIndex); const selectedOption = options[selectedOptionIndex]; const [searchValue, setSearchValue] = useState(selectedOption.value || ""); const [open, setOpen] = useState(false); const inputRef = useRef(null); const [focusedIndex, setFocusedIndex] = useState(0); const [error, setError] = useState(); - - useEffect(() => { - if (!(selectedOption.type === "phone" && searchValue.length < 13)) { - setSearchValue(options[selectedOptionIndex].value); - } - }, [options]); + const isSingleOption = options.length == 1; useEffect(() => { if (clearSearch?.value) { - const clearinput = options - .map((op) => op.key) - .some((element) => clearSearch.params?.includes(element)); - clearinput ? setSearchValue("") : null; + setSearchValue(""); inputRef.current?.focus(); } }, [clearSearch?.value]); @@ -106,43 +128,50 @@ const SearchByMultipleFields: React.FC = ({ [onSearch], ); + const unselectedOptions = useMemo( + () => options.filter((option) => option.key !== selectedOption.key), + [options, selectedOption], + ); + + useEffect(() => { + if (open) { + setFocusedIndex(0); + } + }, [open]); + useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if ( - e.key === "/" && - !(document.activeElement instanceof HTMLInputElement) - ) { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); + e.stopPropagation(); + inputRef.current?.focus(); setOpen(true); } + + if (e.key === "Escape") { + inputRef.current?.focus(); + if (open) { + setOpen(false); + } else { + setSearchValue(""); + } + } + if (open) { if (e.key === "ArrowDown") { setFocusedIndex((prevIndex) => - prevIndex === options.length - 1 ? 0 : prevIndex + 1, + prevIndex === unselectedOptions.length - 1 ? 0 : prevIndex + 1, ); } else if (e.key === "ArrowUp") { setFocusedIndex((prevIndex) => - prevIndex === 0 ? options.length - 1 : prevIndex - 1, + prevIndex === 0 ? unselectedOptions.length - 1 : prevIndex - 1, ); } else if (e.key === "Enter") { - handleOptionChange(focusedIndex); - } - - if (e.key === "Escape") { - inputRef.current?.focus(); - setOpen(false); + const selectedOptionIndex = options.findIndex( + (option) => option.key === unselectedOptions[focusedIndex].key, + ); + handleOptionChange(selectedOptionIndex); } - - options.forEach((option, i) => { - if ( - e.key.toLocaleLowerCase() === - option.shortcutKey.toLocaleLowerCase() && - open - ) { - e.preventDefault(); - handleOptionChange(i); - } - }); } }; @@ -181,28 +210,41 @@ const SearchByMultipleFields: React.FC = ({ switch (selectedOption.type) { case "phone": return ( - setError(error)} - /> +
+ setError(error)} + /> + {!isSingleOption && } +
); default: return ( - +
+ + {!isSingleOption && } +
); } - }, [selectedOption, searchValue, handleSearchChange, t, inputClassName]); + }, [ + selectedOption, + searchValue, + handleSearchChange, + t, + inputClassName, + open, + ]); return (
= ({ aria-haspopup="listbox" className="flex items-center rounded-t-lg" > - - - + + event.preventDefault()} > - / - - - - - - - {options.map((option, index) => ( - handleOptionChange(index)} - className={cn({ - "bg-gray-100": focusedIndex === index, - "hover:bg-secondary-100": true, - })} - > - - {t(option.key)} - - {option.shortcutKey} - - - ))} - - - - - + + + +
+
+

+ {t("search_by")} +

+
+ +
+
+
+
+

+ {t("choose_other_search_type")} +

+
+ {unselectedOptions.map((option, index) => { + if (selectedOption.key === option.key) return null; + + return ( + + handleOptionChange( + options.findIndex( + (option) => + option.key === + unselectedOptions[index].key, + ), + ) + } + className={cn( + "flex items-center p-2 rounded-md cursor-pointer", + { + "bg-gray-100": focusedIndex === index, + "hover:bg-secondary-100": true, + }, + )} + onMouseEnter={() => setFocusedIndex(index)} + onMouseLeave={() => setFocusedIndex(-1)} + > + + {t(option.key)} + + {focusedIndex === index && ( + + ⏎ Enter + + )} + + ); + })} +
+
+
+
+
+
+ + + )}
{renderSearchInput}
{error && ( @@ -282,6 +383,20 @@ const SearchByMultipleFields: React.FC = ({ ))} )} + {searchValue.length !== 0 && ( + + )} ); }; diff --git a/src/components/Facility/FacilityForm.tsx b/src/components/Facility/FacilityForm.tsx index 51463f00cd9..858842bcd41 100644 --- a/src/components/Facility/FacilityForm.tsx +++ b/src/components/Facility/FacilityForm.tsx @@ -44,7 +44,7 @@ import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import { parsePhoneNumber } from "@/Utils/utils"; -import OrganizationSelector from "@/pages/Organization/components/OrganizationSelector"; +import GovtOrganizationSelector from "@/pages/Organization/components/GovtOrganizationSelector"; import { BaseFacility } from "@/types/facility/facility"; import { Organization } from "@/types/organization/organization"; @@ -381,7 +381,7 @@ export default function FacilityForm(props: FacilityProps) { render={({ field }) => ( -
)} /> - {form.watch("nationality") === "India" && ( ( - ( -
diff --git a/src/pages/Encounters/EncounterList.tsx b/src/pages/Encounters/EncounterList.tsx index 0313591dc93..b7195f4b115 100644 --- a/src/pages/Encounters/EncounterList.tsx +++ b/src/pages/Encounters/EncounterList.tsx @@ -148,11 +148,10 @@ export function EncounterList({ encounters: propEncounters, facilityId, }: EncounterListProps) { - const { qParams, updateQuery, Pagination, clearSearch, resultsPerPage } = - useFilters({ - limit: 15, - cacheBlacklist: ["name", "encounter_id", "external_identifier"], - }); + const { qParams, updateQuery, Pagination, resultsPerPage } = useFilters({ + limit: 15, + cacheBlacklist: ["name", "encounter_id", "external_identifier"], + }); const { status, encounter_class: encounterClass, @@ -161,6 +160,16 @@ export function EncounterList({ encounter_id, external_identifier, } = qParams; + const handleFieldChange = () => { + updateQuery({ + status, + encounter_class: encounterClass, + priority, + name: undefined, + encounter_id: undefined, + external_identifier: undefined, + }); + }; const handleSearch = useCallback( (key: string, value: string) => { @@ -202,7 +211,6 @@ export function EncounterList({ }), enabled: !!encounter_id, }); - const searchOptions = [ { key: "name", @@ -210,7 +218,6 @@ export function EncounterList({ type: "text" as const, placeholder: "Search by patient name", value: name || "", - shortcutKey: "n", }, { key: "encounter_id", @@ -218,7 +225,6 @@ export function EncounterList({ type: "text" as const, placeholder: "Search by encounter ID", value: encounter_id || "", - shortcutKey: "i", }, { key: "external_identifier", @@ -226,7 +232,6 @@ export function EncounterList({ type: "text" as const, placeholder: "Search by external ID", value: external_identifier || "", - shortcutKey: "e", }, ]; @@ -265,7 +270,11 @@ export function EncounterList({ )} - + event.preventDefault()} + >

{t("search_encounters")} @@ -273,31 +282,16 @@ export function EncounterList({ option.value !== "", + ), + 0, + )} + onFieldChange={handleFieldChange} onSearch={handleSearch} - clearSearch={clearSearch} className="w-full border-none shadow-none" /> - {(name || encounter_id || external_identifier) && ( - - )}

diff --git a/src/pages/Facility/FacilitiesPage.tsx b/src/pages/Facility/FacilitiesPage.tsx index 0e1830c3d29..36e27f0c528 100644 --- a/src/pages/Facility/FacilitiesPage.tsx +++ b/src/pages/Facility/FacilitiesPage.tsx @@ -24,10 +24,9 @@ import { FacilityCard } from "./components/FacilityCard"; export function FacilitiesPage() { const { mainLogo } = careConfig; - const { qParams, updateQuery, advancedFilter, clearSearch, Pagination } = - useFilters({ - limit: RESULTS_PER_PAGE_LIMIT, - }); + const { qParams, updateQuery, advancedFilter, Pagination } = useFilters({ + limit: RESULTS_PER_PAGE_LIMIT, + }); const { t } = useTranslation(); const [selectedOrgs, setSelectedOrgs] = useState(() => { @@ -102,12 +101,11 @@ export function FacilitiesPage() { type: "text" as const, placeholder: t("facility_search_placeholder_text"), value: qParams.name || "", - shortcutKey: "f", }, ]} + initialOptionIndex={0} className="w-[calc(100vw-2rem)] sm:max-w-min sm:min-w-64" onSearch={(key, value) => updateQuery({ name: value })} - clearSearch={clearSearch} enableOptionButtons={false} /> diff --git a/src/pages/Organization/OrganizationPatients.tsx b/src/pages/Organization/OrganizationPatients.tsx index cae267c6085..1148ef144f5 100644 --- a/src/pages/Organization/OrganizationPatients.tsx +++ b/src/pages/Organization/OrganizationPatients.tsx @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { Link } from "raviger"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import RecordMeta from "@/CAREUI/display/RecordMeta"; @@ -28,10 +28,47 @@ interface Props { export default function OrganizationPatients({ id, navOrganizationId }: Props) { const { t } = useTranslation(); + const { qParams, Pagination, advancedFilter, resultsPerPage, updateQuery } = - useFilters({ limit: 15, cacheBlacklist: ["patient"] }); + useFilters({ limit: 15, cacheBlacklist: ["name", "phone_number"] }); + const [organization, setOrganization] = useState(null); + const searchOptions = [ + { + key: "name", + type: "text" as const, + placeholder: "Search by name", + value: qParams.name || "", + }, + { + key: "phone_number", + type: "phone" as const, + placeholder: "Search by phone number", + value: qParams.phone_number || "", + }, + ]; + + const handleSearch = useCallback((key: string, value: string) => { + const searchParams = { + name: key === "name" ? value : "", + phone_number: + key === "phone_number" + ? value.length >= 13 || value === "" + ? value + : undefined + : undefined, + }; + updateQuery(searchParams); + }, []); + + const handleFieldChange = () => { + updateQuery({ + name: undefined, + phone_number: undefined, + }); + }; + const { data: patients, isLoading } = useQuery({ queryKey: ["organizationPatients", id, qParams], queryFn: query.debounced(organizationApi.listPatients, { @@ -64,40 +101,13 @@ export default function OrganizationPatients({ id, navOrganizationId }: Props) { { - const searchParams = { - name: key === "name" ? value : "", - phone_number: key === "phone_number" ? value : "", - page: 1, - }; - updateQuery(searchParams); - }} - clearSearch={{ value: !qParams.name && !qParams.phone_number }} - onFieldChange={(option) => { - const clearParams = { - name: option.key === "name" ? qParams.name || "" : "", - phone_number: - option.key === "phone_number" ? qParams.phone_number || "" : "", - page: 1, - }; - updateQuery(clearParams); - }} + options={searchOptions} + initialOptionIndex={Math.max( + searchOptions.findIndex((option) => option.value !== ""), + 0, + )} + onSearch={handleSearch} + onFieldChange={handleFieldChange} />
diff --git a/src/pages/Organization/components/OrganizationSelector.tsx b/src/pages/Organization/components/GovtOrganizationSelector.tsx similarity index 97% rename from src/pages/Organization/components/OrganizationSelector.tsx rename to src/pages/Organization/components/GovtOrganizationSelector.tsx index 84d6093cc1c..132bfd23e52 100644 --- a/src/pages/Organization/components/OrganizationSelector.tsx +++ b/src/pages/Organization/components/GovtOrganizationSelector.tsx @@ -12,7 +12,7 @@ import query from "@/Utils/request/query"; import { Organization } from "@/types/organization/organization"; import organizationApi from "@/types/organization/organizationApi"; -interface OrganizationSelectorProps { +interface GovtOrganizationSelectorProps { value?: string; onChange: (value: string) => void; required?: boolean; @@ -26,8 +26,9 @@ interface AutoCompleteOption { value: string; } -// TODO: Rename to GovtOrganizationSelector -export default function OrganizationSelector(props: OrganizationSelectorProps) { +export default function GovtOrganizationSelector( + props: GovtOrganizationSelectorProps, +) { const { onChange, required, selected } = props; const [selectedLevels, setSelectedLevels] = useState([]); const [searchQuery, setSearchQuery] = useState(""); diff --git a/src/pages/PublicAppointments/PatientRegistration.tsx b/src/pages/PublicAppointments/PatientRegistration.tsx index e12780b2133..4879a8647db 100644 --- a/src/pages/PublicAppointments/PatientRegistration.tsx +++ b/src/pages/PublicAppointments/PatientRegistration.tsx @@ -44,7 +44,7 @@ import { TokenSlot, } from "@/types/scheduling/schedule"; -import OrganizationSelector from "../Organization/components/OrganizationSelector"; +import GovtOrganizationSelector from "../Organization/components/GovtOrganizationSelector"; const initialForm: AppointmentPatientRegister & { ageInputType: "age" | "date_of_birth"; @@ -406,7 +406,7 @@ export function PatientRegistration(props: PatientRegistrationProps) { render={({ field }) => ( - {