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:
@@ -192,12 +192,20 @@ export function MedDetailModal({
|
||||
]);
|
||||
|
||||
if (!selectedMed) return null;
|
||||
const isTube = selectedMed.packageType === "tube" || selectedMed.packageType === "liquid_container";
|
||||
const stockUnitLabel = isTube
|
||||
? selectedMed.packageType === "liquid_container" || selectedMed.medicationForm === "liquid"
|
||||
? "ml"
|
||||
: t("form.blisters.applications")
|
||||
: null;
|
||||
|
||||
const medCoverage = coverage.all.find((c) => c.name === getMedDisplayName(selectedMed));
|
||||
const packageSize = getPackageSize(selectedMed);
|
||||
// Structural max = sealed package capacity only (excludes pre-existing looseTablets).
|
||||
const structuralMax =
|
||||
selectedMed.packageType === "bottle"
|
||||
selectedMed.packageType === "bottle" ||
|
||||
selectedMed.packageType === "tube" ||
|
||||
selectedMed.packageType === "liquid_container"
|
||||
? (selectedMed.totalPills ?? packageSize)
|
||||
: selectedMed.packCount * selectedMed.blistersPerPack * selectedMed.pillsPerBlister;
|
||||
const currentStock = medCoverage ? Math.round(medCoverage.medsLeft) : getMedTotal(selectedMed);
|
||||
@@ -209,7 +217,11 @@ export function MedDetailModal({
|
||||
const currentPartialPills = Math.max(0, stock.openBlisterPills);
|
||||
const currentLoosePills = Math.max(0, stock.loosePills);
|
||||
const stockDisplayTotal =
|
||||
selectedMed.packageType === "bottle" ? (selectedMed.totalPills ?? packageSize) : Math.max(0, structuralMax);
|
||||
selectedMed.packageType === "bottle" ||
|
||||
selectedMed.packageType === "tube" ||
|
||||
selectedMed.packageType === "liquid_container"
|
||||
? (selectedMed.totalPills ?? packageSize)
|
||||
: Math.max(0, structuralMax);
|
||||
const maxPartialPills = Math.min(
|
||||
Math.max(0, selectedMed.pillsPerBlister),
|
||||
Math.max(0, structuralMax - Math.max(0, editStockFullBlisters) * selectedMed.pillsPerBlister)
|
||||
@@ -392,7 +404,9 @@ export function MedDetailModal({
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{selectedMed.packageType === "bottle" && (
|
||||
{(selectedMed.packageType === "bottle" ||
|
||||
selectedMed.packageType === "tube" ||
|
||||
selectedMed.packageType === "liquid_container") && (
|
||||
<p className="edit-stock-cap-info">{t("editStock.packageSize", { count: structuralMax })}</p>
|
||||
)}
|
||||
{showStockCapNotice && (
|
||||
@@ -402,7 +416,10 @@ export function MedDetailModal({
|
||||
{(() => {
|
||||
const dbTotal = getMedTotal(selectedMed);
|
||||
const currentTotal = medCoverage ? Math.round(medCoverage.medsLeft) : dbTotal;
|
||||
const isBottle = selectedMed.packageType === "bottle";
|
||||
const isBottle =
|
||||
selectedMed.packageType === "bottle" ||
|
||||
selectedMed.packageType === "tube" ||
|
||||
selectedMed.packageType === "liquid_container";
|
||||
const enteredTotal = isBottle
|
||||
? editStockPartialBlisterPills
|
||||
: editStockFullBlisters * selectedMed.pillsPerBlister +
|
||||
@@ -590,20 +607,25 @@ export function MedDetailModal({
|
||||
<div className="summary-row">
|
||||
<span>{t("editStock.currentTotal")}:</span>
|
||||
<span>
|
||||
{currentTotal} {currentTotal === 1 ? t("common.pill") : t("common.pills")}
|
||||
{currentTotal}
|
||||
{isTube ? ` ${stockUnitLabel}` : ` ${currentTotal === 1 ? t("common.pill") : t("common.pills")}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="summary-row">
|
||||
<span>{t("editStock.newTotal")}:</span>
|
||||
<span>
|
||||
{newTotal} {newTotal === 1 ? t("common.pill") : t("common.pills")}
|
||||
{newTotal}
|
||||
{isTube ? ` ${stockUnitLabel}` : ` ${newTotal === 1 ? t("common.pill") : t("common.pills")}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`summary-row difference ${differenceClass}`}>
|
||||
<span>{t("editStock.difference")}:</span>
|
||||
<span>
|
||||
{difference > 0 ? "+" : ""}
|
||||
{difference} {Math.abs(difference) === 1 ? t("common.pill") : t("common.pills")}
|
||||
{difference}
|
||||
{isTube
|
||||
? ` ${stockUnitLabel}`
|
||||
: ` ${Math.abs(difference) === 1 ? t("common.pill") : t("common.pills")}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -737,7 +759,14 @@ export function MedDetailModal({
|
||||
<div className="med-detail-section">
|
||||
<h3>
|
||||
{t("modal.packageDetails")} (
|
||||
{selectedMed.packageType === "bottle" ? t("form.packageTypeBottle") : t("form.packageTypeBlister")})
|
||||
{selectedMed.packageType === "bottle"
|
||||
? t("form.packageTypeBottle")
|
||||
: selectedMed.packageType === "tube"
|
||||
? t("form.packageTypeTube")
|
||||
: selectedMed.packageType === "liquid_container"
|
||||
? t("form.packageTypeLiquidContainer")
|
||||
: t("form.packageTypeBlister")}
|
||||
)
|
||||
</h3>
|
||||
<div className="med-detail-grid">
|
||||
{selectedMed.packageType === "blister" ? (
|
||||
@@ -757,7 +786,7 @@ export function MedDetailModal({
|
||||
</>
|
||||
) : (
|
||||
<div className="med-detail-item">
|
||||
<span className="med-detail-label">{t("form.totalCapacity")}</span>
|
||||
<span className="med-detail-label">{isTube ? t("form.totalAmount") : t("form.totalCapacity")}</span>
|
||||
<span className="med-detail-value">{(selectedMed.totalPills ?? packageSize) || "—"}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -816,7 +845,8 @@ export function MedDetailModal({
|
||||
return (
|
||||
<div key={`${intake.start}-${intake.usage}-${intake.every}-${idx}`} className="med-schedule-item">
|
||||
<span className="med-schedule-usage">
|
||||
{totalUsage} {totalUsage !== 1 ? t("common.pills") : t("common.pill")}
|
||||
{totalUsage}
|
||||
{isTube ? ` ${stockUnitLabel}` : ` ${totalUsage !== 1 ? t("common.pills") : t("common.pill")}`}
|
||||
{selectedMed.pillWeightMg &&
|
||||
` (${totalUsage * selectedMed.pillWeightMg} ${selectedMed.doseUnit ?? "mg"})`}
|
||||
</span>
|
||||
@@ -955,11 +985,13 @@ export function MedDetailModal({
|
||||
<span className="refill-amount">
|
||||
{(() => {
|
||||
const total =
|
||||
selectedMed.packageType === "bottle"
|
||||
selectedMed.packageType === "bottle" ||
|
||||
selectedMed.packageType === "tube" ||
|
||||
selectedMed.packageType === "liquid_container"
|
||||
? entry.loosePillsAdded
|
||||
: entry.packsAdded * selectedMed.blistersPerPack * selectedMed.pillsPerBlister +
|
||||
entry.loosePillsAdded;
|
||||
return `+${total} ${total === 1 ? t("common.pill") : t("common.pills")}`;
|
||||
return `+${total}${isTube ? ` ${stockUnitLabel}` : ` ${total === 1 ? t("common.pill") : t("common.pills")}`}`;
|
||||
})()}
|
||||
{entry.usedPrescription && (
|
||||
<span className="refill-prescription-badge" title={t("refill.viaPrescription")}>
|
||||
@@ -1128,7 +1160,9 @@ export function MedDetailModal({
|
||||
className="success"
|
||||
onClick={() => onSubmitRefill(selectedMed.id, usePrescriptionRefill)}
|
||||
disabled={
|
||||
(selectedMed.packageType === "bottle"
|
||||
(selectedMed.packageType === "bottle" ||
|
||||
selectedMed.packageType === "tube" ||
|
||||
selectedMed.packageType === "liquid_container"
|
||||
? refillLoose < 1
|
||||
: cappedRefillPacks < 1 && refillLoose < 1) ||
|
||||
exceedsPrescriptionPackLimit ||
|
||||
@@ -1144,7 +1178,8 @@ export function MedDetailModal({
|
||||
: refillLoose;
|
||||
return totalRefill > 0 ? (
|
||||
<span className="refill-preview">
|
||||
+{totalRefill} {totalRefill === 1 ? t("common.pill") : t("common.pills")}
|
||||
+{totalRefill}
|
||||
{isTube ? ` ${stockUnitLabel}` : ` ${totalRefill === 1 ? t("common.pill") : t("common.pills")}`}
|
||||
</span>
|
||||
) : null;
|
||||
})()}
|
||||
|
||||
@@ -298,6 +298,32 @@ function fmtDateTime(iso: string | null | undefined): string {
|
||||
return `${m[3]}.${m[2]}.${m[1]} ${m[4]}:${m[5]}`;
|
||||
}
|
||||
|
||||
function getTubeUnitKey(med: Medication): "form.ml" | "blisters.applications" {
|
||||
if (med.packageType === "liquid_container") return "form.ml";
|
||||
return med.medicationForm === "liquid" ? "form.ml" : "blisters.applications";
|
||||
}
|
||||
|
||||
function getUsageText(med: Medication, usage: number, t: TFn): string {
|
||||
if (med.packageType === "tube" || med.packageType === "liquid_container") {
|
||||
return `${usage} ${t(getTubeUnitKey(med))}`;
|
||||
}
|
||||
return `${usage} ${usage === 1 ? t("common.pill") : t("common.pills")}`;
|
||||
}
|
||||
|
||||
function getTotalCapacityLabel(med: Medication, t: TFn): string {
|
||||
if (med.packageType === "tube" || med.packageType === "liquid_container") {
|
||||
return t("form.totalAmountLabel", { unit: t(getTubeUnitKey(med)) });
|
||||
}
|
||||
return t("report.docTotalCapacity");
|
||||
}
|
||||
|
||||
function getCurrentStockText(med: Medication, t: TFn): string {
|
||||
if (med.packageType === "tube" || med.packageType === "liquid_container") {
|
||||
return `${getPackageSize(med)} ${t(getTubeUnitKey(med))}`;
|
||||
}
|
||||
return `${getPackageSize(med)} ${t("common.pills")}`;
|
||||
}
|
||||
|
||||
function generateTextReport(
|
||||
meds: Medication[],
|
||||
reportData: ReportData,
|
||||
@@ -341,7 +367,16 @@ function generateTextReport(
|
||||
// Package / Stock
|
||||
lines.push(h3(t("report.docPackage")));
|
||||
lines.push(
|
||||
item(t("report.docPackageType"), med.packageType === "bottle" ? t("report.docBottle") : t("report.docBlister"))
|
||||
item(
|
||||
t("report.docPackageType"),
|
||||
med.packageType === "bottle"
|
||||
? t("report.docBottle")
|
||||
: med.packageType === "tube"
|
||||
? t("report.docTube")
|
||||
: med.packageType === "liquid_container"
|
||||
? t("form.packageTypeLiquidContainer")
|
||||
: t("report.docBlister")
|
||||
)
|
||||
);
|
||||
if (med.packageType === "blister") {
|
||||
lines.push(item(t("report.docPacks"), String(med.packCount)));
|
||||
@@ -349,10 +384,11 @@ function generateTextReport(
|
||||
lines.push(item(t("report.docPillsPerBlister"), String(med.pillsPerBlister)));
|
||||
if (med.looseTablets > 0) lines.push(item(t("report.docLoosePills"), String(med.looseTablets)));
|
||||
} else {
|
||||
lines.push(item(t("report.docTotalCapacity"), String(med.totalPills ?? med.looseTablets)));
|
||||
lines.push(item(getTotalCapacityLabel(med, t), String(med.totalPills ?? med.looseTablets)));
|
||||
}
|
||||
lines.push(item(t("report.docCurrentStock"), `${getPackageSize(med)} ${t("common.pills")}`));
|
||||
if (med.pillWeightMg) lines.push(item(t("report.docDosePerPill"), `${med.pillWeightMg} ${med.doseUnit ?? "mg"}`));
|
||||
lines.push(item(t("report.docCurrentStock"), getCurrentStockText(med, t)));
|
||||
if (med.packageType !== "tube" && med.packageType !== "liquid_container" && med.pillWeightMg)
|
||||
lines.push(item(t("report.docDosePerPill"), `${med.pillWeightMg} ${med.doseUnit ?? "mg"}`));
|
||||
if (med.expiryDate) lines.push(item(t("report.docExpiryDate"), fmtDate(med.expiryDate)));
|
||||
if (med.notes) lines.push(item(t("report.docNotes"), med.notes));
|
||||
lines.push("");
|
||||
@@ -365,7 +401,7 @@ function generateTextReport(
|
||||
if (intakes?.length) {
|
||||
lines.push(h3(t("report.docIntakeSchedule")));
|
||||
for (const intake of intakes) {
|
||||
let entry = `${intake.usage} ${intake.usage === 1 ? t("common.pill") : t("common.pills")}`;
|
||||
let entry = getUsageText(med, intake.usage, t);
|
||||
entry += ` ${intake.every === 1 ? t("common.daily") : t("common.everyNDays", { count: intake.every })}`;
|
||||
entry += ` ${t("form.blisters.from")} ${fmtDateTime(intake.start)}`;
|
||||
if ("takenBy" in intake && intake.takenBy)
|
||||
@@ -407,7 +443,7 @@ function generateTextReport(
|
||||
if (data.refills.length > 0) {
|
||||
lines.push(h3(t("report.docRefillHistory")));
|
||||
for (const r of data.refills) {
|
||||
let entry = `${fmtDate(r.refillDate)}: +${r.packsAdded} ${t("report.docPacks")}, +${r.loosePillsAdded} ${t("common.pills")}`;
|
||||
let entry = `${fmtDate(r.refillDate)}: +${r.packsAdded} ${t("report.docPacks")}, +${r.loosePillsAdded} ${med.packageType === "tube" || med.packageType === "liquid_container" ? t(getTubeUnitKey(med)) : t("common.pills")}`;
|
||||
if (r.usedPrescription) entry += ` ${t("report.docRefillPrescription")}`;
|
||||
lines.push(fmt === "md" ? `- ${entry}` : ` • ${entry}`);
|
||||
}
|
||||
@@ -539,7 +575,15 @@ function buildPrintHtml(
|
||||
// Package / Stock
|
||||
s += `<h3>${escHtml(t("report.docPackage"))}</h3>`;
|
||||
s += `<table><tbody>`;
|
||||
s += `<tr><td class="label">${escHtml(t("report.docPackageType"))}</td><td>${escHtml(med.packageType === "bottle" ? t("report.docBottle") : t("report.docBlister"))}</td></tr>`;
|
||||
s += `<tr><td class="label">${escHtml(t("report.docPackageType"))}</td><td>${escHtml(
|
||||
med.packageType === "bottle"
|
||||
? t("report.docBottle")
|
||||
: med.packageType === "tube"
|
||||
? t("report.docTube")
|
||||
: med.packageType === "liquid_container"
|
||||
? t("form.packageTypeLiquidContainer")
|
||||
: t("report.docBlister")
|
||||
)}</td></tr>`;
|
||||
if (med.packageType === "blister") {
|
||||
s += `<tr><td class="label">${escHtml(t("report.docPacks"))}</td><td>${med.packCount}</td></tr>`;
|
||||
s += `<tr><td class="label">${escHtml(t("report.docBlistersPerPack"))}</td><td>${med.blistersPerPack}</td></tr>`;
|
||||
@@ -547,10 +591,10 @@ function buildPrintHtml(
|
||||
if (med.looseTablets > 0)
|
||||
s += `<tr><td class="label">${escHtml(t("report.docLoosePills"))}</td><td>${med.looseTablets}</td></tr>`;
|
||||
} else {
|
||||
s += `<tr><td class="label">${escHtml(t("report.docTotalCapacity"))}</td><td>${med.totalPills ?? med.looseTablets}</td></tr>`;
|
||||
s += `<tr><td class="label">${escHtml(getTotalCapacityLabel(med, t))}</td><td>${med.totalPills ?? med.looseTablets}</td></tr>`;
|
||||
}
|
||||
s += `<tr><td class="label">${escHtml(t("report.docCurrentStock"))}</td><td>${getPackageSize(med)} ${escHtml(t("common.pills"))}</td></tr>`;
|
||||
if (med.pillWeightMg)
|
||||
s += `<tr><td class="label">${escHtml(t("report.docCurrentStock"))}</td><td>${escHtml(getCurrentStockText(med, t))}</td></tr>`;
|
||||
if (med.packageType !== "tube" && med.packageType !== "liquid_container" && med.pillWeightMg)
|
||||
s += `<tr><td class="label">${escHtml(t("report.docDosePerPill"))}</td><td>${med.pillWeightMg} ${escHtml(med.doseUnit ?? "mg")}</td></tr>`;
|
||||
if (med.expiryDate)
|
||||
s += `<tr><td class="label">${escHtml(t("report.docExpiryDate"))}</td><td>${fmtDate(med.expiryDate)}</td></tr>`;
|
||||
@@ -567,7 +611,7 @@ function buildPrintHtml(
|
||||
s += `<h3>${escHtml(t("report.docIntakeSchedule"))}</h3>`;
|
||||
s += `<ul>`;
|
||||
for (const intake of filteredPrintIntakes) {
|
||||
let entry = `${intake.usage} ${escHtml(intake.usage === 1 ? t("common.pill") : t("common.pills"))}`;
|
||||
let entry = escHtml(getUsageText(med, intake.usage, t));
|
||||
entry += ` ${escHtml(intake.every === 1 ? t("common.daily") : t("common.everyNDays", { count: intake.every }))}`;
|
||||
entry += ` ${escHtml(t("form.blisters.from"))} ${fmtDateTime(intake.start)}`;
|
||||
if ("takenBy" in intake && intake.takenBy)
|
||||
@@ -614,7 +658,7 @@ function buildPrintHtml(
|
||||
s += `<h3>${escHtml(t("report.docRefillHistory"))}</h3>`;
|
||||
s += `<ul>`;
|
||||
for (const r of data.refills) {
|
||||
let entry = `${fmtDate(r.refillDate)}: +${r.packsAdded} ${escHtml(t("report.docPacks"))}, +${r.loosePillsAdded} ${escHtml(t("common.pills"))}`;
|
||||
let entry = `${fmtDate(r.refillDate)}: +${r.packsAdded} ${escHtml(t("report.docPacks"))}, +${r.loosePillsAdded} ${escHtml(med.packageType === "tube" || med.packageType === "liquid_container" ? t(getTubeUnitKey(med)) : t("common.pills"))}`;
|
||||
if (r.usedPrescription) entry += ` <em>${escHtml(t("report.docRefillPrescription"))}</em>`;
|
||||
s += `<li>${entry}</li>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user