fix: keep topical stock non-depleting in planner flows (#359)

* fix: keep topical stock non-depleting in planner and reports

* test: stabilize e2e selectors for updated medication semantics

* fix(backend): add missing planner translation keys
This commit is contained in:
Daniel Volz
2026-02-28 23:36:52 +01:00
committed by GitHub
parent 8efd99d738
commit 9e8a6315e7
19 changed files with 807 additions and 130 deletions
+65 -25
View File
@@ -129,6 +129,42 @@ export function DashboardPage() {
const showOnlyToday = settings.upcomingTodayOnly;
const prescriptionEmptyCount = prescriptionLowMeds.filter((med) => med.remainingRefills <= 0).length;
const getTubeUnitLabel = (med: (typeof meds)[number] | undefined) =>
med?.packageType === "liquid_container" || med?.medicationForm === "liquid"
? t("form.ml")
: t("blisters.applications");
const formatStockLabel = (med: (typeof meds)[number] | undefined, medsLeft: number) => {
if (med?.packageType === "liquid_container") {
return `${formatNumber(medsLeft)} ${t("form.ml")}`;
}
if (med?.packageType === "tube") {
return `${formatNumber(medsLeft)} ${getTubeUnitLabel(med)}`;
}
return t("table.pillsCount", { count: Math.round(medsLeft) });
};
const formatDoseUsageLabel = (med: (typeof meds)[number] | undefined, usage: number) => {
if (med?.packageType === "liquid_container") {
return `${usage} ${t("form.ml")}`;
}
if (med?.packageType === "tube") {
return `${usage} ${getTubeUnitLabel(med)}`;
}
return `${usage} ${usage !== 1 ? t("common.pills") : t("common.pill")}`;
};
const formatTotalUsageLabel = (med: (typeof meds)[number] | undefined, total: number) => {
if (med?.packageType === "liquid_container") {
return `${total} ${t("form.ml")}`;
}
if (med?.packageType === "tube") {
return `${total} ${getTubeUnitLabel(med)}`;
}
return t("common.pillsTotal", { count: total });
};
const prescriptionStatus =
prescriptionRemindersEnabled && prescriptionLowMeds.length > 0
? {
@@ -587,15 +623,19 @@ export function DashboardPage() {
</span>
</span>
<span data-label={t("table.stock")} className={textClass}>
{med?.packageType === "bottle"
? t("table.pillsCount", { count: Math.round(row.medsLeft) })
{med?.packageType === "bottle" ||
med?.packageType === "tube" ||
med?.packageType === "liquid_container"
? formatStockLabel(med, row.medsLeft)
: formatFullBlisters(stock.fullBlisters, t)}
</span>
<span
data-label={t("table.stockDetails")}
className={`${textClass}${med?.packageType === "bottle" ? " hide-on-card" : ""}`}
className={`${textClass}${med?.packageType === "bottle" || med?.packageType === "tube" || med?.packageType === "liquid_container" ? " hide-on-card" : ""}`}
>
{med?.packageType === "bottle"
{med?.packageType === "bottle" ||
med?.packageType === "tube" ||
med?.packageType === "liquid_container"
? "—"
: formatOpenBlisterAndLoose(
stock.openBlisterPills,
@@ -772,7 +812,7 @@ export function DashboardPage() {
</div>
</div>
<div className="tag-row">
<span className="tag subtle">{t("common.pillsTotal", { count: item.total })}</span>
<span className="tag subtle">{formatTotalUsageLabel(med, item.total)}</span>
{status && (
<span className={`status-chip small ${status.className}`}>{t(status.label)}</span>
)}
@@ -786,12 +826,12 @@ 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">
{dose.usage} {dose.usage !== 1 ? t("common.pills") : t("common.pill")}
</span>
{med?.pillWeightMg && (
<span className="dose-usage-weight">{`${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"}`}</span>
)}
<span className="dose-usage-main">{formatDoseUsageLabel(med, dose.usage)}</span>
{med?.packageType !== "tube" &&
med?.packageType !== "liquid_container" &&
med?.pillWeightMg && (
<span className="dose-usage-weight">{`${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"}`}</span>
)}
</span>
{dose.intakeRemindersEnabled && (
<span
@@ -1032,7 +1072,7 @@ export function DashboardPage() {
</div>
</div>
<div className="tag-row">
<span className="tag subtle">{t("common.pillsTotal", { count: item.total })}</span>
<span className="tag subtle">{formatTotalUsageLabel(med, item.total)}</span>
{status && (
<span className={`status-chip small ${status.className}`}>{t(status.label)}</span>
)}
@@ -1050,12 +1090,12 @@ export function DashboardPage() {
>
<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")}
</span>
{med?.pillWeightMg && (
<span className="dose-usage-weight">{`${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"}`}</span>
)}
<span className="dose-usage-main">{formatDoseUsageLabel(med, dose.usage)}</span>
{med?.packageType !== "tube" &&
med?.packageType !== "liquid_container" &&
med?.pillWeightMg && (
<span className="dose-usage-weight">{`${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"}`}</span>
)}
</span>
{dose.intakeRemindersEnabled && (
<span
@@ -1263,7 +1303,7 @@ export function DashboardPage() {
</div>
</div>
<div className="tag-row">
<span className="tag subtle">{t("common.pillsTotal", { count: item.total })}</span>
<span className="tag subtle">{formatTotalUsageLabel(med, item.total)}</span>
{status && (
<span className={`status-chip small ${status.className}`}>{t(status.label)}</span>
)}
@@ -1277,12 +1317,12 @@ 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">
{dose.usage} {dose.usage !== 1 ? t("common.pills") : t("common.pill")}
</span>
{med?.pillWeightMg && (
<span className="dose-usage-weight">{`${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"}`}</span>
)}
<span className="dose-usage-main">{formatDoseUsageLabel(med, dose.usage)}</span>
{med?.packageType !== "tube" &&
med?.packageType !== "liquid_container" &&
med?.pillWeightMg && (
<span className="dose-usage-weight">{`${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"}`}</span>
)}
</span>
{dose.intakeRemindersEnabled && (
<span
+34 -4
View File
@@ -122,6 +122,30 @@ export function PlannerPage() {
const canSendNotification =
(settings.emailEnabled && settings.notificationEmail) || (settings.shoutrrrEnabled && settings.shoutrrrUrl);
const getUsageUnitLabel = (medicationId: number, count: number): string => {
const med = meds.find((m) => m.id === medicationId);
if (med?.packageType === "liquid_container") {
return t("form.ml");
}
if (med?.packageType === "tube") {
return med.medicationForm === "liquid" ? t("form.ml") : t("blisters.applications");
}
return count === 1 ? t("common.pill") : t("common.pills");
};
const getAvailableLabel = (medicationId: number, loosePills: number): string => {
const med = meds.find((m) => m.id === medicationId);
const roundedLoose = Math.round(loosePills * 10) / 10;
if (med?.packageType === "liquid_container") {
return `${roundedLoose} ${t("form.ml")}`;
}
if (med?.packageType === "tube") {
const unit = med.medicationForm === "liquid" ? t("form.ml") : t("blisters.applications");
return `${roundedLoose} ${unit}`;
}
return `${roundedLoose} ${roundedLoose === 1 ? t("common.pill") : t("common.pills")}`;
};
async function sendPlannerNotification() {
if (!canSendNotification || plannerRows.length === 0) return;
setSendingPlannerEmail(true);
@@ -226,16 +250,22 @@ export function PlannerPage() {
<span data-label={t("planner.table.usage")}>
<span>
<strong>{row.plannerUsage}</strong>&nbsp;
{row.plannerUsage === 1 ? t("common.pill") : t("common.pills")}
{getUsageUnitLabel(row.medicationId, row.plannerUsage)}
</span>
</span>
<span data-label={t("planner.table.blisters")}>
{row.packageType === "bottle" ? "" : `${row.blistersNeeded} × ${row.blisterSize}`}
{row.packageType === "bottle" ||
row.packageType === "tube" ||
row.packageType === "liquid_container"
? ""
: `${row.blistersNeeded} × ${row.blisterSize}`}
</span>
<span data-label={t("planner.table.prescriptionRefills")}>{remainingRefills ?? ""}</span>
<span data-label={t("planner.table.available")}>
{row.packageType === "bottle" ? (
`${Math.round(row.loosePills * 10) / 10} ${Math.round(row.loosePills * 10) / 10 === 1 ? t("common.pill") : t("common.pills")}`
{row.packageType === "bottle" ||
row.packageType === "tube" ||
row.packageType === "liquid_container" ? (
getAvailableLabel(row.medicationId, row.loosePills)
) : (
<>
{row.fullBlisters} {t("common.blisters")}