fix: UI polish for intake form, dashboard cards, and schedule (#142)

- Intake form: replace remind checkbox with bell icon + toggle switch
- Intake form: smart takenBy dropdown based on medication's people
- Dashboard: hide DETAILS row for pill bottles on mobile cards
- Dashboard: use status-chip with icons in schedule view (past/today/future)
- Dashboard: reduce spacing between icons and status chips on mobile
- MedDetailModal: show package type in PACKAGE DETAILS heading
- PlannerPage: show dash for bottle blisters column
- Shorten Pill Bottle label in EN/DE translations
- Update related tests
This commit is contained in:
Daniel Volz
2026-02-08 22:13:52 +01:00
committed by GitHub
parent b07b586eef
commit 3de1b2ef0c
10 changed files with 120 additions and 59 deletions
+4 -1
View File
@@ -202,7 +202,10 @@ export function MedDetailModal({
{/* Package Details Section */}
<div className="med-detail-section">
<h3>{t("modal.packageDetails")}</h3>
<h3>
{t("modal.packageDetails")} (
{selectedMed.packageType === "bottle" ? t("form.packageTypeBottle") : t("form.packageTypeBlister")})
</h3>
<div className="med-detail-grid">
{selectedMed.packageType === "blister" ? (
<>
+28 -21
View File
@@ -426,26 +426,29 @@ export function MobileEditModal({
onChange={(e) => onSetIntakeValue(idx, "startTime", e.target.value)}
/>
</label>
<label className="compact full-row">
<span>{t("form.blisters.takenByIntake")}</span>
<select value={intake.takenBy} onChange={(e) => onSetIntakeValue(idx, "takenBy", e.target.value)}>
<option value="">{t("form.blisters.takenByEveryone")}</option>
{existingPeople.map((person) => (
<option key={person} value={person}>
{person}
</option>
))}
</select>
</label>
<label className="toggle-switch small" title={t("form.blisters.remindTooltip")}>
<input
type="checkbox"
checked={intake.intakeRemindersEnabled}
onChange={(e) => onSetIntakeValue(idx, "intakeRemindersEnabled", e.target.checked)}
/>
<span className="toggle-slider"></span>
</label>
<span className="legend-hint">🔔</span>
{form.takenBy.length === 0 ? null : (
<label className="compact full-row">
<span>{t("form.blisters.takenByIntake")}</span>
<select value={intake.takenBy} onChange={(e) => onSetIntakeValue(idx, "takenBy", e.target.value)}>
{form.takenBy.map((person) => (
<option key={person} value={person}>
{person}
</option>
))}
</select>
</label>
)}
<div className="remind-toggle-row" title={t("form.blisters.remindTooltip")}>
<span className="legend-hint">🔔</span>
<label className="toggle-switch small">
<input
type="checkbox"
checked={intake.intakeRemindersEnabled}
onChange={(e) => onSetIntakeValue(idx, "intakeRemindersEnabled", e.target.checked)}
/>
<span className="toggle-slider"></span>
</label>
</div>
{form.intakes.length > 1 && (
<button type="button" className="danger remove-blister-btn" onClick={() => onRemoveIntake(idx)}>
{t("common.remove")}
@@ -453,7 +456,11 @@ export function MobileEditModal({
)}
</div>
))}
<button type="button" className="ghost add-blister" onClick={() => onAddIntake()}>
<button
type="button"
className="ghost add-blister"
onClick={() => onAddIntake(form.takenBy.length === 1 ? form.takenBy[0] : undefined)}
>
+ {t("form.blisters.addIntake")}
</button>
</fieldset>
+1 -1
View File
@@ -135,7 +135,7 @@
"takenBy": "Eingenommen von",
"packageType": "Verpackungsart",
"packageTypeBlister": "Blisterpackung",
"packageTypeBottle": "Pillendose / Behälter",
"packageTypeBottle": "Pillendose",
"packs": "Packungen",
"blistersPerPack": "Blister pro Packung",
"pillsPerBlister": "Tabletten pro Blister",
+1 -1
View File
@@ -135,7 +135,7 @@
"takenBy": "Taken by",
"packageType": "Package Type",
"packageTypeBlister": "Blister Pack",
"packageTypeBottle": "Pill Bottle / Container",
"packageTypeBottle": "Pill Bottle",
"packs": "Packs",
"blistersPerPack": "Blisters per pack",
"pillsPerBlister": "Pills per blister",
+17 -4
View File
@@ -439,9 +439,12 @@ export function DashboardPage() {
? t("table.pillsCount", { count: Math.round(row.medsLeft) })
: formatFullBlisters(stock.fullBlisters, t)}
</span>
<span data-label={t("table.stockDetails")} className={textClass}>
<span
data-label={t("table.stockDetails")}
className={`${textClass}${med?.packageType === "bottle" ? " hide-on-card" : ""}`}
>
{med?.packageType === "bottle"
? "-"
? ""
: formatOpenBlisterAndLoose(
stock.openBlisterPills,
stock.loosePills,
@@ -597,6 +600,9 @@ export function DashboardPage() {
const med = meds.find((m) => m.name === item.medName);
const medCov = coverageByMed[item.medName];
const isEmpty = medCov ? medCov.medsLeft <= 0 : false;
const status = medCov
? getStockStatus(medCov.daysLeft, medCov.medsLeft, stockThresholds)
: null;
const itemDoseIds = expandDoseIds(item.doses);
const allTaken = itemDoseIds.every((id) => takenDoses.has(id));
return (
@@ -623,6 +629,9 @@ export function DashboardPage() {
<span className="tag subtle">
{item.total} {t("common.pills")} {t("common.total")}
</span>
{status && (
<span className={`status-chip small ${status.className}`}>{t(status.label)}</span>
)}
</div>
</div>
<div className="doses-col">
@@ -772,7 +781,9 @@ export function DashboardPage() {
<span className="tag subtle">
{item.total} {t("common.pills")} {t("common.total")}
</span>
{status && <span className={`tag ${status.className}`}>{t(status.label)}</span>}
{status && (
<span className={`status-chip small ${status.className}`}>{t(status.label)}</span>
)}
</div>
</div>
<div className="doses-col">
@@ -959,7 +970,9 @@ export function DashboardPage() {
<span className="tag subtle">
{item.total} {t("common.pills")} {t("common.total")}
</span>
{status && <span className={`tag ${status.className}`}>{t(status.label)}</span>}
{status && (
<span className={`status-chip small ${status.className}`}>{t(status.label)}</span>
)}
</div>
</div>
<div className="doses-col">
+27 -19
View File
@@ -632,7 +632,11 @@ export function MedicationsPage() {
<div className="card-head">
<h3>{t("form.blisters.title")}</h3>
<div className="blisters-actions">
<button type="button" className="primary" onClick={() => addIntake()}>
<button
type="button"
className="primary"
onClick={() => addIntake(form.takenBy.length === 1 ? form.takenBy[0] : undefined)}
>
+ {t("form.blisters.addIntake")}
</button>
</div>
@@ -675,25 +679,29 @@ export function MedicationsPage() {
onChange={(e) => setIntakeValue(idx, "startTime", e.target.value)}
/>
</label>
<label title={t("form.blisters.takenByTooltip")}>
{t("form.blisters.takenByIntake")}
<select value={intake.takenBy} onChange={(e) => setIntakeValue(idx, "takenBy", e.target.value)}>
<option value="">{t("form.blisters.takenByEveryone")}</option>
{existingPeople.map((person) => (
<option key={person} value={person}>
{person}
</option>
))}
</select>
</label>
<label className="inline-checkbox" title={t("form.blisters.remindTooltip")}>
<input
type="checkbox"
checked={intake.intakeRemindersEnabled}
onChange={(e) => setIntakeValue(idx, "intakeRemindersEnabled", e.target.checked)}
/>
{form.takenBy.length === 0 ? null : (
<label title={t("form.blisters.takenByTooltip")}>
{t("form.blisters.takenByIntake")}
<select value={intake.takenBy} onChange={(e) => setIntakeValue(idx, "takenBy", e.target.value)}>
{form.takenBy.map((person) => (
<option key={person} value={person}>
{person}
</option>
))}
</select>
</label>
)}
<div className="remind-toggle-row" title={t("form.blisters.remindTooltip")}>
<span>🔔</span>
</label>
<label className="toggle-switch small">
<input
type="checkbox"
checked={intake.intakeRemindersEnabled}
onChange={(e) => setIntakeValue(idx, "intakeRemindersEnabled", e.target.checked)}
/>
<span className="toggle-slider"></span>
</label>
</div>
</div>
{form.intakes.length > 1 && (
<button type="button" className="danger" onClick={() => removeIntake(idx)}>
+1 -3
View File
@@ -213,9 +213,7 @@ export function PlannerPage() {
<strong>{row.plannerUsage}</strong>&nbsp;{t("common.pills")}
</span>
<span data-label={t("planner.table.blisters")}>
{row.packageType === "bottle"
? `${row.plannerUsage} ${t("common.pills")}`
: `${row.blistersNeeded} × ${row.blisterSize}`}
{row.packageType === "bottle" ? "" : `${row.blistersNeeded} × ${row.blisterSize}`}
</span>
<span data-label={t("planner.table.available")}>
{row.packageType === "bottle" ? (
+28 -2
View File
@@ -1886,6 +1886,10 @@ textarea.auto-resize {
.status-chip.high::before {
content: "★";
}
.status-chip.small {
font-size: 0.7rem;
padding: 0.2rem 0.5rem;
}
@media (max-width: 760px) {
.table-head,
@@ -1896,6 +1900,9 @@ textarea.auto-resize {
.table-head {
display: none;
}
.table-row .hide-on-card {
display: none;
}
.table-row {
padding: 0.75rem;
display: flex;
@@ -1922,8 +1929,8 @@ textarea.auto-resize {
/* First span (name cell) - centered horizontal layout */
.table-row span:first-child {
justify-content: center;
padding-bottom: 0.5rem;
margin-bottom: 0.25rem;
padding-bottom: 0.15rem;
margin-bottom: 0;
}
.table-row span:first-child::before {
display: none; /* Hide "NAME" label on mobile */
@@ -1931,6 +1938,11 @@ textarea.auto-resize {
/* Status chip in table row - left aligned */
.table-row span.status-chip {
align-self: flex-start;
justify-content: flex-start;
gap: 0.4rem;
}
.table-row span.status-chip::before {
margin-right: 0;
}
/* Avatar + name layout - centered */
.table-row .cell-with-avatar {
@@ -5523,6 +5535,20 @@ a.about-version-link:hover {
justify-self: start;
}
.mobile-edit-form .blister-row .remind-toggle-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.blister-inputs .remind-toggle-row {
display: flex;
align-items: center;
gap: 0.5rem;
align-self: end;
padding-bottom: 0.5rem;
}
.mobile-edit-form .blister-row .datetime-inputs {
display: flex;
gap: 0.5rem;
@@ -424,8 +424,14 @@ describe("MobileEditModal takenBy", () => {
render(<MobileEditModal {...defaultProps} form={form} />);
expect(screen.getByText("John")).toBeInTheDocument();
expect(screen.getByText("Jane")).toBeInTheDocument();
// Check tags are rendered (use getAllByText since names also appear in intake dropdowns)
const johnElements = screen.getAllByText("John");
const janeElements = screen.getAllByText("Jane");
expect(johnElements.length).toBeGreaterThanOrEqual(1);
expect(janeElements.length).toBeGreaterThanOrEqual(1);
// Verify the tag elements specifically exist
expect(johnElements.some((el) => el.closest(".tag"))).toBe(true);
expect(janeElements.some((el) => el.closest(".tag"))).toBe(true);
});
it("calls onRemoveTakenByPerson when tag removed", () => {
@@ -755,9 +755,9 @@ describe("MedicationsPage intake reminders toggle", () => {
// Desktop form uses class "full blisters" container
const blistersContainer = document.querySelector(".blisters");
expect(blistersContainer).toBeInTheDocument();
// Check for the inline-checkbox that controls intake reminders in each blister row
const intakeCheckbox = document.querySelector(".blister-row .inline-checkbox");
expect(intakeCheckbox).toBeInTheDocument();
// Check for the remind-toggle-row that controls intake reminders in each blister row
const intakeToggle = document.querySelector(".blister-row .remind-toggle-row");
expect(intakeToggle).toBeInTheDocument();
});
it("can toggle intake reminders per intake", () => {
@@ -770,8 +770,8 @@ describe("MedicationsPage intake reminders toggle", () => {
</MemoryRouter>
);
// Each blister row has inline-checkbox for intake reminders
const checkbox = document.querySelector('.blister-row .inline-checkbox input[type="checkbox"]');
// Each blister row has remind-toggle-row for intake reminders
const checkbox = document.querySelector('.blister-row .remind-toggle-row input[type="checkbox"]');
if (checkbox) {
fireEvent.click(checkbox);
expect(setIntakeValue).toHaveBeenCalled();