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:
@@ -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" ? (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -213,9 +213,7 @@ export function PlannerPage() {
|
||||
<strong>{row.plannerUsage}</strong> {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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user