From 397a2cedc792d41c2be361d083818f81acb92f93 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 22 Dec 2022 19:12:11 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A0=20Upgrade=20the=20colors=20of=20ta?= =?UTF-8?q?ilwind=20input=20components=20+=20=F0=9F=9B=A0=20Tailwind=20Con?= =?UTF-8?q?sultation=20Form=20+=20=F0=9F=9B=A0=20Tailiwnd=20discharge=20pa?= =?UTF-8?q?tient=20dialog=20(#4309)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * momentarily copied secondary until #4307 merged * upgrade textarea colors as per figma * consultation: replace mui label and textarea w. tw * tailwind discharge modal * add hints to discharge modal * upgrade `SelectMenuV2` * upgrade `TextFormField` * upgrade `MultiSelectMenuV2` * upgrade AutoCompleteAsync colors * use CareIcons * upgrade `DateInputV2` * upgrade PhoneInput style to perfection * place clear inside the phone input * fix padding * fix `SelectMenuV2` conflict * add missing verified by label * Add form field comp: `PatientCategorySelect` * use PatientCategorySelect in consultation form * cleanup patient category tw-class mess * tw-comp: `SelectMenuFormField` * Upgrade consultation form * tw-comp: `AutocompleteMultiselect` * tw-comp: `AutocompleteV2` (single select, but not form field yet) * add hook: `useAsyncOptions` for use in async dropdowns * tw-comp: `DiagnosisAutocompleteFormField` * MultiSelect, AutocompleteMulti: option chip + remove * consultation form: upgrade diag. and prov. diag. * replicate figma page layout for consultation * fix form submission * empty * rename `SelectMenuFormField` -> `SelectFormField` * tw-comp: `MultiSelectFormField` * Deprecate old `DateRangePicker` * tw-comp: `SymptomsSelect` Form Field * show helpful descriptions in `SymptomsSelect` * consultation form use new `SymptomsSelect` * so far it never felt the date input was getting disabled? * let the dropdown be near the calendar for less mouse movement * consultation form, use `DateFormFields` * hide disabled fields * introduce style: `cui-input-base` * migrate select and multiselect to new colors * migrate text form field and text area * remove conflicting classes * BRING BACK ORIGINAL GRAY PALLETE * update Form Field label color * ensure cui-input-base is properly applied * migrate autocomplete * hack: make online user select to look consistent with the form * migrate autocomplete async design * improve consistency on dropdown options * adds class: `cui-dropdown-base` * migrate date picker * check-circle * `cui-input-base` for prescription builder * `cui-input-base` for investigation builder * fix memory loss of diagnosis select * autocomplete show loading or nothing found * document: `useAsyncOptions` * fix issues w. state handling of DiagnosisSelect --- src/CAREUI/interactive/LegendInput.tsx | 2 +- src/Common/constants.tsx | 52 +- src/Common/hooks/useAsyncOptions.ts | 109 ++ src/Components/Common/DateInputV2.tsx | 53 +- src/Components/Common/DateRangePicker.tsx | 3 + .../Common/DiagnosisSelectFormField.tsx | 45 + src/Components/Common/Dialog.tsx | 4 +- src/Components/Common/HelperInputFields.tsx | 21 +- src/Components/Common/OnlineUsersSelect.tsx | 47 +- src/Components/Common/SymptomsSelect.tsx | 86 ++ src/Components/Common/components/ButtonV2.tsx | 2 +- .../Common/components/TextInputFieldV2.tsx | 4 +- .../InvestigationBuilder.tsx | 2 +- .../PRNPrescriptionBuilder.tsx | 8 +- .../PrescriptionBuilder.tsx | 6 +- .../PrescriptionDropdown.tsx | 5 +- .../PrescriptionMultiselect.tsx | 2 +- .../Facility/ConsultationDetails.tsx | 140 ++- src/Components/Facility/ConsultationForm.tsx | 984 ++++++------------ .../Facility/Consultations/Beds.tsx | 2 +- .../Consultations/DailyRoundsList.tsx | 16 +- src/Components/Facility/models.tsx | 9 +- src/Components/Form/AutoCompleteAsync.tsx | 56 +- src/Components/Form/AutocompleteV2.tsx | 115 ++ .../FormFields/AutocompleteMultiselect.tsx | 175 ++++ .../Form/FormFields/DateFormField.tsx | 9 +- .../Form/FormFields/DateRangeFormField.tsx | 7 +- src/Components/Form/FormFields/FormField.tsx | 18 +- .../Form/FormFields/SelectFormField.tsx | 78 ++ .../Form/FormFields/TextAreaFormField.tsx | 5 +- .../Form/FormFields/TextFormField.tsx | 15 +- src/Components/Form/MultiSelectMenuV2.tsx | 111 +- src/Components/Form/SearchInput.tsx | 7 +- src/Components/Form/SelectMenuV2.tsx | 62 +- src/Components/Patient/ManagePatients.tsx | 15 +- .../Patient/PatientCategorySelect.tsx | 27 + src/Components/Patient/PatientInfoCard.tsx | 22 +- src/style/CAREUI.css | 8 + src/style/index.css | 10 +- tailwind.config.js | 30 +- 40 files changed, 1348 insertions(+), 1024 deletions(-) create mode 100644 src/Common/hooks/useAsyncOptions.ts create mode 100644 src/Components/Common/DiagnosisSelectFormField.tsx create mode 100644 src/Components/Common/SymptomsSelect.tsx create mode 100644 src/Components/Form/AutocompleteV2.tsx create mode 100644 src/Components/Form/FormFields/AutocompleteMultiselect.tsx create mode 100644 src/Components/Form/FormFields/SelectFormField.tsx create mode 100644 src/Components/Patient/PatientCategorySelect.tsx diff --git a/src/CAREUI/interactive/LegendInput.tsx b/src/CAREUI/interactive/LegendInput.tsx index f6e84ebd00..94daf12ffb 100644 --- a/src/CAREUI/interactive/LegendInput.tsx +++ b/src/CAREUI/interactive/LegendInput.tsx @@ -127,7 +127,7 @@ export default function LegendInput(props: InputProps) { required={props.required} autoComplete={props.autoComplete} className={classNames( - "w-full bg-gray-50 focus:bg-gray-100 cui-input", + "w-full border-gray-300 rounded-md shadow-sm bg-gray-50 focus:bg-gray-100 cui-input", props.size === "small" && "text-xs px-3 py-2", (!props.size || !["small", "large"].includes(props.size)) && "px-4 py-3", diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index d1549fa46f..c4d073b9b8 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -210,20 +210,20 @@ export const MEDICAL_HISTORY_CHOICES: Array = [ ]; export const REVIEW_AT_CHOICES: Array = [ - { id: 30, text: "30 minutes" }, - { id: 60, text: "1 hour" }, - { id: 120, text: "2 hours" }, - { id: 180, text: "3 hours" }, - { id: 240, text: "4 hours" }, - { id: 360, text: "6 hours" }, - { id: 480, text: "8 hours" }, - { id: 720, text: "12 hours" }, - { id: 1440, text: "24 hours" }, - { id: 2160, text: "36 hours" }, - { id: 2880, text: "48 hours" }, -]; - -export const SYMPTOM_CHOICES: Array = [ + { id: 30, text: "30 mins" }, + { id: 60, text: "1 hr" }, + { id: 120, text: "2 hr" }, + { id: 180, text: "3 hr" }, + { id: 240, text: "4 hr" }, + { id: 360, text: "6 hr" }, + { id: 480, text: "8 hr" }, + { id: 720, text: "12 hr" }, + { id: 1440, text: "24 hr" }, + { id: 2160, text: "36 hr" }, + { id: 2880, text: "48 hr" }, +]; + +export const SYMPTOM_CHOICES = [ { id: 1, text: "ASYMPTOMATIC" }, { id: 2, text: "FEVER" }, { id: 3, text: "SORE THROAT" }, @@ -287,20 +287,18 @@ export const ADMITTED_TO = [ { id: "7", text: "Regular" }, ]; -export const PATIENT_CATEGORIES = [ - { id: "Comfort", text: "Comfort Care" }, - { id: "Stable", text: "Stable" }, - { id: "Moderate", text: "Slightly Abnormal" }, - { id: "Critical", text: "Critical" }, -]; +export type PatientCategoryID = "Comfort" | "Stable" | "Moderate" | "Critical"; -export const PatientCategoryTailwindClass: Record = { - "Comfort Care": "patient-comfort", - Stable: "patient-stable", - "Slightly Abnormal": "patient-abnormal", - Critical: "patient-critical", - unknown: "patient-unknown", -}; +export const PATIENT_CATEGORIES: { + id: PatientCategoryID; + text: PatientCategory; + twClass: string; +}[] = [ + { id: "Comfort", text: "Comfort Care", twClass: "patient-comfort" }, + { id: "Stable", text: "Stable", twClass: "patient-stable" }, + { id: "Moderate", text: "Slightly Abnormal", twClass: "patient-abnormal" }, + { id: "Critical", text: "Critical", twClass: "patient-critical" }, +]; export const PATIENT_FILTER_CATEGORIES = PATIENT_CATEGORIES; diff --git a/src/Common/hooks/useAsyncOptions.ts b/src/Common/hooks/useAsyncOptions.ts new file mode 100644 index 0000000000..10e9815cb9 --- /dev/null +++ b/src/Common/hooks/useAsyncOptions.ts @@ -0,0 +1,109 @@ +import { debounce } from "lodash"; +import { useMemo, useState } from "react"; +import { useDispatch } from "react-redux"; + +interface IUseAsyncOptionsArgs { + debounceInterval?: number; +} + +/** + * Hook to implement async autocompletes with ease and typesafety. + * + * See `DiagnosisSelectFormField` for usage. + * + * **Example usage:** + * ```jsx + * const { fetchOptions, isLoading, options } = useAsyncOptions("id"); + * + * return ( + * fetchOptions(action({ query }))} + * optionValue={(option) => option} + * ... + * /> + * ); + * ``` + */ +export function useAsyncOptions>( + uniqueKey: keyof T, + args?: IUseAsyncOptionsArgs +) { + const dispatch = useDispatch(); + const [queryOptions, setQueryOptions] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const fetchOptions = useMemo( + () => + debounce(async (action: any) => { + setIsLoading(true); + const res = await dispatch(action); + if (res?.data) setQueryOptions(res.data as T[]); + setIsLoading(false); + }, args?.debounceInterval ?? 300), + [dispatch, args?.debounceInterval] + ); + + const mergeValueWithQueryOptions = (selected?: T[]) => { + if (!selected?.length) return queryOptions; + + return [ + ...queryOptions, + ...selected.filter( + (obj) => !queryOptions.some((s) => s[uniqueKey] === obj[uniqueKey]) + ), + ]; + }; + + return { + /** + * Merges query options and selected options. + * + * **Example usage:** + * ```jsx + * const { isLoading } = useAsyncOptions("id"); + * + * + * ``` + */ + fetchOptions, + + /** + * Merges query options and selected options. + * + * **Example usage:** + * ```jsx + * const { options } = useAsyncOptions("id"); + * + * fetchOptions(action({ query }))} + * ... + * /> + * ``` + */ + isLoading, + + /** + * Merges query options and selected options. + * + * **Example usage:** + * ```jsx + * const { options } = useAsyncOptions("id"); + * + * + * ``` + */ + options: mergeValueWithQueryOptions, + }; +} diff --git a/src/Components/Common/DateInputV2.tsx b/src/Components/Common/DateInputV2.tsx index 751bff9bd8..a2c1e1ec09 100644 --- a/src/Components/Common/DateInputV2.tsx +++ b/src/Components/Common/DateInputV2.tsx @@ -12,6 +12,7 @@ import { import { DropdownTransition } from "./components/HelperComponents"; import { Popover } from "@headlessui/react"; import { classNames } from "../../Utils/utils"; +import CareIcon from "../../CAREUI/icons/CareIcon"; type DatePickerType = "date" | "month" | "year"; export type DatePickerPosition = "LEFT" | "RIGHT" | "CENTER"; @@ -126,10 +127,8 @@ const DateInputV2: React.FC = ({ year = datePickerHeaderDate.getFullYear() ) => { const date = new Date(year, month, day); - if (min) if (date < min) return false; if (max) if (date > max) return false; - return true; }; @@ -183,26 +182,27 @@ const DateInputV2: React.FC = ({ }; return ( -
+
- + -
- +
+
@@ -210,24 +210,24 @@ const DateInputV2: React.FC = ({
{type === "date" && (
{format(datePickerHeaderDate, "MMMM")}
)}

{type == "year" @@ -243,18 +243,18 @@ const DateInputV2: React.FC = ({ new Date().getFullYear() === year.getFullYear()) || !isDateWithinConstraints(getLastDay()) } - className="transition ease-in-out duration-100 h-full p-2 rounded inline-flex items-center justify-center aspect-square cursor-pointer hover:bg-slate-200" + className="transition ease-in-out duration-100 p-2 rounded inline-flex items-center justify-center aspect-square cursor-pointer hover:bg-gray-300" onClick={increment} > - +

{type === "date" && ( <> -
+
{DAYS.map((day) => (
-
+
{day}
@@ -272,11 +272,11 @@ const DateInputV2: React.FC = ({
{d} @@ -294,10 +294,10 @@ const DateInputV2: React.FC = ({
@@ -323,10 +323,10 @@ const DateInputV2: React.FC = ({
@@ -346,7 +346,6 @@ const DateInputV2: React.FC = ({ DateInputV2.defaultProps = { position: "CENTER", - className: "bg-gray-200 border-gray-200", }; export default DateInputV2; diff --git a/src/Components/Common/DateRangePicker.tsx b/src/Components/Common/DateRangePicker.tsx index 4018c66089..377574b0f4 100644 --- a/src/Components/Common/DateRangePicker.tsx +++ b/src/Components/Common/DateRangePicker.tsx @@ -22,6 +22,9 @@ interface IDateRangePickerProps { export const getDate = (value: any) => value && moment(value).isValid() ? moment(value) : null; +/** + * Deprecated. Use `DateRangeFormField` or `DateFormField` instead. + */ export const DateRangePicker: React.FC = ({ label, endDateId = "end_date", diff --git a/src/Components/Common/DiagnosisSelectFormField.tsx b/src/Components/Common/DiagnosisSelectFormField.tsx new file mode 100644 index 0000000000..ae3162ea21 --- /dev/null +++ b/src/Components/Common/DiagnosisSelectFormField.tsx @@ -0,0 +1,45 @@ +import { useAsyncOptions } from "../../Common/hooks/useAsyncOptions"; +import { listICD11Diagnosis } from "../../Redux/actions"; +import { ICD11DiagnosisModel } from "../Facility/models"; +import { AutocompleteMutliSelect } from "../Form/FormFields/AutocompleteMultiselect"; +import FormField from "../Form/FormFields/FormField"; +import { + FormFieldBaseProps, + resolveFormFieldChangeEventHandler, +} from "../Form/FormFields/Utils"; + +type Props = + // | ({ multiple?: false | undefined } & FormFieldBaseProps) // uncomment when single select form field is required and implemented. + { multiple: true } & FormFieldBaseProps; + +export function DiagnosisSelectFormField(props: Props) { + const { name } = props; + const handleChange = resolveFormFieldChangeEventHandler(props); + + const { fetchOptions, isLoading, options } = + useAsyncOptions("id"); + + if (!props.multiple) { + return ( +
+ Component not implemented +
+ ); + } + + return ( + + option.label} + optionValue={(option) => option} + onQuery={(query) => fetchOptions(listICD11Diagnosis({ query }, ""))} + isLoading={isLoading} + onChange={(value) => handleChange({ name, value })} + /> + + ); +} diff --git a/src/Components/Common/Dialog.tsx b/src/Components/Common/Dialog.tsx index 1d31947fdc..0bfc3d509b 100644 --- a/src/Components/Common/Dialog.tsx +++ b/src/Components/Common/Dialog.tsx @@ -40,10 +40,10 @@ const DialogModal = (props: DialogProps) => { > - {title} +

{title}

{description}

diff --git a/src/Components/Common/HelperInputFields.tsx b/src/Components/Common/HelperInputFields.tsx index 35acd86526..dfe2fffee4 100644 --- a/src/Components/Common/HelperInputFields.tsx +++ b/src/Components/Common/HelperInputFields.tsx @@ -32,6 +32,8 @@ import { debounce } from "lodash"; import React, { ChangeEvent } from "react"; import PhoneInput, { ICountryData } from "react-phone-input-2"; import "react-phone-input-2/lib/high-res.css"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import ButtonV2 from "./components/ButtonV2"; export interface DefaultSelectInputProps extends Omit { options: Array; @@ -649,7 +651,6 @@ export const PhoneNumberField = (props: any) => { value, turnOffAutoFormat, disabled, - bgColor, enableTollFree, countryCodeEditable = false, } = props; @@ -666,11 +667,9 @@ export const PhoneNumberField = (props: any) => { return ( <> {label && {label}} -
+
{ }} {...countryRestriction} /> -
onChange("+91")} + onChange("+91")} > - -
+ +
diff --git a/src/Components/Common/OnlineUsersSelect.tsx b/src/Components/Common/OnlineUsersSelect.tsx index 6c17a5d9c2..c2d6901714 100644 --- a/src/Components/Common/OnlineUsersSelect.tsx +++ b/src/Components/Common/OnlineUsersSelect.tsx @@ -6,6 +6,9 @@ import moment from "moment"; import { getUserList } from "../../Redux/actions"; import { UserModel } from "../Users/models"; import { classNames } from "../../Utils/utils"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import ButtonV2 from "./components/ButtonV2"; +import { FieldLabel } from "../Form/FormFields/FormField"; type UserFetchState = { loading: boolean; @@ -29,6 +32,14 @@ const initialState: UserFetchState = { searchFieldRef: React.createRef(), }; +/** + * This component consists temperory design hacks made to look identical to a + * form field during the consultation form redesign. + * + * However to make the design and functionallity consistent, this component is + * to be converted to use `AutocompleteFormField` along with `useAsyncOptions` + * hook and `prefixIcon` for the online state of the users. + */ export const OnlineUsersSelect = (props: Props) => { const dispatchAction: any = useDispatch(); const { selectedUser, userId, onSelect, user_type, outline } = props; @@ -73,12 +84,7 @@ export const OnlineUsersSelect = (props: Props) => { return (
- + Assigned to
@@ -91,10 +97,11 @@ export const OnlineUsersSelect = (props: Props) => { aria-expanded="true" aria-labelledby="listbox-label" className={classNames( - "border-2 h-14 cursor-default relative w-full rounded-md pl-3 pr-10 py-2 text-left transition ease-in-out duration-150 sm:text-sm sm:leading-5", - isDropdownExpanded && + "border border-gray-400 cursor-default relative w-full rounded-md pl-3 pr-10 text-left transition ease-in-out duration-150 sm:text-sm sm:leading-5", + (isDropdownExpanded && outline && - "ring-primary-500 border-primary-500" + "ring-primary-500 border-primary-500 py-0.5") || + "py-3" )} > { type="text" placeholder="Search by name or username" className={classNames( - "py-2 pl-3 w-full outline-none focus:ring-gray-200 border-none", + "pl-3 w-full outline-none border-0", !isDropdownExpanded && "hidden" )} value={searchTerm} @@ -235,21 +242,21 @@ export const OnlineUsersSelect = (props: Props) => {
)}
-
{ onSelect(null); setDropdownExpand(false); }} > -
- -
-
+ +
diff --git a/src/Components/Common/SymptomsSelect.tsx b/src/Components/Common/SymptomsSelect.tsx new file mode 100644 index 0000000000..5e15f0417a --- /dev/null +++ b/src/Components/Common/SymptomsSelect.tsx @@ -0,0 +1,86 @@ +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { SYMPTOM_CHOICES } from "../../Common/constants"; +import FormField from "../Form/FormFields/FormField"; +import { + FormFieldBaseProps, + resolveFormFieldChangeEventHandler, +} from "../Form/FormFields/Utils"; +import MultiSelectMenuV2 from "../Form/MultiSelectMenuV2"; + +const ASYMPTOMATIC_ID = 1; + +/** + * A `FormField` component to select symptoms. + * + * - If "Asymptomatic" is selected, every other selections are unselected. + * - If any non "Asymptomatic" value is selected, ensures "Asymptomatic" is + * unselected. + * - For other scenarios, this simply works like a `MultiSelect`. + */ +export const SymptomsSelect = (props: FormFieldBaseProps) => { + const { name } = props; + const handleChange = resolveFormFieldChangeEventHandler(props); + + const updateSelection = (value: number[]) => { + // Skip the complexities if no initial value was present + if (!props.value?.length) return handleChange({ name, value }); + + const initialValue = props.value || []; + + if (initialValue.includes(ASYMPTOMATIC_ID) && value.length > 1) { + // If asym. already selected, and new selections have more than one value + const asymptomaticIndex = value.indexOf(1); + if (asymptomaticIndex > -1) { + // unselect asym. + value.splice(asymptomaticIndex, 1); + return handleChange({ name, value }); + } + } + + if (!initialValue.includes(ASYMPTOMATIC_ID) && value.includes(1)) { + // If new selections have asym., unselect everything else + return handleChange({ name, value: [ASYMPTOMATIC_ID] }); + } + + handleChange({ name, value }); + }; + + const getDescription = ({ id }: { id: number }) => { + const value = props.value || []; + if (!value.length) return; + + if (value.includes(ASYMPTOMATIC_ID) && id !== ASYMPTOMATIC_ID) + return ( +
+ + + also unselects Asymptomatic + +
+ ); + + if (!value.includes(ASYMPTOMATIC_ID) && id === ASYMPTOMATIC_ID) + return ( + + + {`also unselects the other ${value.length} option(s)`} + + ); + }; + + return ( + + option.text} + optionValue={(option) => option.id} + optionDescription={getDescription} + value={props.value} + onChange={updateSelection} + /> + + ); +}; diff --git a/src/Components/Common/components/ButtonV2.tsx b/src/Components/Common/components/ButtonV2.tsx index 8baf0f9032..dde1d7038d 100644 --- a/src/Components/Common/components/ButtonV2.tsx +++ b/src/Components/Common/components/ButtonV2.tsx @@ -81,7 +81,7 @@ const ButtonV2 = ({ {...props} disabled={disabled || !isAuthorized || loading} className={classNames( - "Button outline-offset-1", + "font-medium h-min flex items-center justify-center gap-2 transition-all duration-200 ease-in-out cursor-pointer disabled:cursor-not-allowed disabled:bg-gray-200 disabled:text-gray-500 outline-offset-1", `button-size-${size}`, `button-shape-${circle ? "circle" : "square"}`, ghost ? `button-${variant}-ghost` : `button-${variant}-default`, diff --git a/src/Components/Common/components/TextInputFieldV2.tsx b/src/Components/Common/components/TextInputFieldV2.tsx index 169b5c3e18..aa39e35572 100644 --- a/src/Components/Common/components/TextInputFieldV2.tsx +++ b/src/Components/Common/components/TextInputFieldV2.tsx @@ -41,9 +41,7 @@ const TextInputFieldV2 = (props: Props) => { )} { diff --git a/src/Components/Common/prescription-builder/PRNPrescriptionBuilder.tsx b/src/Components/Common/prescription-builder/PRNPrescriptionBuilder.tsx index 426f95823e..86ed348da3 100644 --- a/src/Components/Common/prescription-builder/PRNPrescriptionBuilder.tsx +++ b/src/Components/Common/prescription-builder/PRNPrescriptionBuilder.tsx @@ -139,10 +139,10 @@ export default function PRNPrescriptionBuilder(
Dosage -
+
{ @@ -206,7 +206,7 @@ export default function PRNPrescriptionBuilder(
{ diff --git a/src/Components/Common/prescription-builder/PrescriptionDropdown.tsx b/src/Components/Common/prescription-builder/PrescriptionDropdown.tsx index 7eedc0d5b0..081d9a6dc7 100644 --- a/src/Components/Common/prescription-builder/PrescriptionDropdown.tsx +++ b/src/Components/Common/prescription-builder/PrescriptionDropdown.tsx @@ -37,10 +37,7 @@ export function PrescriptionDropdown(props: { setOpen(!open)} value={value} onChange={(e) => setValue(e.target.value)} diff --git a/src/Components/Common/prescription-builder/PrescriptionMultiselect.tsx b/src/Components/Common/prescription-builder/PrescriptionMultiselect.tsx index 394b257a58..8a387e38bd 100644 --- a/src/Components/Common/prescription-builder/PrescriptionMultiselect.tsx +++ b/src/Components/Common/prescription-builder/PrescriptionMultiselect.tsx @@ -61,7 +61,7 @@ export function PrescriptionMultiDropdown(props: { setOpen(!open)} value={value} onChange={(e) => setValue(e.target.value)} diff --git a/src/Components/Facility/ConsultationDetails.tsx b/src/Components/Facility/ConsultationDetails.tsx index 0cd247b824..5195198498 100644 --- a/src/Components/Facility/ConsultationDetails.tsx +++ b/src/Components/Facility/ConsultationDetails.tsx @@ -37,12 +37,7 @@ import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogContentText from "@material-ui/core/DialogContentText"; import DialogTitle from "@material-ui/core/DialogTitle"; -import InputLabel from "@material-ui/core/InputLabel"; -import { - TextInputField, - SelectField, - MultilineInputField, -} from "../Common/HelperInputFields"; +import { TextInputField } from "../Common/HelperInputFields"; import { discharge, dischargePatient } from "../../Redux/actions"; import ReadMore from "../Common/components/Readmore"; import ViewInvestigationSuggestions from "./Investigations/InvestigationSuggestions"; @@ -50,6 +45,11 @@ import { formatDate } from "../../Utils/utils"; import ResponsiveMedicineTable from "../Common/components/ResponsiveMedicineTables"; import PatientInfoCard from "../Patient/PatientInfoCard"; import PatientVitalsCard from "../Patient/PatientVitalsCard"; +import { FieldErrorText, FieldLabel } from "../Form/FormFields/FormField"; +import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import DialogModal from "../Common/Dialog"; +import SelectMenuV2 from "../Form/SelectMenuV2"; import ButtonV2 from "../Common/components/ButtonV2"; interface PreDischargeFormInterface { discharge_reason: string; @@ -384,84 +384,76 @@ export const ConsultationDetails = (props: any) => { - +

Discharge patient from CARE

+ + +

Caution: this action is irreversible.

+
+
+ } + show={openDischargeDialog} onClose={handleDischargeClose} > - - -  Discharge Patient From Care - - -
-
- - Discharge Reason* - - - setPreDischargeForm((prev) => ({ - ...prev, - discharge_reason: e.target.value, - })) - } - errors={errors?.discharge_reason} - /> -
- -
- - {preDischargeForm.discharge_reason == "EXP" - ? "Cause of death *" - : "Discharge notes"} - - - setPreDischargeForm((prev) => ({ - ...prev, - discharge_notes: e.target.value, - })) - } - errors={errors?.discharge_notes} - /> -
+
+
+ Reason + id} + optionLabel={({ text }) => text} + onChange={(value) => + setPreDischargeForm((prev) => ({ + ...prev, + discharge_reason: value, + })) + } + /> +
- - -
+ +
+ Cancel - + {isSendingDischargeApi ? ( ) : ( - + Confirm Discharge + )} - - +
+
diff --git a/src/Components/Facility/ConsultationForm.tsx b/src/Components/Facility/ConsultationForm.tsx index b437a2dc0b..259d17d15c 100644 --- a/src/Components/Facility/ConsultationForm.tsx +++ b/src/Components/Facility/ConsultationForm.tsx @@ -1,15 +1,8 @@ import loadable from "@loadable/component"; -import { - Box, - CardContent, - FormControlLabel, - InputLabel, - Radio, - RadioGroup, -} from "@material-ui/core"; +import { Box, FormControlLabel, Radio, RadioGroup } from "@material-ui/core"; import { navigate } from "raviger"; import moment from "moment"; -import React, { +import { ChangeEventHandler, useCallback, useEffect, @@ -21,7 +14,6 @@ import { useDispatch } from "react-redux"; import { CONSULTATION_SUGGESTION, PATIENT_CATEGORIES, - SYMPTOM_CHOICES, TELEMEDICINE_ACTIONS, REVIEW_AT_CHOICES, KASP_STRING, @@ -36,18 +28,10 @@ import { } from "../../Redux/actions"; import * as Notification from "../../Utils/Notifications.js"; import { FacilitySelect } from "../Common/FacilitySelect"; -import { - DateInputField, - ErrorHelperText, - MultilineInputField, - NativeSelectField, - SelectField, - TextInputField, -} from "../Common/HelperInputFields"; +import { ErrorHelperText } from "../Common/HelperInputFields"; import { BedModel, FacilityModel } from "./models"; import { OnlineUsersSelect } from "../Common/OnlineUsersSelect"; import { UserModel } from "../Users/models"; -import { MaterialUiPickersDate } from "@material-ui/pickers/typings/date"; import { BedSelect } from "../Common/BedSelect"; import Beds from "./Consultations/Beds"; import PrescriptionBuilder, { @@ -56,7 +40,6 @@ import PrescriptionBuilder, { import PRNPrescriptionBuilder, { PRNPrescriptionType, } from "../Common/prescription-builder/PRNPrescriptionBuilder"; -import { DiagnosisSelect } from "../Common/DiagnosisSelect"; import { goBack } from "../../Utils/utils"; import InvestigationBuilder, { InvestigationType, @@ -67,7 +50,15 @@ import ProcedureBuilder, { import { ICD11DiagnosisModel } from "./models"; import ButtonV2 from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; -import MultiSelectMenuV2 from "../Form/MultiSelectMenuV2"; +import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; +import { FieldChangeEventHandler } from "../Form/FormFields/Utils"; +import { FieldLabel } from "../Form/FormFields/FormField"; +import PatientCategorySelect from "../Patient/PatientCategorySelect"; +import { SelectFormField } from "../Form/FormFields/SelectFormField"; +import TextFormField from "../Form/FormFields/TextFormField"; +import { DiagnosisSelectFormField } from "../Common/DiagnosisSelectFormField"; +import { SymptomsSelect } from "../Common/SymptomsSelect"; +import DateFormField from "../Form/FormFields/DateFormField"; const Loading = loadable(() => import("../Common/Loading")); const PageTitle = loadable(() => import("../Common/PageTitle")); @@ -75,23 +66,19 @@ const PageTitle = loadable(() => import("../Common/PageTitle")); type BooleanStrings = "true" | "false"; type FormDetails = { - hasSymptom: boolean; - otherSymptom: boolean; symptoms: number[]; other_symptoms: string; - symptoms_onset_date: any; + symptoms_onset_date?: Date; suggestion: string; patient: string; facility: string; admitted: BooleanStrings; admitted_to: string; category: string; - admission_date: string; + admission_date?: Date; discharge_date: null; referred_to: string; - icd11_diagnoses: string[]; icd11_diagnoses_object: ICD11DiagnosisModel[]; - icd11_provisional_diagnoses: string[]; icd11_provisional_diagnoses_object: ICD11DiagnosisModel[]; verified_by: string; is_kasp: BooleanStrings; @@ -120,23 +107,19 @@ type Action = | { type: "set_error"; errors: FormDetails }; const initForm: FormDetails = { - hasSymptom: false, - otherSymptom: false, symptoms: [], other_symptoms: "", - symptoms_onset_date: null, + symptoms_onset_date: undefined, suggestion: "A", patient: "", facility: "", admitted: "false", admitted_to: "", category: "Comfort", - admission_date: new Date().toISOString(), + admission_date: new Date(), discharge_date: null, referred_to: "", - icd11_diagnoses: [], icd11_diagnoses_object: [], - icd11_provisional_diagnoses: [], icd11_provisional_diagnoses_object: [], verified_by: "", is_kasp: "false", @@ -165,6 +148,9 @@ const initError = Object.assign( ...Object.keys(initForm).map((k) => ({ [k]: "" })) ); +const isoStringToDate = (isoDate: string) => + (moment(isoDate).isValid() && moment(isoDate).toDate()) || undefined; + const initialState = { form: { ...initForm }, errors: { ...initError }, @@ -187,18 +173,8 @@ const consultationFormReducer = (state = initialState, action: Action) => { } }; -const suggestionTypes = [ - { - id: 0, - text: "Select the decision", - }, - ...CONSULTATION_SUGGESTION, -]; - -const symptomChoices = [...SYMPTOM_CHOICES]; - const scrollTo = (id: any) => { - const element = document.querySelector(`#${id}-div`); + const element = document.querySelector(`#${id}`); element?.scrollIntoView({ behavior: "smooth", block: "center" }); }; @@ -222,9 +198,7 @@ export const ConsultationForm = (props: any) => { const [patientName, setPatientName] = useState(""); const [facilityName, setFacilityName] = useState(""); - const headerText = !id ? "Consultation" : "Edit Consultation"; - const buttonText = !id ? "Add Consultation" : "Update Consultation"; - + const isUpdate = !!id; const topRef = useRef(null); useEffect(() => { @@ -249,6 +223,10 @@ export const ConsultationForm = (props: any) => { fetchPatientName(); }, [dispatchAction, patientId]); + const hasSymptoms = + !!state.form.symptoms.length && !state.form.symptoms.includes(1); + const isOtherSymptomsSelected = state.form.symptoms.includes(9); + const fetchData = useCallback( async (status: statusType) => { setIsLoading(true); @@ -270,14 +248,8 @@ export const ConsultationForm = (props: any) => { if (res && res.data) { const formData = { ...res.data, - hasSymptom: - !!res.data.symptoms && - !!res.data.symptoms.length && - !!res.data.symptoms.filter((i: number) => i !== 1).length, - otherSymptom: - !!res.data.symptoms && - !!res.data.symptoms.length && - !!res.data.symptoms.includes(9), + symptoms_onset_date: isoStringToDate(res.data.symptoms_onset_date), + admission_date: isoStringToDate(res.data.admission_date), admitted: res.data.admitted ? String(res.data.admitted) : "false", admitted_to: res.data.admitted_to ? res.data.admitted_to : "", category: res.data.category @@ -316,6 +288,8 @@ export const ConsultationForm = (props: any) => { [dispatch, fetchData] ); + if (isLoading) return ; + const validateForm = () => { const errors = { ...initError }; let invalidForm = false; @@ -331,12 +305,7 @@ export const ConsultationForm = (props: any) => { } return; case "category": - if ( - !state.form[field] || - !PATIENT_CATEGORIES.map((category) => category.id).includes( - state.form[field] - ) - ) { + if (!state.form[field]) { errors[field] = "Please select a category"; if (!error_div) error_div = field; invalidForm = true; @@ -361,14 +330,14 @@ export const ConsultationForm = (props: any) => { } return; case "other_symptoms": - if (state.form.otherSymptom && !state.form[field]) { + if (isOtherSymptomsSelected && !state.form[field]) { errors[field] = "Please enter the other symptom details"; if (!error_div) error_div = field; invalidForm = true; } return; case "symptoms_onset_date": - if (state.form.hasSymptom && !state.form[field]) { + if (hasSymptoms && !state.form[field]) { errors[field] = "Please enter date of onset of the above symptoms"; if (!error_div) error_div = field; invalidForm = true; @@ -480,9 +449,7 @@ export const ConsultationForm = (props: any) => { } case "verified_by": - if ( - !state.form[field].replace(/\s/g, "").length - ) { + if (!state.form[field].replace(/\s/g, "").length) { errors[field] = "Please fill verified by"; if (!error_div) error_div = field; invalidForm = true; @@ -499,9 +466,7 @@ export const ConsultationForm = (props: any) => { const handleSubmit = async (e: any) => { e.preventDefault(); - console.log("handling"); const [validForm, error_div] = validateForm(); - console.log(validForm); if (!validForm) { scrollTo(error_div); @@ -509,17 +474,14 @@ export const ConsultationForm = (props: any) => { setIsLoading(true); const data = { symptoms: state.form.symptoms, - other_symptoms: state.form.otherSymptom + other_symptoms: isOtherSymptomsSelected ? state.form.other_symptoms : undefined, - symptoms_onset_date: state.form.hasSymptom + symptoms_onset_date: hasSymptoms ? state.form.symptoms_onset_date : undefined, suggestion: state.form.suggestion, admitted: state.form.suggestion === "A", - // admitted_to: JSON.parse(state.form.admitted) - // ? state.form.admitted_to - // : undefined, admission_date: state.form.suggestion === "A" ? state.form.admission_date : undefined, category: state.form.category, @@ -530,8 +492,9 @@ export const ConsultationForm = (props: any) => { prescribed_medication: state.form.prescribed_medication, discharge_date: state.form.discharge_date, ip_no: state.form.ip_no, - icd11_diagnoses: state.form.icd11_diagnoses, - icd11_provisional_diagnoses: state.form.icd11_provisional_diagnoses, + icd11_diagnoses: state.form.icd11_diagnoses_object.map((o) => o.id), + icd11_provisional_diagnoses: + state.form.icd11_provisional_diagnoses_object.map((o) => o.id), verified_by: state.form.verified_by, discharge_advice: dischargeAdvice, prn_prescription: PRNAdvice, @@ -577,15 +540,11 @@ export const ConsultationForm = (props: any) => { } }; - const handleChange: - | ChangeEventHandler - | ChangeEventHandler = (e: any) => { - e && - e.target && - dispatch({ - type: "set_form", - form: { ...state.form, [e.target.name]: e.target.value }, - }); + const handleFormFieldChange: FieldChangeEventHandler = (event) => { + dispatch({ + type: "set_form", + form: { ...state.form, [event.name]: event.value }, + }); }; const handleTelemedicineChange: ChangeEventHandler = ( @@ -603,39 +562,6 @@ export const ConsultationForm = (props: any) => { }); }; - const handleDecisionChange = (e: any) => { - e && - e.target && - dispatch({ - type: "set_form", - form: { - ...state.form, - [e.target.name]: e.target.value, - }, - }); - }; - - const handleSymptomChange = (value: number[]) => { - const form = { ...state.form }; - const otherSymptoms = value.filter((i) => i !== 1); - // prevent user from selecting asymptomatic along with other options - if (value.includes(1)) { - form.symptoms = otherSymptoms.length ? [1] : value; - form.hasSymptom = false; - form.otherSymptom = false; - } else { - form.symptoms = otherSymptoms; - form.hasSymptom = !!otherSymptoms.length; - form.otherSymptom = otherSymptoms.includes(9); - } - dispatch({ type: "set_form", form }); - }; - - const handleDateChange = (date: MaterialUiPickersDate, key: string) => { - moment(date).isValid() && - dispatch({ type: "set_form", form: { ...state.form, [key]: date } }); - }; - const handleDoctorSelect = (doctor: UserModel | null) => { if (doctor?.id) { dispatch({ @@ -668,557 +594,325 @@ export const ConsultationForm = (props: any) => { dispatch({ type: "set_form", form }); }; - if (isLoading) { - return ; - } + const field = (name: string) => { + return { + id: name, + name, + value: (state.form as any)[name], + error: state.errors[name], + onChange: handleFormFieldChange, + }; + }; + + const selectField = (name: string) => { + return { + ...field(name), + optionValue: (option: any) => option.id, + optionLabel: (option: any) => option.text, + optionDescription: (option: any) => option.desc, + }; + }; return ( -
+
-
-
-
handleSubmit(e)}> - -
-
- text} - optionValue={({ id }) => id} - onChange={(o) => handleSymptomChange(o)} - /> - -
- {state.form.otherSymptom && ( -
- - Other Symptom Details - - -
- )} - - {state.form.hasSymptom && ( -
- - handleDateChange(date, "symptoms_onset_date") - } - disableFuture={true} - errors={state.errors.symptoms_onset_date} - InputLabelProps={{ shrink: true }} - /> -
- )} -
- - History of present illness - - -
- -
- - Examination details and Clinical conditions - - -
- -
- - Treatment Plan / Treatment Summary - - -
-
- - Category - - -
- -
- - Decision after Consultation* - - - -
- - {state.form.suggestion === "R" && ( -
- Referred To Facility - -
- )} - - {/* {JSON.parse(state.form.admitted) && ( -
- -
- )} - */} - {state.form.suggestion === "A" && ( - <> -
-
- - handleDateChange(date, "admission_date") - } - errors={state.errors.admission_date} - /> -
-
-
- Bed - - {!!id && ( -

- Can't be edited while Consultation update. To change - bed use the form bellow -

- )} -
- - )} -
- -
- General Instructions (Advice)* - -
-
- Investigation Suggestions - -
- -
-
- Procedures - -
- -
-
- Prescription Medication - {/**/} - -
- -
-
- PRN Prescription - -
- -
-
- IP number* - -
-
- Verified By * - + + + {isOtherSymptomsSelected && ( + + )} + + {hasSymptoms && ( + + )} + + + + + + + + + + + + {state.form.suggestion === "R" && ( +
+ Referred To Facility + +
+ )} + + {state.form.suggestion === "A" && ( + <> + + + {!isUpdate && ( +
+ Bed + -
-
- - Provisional Diagnosis - - { - dispatch({ - type: "set_form", - form: { - ...state.form, - icd11_provisional_diagnoses: - selected?.map( - (diagnosis: ICD11DiagnosisModel) => diagnosis.id - ) || [], - }, - }); - }} + unoccupiedOnly={true} + facility={facilityId} />
+ )} + + )} + + + +
+ Investigation Suggestions + + +
-
- Diagnosis - { - dispatch({ - type: "set_form", - form: { - ...state.form, - icd11_diagnoses: - selected?.map( - (diagnosis: ICD11DiagnosisModel) => diagnosis.id - ) || [], - }, - }); - }} - /> -
+
+ Procedures + + +
- {KASP_ENABLED && ( -
- {KASP_STRING}* - - - } - label="Yes" - /> - } - label="No" - /> - - - -
- )} - {/* Telemedicine Fields */} -
-
- Telemedicine - - - } - label="Yes" - /> - } - label="No" - /> - - - -
+
+ Prescription Medication + + +
- {JSON.parse(state.form.is_telemedicine) && ( -
- - Review After{" "} - - -
- )} -
- {JSON.parse(state.form.is_telemedicine) && ( -
- -
- )} - {JSON.parse(state.form.is_telemedicine) && ( -
- - Action - - - -
- )} -
- - Special Instructions - - + PRN Prescription + + +
+ + + + + + + + + + {KASP_ENABLED && ( +
+ {KASP_STRING} + + + } + label="Yes" /> -
+ } + label="No" + /> + + + +
+ )} + + {/* Telemedicine Fields */} +
+ Telemedicine + + + } label="Yes" /> + } label="No" /> + + + +
-
-
- Weight (in Kg) - -
-
- Height (in cm) - -
-
-
- Body Surface area :{" "} - {Math.sqrt( - (Number(state.form.weight) * Number(state.form.height)) / 3600 - ).toFixed(2)}{" "} - m2 -
- {/* End of Telemedicine fields */} -
- - navigate(`/facility/${facilityId}/patient/${patientId}`) - } - > - Cancel - - handleSubmit(e)} - > - - {buttonText} - -
-
-
+ {JSON.parse(state.form.is_telemedicine) && ( +
+ + +
+ +
+ + option.desc} + optionValue={(option) => option.text} + /> +
+ )} + + + +
+
+ + +
+
+ Body Surface area :{" "} + {Math.sqrt( + (Number(state.form.weight) * Number(state.form.height)) / 3600 + ).toFixed(2)}{" "} + m2 +
-
- {!id ? null : ( -
-

Update Bed

- + {/* End of Telemedicine fields */} + +
+ + navigate(`/facility/${facilityId}/patient/${patientId}`) + } + > + Cancel + + + + {isUpdate ? "Update Consultation" : "Create Consultation"} + +
+ + + {isUpdate && ( +
+

Update Bed

+
- + /> +
)}
diff --git a/src/Components/Facility/Consultations/Beds.tsx b/src/Components/Facility/Consultations/Beds.tsx index 32c3efd885..8ec8b056e0 100644 --- a/src/Components/Facility/Consultations/Beds.tsx +++ b/src/Components/Facility/Consultations/Beds.tsx @@ -108,7 +108,7 @@ const Beds = (props: BedsProps) => { return (
-
+
{!discharged ? "Move to bed:" : "Bed History"}
{props.setState && ( diff --git a/src/Components/Facility/Consultations/DailyRoundsList.tsx b/src/Components/Facility/Consultations/DailyRoundsList.tsx index 84cb73e38d..2f9e9be78b 100644 --- a/src/Components/Facility/Consultations/DailyRoundsList.tsx +++ b/src/Components/Facility/Consultations/DailyRoundsList.tsx @@ -9,23 +9,19 @@ import Pagination from "../../Common/Pagination"; import { DailyRoundsModel } from "../../Patient/models"; import { formatDate } from "../../../Utils/utils"; import { PatientCategory } from "../models"; -import { PatientCategoryTailwindClass } from "../../../Common/constants"; +import { PATIENT_CATEGORIES } from "../../../Common/constants"; const PageTitle = loadable(() => import("../../Common/PageTitle")); -export type PatientCategoryBadgeProps = { - category?: PatientCategory; -}; - -export const PatientCategoryBadge = ({ - category, -}: PatientCategoryBadgeProps) => { - const categoryClass = PatientCategoryTailwindClass[category || "unknown"]; +export const PatientCategoryBadge = (props: { category?: PatientCategory }) => { + const categoryClass = props.category + ? PATIENT_CATEGORIES.find((c) => c.text === props.category)?.twClass + : "patient-unknown"; return ( - {category} + {props.category} ); }; diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 1aee3a156c..8acafb2a32 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -76,8 +76,7 @@ export type PatientCategory = | "Comfort Care" | "Stable" | "Slightly Abnormal" - | "Critical" - | "unknown"; + | "Critical"; export interface ConsultationModel { admission_date?: string; @@ -202,8 +201,10 @@ export interface CurrentBed { meta: Record; } -export interface ICD11DiagnosisModel { +// Voluntarily made as `type` for it to achieve type-safety when used with +// `useAsyncOptions` +export type ICD11DiagnosisModel = { id: string; label: string; parentId: string | null; -} +}; diff --git a/src/Components/Form/AutoCompleteAsync.tsx b/src/Components/Form/AutoCompleteAsync.tsx index 48ec255cfc..05b70aab09 100644 --- a/src/Components/Form/AutoCompleteAsync.tsx +++ b/src/Components/Form/AutoCompleteAsync.tsx @@ -1,8 +1,9 @@ import React, { useEffect, useState, useMemo } from "react"; import { Combobox } from "@headlessui/react"; -import Spinner from "../Common/Spinner"; import { debounce } from "lodash"; import { DropdownTransition } from "../Common/components/HelperComponents"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { dropdownOptionClassNames } from "./MultiSelectMenuV2"; interface Props { name?: string; @@ -65,10 +66,10 @@ const AutoCompleteAsync = (props: Props) => { multiple={multiple as any} >
-
+
{ onChange={({ target }) => setQuery(target.value)} /> -
- {loading && } - +
+ {loading ? ( + + ) : ( + + )}
- + {data?.length === 0 ? (
{query !== "" @@ -100,32 +104,16 @@ const AutoCompleteAsync = (props: Props) => { data?.map((item: any) => ( - `relative cursor-default select-none py-2 pl-10 pr-4 ${ - active ? "text-white bg-primary-500" : "text-gray-900" - }` - } + className={dropdownOptionClassNames} value={item} > - {({ selected, active }) => ( - <> - - {optionLabel(item)} - - {selected ? ( - - - - ) : null} - + {({ selected }) => ( +
+ {optionLabel(item)} + {selected && ( + + )} +
)}
)) @@ -138,13 +126,15 @@ const AutoCompleteAsync = (props: Props) => { {optionLabel(option)} { onChange( selected.filter((item: any) => item.id !== option.id) ); }} - /> + > + + ))}
diff --git a/src/Components/Form/AutocompleteV2.tsx b/src/Components/Form/AutocompleteV2.tsx new file mode 100644 index 0000000000..1fda44a931 --- /dev/null +++ b/src/Components/Form/AutocompleteV2.tsx @@ -0,0 +1,115 @@ +import React, { useState } from "react"; +import { Combobox } from "@headlessui/react"; +import { DropdownTransition } from "../Common/components/HelperComponents"; +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { dropdownOptionClassNames } from "./MultiSelectMenuV2"; + +type OptionCallback = (option: T) => R; + +type AutocompleteProps = { + id?: string; + options: T[]; + disabled?: boolean | undefined; + value: V | undefined; + placeholder?: string; + optionLabel: OptionCallback; + optionIcon?: OptionCallback; + optionValue?: OptionCallback; + showIconWhenSelected?: boolean; + className?: string; + chevronIcon?: React.ReactNode | undefined; +} & ( + | { + required?: false; + onChange: OptionCallback; + } + | { + required: true; + onChange: OptionCallback; + } +); + +/** + * Avoid using this component directly. Use `AutocompleteFormField` instead as + * its API is easier to use and compliant with `FormField` based components. + * + * Use this only when you want to hack into the design and get more + * customizability. + */ +export const AutocompleteV2 = (props: AutocompleteProps) => { + const [query, setQuery] = useState(""); // Ensure lower case + + const valueOptions = props.options.map((option) => { + const label = props.optionLabel(option); + return { + label, + search: label.toLowerCase(), + icon: props.optionIcon && props.optionIcon(option), + value: props.optionValue ? props.optionValue(option) : option, + }; + }); + + const placeholder = props.placeholder ?? "Select"; + const defaultOption = { + label: placeholder, + selectedLabel: ( +

{placeholder}

+ ), + icon: undefined, + value: undefined, + }; + + const options = props.required + ? valueOptions + : [defaultOption, ...valueOptions]; + + const value = options.find((o) => props.value == o.value) || defaultOption; + + const filteredOptions = valueOptions.filter((o) => o.search.includes(query)); + + return ( +
+ props.onChange(selection.value)} + > +
+
+ setQuery(event.target.value.toLowerCase())} + /> + +
+ {props.chevronIcon || ( + + )} +
+
+
+ + + + {filteredOptions.map((option, index) => ( + +
+ {option.label} + {option.icon} +
+
+ ))} +
+
+
+
+
+ ); +}; diff --git a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx new file mode 100644 index 0000000000..76326a624f --- /dev/null +++ b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx @@ -0,0 +1,175 @@ +import React, { useEffect, useState } from "react"; +import { Combobox } from "@headlessui/react"; +import { DropdownTransition } from "../../Common/components/HelperComponents"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import { + FormFieldBaseProps, + resolveFormFieldChangeEventHandler, +} from "./Utils"; +import FormField from "./FormField"; +import { + dropdownOptionClassNames, + MultiSelectOptionChip, +} from "../MultiSelectMenuV2"; + +type OptionCallback = (option: T) => R; + +type AutocompleteMultiSelectFormFieldProps = FormFieldBaseProps & { + placeholder?: string; + options: T[]; + optionLabel: OptionCallback; + optionValue?: OptionCallback; + onQuery?: (query: string) => void; + dropdownIcon?: React.ReactNode | undefined; +}; + +const AutocompleteMultiSelectFormField = ( + props: AutocompleteMultiSelectFormFieldProps +) => { + const { name } = props; + const handleChange = resolveFormFieldChangeEventHandler(props); + + return ( + + handleChange({ name, value })} + /> + + ); +}; + +export default AutocompleteMultiSelectFormField; + +type AutocompleteMutliSelectProps = { + id?: string; + options: T[]; + disabled?: boolean | undefined; + value: V[]; + placeholder?: string; + optionLabel: OptionCallback; + optionValue?: OptionCallback; + className?: string; + onChange: OptionCallback; + onQuery?: (query: string) => void; + isLoading?: boolean; +}; + +/** + * Avoid using this component directly. Use `AutocompleteMultiSelectFormField` + * instead as its API is easier to use and compliant with `FormField` based + * components. + * + * Use this only when you want to hack into the design and get more + * customizability. + */ +export const AutocompleteMutliSelect = ( + props: AutocompleteMutliSelectProps +) => { + const [query, setQuery] = useState(""); // Ensure lower case + useEffect(() => { + props.onQuery && props.onQuery(query); + }, [query]); + + const options = props.options.map((option) => { + const label = props.optionLabel(option); + return { + label, + search: label.toLowerCase(), + value: (props.optionValue ? props.optionValue(option) : option) as V, + }; + }); + + const value = options.filter((o) => props.value.includes(o.value)); + const filteredOptions = options.filter((o) => o.search.includes(query)); + + return ( +
+ props.onChange(selection.map((o) => o.value))} + > +
+
+ setQuery(event.target.value.toLowerCase())} + /> + +
+ {props.isLoading ? ( + + ) : ( + + )} +
+
+
+ {value.length !== 0 && ( +
+ {value.map((v) => ( + + props.onChange( + value.map((o) => o.value).filter((o) => o !== v.value) + ) + } + /> + ))} +
+ )} + + + + {props.isLoading ? ( + + ) : filteredOptions.length ? ( + filteredOptions.map((option, index) => ( + + {({ selected }) => ( +
+ {option.label} + {selected && ( + + )} +
+ )} +
+ )) + ) : ( + + {!query && } + {query ? "No results" : "Type to search"} + + )} +
+
+
+
+
+ ); +}; + +const Searching = () => { + return ( +
+ + Searching... +
+ ); +}; diff --git a/src/Components/Form/FormFields/DateFormField.tsx b/src/Components/Form/FormFields/DateFormField.tsx index c2f2319b55..f99f8a1f73 100644 --- a/src/Components/Form/FormFields/DateFormField.tsx +++ b/src/Components/Form/FormFields/DateFormField.tsx @@ -1,3 +1,4 @@ +import { classNames } from "../../../Utils/utils"; import DateInputV2, { DatePickerPosition } from "../../Common/DateInputV2"; import FormField from "./FormField"; import { @@ -11,19 +12,15 @@ type Props = FormFieldBaseProps & { position?: DatePickerPosition; }; -const DateFormField = ({ position = "CENTER", ...props }: Props) => { +const DateFormField = ({ position = "RIGHT", ...props }: Props) => { const handleChange = resolveFormFieldChangeEventHandler(props); const error = resolveFormFieldError(props); - - const bgColor = error ? "bg-red-50" : "bg-gray-200"; - const borderColor = error ? "border-red-500" : "border-gray-200"; - const name = props.name; return ( handleChange({ name, value })} position={position} diff --git a/src/Components/Form/FormFields/DateRangeFormField.tsx b/src/Components/Form/FormFields/DateRangeFormField.tsx index 411613d02c..4f01a2773b 100644 --- a/src/Components/Form/FormFields/DateRangeFormField.tsx +++ b/src/Components/Form/FormFields/DateRangeFormField.tsx @@ -1,3 +1,4 @@ +import { classNames } from "../../../Utils/utils"; import DateRangeInputV2, { DateRange } from "../../Common/DateRangeInputV2"; import FormField from "./FormField"; import { @@ -13,16 +14,12 @@ type Props = FormFieldBaseProps & { const DateRangeFormField = (props: Props) => { const handleChange = resolveFormFieldChangeEventHandler(props); const error = resolveFormFieldError(props); - - const bgColor = error ? "bg-red-50" : "bg-gray-200"; - const borderColor = error ? "border-red-500" : "border-gray-200"; - const name = props.name; return ( handleChange({ name, value })} disabled={props.disabled} diff --git a/src/Components/Form/FormFields/FormField.tsx b/src/Components/Form/FormFields/FormField.tsx index 093b50a80e..0ab1bfb36e 100644 --- a/src/Components/Form/FormFields/FormField.tsx +++ b/src/Components/Form/FormFields/FormField.tsx @@ -1,7 +1,9 @@ +import { classNames } from "../../../Utils/utils"; import { FieldError } from "../FieldValidators"; import { FormFieldBaseProps, resolveFormFieldError } from "./Utils"; type LabelProps = { + id?: string | undefined; required?: boolean; htmlFor?: string; children: React.ReactNode; @@ -10,9 +12,13 @@ type LabelProps = { export const FieldLabel = (props: LabelProps) => { return ( -