mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
Merge pull request #720 from MBombeck/fix/handover-p1-batch
Four data fixes: backfilled doses, impossible readings, stale today-claims, sleep units
This commit is contained in:
19 files changed
+1408
-30
No files matched your search
@@ -37,6 +37,7 @@ function deviation(
|
||||
high: 62,
|
||||
outside,
|
||||
direction: outside ? "above" : "in",
|
||||
daysAgo: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Activity, ArrowDownRight, ArrowUpRight } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { metricFractionDigits } from "@/lib/measurements/value-domain";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -44,6 +45,7 @@ export function HealthStatusCard({
|
||||
className,
|
||||
}: HealthStatusCardProps) {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
const { isAuthenticated } = useAuth();
|
||||
|
||||
const { data } = useQuery({
|
||||
@@ -60,6 +62,14 @@ export function HealthStatusCard({
|
||||
return key ? t(key) : type;
|
||||
};
|
||||
|
||||
// The engines hand over raw doubles. Interpolating one straight into the
|
||||
// sentence prints whatever floating-point produced — a pulse mean reads
|
||||
// "68.33333333333333" — and reads in the wrong number convention for every
|
||||
// locale that does not use a dot. Render each figure at the metric's own
|
||||
// decimal precision, through the same formatter the rest of the app uses.
|
||||
const metricValue = (type: string, value: number): string =>
|
||||
fmt.number(value, metricFractionDigits(type));
|
||||
|
||||
return (
|
||||
<section
|
||||
data-slot="health-status-section"
|
||||
@@ -100,9 +110,9 @@ export function HealthStatusCard({
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs leading-snug">
|
||||
{t("insights.healthStatus.rangeContext", {
|
||||
value: d.value,
|
||||
low: d.low,
|
||||
high: d.high,
|
||||
value: metricValue(d.type, d.value),
|
||||
low: metricValue(d.type, d.low),
|
||||
high: metricValue(d.type, d.high),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
@@ -137,8 +147,8 @@ export function HealthStatusCard({
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs leading-snug">
|
||||
{t("insights.healthStatus.shiftContext", {
|
||||
before: s.beforeMean,
|
||||
after: s.afterMean,
|
||||
before: metricValue(s.metric, s.beforeMean),
|
||||
after: metricValue(s.metric, s.afterMean),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,10 @@ import { Loader2, MoreHorizontal, Plus, RotateCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useUnitDisplay } from "@/hooks/use-unit-display";
|
||||
import {
|
||||
entryValueToCanonical,
|
||||
parseDecimalEntry,
|
||||
} from "@/lib/measurements/entry-units";
|
||||
import {
|
||||
invalidateKeys,
|
||||
measurementDependentKeys,
|
||||
@@ -343,7 +347,17 @@ export function MeasurementForm({
|
||||
// identity path — `fromDisplay` returns the typed number unchanged, so
|
||||
// the payload is byte-identical to before. Round only the converted
|
||||
// imperial value so 210 lb stores as 95.25 kg, not 95.25439770….
|
||||
const typed = parseFloat(value);
|
||||
//
|
||||
// A second boundary sits beside it: a few types are stored in a unit
|
||||
// the form does not ask for. Sleep is entered in hours and stored in
|
||||
// minutes, and sending the typed number through unconverted filed a
|
||||
// 7.5-hour night as seven and a half minutes — inside the column's
|
||||
// plausibility band, so nothing objected.
|
||||
const typed = parseDecimalEntry(value);
|
||||
if (typed === null) {
|
||||
setError(t("measurements.saveError"));
|
||||
return;
|
||||
}
|
||||
let canonicalValue = typed;
|
||||
if (unitDisplay.isTransformed(type)) {
|
||||
const inverted = unitDisplay.fromDisplay(type, typed);
|
||||
@@ -352,6 +366,7 @@ export function MeasurementForm({
|
||||
? Math.round(inverted * 100) / 100
|
||||
: inverted;
|
||||
}
|
||||
canonicalValue = entryValueToCanonical(type, canonicalValue);
|
||||
await apiPost("/api/measurements", {
|
||||
type,
|
||||
value: canonicalValue,
|
||||
@@ -533,9 +548,13 @@ export function MeasurementForm({
|
||||
>
|
||||
<Input
|
||||
id="value"
|
||||
type="number"
|
||||
// Not `type="number"`: its sanitisation drops a decimal comma on
|
||||
// the engines that do not localise the field, and "7,5" then
|
||||
// arrives as an empty string with no explanation. `inputMode`
|
||||
// still raises the numeric keypad, and the submit path reads the
|
||||
// comma — the same shape the labs and custom-metric fields use.
|
||||
inputMode="decimal"
|
||||
enterKeyHint="next"
|
||||
step="any"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder={
|
||||
|
||||
Reference in new issue
Block a user