refactor: decompose frontend state and medication dialog flows
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
type ScheduleSectionCardProps = {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
headerRight?: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ScheduleSectionCard({ title, children, headerRight, className }: ScheduleSectionCardProps) {
|
||||
return (
|
||||
<article className={className ?? "card schedule-full"}>
|
||||
<div className="card-head">
|
||||
<h2>{title}</h2>
|
||||
{headerRight}
|
||||
</div>
|
||||
{children}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
type ScheduleUsageTagProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function ScheduleUsageTag({ children }: ScheduleUsageTagProps) {
|
||||
return <span className="tag subtle">{children}</span>;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ScheduleSectionCard } from "./ScheduleSectionCard";
|
||||
export { ScheduleUsageTag } from "./ScheduleUsageTag";
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { IntakeUnit } from "../../types";
|
||||
import { allowsPillFormSelection, isLiquidContainerPackageType, isTubePackageType } from "../../types";
|
||||
import { formatNumber } from "../../utils/formatters";
|
||||
import { convertLiquidUsageToMl, getLiquidCountUnitLabel } from "../../utils/intake-units";
|
||||
|
||||
type Translate = (key: string, options?: Record<string, unknown>) => string;
|
||||
type MedicationLike = { packageType?: string | null; medicationForm?: string | null } | undefined;
|
||||
|
||||
function formatLiquidUsageLabel(usage: number, unit: IntakeUnit | null | undefined, t: Translate): string {
|
||||
const normalizedUsage = Number(usage);
|
||||
if (!Number.isFinite(normalizedUsage) || normalizedUsage <= 0) {
|
||||
return `0 ${t("form.packageAmountUnitMl")}`;
|
||||
}
|
||||
|
||||
if (unit === "ml" || unit == null) {
|
||||
return `${formatNumber(normalizedUsage)} ${t("form.packageAmountUnitMl")}`;
|
||||
}
|
||||
|
||||
const mlTotal = convertLiquidUsageToMl(normalizedUsage, unit);
|
||||
return `${formatNumber(normalizedUsage)} ${getLiquidCountUnitLabel(unit, normalizedUsage, t)} ${formatNumber(mlTotal)} ${t("form.packageAmountUnitMl")}`;
|
||||
}
|
||||
|
||||
function getTubeUnitLabel(med: MedicationLike, value: number, t: Translate): string {
|
||||
if (isLiquidContainerPackageType(med?.packageType) || med?.medicationForm === "liquid") {
|
||||
return t("form.packageAmountUnitMl");
|
||||
}
|
||||
return t("form.blisters.applications", { count: Math.abs(value) });
|
||||
}
|
||||
|
||||
export function formatScheduleDoseUsageLabel(
|
||||
med: MedicationLike,
|
||||
usage: number,
|
||||
t: Translate,
|
||||
intakeUnit?: IntakeUnit | null
|
||||
): string {
|
||||
if (isLiquidContainerPackageType(med?.packageType)) {
|
||||
return formatLiquidUsageLabel(usage, intakeUnit, t);
|
||||
}
|
||||
|
||||
if (isTubePackageType(med?.packageType)) {
|
||||
return `${usage} ${getTubeUnitLabel(med, usage, t)}`;
|
||||
}
|
||||
|
||||
return `${usage} ${usage !== 1 ? t("common.pills") : t("common.pill")}`;
|
||||
}
|
||||
|
||||
export function formatScheduleTotalUsageLabel(
|
||||
med: MedicationLike,
|
||||
total: number,
|
||||
t: Translate,
|
||||
doses?: Array<{ usage: number; intakeUnit?: IntakeUnit | null }>,
|
||||
fallbackIntakeUnit?: IntakeUnit | null
|
||||
): string {
|
||||
if (isLiquidContainerPackageType(med?.packageType)) {
|
||||
if (doses && doses.length > 0) {
|
||||
const normalizedDoses = doses.filter((dose) => Number.isFinite(Number(dose.usage)) && Number(dose.usage) > 0);
|
||||
if (normalizedDoses.length > 0) {
|
||||
const allUnits = new Set(normalizedDoses.map((dose) => dose.intakeUnit ?? "ml"));
|
||||
if (allUnits.size === 1) {
|
||||
const onlyUnit = normalizedDoses[0]?.intakeUnit ?? "ml";
|
||||
const totalUsageInUnit = normalizedDoses.reduce((sum, dose) => sum + Number(dose.usage), 0);
|
||||
return formatLiquidUsageLabel(totalUsageInUnit, onlyUnit, t);
|
||||
}
|
||||
|
||||
const totalMl = normalizedDoses.reduce(
|
||||
(sum, dose) => sum + convertLiquidUsageToMl(Number(dose.usage), dose.intakeUnit ?? "ml"),
|
||||
0
|
||||
);
|
||||
return `${formatNumber(totalMl)} ${t("form.packageAmountUnitMl")}`;
|
||||
}
|
||||
}
|
||||
|
||||
return formatLiquidUsageLabel(total, fallbackIntakeUnit, t);
|
||||
}
|
||||
|
||||
if (isTubePackageType(med?.packageType)) {
|
||||
return `${total} ${getTubeUnitLabel(med, total, t)}`;
|
||||
}
|
||||
|
||||
if (allowsPillFormSelection(med?.packageType)) {
|
||||
return t("common.pillsTotal", { count: total });
|
||||
}
|
||||
|
||||
return t("common.pillsTotal", { count: total });
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export function toggleDateInSet(previous: Set<string>, dateStr: string): Set<string> {
|
||||
const next = new Set(previous);
|
||||
if (next.has(dateStr)) {
|
||||
next.delete(dateStr);
|
||||
} else {
|
||||
next.add(dateStr);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function resolveCollapsedState(
|
||||
isAutoCollapsed: boolean,
|
||||
dateStr: string,
|
||||
manuallyExpandedDays: Set<string>,
|
||||
manuallyCollapsedDays: Set<string>
|
||||
): boolean {
|
||||
if (isAutoCollapsed) {
|
||||
return !manuallyExpandedDays.has(dateStr);
|
||||
}
|
||||
return manuallyCollapsedDays.has(dateStr);
|
||||
}
|
||||
|
||||
export function countTakenDoseIds(doseIds: string[], isDoseTaken: (doseId: string) => boolean): number {
|
||||
return doseIds.filter((id) => isDoseTaken(id)).length;
|
||||
}
|
||||
|
||||
export function areAllDoseIdsTaken(doseIds: string[], isDoseTaken: (doseId: string) => boolean): boolean {
|
||||
return doseIds.length > 0 && doseIds.every((id) => isDoseTaken(id));
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { loadCollapsedDaysFromStorage } from "../../utils/storage";
|
||||
|
||||
export type ScheduleCollapseState = {
|
||||
collapsed: Set<string>;
|
||||
expanded: Set<string>;
|
||||
};
|
||||
|
||||
export function loadScheduleCollapseState(collapseKey: string, expandKey: string): ScheduleCollapseState {
|
||||
return loadCollapsedDaysFromStorage(collapseKey, expandKey);
|
||||
}
|
||||
|
||||
export function saveCollapsedDaySet(storageKey: string, value: Set<string>): void {
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify([...value]));
|
||||
} catch {
|
||||
// Ignore storage failures and keep UI responsive.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user