import { useEffect, useState, type ReactNode } from "react"; import { Check, Search, X } from "lucide-react"; import { Badge } from "../ui/badge"; import { EmptyState } from "../ui/empty-state"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; import { cn } from "../../lib/utils"; import { IValueSet, useValueSetServiceContext } from "../../services/ValueSetService"; import { codedAnswerLabel } from "../../utils/codedAnswerLabel"; import { LANGUAGE_OPTIONS, type Demographics } from "./demographics"; import { ENROLLED_OPTIONS, enrolledChoice, enrolledFromChoice } from "../../utils/tribalAffiliation"; // Read, searched and sorted by its label; the item itself (system, code, display) is what is chosen and stored. const sortByDisplay = (items: IValueSet[]): IValueSet[] => [...items].sort((a, b) => codedAnswerLabel(a).localeCompare(codedAnswerLabel(b))); const ChipGroup = ({ items, onRemove, disabled }: { items: IValueSet[]; onRemove: (code: string) => void; disabled: boolean }) => items.length ? (
{items.map((item) => ( {codedAnswerLabel(item)} ))}
) : null; type PickerProps = { id: string; options: IValueSet[]; selected: IValueSet[]; onSelect: (item: IValueSet) => void; placeholder: string; disabled: boolean; }; const SearchableMultiSelect = ({ id, options, selected, onSelect, placeholder, disabled }: PickerProps) => { const [search, setSearch] = useState(""); const [resetKey, setResetKey] = useState(0); const filtered = search.trim() ? options.filter((option) => codedAnswerLabel(option).toLowerCase().includes(search.toLowerCase())) : options; return ( setSearch(event.target.value)} // Typing filters the list; only Escape reaches the select, so its type-ahead does not take the keys. onKeyDown={(event) => { if (event.key !== "Escape") event.stopPropagation(); }} /> {filtered.length === 0 ? ( ) : ( filtered.map((option) => ( item.code === option.code) ? "opacity-100" : "opacity-0")} /> {codedAnswerLabel(option)} )) )} ); }; const toggle = (item: IValueSet, list: IValueSet[]): IValueSet[] => list.some((entry) => entry.code === item.code) ? list.filter((entry) => entry.code !== item.code) : [...list, item]; type OptionLists = { races: IValueSet[]; detailedRaces: IValueSet[]; ethnicities: IValueSet[]; detailedEthnicities: IValueSet[]; tribes: IValueSet[]; }; const NO_OPTIONS: OptionLists = { races: [], detailedRaces: [], ethnicities: [], detailedEthnicities: [], tribes: [] }; /** Languages, race, ethnicity and tribal affiliation, edited inside the You section's form. */ export const DemographicsEditor = ({ value, onChange, disabled, afterLanguages, }: { value: Demographics; onChange: (next: Demographics) => void; disabled: boolean; /** A control of the caller's that the form places straight after Languages. */ afterLanguages?: ReactNode; }) => { const { fetchValueSet, valueSetUrls } = useValueSetServiceContext(); const [options, setOptions] = useState(NO_OPTIONS); const [optionsFailed, setOptionsFailed] = useState(false); useEffect(() => { let current = true; Promise.all([ fetchValueSet(valueSetUrls.ombRaceCategory), fetchValueSet(valueSetUrls.detailedRace), fetchValueSet(valueSetUrls.ombEthnicityCategory), fetchValueSet(valueSetUrls.detailedEthnicity), fetchValueSet(valueSetUrls.tribalEntityUS), ]) .then(([races, detailedRaces, ethnicities, detailedEthnicities, tribes]) => { if (!current) return; setOptions({ races: sortByDisplay(races), detailedRaces: sortByDisplay(detailedRaces), ethnicities: sortByDisplay(ethnicities), detailedEthnicities: sortByDisplay(detailedEthnicities), tribes: sortByDisplay(tribes), }); }) .catch(() => { if (current) setOptionsFailed(true); }); return () => { current = false; }; }, [fetchValueSet, valueSetUrls]); const picker = ( key: keyof Omit, label: string, list: IValueSet[], placeholder: string, onRemove?: (next: IValueSet[]) => Partial, single = false, ) => (
onChange({ ...value, [key]: single ? toggle(item, value[key]).slice(-1) : toggle(item, value[key]) })} /> { const next = value[key].filter((item) => item.code !== code); onChange({ ...value, [key]: next, ...(onRemove?.(next) ?? {}) }); }} />
); return ( <> {optionsFailed && (

We could not load the race, ethnicity and tribe lists just now.

)} {picker("languages", "Languages you speak", LANGUAGE_OPTIONS, "Select language")} {afterLanguages} {picker("races", "Race", options.races, "Select race", (next) => (next.length ? {} : { detailedRaces: [] }))} {value.races.length > 0 && picker("detailedRaces", "More detail, if you want to give it", options.detailedRaces, "Select detail")} {picker("ethnicities", "Ethnicity", options.ethnicities, "Select ethnicity", (next) => (next.length ? {} : { detailedEthnicities: [] }), true)} {value.ethnicities.length > 0 && picker("detailedEthnicities", "More detail, if you want to give it", options.detailedEthnicities, "Select detail")} {picker("tribes", "Tribal affiliation", options.tribes, "Select tribal affiliation", (next) => (next.length ? {} : { enrolled: undefined }), true)}
); }; export default DemographicsEditor;