@@ -1,11 +1,24 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type {
|
||||
MedicationEnrichmentEnrichResponse,
|
||||
MedicationEnrichmentPackageOption,
|
||||
MedicationEnrichmentSearchResult,
|
||||
MedicationEnrichmentStrengthOption,
|
||||
} from "../types";
|
||||
import { formatDate } from "../utils/formatters";
|
||||
import { getMedicationEnrichmentDisplayResultKey } from "../utils/medication-enrichment";
|
||||
|
||||
const OPEN_FDA_PACKAGE_CODE_PATTERN = /\s*\(([0-9A-Z]{4,}(?:-[0-9A-Z]{1,})+)\)\s*/gi;
|
||||
const PACKAGE_CONTENT_UNIT_PATTERNS = [
|
||||
{ pattern: /\bcapsules?\b/i, key: "capsule" },
|
||||
{ pattern: /\btablets?\b/i, key: "tablet" },
|
||||
{ pattern: /\bcaplets?\b/i, key: "caplet" },
|
||||
{ pattern: /\bpills?\b/i, key: "pill" },
|
||||
] as const;
|
||||
const INITIAL_VISIBLE_STRENGTH_OPTIONS = 12;
|
||||
|
||||
type TranslateFunction = (key: string, options?: Record<string, unknown>) => string;
|
||||
|
||||
export interface MedicationEnrichmentViewModel {
|
||||
query: string;
|
||||
@@ -15,12 +28,15 @@ export interface MedicationEnrichmentViewModel {
|
||||
hasSearched: boolean;
|
||||
searchError: string | null;
|
||||
applyingCode: string | null;
|
||||
applyingPackageLabel: string | null;
|
||||
activeResultCode: string | null;
|
||||
appliedSelection: MedicationEnrichmentEnrichResponse["selection"] | null;
|
||||
enrichError: string | null;
|
||||
meta: MedicationEnrichmentEnrichResponse["meta"] | null;
|
||||
strengthOptions: MedicationEnrichmentStrengthOption[];
|
||||
packageOptions: MedicationEnrichmentPackageOption[];
|
||||
appliedStrengthLabel: string | null;
|
||||
appliedPackageLabel: string | null;
|
||||
}
|
||||
|
||||
export interface MedicationEnrichmentSectionProps {
|
||||
@@ -28,8 +44,194 @@ export interface MedicationEnrichmentSectionProps {
|
||||
onQueryChange: (value: string) => void;
|
||||
onSearch: () => void;
|
||||
onLoadMoreResults?: () => void;
|
||||
onApplyResult: (result: MedicationEnrichmentSearchResult) => void;
|
||||
onApplyResult: (
|
||||
result: MedicationEnrichmentSearchResult,
|
||||
preferredPackageOption?: MedicationEnrichmentPackageOption
|
||||
) => void;
|
||||
onApplyStrength: (option: MedicationEnrichmentStrengthOption) => void;
|
||||
onApplyPackage: (option: MedicationEnrichmentPackageOption) => void;
|
||||
}
|
||||
|
||||
type MedicationEnrichmentPackageChoice = {
|
||||
option: MedicationEnrichmentPackageOption;
|
||||
sourceResult: MedicationEnrichmentSearchResult;
|
||||
};
|
||||
|
||||
type MedicationEnrichmentDisplayResult = {
|
||||
displayKey: string;
|
||||
representative: MedicationEnrichmentSearchResult;
|
||||
sourceResults: MedicationEnrichmentSearchResult[];
|
||||
packageChoices: MedicationEnrichmentPackageChoice[];
|
||||
firstIndex: number;
|
||||
};
|
||||
|
||||
function normalizePackageOptionDisplayText(value: string): string {
|
||||
return value
|
||||
.replace(OPEN_FDA_PACKAGE_CODE_PATTERN, " ")
|
||||
.replace(/\b([A-Z]{2,})\b/g, (match) => match.toLowerCase())
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getPackageContainerTranslationKey(packageType: MedicationEnrichmentPackageOption["packageType"]): string {
|
||||
switch (packageType) {
|
||||
case "blister":
|
||||
return "form.enrichment.packageContainers.blister";
|
||||
case "bottle":
|
||||
return "form.enrichment.packageContainers.bottle";
|
||||
case "liquid_container":
|
||||
return "form.enrichment.packageContainers.liquidContainer";
|
||||
case "tube":
|
||||
return "form.enrichment.packageContainers.tube";
|
||||
default:
|
||||
return "form.enrichment.packageContainers.bottle";
|
||||
}
|
||||
}
|
||||
|
||||
function detectPackageContentUnitKey(value: string): string {
|
||||
for (const candidate of PACKAGE_CONTENT_UNIT_PATTERNS) {
|
||||
if (candidate.pattern.test(value)) {
|
||||
return candidate.key;
|
||||
}
|
||||
}
|
||||
|
||||
return "tablet";
|
||||
}
|
||||
|
||||
function formatSolidPackageCount(count: number, sourceText: string, t: TranslateFunction): string {
|
||||
const unitKey = detectPackageContentUnitKey(sourceText);
|
||||
return `${count} ${t(`form.enrichment.packageUnits.${unitKey}`, { count })}`;
|
||||
}
|
||||
|
||||
function formatPackageContainerCount(option: MedicationEnrichmentPackageOption, t: TranslateFunction): string {
|
||||
return t(getPackageContainerTranslationKey(option.packageType), { count: Math.max(option.packCount, 1) });
|
||||
}
|
||||
|
||||
function buildPackageOptionKey(option: MedicationEnrichmentPackageOption): string {
|
||||
const sourceText = normalizePackageOptionDisplayText(option.description || option.label);
|
||||
const detectedUnit =
|
||||
option.packageType === "bottle" || option.packageType === "blister"
|
||||
? detectPackageContentUnitKey(sourceText)
|
||||
: null;
|
||||
|
||||
return JSON.stringify([
|
||||
option.packageType,
|
||||
option.packCount,
|
||||
option.blistersPerPack,
|
||||
option.pillsPerBlister,
|
||||
option.totalPills,
|
||||
option.looseTablets,
|
||||
option.packageAmountValue,
|
||||
option.packageAmountUnit,
|
||||
detectedUnit,
|
||||
]);
|
||||
}
|
||||
|
||||
function dedupePackageOptions(options: MedicationEnrichmentPackageOption[]): MedicationEnrichmentPackageOption[] {
|
||||
const uniqueOptions = new Map<string, MedicationEnrichmentPackageOption>();
|
||||
|
||||
for (const option of options) {
|
||||
const key = buildPackageOptionKey(option);
|
||||
if (!uniqueOptions.has(key)) {
|
||||
uniqueOptions.set(key, option);
|
||||
}
|
||||
}
|
||||
|
||||
return [...uniqueOptions.values()];
|
||||
}
|
||||
|
||||
function formatPackageOptionDisplayText(
|
||||
value: MedicationEnrichmentPackageOption | string,
|
||||
t: TranslateFunction
|
||||
): string {
|
||||
const rawText = typeof value === "string" ? value : value.description || value.label;
|
||||
const cleanedText = normalizePackageOptionDisplayText(rawText);
|
||||
|
||||
if (typeof value === "string") {
|
||||
return cleanedText || rawText;
|
||||
}
|
||||
|
||||
const packageContainerLabel = formatPackageContainerCount(value, t);
|
||||
|
||||
if (value.packageType === "blister") {
|
||||
if (value.blistersPerPack !== null && value.blistersPerPack > 1 && value.pillsPerBlister !== null) {
|
||||
return `${packageContainerLabel} · ${value.blistersPerPack} × ${formatSolidPackageCount(
|
||||
value.pillsPerBlister,
|
||||
cleanedText,
|
||||
t
|
||||
)}`;
|
||||
}
|
||||
|
||||
const blisterCount = value.pillsPerBlister ?? value.totalPills;
|
||||
if (blisterCount !== null && blisterCount > 0) {
|
||||
return `${packageContainerLabel} · ${formatSolidPackageCount(blisterCount, cleanedText, t)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (value.packageType === "bottle") {
|
||||
const totalCount = value.totalPills ?? value.looseTablets;
|
||||
if (totalCount !== null && totalCount > 0) {
|
||||
const countPerContainer =
|
||||
value.packCount > 1 && totalCount % value.packCount === 0 ? totalCount / value.packCount : totalCount;
|
||||
return `${packageContainerLabel} · ${formatSolidPackageCount(countPerContainer, cleanedText, t)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(value.packageType === "liquid_container" || value.packageType === "tube") &&
|
||||
value.packageAmountValue !== null &&
|
||||
value.packageAmountUnit
|
||||
) {
|
||||
return `${packageContainerLabel} · ${value.packageAmountValue} ${value.packageAmountUnit}`;
|
||||
}
|
||||
|
||||
return cleanedText || rawText;
|
||||
}
|
||||
|
||||
function buildMedicationDisplayResults(
|
||||
results: MedicationEnrichmentSearchResult[]
|
||||
): MedicationEnrichmentDisplayResult[] {
|
||||
const grouped = new Map<
|
||||
string,
|
||||
MedicationEnrichmentDisplayResult & { packageChoicesByKey: Map<string, MedicationEnrichmentPackageChoice> }
|
||||
>();
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const displayKey = getMedicationEnrichmentDisplayResultKey(result);
|
||||
const existing = grouped.get(displayKey);
|
||||
|
||||
if (!existing) {
|
||||
const packageChoicesByKey = new Map<string, MedicationEnrichmentPackageChoice>();
|
||||
for (const option of result.packageOptions) {
|
||||
packageChoicesByKey.set(buildPackageOptionKey(option), { option, sourceResult: result });
|
||||
}
|
||||
|
||||
grouped.set(displayKey, {
|
||||
displayKey,
|
||||
representative: result,
|
||||
sourceResults: [result],
|
||||
packageChoices: [...packageChoicesByKey.values()],
|
||||
packageChoicesByKey,
|
||||
firstIndex: index,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
existing.sourceResults.push(result);
|
||||
for (const option of result.packageOptions) {
|
||||
const key = buildPackageOptionKey(option);
|
||||
if (!existing.packageChoicesByKey.has(key)) {
|
||||
existing.packageChoicesByKey.set(key, { option, sourceResult: result });
|
||||
}
|
||||
}
|
||||
existing.packageChoices = [...existing.packageChoicesByKey.values()];
|
||||
});
|
||||
|
||||
return [...grouped.values()]
|
||||
.sort(
|
||||
(left, right) => right.packageChoices.length - left.packageChoices.length || left.firstIndex - right.firstIndex
|
||||
)
|
||||
.map(({ packageChoicesByKey: _packageChoicesByKey, ...result }) => result);
|
||||
}
|
||||
|
||||
export function MedicationEnrichmentSection({
|
||||
@@ -39,6 +241,7 @@ export function MedicationEnrichmentSection({
|
||||
onLoadMoreResults,
|
||||
onApplyResult,
|
||||
onApplyStrength,
|
||||
onApplyPackage,
|
||||
}: MedicationEnrichmentSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const canSearch = state.query.trim().length > 0 && !state.isSearching && !state.applyingCode;
|
||||
@@ -49,13 +252,29 @@ export function MedicationEnrichmentSection({
|
||||
state.enrichError !== null ||
|
||||
state.results.length > 0 ||
|
||||
state.appliedSelection !== null ||
|
||||
state.packageOptions.length > 0 ||
|
||||
state.strengthOptions.length > 0 ||
|
||||
state.appliedPackageLabel !== null ||
|
||||
state.appliedStrengthLabel !== null ||
|
||||
Boolean(state.meta?.partial);
|
||||
const [isExpanded, setIsExpanded] = useState(shouldAutoExpand);
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
const [expandedResultCode, setExpandedResultCode] = useState<string | null>(null);
|
||||
const [visibleStrengthOptionCount, setVisibleStrengthOptionCount] = useState(INITIAL_VISIBLE_STRENGTH_OPTIONS);
|
||||
const autoExpandStateRef = useRef(shouldAutoExpand);
|
||||
const resultRefs = useRef(new Map<string, HTMLElement>());
|
||||
const displayResults = useMemo(() => buildMedicationDisplayResults(state.results), [state.results]);
|
||||
const uniqueStatePackageOptions = useMemo(() => dedupePackageOptions(state.packageOptions), [state.packageOptions]);
|
||||
const visibleStrengthOptions = state.strengthOptions.slice(0, visibleStrengthOptionCount);
|
||||
const hasMoreStrengthOptions = state.strengthOptions.length > visibleStrengthOptions.length;
|
||||
const appliedPackageOption = useMemo(
|
||||
() => state.packageOptions.find((option) => option.label === state.appliedPackageLabel) ?? null,
|
||||
[state.appliedPackageLabel, state.packageOptions]
|
||||
);
|
||||
const isLoadingInitialSearch = state.isSearching && displayResults.length === 0;
|
||||
const isLoadingMoreResults = state.isSearching && displayResults.length > 0;
|
||||
const showLoadMoreAction =
|
||||
displayResults.length > 0 && (state.hasMoreResults || isLoadingMoreResults) && onLoadMoreResults;
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoExpand && !autoExpandStateRef.current) {
|
||||
@@ -65,6 +284,26 @@ export function MedicationEnrichmentSection({
|
||||
autoExpandStateRef.current = shouldAutoExpand;
|
||||
}, [shouldAutoExpand]);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleStrengthOptionCount(INITIAL_VISIBLE_STRENGTH_OPTIONS);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!expandedResultCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const animationFrameId = window.requestAnimationFrame(() => {
|
||||
resultRefs.current.get(expandedResultCode)?.scrollIntoView({
|
||||
block: "nearest",
|
||||
inline: "nearest",
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
|
||||
return () => window.cancelAnimationFrame(animationFrameId);
|
||||
}, [expandedResultCode]);
|
||||
|
||||
return (
|
||||
<div className="full medication-enrichment-section">
|
||||
<div className="medication-enrichment-header">
|
||||
@@ -74,7 +313,7 @@ export function MedicationEnrichmentSection({
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary small"
|
||||
className={`medication-enrichment-toggle-button ${isExpanded ? "secondary small" : "primary small"}`}
|
||||
aria-expanded={isExpanded}
|
||||
onClick={() => setIsExpanded((current) => !current)}
|
||||
>
|
||||
@@ -119,8 +358,16 @@ export function MedicationEnrichmentSection({
|
||||
}}
|
||||
placeholder={t("form.enrichment.searchPlaceholder")}
|
||||
/>
|
||||
<button type="button" className="secondary small" onClick={onSearch} disabled={!canSearch}>
|
||||
{state.isSearching ? t("form.enrichment.searching") : t("form.enrichment.searchAction")}
|
||||
<button
|
||||
type="button"
|
||||
className={`secondary small medication-enrichment-action-button${isLoadingInitialSearch ? " is-loading" : ""}`}
|
||||
onClick={onSearch}
|
||||
disabled={!canSearch}
|
||||
>
|
||||
{isLoadingInitialSearch ? <span className="medication-enrichment-spinner" aria-hidden="true" /> : null}
|
||||
<span>
|
||||
{isLoadingInitialSearch ? t("form.enrichment.loadingSearch") : t("form.enrichment.searchAction")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
@@ -128,41 +375,79 @@ export function MedicationEnrichmentSection({
|
||||
{state.searchError ? <p className="danger-text">{state.searchError}</p> : null}
|
||||
{state.enrichError ? <p className="danger-text">{state.enrichError}</p> : null}
|
||||
{state.meta?.partial ? <p className="info-text">{t("form.enrichment.partialNote")}</p> : null}
|
||||
{state.hasSearched && !state.isSearching && state.results.length === 0 ? (
|
||||
{state.hasSearched && !state.isSearching && state.results.length === 0 && !state.searchError ? (
|
||||
<p className="info-text">{t("form.enrichment.noResults")}</p>
|
||||
) : null}
|
||||
|
||||
{state.results.length > 0 ? (
|
||||
{displayResults.length > 0 ? (
|
||||
<div className="medication-enrichment-results">
|
||||
{state.results.map((result) => {
|
||||
const isActive = state.activeResultCode === result.code;
|
||||
{displayResults.map((displayResult) => {
|
||||
const { representative, sourceResults, packageChoices, displayKey } = displayResult;
|
||||
const isActive = sourceResults.some((result) => result.code === state.activeResultCode);
|
||||
const authorisationHolder =
|
||||
sourceResults.find((result) => result.authorisationHolder)?.authorisationHolder ?? null;
|
||||
const therapeuticArea = sourceResults.find((result) => result.therapeuticArea)?.therapeuticArea ?? null;
|
||||
const authorisationDate =
|
||||
sourceResults.find((result) => result.authorisationDate)?.authorisationDate ?? null;
|
||||
const hasPackageOptions = packageChoices.length > 0;
|
||||
const hasActiveStrengthOptions = isActive && state.strengthOptions.length > 0;
|
||||
const isApplyingPackageSelection =
|
||||
isActive && state.applyingCode !== null && state.applyingPackageLabel !== null;
|
||||
const hasDetails = Boolean(
|
||||
result.authorisationHolder || result.therapeuticArea || result.authorisationDate
|
||||
authorisationHolder ||
|
||||
therapeuticArea ||
|
||||
authorisationDate ||
|
||||
hasPackageOptions ||
|
||||
hasActiveStrengthOptions ||
|
||||
isApplyingPackageSelection
|
||||
);
|
||||
const isDetailsExpanded = expandedResultCode === result.code;
|
||||
const genericStatusClass = result.genericStatus === "generic" ? "success" : "neutral";
|
||||
const sourceClass = result.source === "openfda" ? "warning" : "neutral";
|
||||
const isDetailsExpanded = expandedResultCode === displayKey;
|
||||
const activePackageOptions =
|
||||
isActive && uniqueStatePackageOptions.length > 0
|
||||
? uniqueStatePackageOptions
|
||||
: packageChoices.map((choice) => choice.option);
|
||||
const showInlinePackageChoices = activePackageOptions.length > 1;
|
||||
const genericStatusClass = representative.genericStatus === "generic" ? "success" : "neutral";
|
||||
const sourceClass = representative.source === "openfda" ? "warning" : "neutral";
|
||||
let applyLabel = t("form.enrichment.applyAction");
|
||||
if (state.applyingCode === result.code) {
|
||||
if (isActive && state.applyingCode !== null) {
|
||||
applyLabel = t("form.enrichment.applying");
|
||||
} else if (isActive && state.appliedSelection) {
|
||||
applyLabel = t("form.enrichment.applied");
|
||||
}
|
||||
|
||||
return (
|
||||
<article key={result.code} className={`medication-enrichment-result${isActive ? " active" : ""}`}>
|
||||
<article
|
||||
key={displayKey}
|
||||
className={`medication-enrichment-result${isActive ? " active" : ""}`}
|
||||
ref={(element) => {
|
||||
if (element) {
|
||||
resultRefs.current.set(displayKey, element);
|
||||
return;
|
||||
}
|
||||
|
||||
resultRefs.current.delete(displayKey);
|
||||
}}
|
||||
>
|
||||
<div className="medication-enrichment-result-header">
|
||||
<div className="medication-enrichment-result-names">
|
||||
<strong>{result.name}</strong>
|
||||
{result.genericName ? (
|
||||
<span className="medication-enrichment-result-generic">{result.genericName}</span>
|
||||
<strong>{representative.name}</strong>
|
||||
{representative.genericName ? (
|
||||
<span className="medication-enrichment-result-generic">{representative.genericName}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="medication-enrichment-result-actions">
|
||||
<span className={`pill ${sourceClass}`}>{t(`form.enrichment.sources.${result.source}`)}</span>
|
||||
{result.source === "ema" ? (
|
||||
<span className={`pill ${hasPackageOptions ? "success" : "neutral"}`}>
|
||||
{hasPackageOptions
|
||||
? t("form.enrichment.packageAvailable")
|
||||
: t("form.enrichment.packageUnavailable")}
|
||||
</span>
|
||||
<span className={`pill ${sourceClass}`}>
|
||||
{t(`form.enrichment.sources.${representative.source}`)}
|
||||
</span>
|
||||
{representative.source === "ema" ? (
|
||||
<span className={`pill ${genericStatusClass}`}>
|
||||
{t(`form.enrichment.genericStatus.${result.genericStatus}`)}
|
||||
{t(`form.enrichment.genericStatus.${representative.genericStatus}`)}
|
||||
</span>
|
||||
) : null}
|
||||
{hasDetails ? (
|
||||
@@ -171,7 +456,7 @@ export function MedicationEnrichmentSection({
|
||||
className="ghost small"
|
||||
aria-expanded={isDetailsExpanded}
|
||||
onClick={() =>
|
||||
setExpandedResultCode((current) => (current === result.code ? null : result.code))
|
||||
setExpandedResultCode((current) => (current === displayKey ? null : displayKey))
|
||||
}
|
||||
>
|
||||
{isDetailsExpanded
|
||||
@@ -179,38 +464,162 @@ export function MedicationEnrichmentSection({
|
||||
: t("form.enrichment.details.showAction")}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className={isActive ? "secondary small" : "primary small"}
|
||||
onClick={() => {
|
||||
setExpandedResultCode(result.code);
|
||||
onApplyResult(result);
|
||||
}}
|
||||
disabled={state.applyingCode === result.code}
|
||||
>
|
||||
{applyLabel}
|
||||
</button>
|
||||
{showInlinePackageChoices ? null : (
|
||||
<button
|
||||
type="button"
|
||||
className={isActive ? "secondary small" : "primary small"}
|
||||
onClick={() => {
|
||||
setExpandedResultCode(displayKey);
|
||||
onApplyResult(representative);
|
||||
}}
|
||||
disabled={isActive && state.applyingCode !== null}
|
||||
>
|
||||
{applyLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasDetails && isDetailsExpanded ? (
|
||||
<dl className="medication-enrichment-result-meta">
|
||||
{result.authorisationHolder ? (
|
||||
{authorisationHolder ? (
|
||||
<div>
|
||||
<dt>{t("form.enrichment.details.authorisationHolder")}</dt>
|
||||
<dd>{result.authorisationHolder}</dd>
|
||||
<dd>{authorisationHolder}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{result.therapeuticArea ? (
|
||||
{therapeuticArea ? (
|
||||
<div>
|
||||
<dt>{t("form.enrichment.details.therapeuticArea")}</dt>
|
||||
<dd>{result.therapeuticArea}</dd>
|
||||
<dd>{therapeuticArea}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{result.authorisationDate ? (
|
||||
{authorisationDate ? (
|
||||
<div>
|
||||
<dt>{t("form.enrichment.details.authorisationDate")}</dt>
|
||||
<dd>{formatDate(result.authorisationDate)}</dd>
|
||||
<dd>{formatDate(authorisationDate)}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{activePackageOptions.length > 0 ? (
|
||||
<div className="medication-enrichment-result-meta-full">
|
||||
<dt>{t("form.enrichment.details.packageSizes")}</dt>
|
||||
<dd>
|
||||
<div className="medication-enrichment-detail-stack">
|
||||
{showInlinePackageChoices ? (
|
||||
<div className="medication-enrichment-strength-list medication-enrichment-package-choice-list">
|
||||
{activePackageOptions.map((option) => {
|
||||
const isApplyingPending =
|
||||
isApplyingPackageSelection && state.applyingPackageLabel === option.label;
|
||||
const isSelected =
|
||||
isActive &&
|
||||
(state.appliedPackageLabel === option.label ||
|
||||
(appliedPackageOption !== null &&
|
||||
buildPackageOptionKey(appliedPackageOption) ===
|
||||
buildPackageOptionKey(option)));
|
||||
const packageLabel = formatPackageOptionDisplayText(option, t);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.label}
|
||||
type="button"
|
||||
className={`medication-enrichment-package-choice-button ${isSelected || isApplyingPending ? "primary small" : "secondary small"}${isApplyingPending ? " is-loading" : ""}`}
|
||||
aria-pressed={isSelected}
|
||||
title={packageLabel}
|
||||
onClick={() =>
|
||||
isActive && uniqueStatePackageOptions.length > 0
|
||||
? onApplyPackage(option)
|
||||
: onApplyResult(
|
||||
packageChoices.find((choice) => choice.option.label === option.label)
|
||||
?.sourceResult ?? representative,
|
||||
option
|
||||
)
|
||||
}
|
||||
disabled={isActive && state.applyingCode !== null}
|
||||
>
|
||||
{isApplyingPending ? (
|
||||
<span className="medication-enrichment-spinner" aria-hidden="true" />
|
||||
) : null}
|
||||
<span>{packageLabel}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<ul className="medication-enrichment-package-details">
|
||||
{activePackageOptions.map((option) => (
|
||||
<li key={option.label}>{formatPackageOptionDisplayText(option, t)}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{isActive && state.appliedPackageLabel ? (
|
||||
<p className="success-text medication-enrichment-applied-note">
|
||||
{t("form.enrichment.appliedPackage", {
|
||||
label: formatPackageOptionDisplayText(
|
||||
appliedPackageOption ?? state.appliedPackageLabel,
|
||||
t
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{isApplyingPackageSelection ? (
|
||||
<div className="medication-enrichment-result-meta-full">
|
||||
<dt>{t("form.enrichment.strengthTitle")}</dt>
|
||||
<dd>
|
||||
<div className="medication-enrichment-pending-panel" aria-live="polite">
|
||||
<span className="medication-enrichment-spinner" aria-hidden="true" />
|
||||
<span>{t("form.enrichment.applying")}</span>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{hasActiveStrengthOptions ? (
|
||||
<div className="medication-enrichment-result-meta-full">
|
||||
<dt>{t("form.enrichment.strengthTitle")}</dt>
|
||||
<dd>
|
||||
<div className="medication-enrichment-detail-stack">
|
||||
<p className="sub medication-enrichment-detail-hint">
|
||||
{t("form.enrichment.strengthHint")}
|
||||
</p>
|
||||
<div className="medication-enrichment-strength-list">
|
||||
{visibleStrengthOptions.map((option) => {
|
||||
const isSelected = state.appliedStrengthLabel === option.label;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.label}
|
||||
type="button"
|
||||
className={isSelected ? "primary small" : "secondary small"}
|
||||
onClick={() => onApplyStrength(option)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{hasMoreStrengthOptions ? (
|
||||
<button
|
||||
type="button"
|
||||
className="secondary small medication-enrichment-inline-action"
|
||||
onClick={() =>
|
||||
setVisibleStrengthOptionCount(
|
||||
(current) => current + INITIAL_VISIBLE_STRENGTH_OPTIONS
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("form.enrichment.showMoreStrengthsAction")}
|
||||
</button>
|
||||
) : null}
|
||||
{state.appliedStrengthLabel ? (
|
||||
<p className="success-text medication-enrichment-applied-note">
|
||||
{t("form.enrichment.appliedStrength", { label: state.appliedStrengthLabel })}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
@@ -221,61 +630,21 @@ export function MedicationEnrichmentSection({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{state.results.length > 0 && state.hasMoreResults && onLoadMoreResults ? (
|
||||
{showLoadMoreAction ? (
|
||||
<div className="medication-enrichment-results-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary small"
|
||||
className={`secondary small medication-enrichment-action-button medication-enrichment-load-more-button${isLoadingMoreResults ? " is-loading" : ""}`}
|
||||
onClick={onLoadMoreResults}
|
||||
disabled={state.isSearching || Boolean(state.applyingCode)}
|
||||
>
|
||||
{t("form.enrichment.showMoreAction")}
|
||||
{isLoadingMoreResults ? <span className="medication-enrichment-spinner" aria-hidden="true" /> : null}
|
||||
<span>
|
||||
{isLoadingMoreResults ? t("form.enrichment.loadingMoreResults") : t("form.enrichment.showMoreAction")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{state.appliedSelection || state.strengthOptions.length > 0 || state.appliedStrengthLabel ? (
|
||||
<div className="medication-enrichment-followup">
|
||||
{state.appliedSelection ? (
|
||||
<div>
|
||||
<p className="success-text">{t("form.enrichment.applied")}</p>
|
||||
<p className="sub medication-enrichment-selection-summary">
|
||||
<strong>{state.appliedSelection.name}</strong>
|
||||
{state.appliedSelection.genericName ? ` • ${state.appliedSelection.genericName}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{state.strengthOptions.length > 0 ? (
|
||||
<div className="medication-enrichment-strengths">
|
||||
<p className="medication-enrichment-strength-title">{t("form.enrichment.strengthTitle")}</p>
|
||||
<p className="sub">{t("form.enrichment.strengthHint")}</p>
|
||||
<div className="medication-enrichment-strength-list">
|
||||
{state.strengthOptions.map((option) => {
|
||||
const isSelected = state.appliedStrengthLabel === option.label;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.label}
|
||||
type="button"
|
||||
className={isSelected ? "primary small" : "secondary small"}
|
||||
onClick={() => onApplyStrength(option)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{state.appliedStrengthLabel ? (
|
||||
<p className="success-text">
|
||||
{t("form.enrichment.appliedStrength", { label: state.appliedStrengthLabel })}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
FormIntake,
|
||||
FormState,
|
||||
Medication,
|
||||
MedicationEnrichmentPackageOption,
|
||||
MedicationEnrichmentSearchResult,
|
||||
MedicationEnrichmentStrengthOption,
|
||||
} from "../types";
|
||||
@@ -59,12 +60,15 @@ const EMPTY_MEDICATION_ENRICHMENT: MedicationEnrichmentViewModel = {
|
||||
hasSearched: false,
|
||||
searchError: null,
|
||||
applyingCode: null,
|
||||
applyingPackageLabel: null,
|
||||
activeResultCode: null,
|
||||
appliedSelection: null,
|
||||
enrichError: null,
|
||||
meta: null,
|
||||
strengthOptions: [],
|
||||
packageOptions: [],
|
||||
appliedStrengthLabel: null,
|
||||
appliedPackageLabel: null,
|
||||
};
|
||||
|
||||
export interface MobileEditModalProps {
|
||||
@@ -76,8 +80,12 @@ export interface MobileEditModalProps {
|
||||
onMedicationEnrichmentQueryChange?: (value: string) => void;
|
||||
onMedicationEnrichmentSearch?: () => void;
|
||||
onMedicationEnrichmentLoadMore?: () => void;
|
||||
onMedicationEnrichmentApply?: (result: MedicationEnrichmentSearchResult) => void;
|
||||
onMedicationEnrichmentApply?: (
|
||||
result: MedicationEnrichmentSearchResult,
|
||||
preferredPackageOption?: MedicationEnrichmentPackageOption
|
||||
) => void;
|
||||
onMedicationEnrichmentStrengthApply?: (option: MedicationEnrichmentStrengthOption) => void;
|
||||
onMedicationEnrichmentPackageApply?: (option: MedicationEnrichmentPackageOption) => void;
|
||||
fieldErrors: FieldErrors;
|
||||
saving: boolean;
|
||||
formSaved: boolean;
|
||||
@@ -136,6 +144,7 @@ export function MobileEditModal({
|
||||
onMedicationEnrichmentLoadMore = () => {},
|
||||
onMedicationEnrichmentApply = () => {},
|
||||
onMedicationEnrichmentStrengthApply = () => {},
|
||||
onMedicationEnrichmentPackageApply = () => {},
|
||||
fieldErrors,
|
||||
saving,
|
||||
formSaved,
|
||||
@@ -492,6 +501,7 @@ export function MobileEditModal({
|
||||
onLoadMoreResults={onMedicationEnrichmentLoadMore}
|
||||
onApplyResult={onMedicationEnrichmentApply}
|
||||
onApplyStrength={onMedicationEnrichmentStrengthApply}
|
||||
onApplyPackage={onMedicationEnrichmentPackageApply}
|
||||
/>
|
||||
<div className="full date-pair-group">
|
||||
<label className="date-pair-field">
|
||||
|
||||
Reference in New Issue
Block a user