mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Merge v189/recon-b into release/v1.18.9
Enlarge the OCR confirm and lab result-type tap targets to 44px; clamp the coach hero chips; switch the Vorsorge due-status to a calendar-day delta so an item due earlier today still reads as due today and a prior day reads overdue.
This commit is contained in:
commit
f9d3e83bab
5 files changed
+101
-16
No files matched your search
@@ -110,16 +110,20 @@ export function CoachHero({ composer, prompts, onPickPrompt }: CoachHeroProps) {
|
||||
type="button"
|
||||
onClick={() => onPickPrompt(prompt)}
|
||||
data-slot="coach-hero-chip"
|
||||
title={prompt}
|
||||
className={cn(
|
||||
"border-dracula-purple/20 hover:border-dracula-purple/45 hover:text-foreground",
|
||||
"text-muted-foreground inline-flex min-h-11 items-center gap-1.5 sm:min-h-9",
|
||||
"text-muted-foreground inline-flex min-h-11 max-w-full items-center gap-1.5 sm:min-h-9 sm:max-w-[18rem]",
|
||||
"rounded-full border bg-transparent px-3.5 py-1.5 text-xs sm:text-sm",
|
||||
"transition-colors focus-visible:ring-2 focus-visible:outline-none",
|
||||
"focus-visible:ring-dracula-purple/50",
|
||||
)}
|
||||
>
|
||||
<Quote className="size-3 shrink-0" aria-hidden="true" />
|
||||
<span>{prompt}</span>
|
||||
{/* Cap the chip to a tidy single line — long localized prompts
|
||||
(notably DE) otherwise wrap to ragged multi-line pills on a
|
||||
narrow viewport. The full prompt stays available via title. */}
|
||||
<span className="truncate">{prompt}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -245,7 +245,7 @@ export function LabForm({
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={resultType === "numeric" ? "secondary" : "ghost"}
|
||||
className="h-8 flex-1"
|
||||
className="min-h-11 flex-1 sm:min-h-9"
|
||||
onClick={() => setResultType("numeric")}
|
||||
aria-pressed={resultType === "numeric"}
|
||||
>
|
||||
@@ -255,7 +255,7 @@ export function LabForm({
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={resultType === "qualitative" ? "secondary" : "ghost"}
|
||||
className="h-8 flex-1"
|
||||
className="min-h-11 flex-1 sm:min-h-9"
|
||||
onClick={() => setResultType("qualitative")}
|
||||
aria-pressed={resultType === "qualitative"}
|
||||
>
|
||||
|
||||
@@ -46,15 +46,25 @@ export function OcrRowEditor({
|
||||
return (
|
||||
<div className="space-y-3 rounded-lg border p-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
id={`${fieldId}-confirm`}
|
||||
checked={row.confirmed}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...row, confirmed: checked === true })
|
||||
}
|
||||
className="mt-1 min-h-5 min-w-5"
|
||||
aria-label={t("labs.ocr.confirmRow")}
|
||||
/>
|
||||
{/* The checkbox is the PRIMARY per-row confirm on a touch-first OCR
|
||||
review screen, so it needs a ≥44px hit area on coarse pointers. The
|
||||
wrapping label supplies the touch target (and forwards the click to
|
||||
the control) without enlarging the 20px glyph or bloating the row on
|
||||
desktop, where the negative margins collapse the padding back. */}
|
||||
<label
|
||||
htmlFor={`${fieldId}-confirm`}
|
||||
className="-m-3 flex min-h-11 min-w-11 cursor-pointer items-start justify-center p-3 sm:m-0 sm:min-h-0 sm:min-w-0 sm:p-0"
|
||||
>
|
||||
<Checkbox
|
||||
id={`${fieldId}-confirm`}
|
||||
checked={row.confirmed}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...row, confirmed: checked === true })
|
||||
}
|
||||
className="mt-0.5 min-h-5 min-w-5 sm:mt-1"
|
||||
aria-label={t("labs.ocr.confirmRow")}
|
||||
/>
|
||||
</label>
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<Input
|
||||
value={row.analyte}
|
||||
|
||||
@@ -136,6 +136,68 @@ describe("<VorsorgeSection> loading + empty", () => {
|
||||
expect(html).not.toContain("Log value");
|
||||
});
|
||||
|
||||
it("reads a same-day due time as 'today' (calendar-day delta, not rolling 24h)", () => {
|
||||
// v1.18.9 recon — a reminder whose nextDue falls on the SAME local
|
||||
// calendar day must read "Due today" with the green status hue, even when
|
||||
// the wall-clock gap to now exceeds the 24h a rolling delta would key on.
|
||||
// Anchor the due instant to local noon today so the floored calendar-day
|
||||
// delta is 0 regardless of the hour the suite runs.
|
||||
const todayNoon = new Date();
|
||||
todayNoon.setHours(12, 0, 0, 0);
|
||||
const reminder: MeasurementReminder = {
|
||||
id: "due-today",
|
||||
label: "Blood pressure check",
|
||||
measurementType: "BLOOD_PRESSURE_SYS",
|
||||
intervalDays: 7,
|
||||
rrule: null,
|
||||
anchorDate: null,
|
||||
endsOn: null,
|
||||
origin: "VORSORGE",
|
||||
nextDueAt: todayNoon.toISOString(),
|
||||
notifyHour: 9,
|
||||
location: null,
|
||||
lastSatisfiedAt: null,
|
||||
enabled: true,
|
||||
createdAt: "2030-01-01T00:00:00.000Z",
|
||||
updatedAt: "2030-01-01T00:00:00.000Z",
|
||||
} as MeasurementReminder;
|
||||
remindersMock.mockReturnValue({ data: [reminder], isLoading: false });
|
||||
const html = render(<VorsorgeSection />);
|
||||
expect(html).toContain("Due today");
|
||||
// The status hue is the discreet success colour, never a card-bg tint.
|
||||
expect(html).toContain("text-success");
|
||||
});
|
||||
|
||||
it("reads a prior-day due time as 'overdue' once the calendar day has passed", () => {
|
||||
// A reminder whose due CALENDAR day is before today reads "Overdue", with
|
||||
// the warning hue — a rolling-24h delta would mis-bucket a yesterday-evening
|
||||
// due viewed this morning as "today".
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
yesterday.setHours(20, 0, 0, 0);
|
||||
const reminder: MeasurementReminder = {
|
||||
id: "overdue",
|
||||
label: "Annual physical",
|
||||
measurementType: null,
|
||||
intervalDays: 365,
|
||||
rrule: null,
|
||||
anchorDate: null,
|
||||
endsOn: null,
|
||||
origin: "VORSORGE",
|
||||
nextDueAt: yesterday.toISOString(),
|
||||
notifyHour: 9,
|
||||
location: null,
|
||||
lastSatisfiedAt: null,
|
||||
enabled: true,
|
||||
createdAt: "2030-01-01T00:00:00.000Z",
|
||||
updatedAt: "2030-01-01T00:00:00.000Z",
|
||||
} as MeasurementReminder;
|
||||
remindersMock.mockReturnValue({ data: [reminder], isLoading: false });
|
||||
const html = render(<VorsorgeSection />);
|
||||
expect(html).toContain("Overdue by 1 days");
|
||||
expect(html).toContain("text-warning");
|
||||
});
|
||||
|
||||
it("translates a COACH-origin label i18n key and shows the neutral badge", () => {
|
||||
const reminder: MeasurementReminder = {
|
||||
id: "c1",
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { relativeCalendarDate } from "@/lib/i18n/relative-time";
|
||||
import { startOfLocalDayInTz } from "@/lib/tz/local-day";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { applyOrder, useModuleListPrefs } from "@/lib/module-list-prefs";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
@@ -124,9 +125,17 @@ function relativeDueKey(
|
||||
now: number,
|
||||
): { key: string; days: number } {
|
||||
if (!nextDueAt) return { key: "nextDue.none", days: 0 };
|
||||
const due = new Date(nextDueAt).getTime();
|
||||
// Compare calendar-day deltas so "today" / "in 1 day" read cleanly.
|
||||
const deltaDays = Math.round((due - now) / DAY_MS);
|
||||
const due = new Date(nextDueAt);
|
||||
if (Number.isNaN(due.getTime())) return { key: "nextDue.none", days: 0 };
|
||||
// v1.18.9 (recon) — compare CALENDAR-day deltas in the user's local zone,
|
||||
// not a rolling-24h delta. A reminder due at 09:00 viewed the same evening
|
||||
// at 20:00 must still read "heute", and one whose due calendar-day is before
|
||||
// today must read "überfällig" — a raw `(due - now) / DAY_MS` round would
|
||||
// mis-bucket both. Floor each instant to its local day start (host-local zone
|
||||
// = the browser user's zone for this client component) before differencing.
|
||||
const dueDay = startOfLocalDayInTz(due, undefined).getTime();
|
||||
const nowDay = startOfLocalDayInTz(new Date(now), undefined).getTime();
|
||||
const deltaDays = Math.round((dueDay - nowDay) / DAY_MS);
|
||||
if (deltaDays < 0) return { key: "overdueByDays", days: Math.abs(deltaDays) };
|
||||
if (deltaDays === 0) return { key: "nextDue.today", days: 0 };
|
||||
if (deltaDays === 1) return { key: "nextDue.tomorrow", days: 1 };
|
||||
|
||||
Reference in new issue
Block a user