diff --git a/src/components/medications/glp1-medication-card.tsx b/src/components/medications/glp1-medication-card.tsx index 8a07ef2c9..50d6687c6 100644 --- a/src/components/medications/glp1-medication-card.tsx +++ b/src/components/medications/glp1-medication-card.tsx @@ -5,6 +5,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { Card, CardContent } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader"; import { MedicationCardMenu } from "@/components/medications/medication-card-menu"; import { MedicationStateBadges } from "@/components/medications/card-parts/medication-state-badges"; @@ -394,7 +395,7 @@ export function Glp1MedicationCard({ const categoryLabel = getMedicationCategoryLabel(medication.category, t); return ( - + - + {/* Take-now / overdue / very-overdue pill, shared with the generic medication card. The GLP-1 card historically omitted this row, which made Mounjaro feel different from @@ -419,8 +420,12 @@ export function Glp1MedicationCard({ /> )} - {/* Injection state — last + next */} -
+ {/* Injection state — last + next. The reserved min-height keeps a + card whose last-injection line is absent the same vertical + footprint as one that renders both lines, so the compliance bars + and action row below align row-for-row across the grid — matching + the generic medication card's reserved last/next slot. */} +
{medication.lastTakenAt && (

{lastSite @@ -493,12 +498,17 @@ export function Glp1MedicationCard({ {/* Primary actions row — shared with the generic medication card. The GLP-1-specific side-effect quick-log lives in the header-actions overflow (kebab), not this row, so Mounjaro - and Ramipril share the canonical two-button primary row. */} + and Ramipril share the canonical two-button primary row. + Pinned to the bottom with `mt-auto` so the action baseline is + shared across every card in the grid row, identical to the + generic medication card. */} {medication.active && ( - +

+ +
)} diff --git a/src/components/medications/medication-card.tsx b/src/components/medications/medication-card.tsx index c4500a1ff..26a2c9a11 100644 --- a/src/components/medications/medication-card.tsx +++ b/src/components/medications/medication-card.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useReducer } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { Card, CardContent } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader"; import { MedicationCardMenu } from "@/components/medications/medication-card-menu"; import { MedicationStateBadges } from "@/components/medications/card-parts/medication-state-badges"; @@ -299,7 +300,7 @@ export function MedicationCard({ ); return ( - + - + {/* Status, last & next intake info */} {currentWindowStatus.status && ( )} - {nextSchedule && - currentWindowStatus.status !== "in_window" && - (() => { - const s = nextSchedule; + {/* Next / last intake — wrapped in a fixed min-height slot so a + card missing its last-dose line keeps the same vertical + footprint as a sibling that renders both lines. Without the + reserved height the compliance bars and action row below would + start at a different Y card-to-card across the 2-col grid. */} +
+ {nextSchedule && + currentWindowStatus.status !== "in_window" && + (() => { + const s = nextSchedule; - // Format day label relative to today - let dayLabel = ""; - if (nextAt) { - const nextDate = toBerlinDate(new Date(nextAt)); - const todayStr = `${nowBerlin.getFullYear()}-${nowBerlin.getMonth()}-${nowBerlin.getDate()}`; - const nextStr = `${nextDate.getFullYear()}-${nextDate.getMonth()}-${nextDate.getDate()}`; - const tomorrow = new Date(nowBerlin); - tomorrow.setDate(tomorrow.getDate() + 1); - const tomorrowStr = `${tomorrow.getFullYear()}-${tomorrow.getMonth()}-${tomorrow.getDate()}`; + // Format day label relative to today + let dayLabel = ""; + if (nextAt) { + const nextDate = toBerlinDate(new Date(nextAt)); + const todayStr = `${nowBerlin.getFullYear()}-${nowBerlin.getMonth()}-${nowBerlin.getDate()}`; + const nextStr = `${nextDate.getFullYear()}-${nextDate.getMonth()}-${nextDate.getDate()}`; + const tomorrow = new Date(nowBerlin); + tomorrow.setDate(tomorrow.getDate() + 1); + const tomorrowStr = `${tomorrow.getFullYear()}-${tomorrow.getMonth()}-${tomorrow.getDate()}`; - const diffDays = Math.round( - (nextDate.getTime() - nowBerlin.getTime()) / - (24 * 60 * 60 * 1000), - ); + const diffDays = Math.round( + (nextDate.getTime() - nowBerlin.getTime()) / + (24 * 60 * 60 * 1000), + ); - if (nextStr === todayStr) { - dayLabel = t("medications.today"); - } else if (nextStr === tomorrowStr) { - dayLabel = t("medications.tomorrow"); - } else if (diffDays <= 5) { - const weekdayLabels = [ - t("medications.weekdaySunday"), - t("medications.weekdayMonday"), - t("medications.weekdayTuesday"), - t("medications.weekdayWednesday"), - t("medications.weekdayThursday"), - t("medications.weekdayFriday"), - t("medications.weekdaySaturday"), - ]; - dayLabel = weekdayLabels[nextDate.getDay()]; - } else { - dayLabel = fmt.dateWithWeekday(nextDate); + if (nextStr === todayStr) { + dayLabel = t("medications.today"); + } else if (nextStr === tomorrowStr) { + dayLabel = t("medications.tomorrow"); + } else if (diffDays <= 5) { + const weekdayLabels = [ + t("medications.weekdaySunday"), + t("medications.weekdayMonday"), + t("medications.weekdayTuesday"), + t("medications.weekdayWednesday"), + t("medications.weekdayThursday"), + t("medications.weekdayFriday"), + t("medications.weekdaySaturday"), + ]; + dayLabel = weekdayLabels[nextDate.getDay()]; + } else { + dayLabel = fmt.dateWithWeekday(nextDate); + } } - } - return ( -

- - {t("medications.nextIntake")} - {" "} - {dayLabel && `${dayLabel}, `} - {formatTimeWindowRange(s.windowStart, s.windowEnd, locale)} - {s.label && ( - ({s.label}) - )} - {s.dose && ( - - {" "} - — {s.dose} - - )} -

- ); - })()} + return ( +

+ + {t("medications.nextIntake")} + {" "} + {dayLabel && `${dayLabel}, `} + {formatTimeWindowRange(s.windowStart, s.windowEnd, locale)} + {s.label && ( + ({s.label}) + )} + {s.dose && ( + + {" "} + — {s.dose} + + )} +

+ ); + })()} - {medication.lastTakenAt && ( -

- {t("medications.lastIntake")}{" "} - {formatLastTakenAt(medication.lastTakenAt)} -

- )} + {medication.lastTakenAt && ( +

+ {t("medications.lastIntake")}{" "} + {formatLastTakenAt(medication.lastTakenAt)} +

+ )} +
{/* Compliance bars — always two rows. The server scales the two windows to the dosing cadence (7 / 30 days for dense meds, @@ -401,12 +409,17 @@ export function MedicationCard({ /> )} - {/* Quick actions — primary buttons of the medication card. */} + {/* Quick actions — primary buttons of the medication card. + Pinned to the bottom with `mt-auto` so the action baseline is + shared across every card in the grid row regardless of how many + optional rows above it rendered. */} {medication.active && ( - +
+ +
)}