fix: align frontend tube/liquid container semantics (#364)
* fix: align frontend tube/liquid container semantics * test(frontend): fix PR #364 CI regressions
This commit is contained in:
@@ -87,6 +87,7 @@ export function DashboardPage() {
|
||||
settings.lowStockDays,
|
||||
coverage.low,
|
||||
coverage.all,
|
||||
meds,
|
||||
settings.lastAutoEmailSent,
|
||||
settings.lastNotificationType,
|
||||
settings.lastNotificationChannel,
|
||||
@@ -130,41 +131,157 @@ export function DashboardPage() {
|
||||
|
||||
const prescriptionEmptyCount = prescriptionLowMeds.filter((med) => med.remainingRefills <= 0).length;
|
||||
|
||||
const getTubeUnitLabel = (med: (typeof meds)[number] | undefined) =>
|
||||
const getTubeUnitLabel = (med: (typeof meds)[number] | undefined, value: number) =>
|
||||
med?.packageType === "liquid_container" || med?.medicationForm === "liquid"
|
||||
? t("form.ml")
|
||||
: t("blisters.applications");
|
||||
? t("form.packageAmountUnitMl")
|
||||
: t("form.blisters.applications", { count: Math.abs(value) });
|
||||
|
||||
const formatStockLabel = (med: (typeof meds)[number] | undefined, medsLeft: number) => {
|
||||
if (med?.packageType === "liquid_container") {
|
||||
return `${formatNumber(medsLeft)} ${t("form.ml")}`;
|
||||
return `${formatNumber(medsLeft)} ${t("form.packageAmountUnitMl")}`;
|
||||
}
|
||||
if (med?.packageType === "tube") {
|
||||
return `${formatNumber(medsLeft)} ${getTubeUnitLabel(med)}`;
|
||||
return `${formatNumber(medsLeft)} ${getTubeUnitLabel(med, medsLeft)}`;
|
||||
}
|
||||
return t("table.pillsCount", { count: Math.round(medsLeft) });
|
||||
};
|
||||
|
||||
const formatDoseUsageLabel = (med: (typeof meds)[number] | undefined, usage: number) => {
|
||||
const convertLiquidUsageToMl = (usage: number, unit: "ml" | "tsp" | "tbsp" | null | undefined): number => {
|
||||
if (unit === "tsp") return usage * 5;
|
||||
if (unit === "tbsp") return usage * 15;
|
||||
return usage;
|
||||
};
|
||||
|
||||
const getLiquidCountUnitLabel = (unit: "ml" | "tsp" | "tbsp" | null | undefined, usage: number): string => {
|
||||
if (unit === "tsp") return t("form.blisters.teaspoons", { count: Math.abs(usage) });
|
||||
if (unit === "tbsp") return t("form.blisters.tablespoons", { count: Math.abs(usage) });
|
||||
return t("form.packageAmountUnitMl");
|
||||
};
|
||||
|
||||
const formatLiquidUsageLabel = (usage: number, unit: "ml" | "tsp" | "tbsp" | null | undefined): 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)} ${formatNumber(mlTotal)} ${t("form.packageAmountUnitMl")}`;
|
||||
};
|
||||
|
||||
const formatDoseUsageLabel = (
|
||||
med: (typeof meds)[number] | undefined,
|
||||
usage: number,
|
||||
intakeUnit?: "ml" | "tsp" | "tbsp" | null
|
||||
) => {
|
||||
if (med?.packageType === "liquid_container") {
|
||||
return `${usage} ${t("form.ml")}`;
|
||||
return formatLiquidUsageLabel(usage, intakeUnit);
|
||||
}
|
||||
if (med?.packageType === "tube") {
|
||||
return `${usage} ${getTubeUnitLabel(med)}`;
|
||||
return `${usage} ${getTubeUnitLabel(med, usage)}`;
|
||||
}
|
||||
return `${usage} ${usage !== 1 ? t("common.pills") : t("common.pill")}`;
|
||||
};
|
||||
|
||||
const formatTotalUsageLabel = (med: (typeof meds)[number] | undefined, total: number) => {
|
||||
const formatTotalUsageLabel = (
|
||||
med: (typeof meds)[number] | undefined,
|
||||
total: number,
|
||||
intakeUnit?: "ml" | "tsp" | "tbsp" | null,
|
||||
doses?: Array<{ usage: number; intakeUnit?: "ml" | "tsp" | "tbsp" | null }>
|
||||
) => {
|
||||
if (med?.packageType === "liquid_container") {
|
||||
return `${total} ${t("form.ml")}`;
|
||||
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"));
|
||||
const totalMl = normalizedDoses.reduce(
|
||||
(sum, dose) => sum + convertLiquidUsageToMl(Number(dose.usage), dose.intakeUnit ?? "ml"),
|
||||
0
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return `${formatNumber(totalMl)} ${t("form.packageAmountUnitMl")}`;
|
||||
}
|
||||
}
|
||||
|
||||
return formatLiquidUsageLabel(total, intakeUnit);
|
||||
}
|
||||
if (med?.packageType === "tube") {
|
||||
return `${total} ${getTubeUnitLabel(med)}`;
|
||||
return `${total} ${getTubeUnitLabel(med, total)}`;
|
||||
}
|
||||
return t("common.pillsTotal", { count: total });
|
||||
};
|
||||
|
||||
const formatDailyConsumption = (med: (typeof meds)[number] | undefined) => {
|
||||
if (!med) return "-";
|
||||
|
||||
const intakes =
|
||||
med.intakes && med.intakes.length > 0
|
||||
? med.intakes
|
||||
: med.blisters.map((blister) => ({
|
||||
usage: blister.usage,
|
||||
every: blister.every,
|
||||
intakeUnit: null as "ml" | "tsp" | "tbsp" | null,
|
||||
takenBy: null as string | null,
|
||||
}));
|
||||
|
||||
if (intakes.length === 0) return "-";
|
||||
|
||||
let dailyTotal = 0;
|
||||
for (const intake of intakes) {
|
||||
const usage = Number(intake.usage);
|
||||
const every = Math.max(1, Number(intake.every) || 1);
|
||||
if (!Number.isFinite(usage) || usage <= 0) continue;
|
||||
|
||||
const hasPerIntakeTakenBy = typeof intake.takenBy === "string" && intake.takenBy.trim().length > 0;
|
||||
const personMultiplier = hasPerIntakeTakenBy ? 1 : Math.max(1, med.takenBy?.length ?? 0);
|
||||
const normalizedUsage = (usage * personMultiplier) / every;
|
||||
|
||||
if (med.packageType === "liquid_container") {
|
||||
dailyTotal += convertLiquidUsageToMl(normalizedUsage, intake.intakeUnit ?? "ml");
|
||||
} else {
|
||||
dailyTotal += normalizedUsage;
|
||||
}
|
||||
}
|
||||
|
||||
if (dailyTotal <= 0) return "-";
|
||||
|
||||
if (med.packageType === "liquid_container") {
|
||||
return t("table.perDayWithUnit", { value: formatNumber(dailyTotal), unit: t("form.packageAmountUnitMl") });
|
||||
}
|
||||
|
||||
if (med.packageType === "tube") {
|
||||
const tubeUnit =
|
||||
med.medicationForm === "liquid"
|
||||
? t("form.packageAmountUnitMl")
|
||||
: t("form.blisters.applications", { count: Math.abs(dailyTotal) });
|
||||
return t("table.perDayWithUnit", { value: formatNumber(dailyTotal), unit: tubeUnit });
|
||||
}
|
||||
|
||||
const pillUnit = dailyTotal === 1 ? t("common.pill") : t("common.pills");
|
||||
return t("table.perDayWithUnit", { value: formatNumber(dailyTotal), unit: pillUnit });
|
||||
};
|
||||
|
||||
const shouldHideNoScheduleStatusForTube = (
|
||||
med: (typeof meds)[number] | undefined,
|
||||
status: { className: string; label: string } | null
|
||||
) => med?.packageType === "tube" && status?.label === "status.noSchedule";
|
||||
|
||||
const getVisibleStockStatus = (
|
||||
med: (typeof meds)[number] | undefined,
|
||||
status: { className: string; label: string } | null
|
||||
) => (shouldHideNoScheduleStatusForTube(med, status) ? null : status);
|
||||
|
||||
const getMedByName = (name: string) => meds.find((m) => getMedDisplayName(m) === name);
|
||||
|
||||
const prescriptionStatus =
|
||||
prescriptionRemindersEnabled && prescriptionLowMeds.length > 0
|
||||
? {
|
||||
@@ -289,7 +406,9 @@ export function DashboardPage() {
|
||||
{reminderData.lowStockMeds.map((med, idx) => {
|
||||
const medication = meds.find((m) => getMedDisplayName(m) === med.name);
|
||||
const cov = coverage.all.find((c) => c.name === med.name);
|
||||
const status = cov ? getStockStatus(cov.daysLeft, cov.medsLeft, stockThresholds) : null;
|
||||
const status = cov
|
||||
? getStockStatus(cov.daysLeft, cov.medsLeft, stockThresholds, medication?.packageType)
|
||||
: null;
|
||||
const textClass =
|
||||
status?.className === "danger"
|
||||
? "danger-text"
|
||||
@@ -447,7 +566,9 @@ export function DashboardPage() {
|
||||
const lowStockMap = new Map<string, Coverage>();
|
||||
for (const c of coverage.all) {
|
||||
if (c.daysLeft === null && c.medsLeft > 0) continue; // no schedule, has stock
|
||||
if (c.medsLeft <= 0 || c.daysLeft === null || c.daysLeft < settings.lowStockDays) {
|
||||
const med = getMedByName(c.name);
|
||||
const status = getStockStatus(c.daysLeft, c.medsLeft, stockThresholds, med?.packageType);
|
||||
if (status.className === "danger" || status.className === "warning") {
|
||||
const existing = lowStockMap.get(c.name);
|
||||
if (!existing || (c.daysLeft ?? 0) < (existing.daysLeft ?? 0)) {
|
||||
lowStockMap.set(c.name, c);
|
||||
@@ -468,7 +589,7 @@ export function DashboardPage() {
|
||||
{t("dashboard.reorder.lowWarningPrefix")}{" "}
|
||||
{lowStockMeds.map((c, idx) => {
|
||||
const med = meds.find((m) => getMedDisplayName(m) === c.name);
|
||||
const status = getStockStatus(c.daysLeft, c.medsLeft, stockThresholds);
|
||||
const status = getStockStatus(c.daysLeft, c.medsLeft, stockThresholds, med?.packageType);
|
||||
const textClass =
|
||||
status.className === "danger"
|
||||
? "danger-text"
|
||||
@@ -512,10 +633,11 @@ export function DashboardPage() {
|
||||
<div className="card-head">
|
||||
<h2>{t("dashboard.overview.title")}</h2>
|
||||
</div>
|
||||
<div className="table table-7">
|
||||
<div className="table table-8">
|
||||
<div className="table-head">
|
||||
<span>{t("table.name")}</span>
|
||||
<span>{t("table.stock")}</span>
|
||||
<span>{t("table.dailyConsumption")}</span>
|
||||
<span>{t("table.stockDetails")}</span>
|
||||
<span>{t("table.daysLeft")}</span>
|
||||
<span>{t("table.runsOut")}</span>
|
||||
@@ -523,13 +645,14 @@ export function DashboardPage() {
|
||||
<span>{t("table.status")}</span>
|
||||
</div>
|
||||
{coverage.all.map((row) => {
|
||||
const status = getStockStatus(row.daysLeft, row.medsLeft, stockThresholds);
|
||||
const med = meds.find((m) => getMedDisplayName(m) === row.name);
|
||||
const rawStatus = getStockStatus(row.daysLeft, row.medsLeft, stockThresholds, med?.packageType);
|
||||
const status = getVisibleStockStatus(med, rawStatus);
|
||||
const expiryClass = getExpiryClass(med?.expiryDate, settings.expiryWarningDays);
|
||||
const textClass =
|
||||
status.className === "danger"
|
||||
rawStatus.className === "danger"
|
||||
? "danger-text"
|
||||
: status.className === "warning"
|
||||
: rawStatus.className === "warning"
|
||||
? "warning-text"
|
||||
: "success-text";
|
||||
const stock = getBlisterStock(
|
||||
@@ -629,6 +752,9 @@ export function DashboardPage() {
|
||||
? formatStockLabel(med, row.medsLeft)
|
||||
: formatFullBlisters(stock.fullBlisters, t)}
|
||||
</span>
|
||||
<span data-label={t("table.dailyConsumption")} className={textClass}>
|
||||
{formatDailyConsumption(med)}
|
||||
</span>
|
||||
<span
|
||||
data-label={t("table.stockDetails")}
|
||||
className={`${textClass}${med?.packageType === "bottle" || med?.packageType === "tube" || med?.packageType === "liquid_container" ? " hide-on-card" : ""}`}
|
||||
@@ -657,8 +783,8 @@ export function DashboardPage() {
|
||||
})
|
||||
: "-"}
|
||||
</span>
|
||||
<span data-label={t("table.status")} className={`status-chip ${status.className}`}>
|
||||
{t(status.label)}
|
||||
<span data-label={t("table.status")} className={status ? `status-chip ${status.className}` : ""}>
|
||||
{status ? t(status.label) : "-"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -775,9 +901,10 @@ export function DashboardPage() {
|
||||
const med = meds.find((m) => getMedDisplayName(m) === item.medName);
|
||||
const medCov = coverageByMed[item.medName];
|
||||
const isEmpty = medCov ? medCov.medsLeft <= 0 : false;
|
||||
const status = medCov
|
||||
? getStockStatus(medCov.daysLeft, medCov.medsLeft, stockThresholds)
|
||||
const rawStatus = medCov
|
||||
? getStockStatus(medCov.daysLeft, medCov.medsLeft, stockThresholds, med?.packageType)
|
||||
: null;
|
||||
const status = getVisibleStockStatus(med, rawStatus);
|
||||
const itemDoseIds = expandDoseIds(item.doses);
|
||||
const allTaken = itemDoseIds.every((id) => takenDoses.has(id));
|
||||
return (
|
||||
@@ -812,7 +939,9 @@ export function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="tag-row">
|
||||
<span className="tag subtle">{formatTotalUsageLabel(med, item.total)}</span>
|
||||
<span className="tag subtle">
|
||||
{formatTotalUsageLabel(med, item.total, item.doses[0]?.intakeUnit, item.doses)}
|
||||
</span>
|
||||
{status && (
|
||||
<span className={`status-chip small ${status.className}`}>{t(status.label)}</span>
|
||||
)}
|
||||
@@ -826,7 +955,9 @@ export function DashboardPage() {
|
||||
<div key={dose.id} className="dose-item past">
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
<span className="dose-usage">
|
||||
<span className="dose-usage-main">{formatDoseUsageLabel(med, dose.usage)}</span>
|
||||
<span className="dose-usage-main">
|
||||
{formatDoseUsageLabel(med, dose.usage, dose.intakeUnit)}
|
||||
</span>
|
||||
{med?.packageType !== "tube" &&
|
||||
med?.packageType !== "liquid_container" &&
|
||||
med?.pillWeightMg && (
|
||||
@@ -986,7 +1117,13 @@ export function DashboardPage() {
|
||||
const willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime;
|
||||
if (willBeOutOfStock) return "danger";
|
||||
if (!medCoverage) return "success";
|
||||
const status = getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, stockThresholds);
|
||||
const med = getMedByName(item.medName);
|
||||
const status = getStockStatus(
|
||||
medCoverage.daysLeft,
|
||||
medCoverage.medsLeft,
|
||||
stockThresholds,
|
||||
med?.packageType
|
||||
);
|
||||
return status.className;
|
||||
});
|
||||
const worstStatus = dayStockStatuses.includes("danger")
|
||||
@@ -1036,8 +1173,14 @@ export function DashboardPage() {
|
||||
const status = willBeOutOfStock
|
||||
? { className: "danger", label: "status.outOfStock" }
|
||||
: medCoverage
|
||||
? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, stockThresholds)
|
||||
? getStockStatus(
|
||||
medCoverage.daysLeft,
|
||||
medCoverage.medsLeft,
|
||||
stockThresholds,
|
||||
med?.packageType
|
||||
)
|
||||
: null;
|
||||
const visibleStatus = getVisibleStockStatus(med, status);
|
||||
const itemDoseIds = expandDoseIds(item.doses);
|
||||
const allTaken = itemDoseIds.every((id) => takenDoses.has(id));
|
||||
return (
|
||||
@@ -1072,9 +1215,13 @@ export function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="tag-row">
|
||||
<span className="tag subtle">{formatTotalUsageLabel(med, item.total)}</span>
|
||||
{status && (
|
||||
<span className={`status-chip small ${status.className}`}>{t(status.label)}</span>
|
||||
<span className="tag subtle">
|
||||
{formatTotalUsageLabel(med, item.total, item.doses[0]?.intakeUnit, item.doses)}
|
||||
</span>
|
||||
{visibleStatus && (
|
||||
<span className={`status-chip small ${visibleStatus.className}`}>
|
||||
{t(visibleStatus.label)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1090,7 +1237,9 @@ export function DashboardPage() {
|
||||
>
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
<span className="dose-usage">
|
||||
<span className="dose-usage-main">{formatDoseUsageLabel(med, dose.usage)}</span>
|
||||
<span className="dose-usage-main">
|
||||
{formatDoseUsageLabel(med, dose.usage, dose.intakeUnit)}
|
||||
</span>
|
||||
{med?.packageType !== "tube" &&
|
||||
med?.packageType !== "liquid_container" &&
|
||||
med?.pillWeightMg && (
|
||||
@@ -1218,7 +1367,13 @@ export function DashboardPage() {
|
||||
const willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime;
|
||||
if (willBeOutOfStock) return "danger";
|
||||
if (!medCoverage) return "success";
|
||||
const status = getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, stockThresholds);
|
||||
const med = getMedByName(item.medName);
|
||||
const status = getStockStatus(
|
||||
medCoverage.daysLeft,
|
||||
medCoverage.medsLeft,
|
||||
stockThresholds,
|
||||
med?.packageType
|
||||
);
|
||||
return status.className;
|
||||
});
|
||||
const worstStatus = dayStockStatuses.includes("danger")
|
||||
@@ -1267,8 +1422,14 @@ export function DashboardPage() {
|
||||
const status = willBeOutOfStock
|
||||
? { className: "danger", label: "status.outOfStock" }
|
||||
: medCoverage
|
||||
? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, stockThresholds)
|
||||
? getStockStatus(
|
||||
medCoverage.daysLeft,
|
||||
medCoverage.medsLeft,
|
||||
stockThresholds,
|
||||
med?.packageType
|
||||
)
|
||||
: null;
|
||||
const visibleStatus = getVisibleStockStatus(med, status);
|
||||
const itemDoseIds = expandDoseIds(item.doses);
|
||||
const allTaken = itemDoseIds.every((id) => takenDoses.has(id));
|
||||
return (
|
||||
@@ -1303,9 +1464,13 @@ export function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="tag-row">
|
||||
<span className="tag subtle">{formatTotalUsageLabel(med, item.total)}</span>
|
||||
{status && (
|
||||
<span className={`status-chip small ${status.className}`}>{t(status.label)}</span>
|
||||
<span className="tag subtle">
|
||||
{formatTotalUsageLabel(med, item.total, item.doses[0]?.intakeUnit, item.doses)}
|
||||
</span>
|
||||
{visibleStatus && (
|
||||
<span className={`status-chip small ${visibleStatus.className}`}>
|
||||
{t(visibleStatus.label)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1317,7 +1482,9 @@ export function DashboardPage() {
|
||||
<div key={dose.id} className={`dose-item future ${allTaken ? "all-taken" : ""}`}>
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
<span className="dose-usage">
|
||||
<span className="dose-usage-main">{formatDoseUsageLabel(med, dose.usage)}</span>
|
||||
<span className="dose-usage-main">
|
||||
{formatDoseUsageLabel(med, dose.usage, dose.intakeUnit)}
|
||||
</span>
|
||||
{med?.packageType !== "tube" &&
|
||||
med?.packageType !== "liquid_container" &&
|
||||
med?.pillWeightMg && (
|
||||
|
||||
@@ -295,6 +295,37 @@ export function MedicationsPage() {
|
||||
const currentStockLabel = usesAmountLabels ? t("form.currentAmount") : t("form.currentPills");
|
||||
const totalLabel = usesAmountLabels ? t("form.totalAmountLabel") : t("form.total");
|
||||
|
||||
const getMedicationPackageTypeLabel = useCallback(
|
||||
(med: Medication) => {
|
||||
if (med.packageType === "bottle") return t("form.packageTypeBottle");
|
||||
if (med.packageType === "tube") return t("form.packageTypeTube");
|
||||
if (med.packageType === "liquid_container") return t("form.packageTypeLiquidContainer");
|
||||
return t("form.packageTypeBlister");
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
const getMedicationStockSuffix = useCallback(
|
||||
(med: Medication) => {
|
||||
if (med.packageType === "tube") return "";
|
||||
if (med.packageType === "liquid_container") return " ml";
|
||||
return ` ${getPackageSize(med) === 1 ? t("common.pill") : t("common.pills")}`;
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
const getMedicationUsageUnitLabel = useCallback(
|
||||
(med: Medication, usage: number) => {
|
||||
if (med.packageType === "tube") {
|
||||
return med.medicationForm === "liquid" ? "ml" : t("form.blisters.usageApplication");
|
||||
}
|
||||
if (med.packageType === "liquid_container") return "ml";
|
||||
if (usage === 1) return t("common.pill");
|
||||
return t("common.pills");
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
const clearEditMedIdParam = useCallback(() => {
|
||||
setSearchParams(
|
||||
(prevParams) => {
|
||||
@@ -507,18 +538,26 @@ export function MedicationsPage() {
|
||||
const remainingRefills = Math.min(Number(form.prescriptionRemainingRefills || 0), authorizedRefills);
|
||||
const lowRefillThreshold = Math.min(Number(form.prescriptionLowRefillThreshold || 1), authorizedRefills);
|
||||
|
||||
const derivedMedicationForm =
|
||||
form.packageType === "tube"
|
||||
? form.medicationForm === "liquid" || form.medicationForm === "topical"
|
||||
? form.medicationForm
|
||||
: "topical"
|
||||
: form.packageType === "liquid_container"
|
||||
? "liquid"
|
||||
: form.pillForm;
|
||||
let derivedMedicationForm: string;
|
||||
if (form.packageType === "tube") {
|
||||
derivedMedicationForm =
|
||||
form.medicationForm === "liquid" || form.medicationForm === "topical" ? form.medicationForm : "topical";
|
||||
} else if (form.packageType === "liquid_container") {
|
||||
derivedMedicationForm = "liquid";
|
||||
} else {
|
||||
derivedMedicationForm = form.pillForm;
|
||||
}
|
||||
|
||||
const tubeTotalAmount =
|
||||
form.packageType === "tube" ? (Number(form.packCount) || 0) * (Number(form.packageAmountValue ?? 0) || 0) : null;
|
||||
|
||||
let packageAmountUnit = form.packageAmountUnit ?? "ml";
|
||||
if (form.packageType === "tube") {
|
||||
packageAmountUnit = "g";
|
||||
} else if (form.packageType === "liquid_container") {
|
||||
packageAmountUnit = "ml";
|
||||
}
|
||||
|
||||
const body = {
|
||||
name: form.name.trim(),
|
||||
genericName: form.genericName.trim() || null,
|
||||
@@ -531,12 +570,7 @@ export function MedicationsPage() {
|
||||
blistersPerPack: form.packageType === "tube" ? 1 : Number(form.blistersPerPack) || 1,
|
||||
pillsPerBlister: form.packageType === "tube" ? 1 : Number(form.pillsPerBlister) || 1,
|
||||
packageAmountValue: Number(form.packageAmountValue ?? 0) || 0,
|
||||
packageAmountUnit:
|
||||
form.packageType === "tube"
|
||||
? "g"
|
||||
: form.packageType === "liquid_container"
|
||||
? "ml"
|
||||
: (form.packageAmountUnit ?? "ml"),
|
||||
packageAmountUnit,
|
||||
totalPills: form.packageType === "tube" ? tubeTotalAmount : Number(form.totalPills) || null,
|
||||
looseTablets: form.packageType === "tube" ? tubeTotalAmount || 0 : Number(form.looseTablets) || 0,
|
||||
pillWeightMg: Number(form.pillWeightMg) || null,
|
||||
@@ -940,16 +974,7 @@ export function MedicationsPage() {
|
||||
</div>
|
||||
<div className="med-details">
|
||||
<span>
|
||||
{t("medications.details.type")}:{" "}
|
||||
<strong>
|
||||
{med.packageType === "bottle"
|
||||
? t("form.packageTypeBottle")
|
||||
: med.packageType === "tube"
|
||||
? t("form.packageTypeTube")
|
||||
: med.packageType === "liquid_container"
|
||||
? t("form.packageTypeLiquidContainer")
|
||||
: t("form.packageTypeBlister")}
|
||||
</strong>
|
||||
{t("medications.details.type")}: <strong>{getMedicationPackageTypeLabel(med)}</strong>
|
||||
</span>
|
||||
{med.packageType === "blister" ? (
|
||||
<>
|
||||
@@ -984,11 +1009,7 @@ export function MedicationsPage() {
|
||||
? Math.round(coverageByMed[getMedDisplayName(med)].medsLeft)
|
||||
: getPackageSize(med)}{" "}
|
||||
/ {getPackageSize(med)}
|
||||
{med.packageType === "tube"
|
||||
? ""
|
||||
: med.packageType === "liquid_container"
|
||||
? " ml"
|
||||
: ` ${getPackageSize(med) === 1 ? t("common.pill") : t("common.pills")}`}
|
||||
{med.packageType === "tube" ? "" : getMedicationStockSuffix(med)}
|
||||
{(coverageByMed[getMedDisplayName(med)]
|
||||
? Math.round(coverageByMed[getMedDisplayName(med)].medsLeft)
|
||||
: getPackageSize(med)) > getPackageSize(med) && (
|
||||
@@ -1006,17 +1027,8 @@ export function MedicationsPage() {
|
||||
<div className="blister-list">
|
||||
{(med.intakes ?? med.blisters).map((s, idx) => (
|
||||
<div key={`${med.id}-${idx}`} className="blister-row-simple">
|
||||
{s.usage}{" "}
|
||||
{med.packageType === "tube"
|
||||
? med.medicationForm === "liquid"
|
||||
? "ml"
|
||||
: t("form.blisters.usageApplication")
|
||||
: med.packageType === "liquid_container"
|
||||
? "ml"
|
||||
: s.usage === 1
|
||||
? t("common.pill")
|
||||
: t("common.pills")}{" "}
|
||||
· {s.every === 1 ? t("common.daily") : t("common.everyNDays", { count: s.every })} ·{" "}
|
||||
{s.usage} {getMedicationUsageUnitLabel(med, s.usage)} ·{" "}
|
||||
{s.every === 1 ? t("common.daily") : t("common.everyNDays", { count: s.every })} ·{" "}
|
||||
{t("form.blisters.from")} {formatDateTime(s.start)}
|
||||
{"takenBy" in s && (s as import("../types").Intake).takenBy && (
|
||||
<span className="blister-taken-by"> · {(s as import("../types").Intake).takenBy}</span>
|
||||
@@ -1405,108 +1417,120 @@ export function MedicationsPage() {
|
||||
<div className={`form-tab-panel${activeTab === "stock" ? " active" : ""}`}>
|
||||
<div className="full form-category">
|
||||
<h4 className="form-category-title">{t("form.sections.stock")}</h4>
|
||||
{form.packageType === "blister" ? (
|
||||
<>
|
||||
<label>
|
||||
{t("form.packs")}
|
||||
<FormNumberStepper
|
||||
value={form.packCount}
|
||||
onChange={(nextValue) => handleValueChange("packCount", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.blistersPerPack")}
|
||||
<FormNumberStepper
|
||||
value={form.blistersPerPack}
|
||||
onChange={(nextValue) => handleValueChange("blistersPerPack", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.pillsPerBlister")}
|
||||
<FormNumberStepper
|
||||
value={form.pillsPerBlister}
|
||||
onChange={(nextValue) => handleValueChange("pillsPerBlister", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.total")}
|
||||
<div className="static-value">{formatNumber(totalTablets)}</div>
|
||||
</label>
|
||||
</>
|
||||
) : form.packageType === "tube" ? (
|
||||
<>
|
||||
<label>
|
||||
{t("form.tubes")}
|
||||
<FormNumberStepper
|
||||
value={form.packCount}
|
||||
onChange={(nextValue) => handleValueChange("packCount", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label className="full">
|
||||
{t("form.packageAmountPerTube")}
|
||||
<div className="dose-input-group">
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
pattern="[0-9]*\.?[0-9]*"
|
||||
value={form.packageAmountValue ?? "0"}
|
||||
onChange={(e) => handleValueChange("packageAmountValue", e.target.value)}
|
||||
placeholder="0"
|
||||
{(() => {
|
||||
if (form.packageType === "blister") {
|
||||
return (
|
||||
<>
|
||||
<label>
|
||||
{t("form.packs")}
|
||||
<FormNumberStepper
|
||||
value={form.packCount}
|
||||
onChange={(nextValue) => handleValueChange("packCount", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.blistersPerPack")}
|
||||
<FormNumberStepper
|
||||
value={form.blistersPerPack}
|
||||
onChange={(nextValue) => handleValueChange("blistersPerPack", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.pillsPerBlister")}
|
||||
<FormNumberStepper
|
||||
value={form.pillsPerBlister}
|
||||
onChange={(nextValue) => handleValueChange("pillsPerBlister", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.total")}
|
||||
<div className="static-value">{formatNumber(totalTablets)}</div>
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (form.packageType === "tube") {
|
||||
return (
|
||||
<>
|
||||
<label>
|
||||
{t("form.tubes")}
|
||||
<FormNumberStepper
|
||||
value={form.packCount}
|
||||
onChange={(nextValue) => handleValueChange("packCount", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label className="full">
|
||||
{t("form.packageAmountPerTube")}
|
||||
<div className="dose-input-group">
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
pattern="[0-9]*\.?[0-9]*"
|
||||
value={form.packageAmountValue ?? "0"}
|
||||
onChange={(e) => handleValueChange("packageAmountValue", e.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
<select
|
||||
value="g"
|
||||
disabled
|
||||
className="dose-unit-select"
|
||||
aria-label={t("form.packageAmountUnitG")}
|
||||
>
|
||||
<option value="g">{t("form.packageAmountUnitG")}</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.totalAmount")}
|
||||
<div className="static-value">
|
||||
{formatNumber(
|
||||
(Number(form.packCount) || 0) * (Number(form.packageAmountValue ?? 0) || 0)
|
||||
)}
|
||||
{t("form.packageAmountUnitG")}
|
||||
</div>
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<label>
|
||||
{totalCapacityLabel}
|
||||
<FormNumberStepper
|
||||
value={form.totalPills}
|
||||
onChange={(nextValue) => handleValueChange("totalPills", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
<select
|
||||
value="g"
|
||||
disabled
|
||||
className="dose-unit-select"
|
||||
aria-label={t("form.packageAmountUnitG")}
|
||||
>
|
||||
<option value="g">{t("form.packageAmountUnitG")}</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.totalAmount")}
|
||||
<div className="static-value">
|
||||
{formatNumber((Number(form.packCount) || 0) * (Number(form.packageAmountValue ?? 0) || 0))}
|
||||
{t("form.packageAmountUnitG")}
|
||||
</div>
|
||||
</label>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<label>
|
||||
{totalCapacityLabel}
|
||||
<FormNumberStepper
|
||||
value={form.totalPills}
|
||||
onChange={(nextValue) => handleValueChange("totalPills", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{currentStockLabel}
|
||||
<FormNumberStepper
|
||||
value={form.looseTablets}
|
||||
onChange={(nextValue) => handleValueChange("looseTablets", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
<label>
|
||||
{currentStockLabel}
|
||||
<FormNumberStepper
|
||||
value={form.looseTablets}
|
||||
onChange={(nextValue) => handleValueChange("looseTablets", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
{form.packageType !== "tube" && form.packageType !== "liquid_container" && (
|
||||
<label className="full">
|
||||
{t("form.pillWeight")} ({form.doseUnit})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAuth } from "../components/Auth";
|
||||
import { useAppContext } from "../context";
|
||||
import type { Coverage } from "../types";
|
||||
import { getMedDisplayName } from "../types";
|
||||
import { formatNumber } from "../utils/formatters";
|
||||
import { expandDoseIds, isDoseDismissed } from "../utils/schedule";
|
||||
|
||||
// Helper for user-specific localStorage keys
|
||||
@@ -17,12 +18,21 @@ function userStorageKey(userId: number | undefined, key: string): string {
|
||||
function getStockStatus(
|
||||
daysLeft: number | null,
|
||||
medsLeft: number,
|
||||
settings: { lowStockDays: number; normalStockDays: number; highStockDays: number; reminderDaysBefore: number }
|
||||
settings: { lowStockDays: number; normalStockDays: number; highStockDays: number; reminderDaysBefore: number },
|
||||
packageType?: string
|
||||
) {
|
||||
if (packageType === "tube") return { className: "success", label: "status.noSchedule" };
|
||||
// Out of stock or completely depleted = danger (red)
|
||||
if (medsLeft <= 0 || daysLeft === 0) return { className: "danger", label: "status.outOfStock" };
|
||||
// No schedule, but has stock = normal
|
||||
if (daysLeft === null) return { className: "success", label: "status.noSchedule" };
|
||||
if (packageType === "liquid_container") {
|
||||
const lowDays = Math.max(1, Math.floor(settings.reminderDaysBefore));
|
||||
const criticalDays = Math.max(1, Math.ceil(lowDays / 2));
|
||||
if (daysLeft <= criticalDays) return { className: "danger", label: "status.criticalStock" };
|
||||
if (daysLeft <= lowDays) return { className: "warning", label: "status.lowStock" };
|
||||
return { className: "success", label: "status.normal" };
|
||||
}
|
||||
// Critical: at or below reminder threshold = danger (red)
|
||||
if (daysLeft <= settings.reminderDaysBefore) return { className: "danger", label: "status.criticalStock" };
|
||||
// Low: below low stock threshold = warning (yellow)
|
||||
@@ -37,13 +47,15 @@ function getStockStatus(
|
||||
function getDayStockStatus(
|
||||
dayMeds: Array<{ medName: string }>,
|
||||
coverageByMed: Record<string, Coverage>,
|
||||
settings: { lowStockDays: number; normalStockDays: number; highStockDays: number; reminderDaysBefore: number }
|
||||
settings: { lowStockDays: number; normalStockDays: number; highStockDays: number; reminderDaysBefore: number },
|
||||
meds: Array<{ name: string; genericName?: string | null; packageType?: string }>
|
||||
): string {
|
||||
let worstLevel = 3; // 3=success, 2=warning, 1=danger
|
||||
for (const item of dayMeds) {
|
||||
const cov = coverageByMed[item.medName];
|
||||
if (!cov) continue;
|
||||
const status = getStockStatus(cov.daysLeft, cov.medsLeft, settings);
|
||||
const med = meds.find((m) => getMedDisplayName(m) === item.medName);
|
||||
const status = getStockStatus(cov.daysLeft, cov.medsLeft, settings, med?.packageType);
|
||||
if (status.className === "danger") worstLevel = Math.min(worstLevel, 1);
|
||||
else if (status.className === "warning") worstLevel = Math.min(worstLevel, 2);
|
||||
}
|
||||
@@ -80,6 +92,87 @@ export function SchedulePage() {
|
||||
missedPastDoseIds,
|
||||
} = useAppContext();
|
||||
|
||||
const shouldHideNoScheduleStatusForTube = (
|
||||
med: (typeof meds)[number] | undefined,
|
||||
status: { className: string; label: string } | null
|
||||
) => med?.packageType === "tube" && status?.label === "status.noSchedule";
|
||||
|
||||
const getTubeUnitLabel = (med: (typeof meds)[number] | undefined, value: number) =>
|
||||
med?.packageType === "liquid_container" || med?.medicationForm === "liquid"
|
||||
? t("form.packageAmountUnitMl")
|
||||
: t("form.blisters.applications", { count: Math.abs(value) });
|
||||
|
||||
const convertLiquidUsageToMl = (usage: number, unit: "ml" | "tsp" | "tbsp" | null | undefined): number => {
|
||||
if (unit === "tsp") return usage * 5;
|
||||
if (unit === "tbsp") return usage * 15;
|
||||
return usage;
|
||||
};
|
||||
|
||||
const getLiquidCountUnitLabel = (unit: "ml" | "tsp" | "tbsp" | null | undefined, usage: number): string => {
|
||||
if (unit === "tsp") return t("form.blisters.teaspoons", { count: Math.abs(usage) });
|
||||
if (unit === "tbsp") return t("form.blisters.tablespoons", { count: Math.abs(usage) });
|
||||
return t("form.packageAmountUnitMl");
|
||||
};
|
||||
|
||||
const formatLiquidUsageLabel = (usage: number, unit: "ml" | "tsp" | "tbsp" | null | undefined): 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)} ${formatNumber(mlTotal)} ${t("form.packageAmountUnitMl")}`;
|
||||
};
|
||||
|
||||
const formatDoseUsageLabel = (
|
||||
med: (typeof meds)[number] | undefined,
|
||||
usage: number,
|
||||
intakeUnit?: "ml" | "tsp" | "tbsp" | null
|
||||
) => {
|
||||
if (med?.packageType === "liquid_container") {
|
||||
return formatLiquidUsageLabel(usage, intakeUnit);
|
||||
}
|
||||
if (med?.packageType === "tube") {
|
||||
return `${usage} ${getTubeUnitLabel(med, usage)}`;
|
||||
}
|
||||
return `${usage} ${usage !== 1 ? t("common.pills") : t("common.pill")}`;
|
||||
};
|
||||
|
||||
const formatTotalUsageLabel = (
|
||||
med: (typeof meds)[number] | undefined,
|
||||
total: number,
|
||||
doses?: Array<{ usage: number; intakeUnit?: "ml" | "tsp" | "tbsp" | null }>
|
||||
) => {
|
||||
if (med?.packageType === "liquid_container") {
|
||||
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);
|
||||
}
|
||||
|
||||
const totalMl = normalizedDoses.reduce(
|
||||
(sum, dose) => sum + convertLiquidUsageToMl(Number(dose.usage), dose.intakeUnit ?? "ml"),
|
||||
0
|
||||
);
|
||||
return `${formatNumber(totalMl)} ${t("form.packageAmountUnitMl")}`;
|
||||
}
|
||||
}
|
||||
return `${formatNumber(total)} ${t("form.packageAmountUnitMl")}`;
|
||||
}
|
||||
if (med?.packageType === "tube") {
|
||||
return `${total} ${getTubeUnitLabel(med, total)}`;
|
||||
}
|
||||
return t("common.pillsTotal", { count: total });
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="grid">
|
||||
<article className="card schedule-full">
|
||||
@@ -133,7 +226,7 @@ export function SchedulePage() {
|
||||
|
||||
const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr);
|
||||
const isCollapsed = !isManuallyExpanded;
|
||||
const _worstStatus = getDayStockStatus(day.meds, coverageByMed, settings);
|
||||
const _worstStatus = getDayStockStatus(day.meds, coverageByMed, settings, meds);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -185,7 +278,7 @@ export function SchedulePage() {
|
||||
<span className="med-name-text">{item.medName}</span>
|
||||
</div>
|
||||
<div className="tag-row">
|
||||
<span className="tag subtle">{t("common.pillsTotal", { count: item.total })}</span>
|
||||
<span className="tag subtle">{formatTotalUsageLabel(med, item.total, item.doses)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="doses-col">
|
||||
@@ -197,7 +290,7 @@ export function SchedulePage() {
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
<span className="dose-usage">
|
||||
<span className="dose-usage-main">
|
||||
{dose.usage} {dose.usage !== 1 ? t("common.pills") : t("common.pill")}
|
||||
{formatDoseUsageLabel(med, dose.usage, dose.intakeUnit)}
|
||||
</span>
|
||||
{med?.pillWeightMg && (
|
||||
<span className="dose-usage-weight">{`${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"}`}</span>
|
||||
@@ -341,8 +434,9 @@ export function SchedulePage() {
|
||||
const status = willBeOutOfStock
|
||||
? { className: "danger", label: "status.outOfStock" }
|
||||
: medCoverage
|
||||
? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings)
|
||||
? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings, med?.packageType)
|
||||
: null;
|
||||
const visibleStatus = shouldHideNoScheduleStatusForTube(med, status) ? null : status;
|
||||
const itemDoseIds = expandDoseIds(item.doses);
|
||||
const allTaken = itemDoseIds.every((id) => takenDoses.has(id));
|
||||
return (
|
||||
@@ -353,8 +447,10 @@ export function SchedulePage() {
|
||||
<span className="med-name-text">{item.medName}</span>
|
||||
</div>
|
||||
<div className="tag-row">
|
||||
<span className="tag subtle">{t("common.pillsTotal", { count: item.total })}</span>
|
||||
{status && <span className={`tag ${status.className}`}>{t(status.label)}</span>}
|
||||
<span className="tag subtle">{formatTotalUsageLabel(med, item.total, item.doses)}</span>
|
||||
{visibleStatus && (
|
||||
<span className={`tag ${visibleStatus.className}`}>{t(visibleStatus.label)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="doses-col">
|
||||
@@ -368,7 +464,7 @@ export function SchedulePage() {
|
||||
<span className="dose-time">{dose.timeStr}</span>
|
||||
<span className="dose-usage">
|
||||
<span className="dose-usage-main">
|
||||
{dose.usage} {dose.usage !== 1 ? t("common.pills") : t("common.pill")}
|
||||
{formatDoseUsageLabel(med, dose.usage, dose.intakeUnit)}
|
||||
</span>
|
||||
{med?.pillWeightMg && (
|
||||
<span className="dose-usage-weight">{`${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"}`}</span>
|
||||
|
||||
@@ -663,6 +663,9 @@ export function SettingsPage() {
|
||||
settings.lowStockDays >= settings.highStockDays) && (
|
||||
<p className="threshold-validation-error">{t("settings.stock.thresholdValidation")}</p>
|
||||
)}
|
||||
<p className="hint-text" style={{ marginTop: "12px" }}>
|
||||
ℹ️ {t("settings.stock.packageTypesNote")}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Coverage, PackageType } from "../types";
|
||||
import type { Coverage, Medication, PackageType } from "../types";
|
||||
import { getMedTotal as getMedTotalFromTypes } from "../types";
|
||||
import { splitCurrentBlisterStock } from "../utils/stock";
|
||||
|
||||
@@ -56,6 +56,7 @@ export function getReminderStatusData(
|
||||
lowStockDays: number,
|
||||
_allLowCoverage: Coverage[],
|
||||
allCoverage: Coverage[],
|
||||
meds: Medication[],
|
||||
lastAutoEmailSent: string | null,
|
||||
_lastNotificationType: string | null,
|
||||
_lastNotificationChannel: string | null,
|
||||
@@ -73,8 +74,12 @@ export function getReminderStatusData(
|
||||
lastIntakeSent: { date: string; medName: string | null; takenBy: string | null } | null;
|
||||
} {
|
||||
const lowStockMap = new Map<string, { name: string; daysLeft: number; isCritical: boolean }>();
|
||||
const medByName = new Map(meds.map((med) => [med.name || med.genericName || "", med] as const));
|
||||
|
||||
for (const c of allCoverage) {
|
||||
const med = medByName.get(c.name);
|
||||
if (med?.packageType === "tube") continue;
|
||||
|
||||
if (c.medsLeft <= 0) {
|
||||
lowStockMap.set(c.name, { name: c.name, daysLeft: 0, isCritical: true });
|
||||
continue;
|
||||
@@ -83,8 +88,11 @@ export function getReminderStatusData(
|
||||
if (c.daysLeft === null) continue;
|
||||
|
||||
const roundedDaysLeft = Math.round(c.daysLeft);
|
||||
const isCritical = c.daysLeft <= reminderDaysBefore;
|
||||
const isLow = c.daysLeft < lowStockDays;
|
||||
const isLiquid = med?.packageType === "liquid_container";
|
||||
const liquidLowDays = Math.max(1, Math.floor(reminderDaysBefore));
|
||||
const liquidCriticalDays = Math.max(1, Math.ceil(liquidLowDays / 2));
|
||||
const isCritical = isLiquid ? c.daysLeft <= liquidCriticalDays : c.daysLeft <= reminderDaysBefore;
|
||||
const isLow = isLiquid ? c.daysLeft <= liquidLowDays : c.daysLeft < lowStockDays;
|
||||
if (!isCritical && !isLow) continue;
|
||||
|
||||
const existing = lowStockMap.get(c.name);
|
||||
|
||||
Reference in New Issue
Block a user