feat: obsolete medication archiving, start date, and UI improvements (#215)

* feat: obsolete medication archiving, start date, and UI improvements

- Add soft-archive (obsolete) for medications with dedicated section and toggle
- Add medication start date field with date picker and validation
- Add obsolete/reactivate API endpoints with proper auth
- Filter obsolete meds from schedule, coverage, planner, and notifications
- Improve UserFilterModal with intake schedules, stock badges, and click-to-open
- Improve dashboard taken-by badges with per-intake bell icons
- Add Escape key support to ConfirmModal and MobileEditModal
- Fix Lightbox close button positioning near image
- Add read-only mode support for MobileEditModal
- DB migrations: 0008 (is_obsolete, obsolete_at), 0009 (medication_start_date)
- All user-facing text uses i18n keys (en + de)

* test: fix tests for obsolete medications and UI changes

- Backend: add is_obsolete, obsolete_at, medication_start_date columns to test schemas
- Backend: add test medication inserts in planner tests for active-med filtering
- Frontend: update useMedications URL to include includeObsolete param
- Frontend: fix MobileEditModal selectors and validation assertions
- Frontend: add onClearUser prop to UserFilterModal test renders
- Frontend: fix MedicationsPage and DashboardPage test assertions
This commit is contained in:
Daniel Volz
2026-02-15 23:23:38 +01:00
committed by GitHub
parent c47a35d642
commit 4b697374f6
38 changed files with 2042 additions and 907 deletions
+26 -34
View File
@@ -607,42 +607,34 @@ export function DashboardPage() {
<span data-label={t("table.name")} className="cell-with-avatar">
<span className="med-name-line">
<MedicationAvatar name={row.name} imageUrl={med?.imageUrl} />
<span className="med-name-text">{row.name}</span>
{med?.takenBy &&
med.takenBy.length > 0 &&
med.takenBy.map((person) => (
<span
key={person}
className="taken-by-badge clickable"
onClick={(e) => {
e.stopPropagation();
openUserFilter(person);
}}
>
{person}
<span className="med-name-block-dash">
<span className="med-name-text">{row.name}</span>
{med?.takenBy && med.takenBy.length > 0 && (
<span className="med-taken-by-line">
{med.takenBy.map((person) => (
<span
key={person}
className="taken-by-badge clickable"
onClick={(e) => {
e.stopPropagation();
openUserFilter(person);
}}
>
{person}
{med.intakes?.some((i) => i.takenBy === person && i.intakeRemindersEnabled) && " 🔔"}
</span>
))}
</span>
))}
)}
</span>
</span>
{(() => {
const hasIntakeReminders =
med?.intakes?.some((i) => i.intakeRemindersEnabled) ?? med?.intakeRemindersEnabled;
return (
(hasIntakeReminders || med?.notes) && (
<span className="med-icons">
{hasIntakeReminders && (
<span className="reminder-icon info-tooltip" data-tooltip={t("tooltips.intakeReminders")}>
🔔
</span>
)}
{med?.notes && (
<span className="notes-icon info-tooltip" data-tooltip={t("tooltips.hasNotes")}>
📝
</span>
)}
</span>
)
);
})()}
{med?.notes && (
<span className="med-icons">
<span className="notes-icon info-tooltip" data-tooltip={t("tooltips.hasNotes")}>
📝
</span>
</span>
)}
</span>
<span data-label={t("table.stock")} className={textClass}>
{med?.packageType === "bottle"
File diff suppressed because it is too large Load Diff
+3 -9
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { MedicationAvatar } from "../components";
import { DateTimeInput, MedicationAvatar } from "../components";
import { useAuth } from "../components/Auth";
import { useAppContext } from "../context";
import type { PlannerRow } from "../types";
@@ -158,8 +158,7 @@ export function PlannerPage() {
<form className="planner" onSubmit={runPlanner}>
<label>
{t("planner.from")}
<input
type="datetime-local"
<DateTimeInput
step="60"
value={range.start}
onChange={(e) => setRange({ ...range, start: e.target.value })}
@@ -167,12 +166,7 @@ export function PlannerPage() {
</label>
<label>
{t("planner.until")}
<input
type="datetime-local"
step="60"
value={range.end}
onChange={(e) => setRange({ ...range, end: e.target.value })}
/>
<DateTimeInput step="60" value={range.end} onChange={(e) => setRange({ ...range, end: e.target.value })} />
</label>
<label className="planner-checkbox">
<input