feat: frontend improvements - shared schedule, bottle type, settings UI, planner notifications (#146)

- Rewrite SharedSchedule to match DashboardPage rendering with time-based consumption
- Add bottle package type support across all views (MedDetail, Refill, Planner, Dashboard)
- Redesign settings page with colored threshold chips, validation, and stock reminder display
- Add shareStockStatus toggle and send manual reminder button
- Pill/pills singular/plural consistency across all views
- Planner send notification via push (Shoutrrr) in addition to email
- Stock overflow warning and past-missed day styling
- Update README: bottles in Smart Inventory, push in Trip Planner, new ENV section
- 708 passing frontend tests including new coverage for all changes
This commit is contained in:
Daniel Volz
2026-02-09 19:33:54 +01:00
committed by GitHub
parent f56f2b7c88
commit 3ec1460c4e
24 changed files with 2115 additions and 572 deletions
+21 -11
View File
@@ -117,8 +117,11 @@ export function PlannerPage() {
}
}
async function sendPlannerEmail() {
if (!settings.notificationEmail || plannerRows.length === 0) return;
const canSendNotification =
(settings.emailEnabled && settings.notificationEmail) || (settings.shoutrrrEnabled && settings.shoutrrrUrl);
async function sendPlannerNotification() {
if (!canSendNotification || plannerRows.length === 0) return;
setSendingPlannerEmail(true);
setPlannerEmailResult(null);
@@ -136,12 +139,12 @@ export function PlannerPage() {
});
const data = await res.json();
if (res.ok) {
setPlannerEmailResult({ success: true, message: data.message || "Email sent!" });
setPlannerEmailResult({ success: true, message: data.message || t("common.sent") });
} else {
setPlannerEmailResult({ success: false, message: data.error || "Failed to send" });
setPlannerEmailResult({ success: false, message: data.error || t("common.sendFailed") });
}
} catch {
setPlannerEmailResult({ success: false, message: "Network error" });
setPlannerEmailResult({ success: false, message: t("common.networkError") });
}
setSendingPlannerEmail(false);
}
@@ -210,18 +213,20 @@ export function PlannerPage() {
{row.medicationName}
</span>
<span data-label={t("planner.table.usage")}>
<strong>{row.plannerUsage}</strong>&nbsp;{t("common.pills")}
<strong>{row.plannerUsage}</strong>&nbsp;
{row.plannerUsage === 1 ? t("common.pill") : t("common.pills")}
</span>
<span data-label={t("planner.table.blisters")}>
{row.packageType === "bottle" ? "" : `${row.blistersNeeded} × ${row.blisterSize}`}
</span>
<span data-label={t("planner.table.available")}>
{row.packageType === "bottle" ? (
`${Math.round(row.loosePills * 10) / 10} ${t("common.pills")}`
`${Math.round(row.loosePills * 10) / 10} ${Math.round(row.loosePills * 10) / 10 === 1 ? t("common.pill") : t("common.pills")}`
) : (
<>
{row.fullBlisters} {t("common.blisters")}
{row.loosePills > 0 && ` + ${Math.round(row.loosePills * 10) / 10} ${t("common.pills")}`}
{row.loosePills > 0 &&
` + ${Math.round(row.loosePills * 10) / 10} ${Math.round(row.loosePills * 10) / 10 === 1 ? t("common.pill") : t("common.pills")}`}
</>
)}
</span>
@@ -235,10 +240,15 @@ export function PlannerPage() {
);
})}
</div>
{settings.emailEnabled && settings.notificationEmail && (
{canSendNotification && (
<div className="planner-email-action">
<button type="button" className="ghost" onClick={sendPlannerEmail} disabled={sendingPlannerEmail}>
{sendingPlannerEmail ? t("common.sending") : t("planner.sendEmail")}
<button
type="button"
className="ghost"
onClick={sendPlannerNotification}
disabled={sendingPlannerEmail}
>
{sendingPlannerEmail ? t("common.sending") : t("planner.sendNotification")}
</button>
{plannerEmailResult && (
<span className={plannerEmailResult.success ? "success-text" : "danger-text"}>