mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
feat(vorsorge): drop trend strip, relative last-done, discreet due status
Remove the 7-day bar strip under the Vorsorge card last-done line and its backing component, query key, and i18n strings. Render the last-done value as a relative date (today / yesterday / absolute) through a shared relativeCalendarDate helper that reuses the medication today/yesterday keys. Show the next-due status as discreet coloured text - green today, orange overdue - leaving the card surface neutral.
This commit is contained in:
1 parent
a5b3715a46
commit
590b08286a
11 files changed
+69
-189
No files matched your search
@@ -26,7 +26,6 @@ import { ListRow } from "@/components/ui/list-row";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { MeasurementForm } from "@/components/measurements/measurement-form";
|
||||
import { VorsorgeTrendStrip } from "@/components/measurement-reminders/vorsorge-trend-strip";
|
||||
import {
|
||||
useMeasurementReminders,
|
||||
useMeasurementReminderMutations,
|
||||
@@ -136,12 +135,6 @@ export function VorsorgeDashboardCard() {
|
||||
days: due.days,
|
||||
})}
|
||||
</Badge>
|
||||
{/* v1.18.7 (Wave E) — discreet 7-day strip under the
|
||||
metric context; renders nothing for a free-text
|
||||
reminder or a too-thin window. */}
|
||||
<VorsorgeTrendStrip
|
||||
measurementType={reminder.measurementType}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { relativeCalendarDate } from "@/lib/i18n/relative-time";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { applyOrder, useModuleListPrefs } from "@/lib/module-list-prefs";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
@@ -71,7 +72,6 @@ import { Progress } from "@/components/ui/progress";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { MeasurementForm } from "@/components/measurements/measurement-form";
|
||||
import { VorsorgeTrendStrip } from "@/components/measurement-reminders/vorsorge-trend-strip";
|
||||
import { MedicationCardHeader } from "@/components/medications/medication-card-header";
|
||||
import {
|
||||
useMeasurementReminders,
|
||||
@@ -929,20 +929,37 @@ function VorsorgeCard({
|
||||
// (MOD-06: never "Einnahme" / intake — a measurement is not an intake). The
|
||||
// "next" value carries the discreet neutral due badge; the "last" value is
|
||||
// the last satisfied date.
|
||||
// v1.18.6.1 — the due/overdue status renders as plain TEXT (not a chip),
|
||||
// and the "zuletzt erledigt" line ALWAYS renders, falling back to "—" when
|
||||
// there is no last-done value — mirroring the medication card's next/last
|
||||
// text rows.
|
||||
// v1.18.9 (#39) — "zuletzt erledigt" reads as a RELATIVE date in the
|
||||
// medication-card grammar (today → "Heute", yesterday → "Gestern", else the
|
||||
// absolute date) through the shared `relativeCalendarDate` helper, reusing
|
||||
// the same `medications.today` / `medications.yesterday` keys the medication
|
||||
// card renders. Falls back to "—" when there is no last-done value.
|
||||
const lastValue = reminder.lastSatisfiedAt
|
||||
? fmt.date(new Date(reminder.lastSatisfiedAt))
|
||||
? relativeCalendarDate(reminder.lastSatisfiedAt, t, (d) => fmt.date(d))
|
||||
: "—";
|
||||
|
||||
// v1.18.9 (#39) — the next-due status reads as DISCREET COLOURED TEXT (never
|
||||
// a card-background tint): due today → green, overdue → orange, otherwise the
|
||||
// normal muted-resolved date. The card surface stays neutral per the house
|
||||
// rule; only this inline label carries the status hue.
|
||||
const dueIsToday = due.key === "nextDue.today";
|
||||
const dueIsOverdue = due.key === "overdueByDays";
|
||||
const nextLastSlot = (
|
||||
<div className="min-h-[2.75rem] space-y-1.5 text-sm">
|
||||
<div className="text-muted-foreground flex items-baseline justify-between gap-3">
|
||||
<span className="min-w-0 flex-shrink truncate font-medium">
|
||||
{t("measurementReminders.nextDueLabel")}
|
||||
</span>
|
||||
<span className="text-foreground text-right">
|
||||
<span
|
||||
className={cn(
|
||||
"text-right",
|
||||
dueIsToday
|
||||
? "text-success font-medium"
|
||||
: dueIsOverdue
|
||||
? "text-warning font-medium"
|
||||
: "text-foreground",
|
||||
)}
|
||||
>
|
||||
{t(`measurementReminders.${due.key}`, { days: due.days })}
|
||||
</span>
|
||||
</div>
|
||||
@@ -972,12 +989,6 @@ function VorsorgeCard({
|
||||
<CardContent className="flex h-full flex-col space-y-3.5">
|
||||
{nextLastSlot}
|
||||
|
||||
{/* v1.18.7 (Wave E) — discreet 7-day strip of the metric's last
|
||||
readings, sitting under the metric context like the dashboard
|
||||
tiles. Renders nothing for a free-text reminder or a too-thin
|
||||
window, so the card height stays stable when absent. */}
|
||||
<VorsorgeTrendStrip measurementType={reminder.measurementType} />
|
||||
|
||||
{/* v1.18.6 (MOD-06) — light gamification: progress through the
|
||||
current interval toward next-due, reusing the medication card's
|
||||
Progress building block. Honest "on track / due" signal without
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* v1.18.7 (Wave E) — a small, DISCREET 7-day trend strip for a Vorsorge
|
||||
* card. Renders the reminder metric's last seven readings as seven quiet
|
||||
* bars (newest → rightmost), heights normalised across the window. The
|
||||
* colour is intentionally muted (`bg-muted-foreground/30`) so the strip
|
||||
* reads as calm context sitting under the metric — never an alarming
|
||||
* status signal, in keeping with the no-loud-colour card ethos.
|
||||
*
|
||||
* Pulls the bounded last-7 readings for one `MeasurementType` through the
|
||||
* existing measurements list endpoint (a single `limit=7` read per card,
|
||||
* cached by type) — no new heavy aggregate query. A free-text reminder
|
||||
* (no `measurementType`) renders nothing; a metric with fewer than two
|
||||
* readings in the window renders nothing (a single dot is no trend).
|
||||
*/
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
interface RecentMeasurement {
|
||||
value: number;
|
||||
measuredAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounded last-7 read for one measurement type, sorted newest-first by the
|
||||
* server. Re-sorted oldest → newest here so the bars read left (older) to
|
||||
* right (newest). Shares the `measurements` invalidation prefix so a fresh
|
||||
* reading (e.g. completing this very reminder) repaints the strip.
|
||||
*/
|
||||
function useRecentValues(measurementType: string | null, enabled: boolean) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.measurementRecentValues(measurementType ?? ""),
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("type", measurementType as string);
|
||||
params.set("limit", "7");
|
||||
params.set("sortBy", "measuredAt");
|
||||
params.set("sortDir", "desc");
|
||||
const res = await apiGet<{ measurements: RecentMeasurement[] }>(
|
||||
`/api/measurements?${params}`,
|
||||
);
|
||||
return res.measurements.slice().reverse();
|
||||
},
|
||||
enabled: enabled && measurementType != null,
|
||||
});
|
||||
}
|
||||
|
||||
export function VorsorgeTrendStrip({
|
||||
measurementType,
|
||||
enabled = true,
|
||||
}: {
|
||||
measurementType: string | null;
|
||||
enabled?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
const { data } = useRecentValues(measurementType, enabled);
|
||||
|
||||
// Nothing to show for a free-text reminder or a too-thin window.
|
||||
if (measurementType == null) return null;
|
||||
const readings = data ?? [];
|
||||
if (readings.length < 2) return null;
|
||||
|
||||
const values = readings.map((r) => r.value);
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const span = max - min || 1;
|
||||
|
||||
// 7-day direction from first → last reading in the window. Latest value
|
||||
// and direction feed the sr-only summary so a screen reader gets the
|
||||
// trend, not just the generic "Last 7 days" label.
|
||||
const latest = values[values.length - 1];
|
||||
const delta = latest - values[0];
|
||||
const direction =
|
||||
delta > 0
|
||||
? t("measurementReminders.trendStrip.rising")
|
||||
: delta < 0
|
||||
? t("measurementReminders.trendStrip.falling")
|
||||
: t("measurementReminders.trendStrip.steady");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-6 items-end gap-0.5"
|
||||
role="img"
|
||||
aria-label={t("measurementReminders.trendStrip.label")}
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t("measurementReminders.trendStrip.summary", {
|
||||
value: latest,
|
||||
direction,
|
||||
})}
|
||||
</span>
|
||||
{readings.map((r, i) => {
|
||||
// Floor at ~18% so a flat series still reads as bars, not a line.
|
||||
const pct = 0.18 + ((r.value - min) / span) * 0.82;
|
||||
return (
|
||||
<span
|
||||
key={r.measuredAt}
|
||||
data-index={i}
|
||||
className="bg-muted-foreground/30 w-1.5 rounded-sm"
|
||||
style={{ height: `${Math.round(pct * 100)}%` }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user