From 3aadc03424436836a5b8a9aedad8a704e6789f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 20 Oct 2026 13:24:01 +0200 Subject: [PATCH] refactor(insights): one MetricCrosstab for the three crosstab boards The mood tag, rated mood factor and cycle phase boards were three copies of the same list: pair label, neutral signed delta, confidence chip, detail line, optional dismissal. MetricCrosstab owns that shape; each board keeps its labels, unit conversion and number formatting. The cycle board's own confidence strings were identical to the shared ones and are gone. --- messages/de.json | 5 +- messages/en.json | 5 +- messages/es.json | 5 +- messages/fr.json | 5 +- messages/it.json | 5 +- messages/ko.json | 5 +- messages/nl.json | 5 +- messages/pl.json | 5 +- src/components/cycle/cycle-phase-crosstab.tsx | 118 ++++-------- .../__tests__/metric-crosstab.test.tsx | 71 +++++++ src/components/insights/metric-crosstab.tsx | 140 ++++++++++++++ .../mood/mood-factor-metric-crosstab.tsx | 179 +++++------------- .../mood/mood-tag-metric-crosstab.tsx | 153 ++++----------- 13 files changed, 338 insertions(+), 363 deletions(-) create mode 100644 src/components/insights/__tests__/metric-crosstab.test.tsx create mode 100644 src/components/insights/metric-crosstab.tsx diff --git a/messages/de.json b/messages/de.json index 231873ea2..a583ed16d 100644 --- a/messages/de.json +++ b/messages/de.json @@ -9191,10 +9191,7 @@ "unitSteps": "Schritte", "unitBpm": "S/min", "unitMs": "ms", - "unitMood": "Pkt.", - "confidenceLow": "Geringe Sicherheit", - "confidenceMedium": "Mittlere Sicherheit", - "confidenceHigh": "Hohe Sicherheit" + "unitMood": "Pkt." }, "title": "Zyklus-Erkenntnisse", "symptomPatternsTitle": "Symptommuster nach Phase", diff --git a/messages/en.json b/messages/en.json index 1a855b9e2..35ae9c711 100644 --- a/messages/en.json +++ b/messages/en.json @@ -9191,10 +9191,7 @@ "unitSteps": "steps", "unitBpm": "bpm", "unitMs": "ms", - "unitMood": "pts", - "confidenceLow": "Low confidence", - "confidenceMedium": "Medium confidence", - "confidenceHigh": "High confidence" + "unitMood": "pts" }, "title": "Cycle insights", "symptomPatternsTitle": "Symptom patterns by phase", diff --git a/messages/es.json b/messages/es.json index 6b77db170..2cfd320aa 100644 --- a/messages/es.json +++ b/messages/es.json @@ -9191,10 +9191,7 @@ "unitSteps": "pasos", "unitBpm": "lpm", "unitMs": "ms", - "unitMood": "pts", - "confidenceLow": "Confianza baja", - "confidenceMedium": "Confianza media", - "confidenceHigh": "Confianza alta" + "unitMood": "pts" }, "title": "Análisis del ciclo", "symptomPatternsTitle": "Patrones de síntomas por fase", diff --git a/messages/fr.json b/messages/fr.json index 01b82668d..1370a2b4e 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -9191,10 +9191,7 @@ "unitSteps": "pas", "unitBpm": "bpm", "unitMs": "ms", - "unitMood": "pts", - "confidenceLow": "Confiance faible", - "confidenceMedium": "Confiance moyenne", - "confidenceHigh": "Confiance élevée" + "unitMood": "pts" }, "title": "Analyses du cycle", "symptomPatternsTitle": "Tendances des symptômes par phase", diff --git a/messages/it.json b/messages/it.json index 426bf3535..6d1583fda 100644 --- a/messages/it.json +++ b/messages/it.json @@ -9191,10 +9191,7 @@ "unitSteps": "passi", "unitBpm": "bpm", "unitMs": "ms", - "unitMood": "pts", - "confidenceLow": "Affidabilità bassa", - "confidenceMedium": "Affidabilità media", - "confidenceHigh": "Affidabilità alta" + "unitMood": "pts" }, "title": "Analisi del ciclo", "symptomPatternsTitle": "Pattern dei sintomi per fase", diff --git a/messages/ko.json b/messages/ko.json index 88ba08f68..c98d10cf8 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -9191,10 +9191,7 @@ "unitSteps": "걸음", "unitBpm": "bpm", "unitMs": "ms", - "unitMood": "점", - "confidenceLow": "신뢰도 낮음", - "confidenceMedium": "신뢰도 보통", - "confidenceHigh": "신뢰도 높음" + "unitMood": "점" }, "title": "주기 인사이트", "symptomPatternsTitle": "단계별 증상 패턴", diff --git a/messages/nl.json b/messages/nl.json index 0a330b827..c51305972 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -9191,10 +9191,7 @@ "unitSteps": "stappen", "unitBpm": "bpm", "unitMs": "ms", - "unitMood": "pnt", - "confidenceLow": "Lage betrouwbaarheid", - "confidenceMedium": "Gemiddelde betrouwbaarheid", - "confidenceHigh": "Hoge betrouwbaarheid" + "unitMood": "pnt" }, "title": "Cyclusinzichten", "symptomPatternsTitle": "Symptoompatronen per fase", diff --git a/messages/pl.json b/messages/pl.json index 09e13cad4..06a06f600 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -9191,10 +9191,7 @@ "unitSteps": "kroki", "unitBpm": "ud./min", "unitMs": "ms", - "unitMood": "pkt", - "confidenceLow": "Niska pewność", - "confidenceMedium": "Średnia pewność", - "confidenceHigh": "Wysoka pewność" + "unitMood": "pkt" }, "title": "Analizy cyklu", "symptomPatternsTitle": "Wzorce objawów wg fazy", diff --git a/src/components/cycle/cycle-phase-crosstab.tsx b/src/components/cycle/cycle-phase-crosstab.tsx index 57433890b..a46a12917 100644 --- a/src/components/cycle/cycle-phase-crosstab.tsx +++ b/src/components/cycle/cycle-phase-crosstab.tsx @@ -8,7 +8,7 @@ import { applyDisplayTransformDelta, getReadingTransform, } from "@/lib/measurements/display-transform"; -import { cn } from "@/lib/utils"; +import { MetricCrosstab } from "@/components/insights/metric-crosstab"; /** * v1.15 — CYCLE_PHASE × vital crosstab card. @@ -69,18 +69,6 @@ const UNIT_KEY: Partial> = { mood: "cycle.insights.crosstab.unitMood", }; -const CONFIDENCE_KEY: Record = { - low: "cycle.insights.crosstab.confidenceLow", - medium: "cycle.insights.crosstab.confidenceMedium", - high: "cycle.insights.crosstab.confidenceHigh", -}; - -const CONFIDENCE_CLASS: Record = { - low: "bg-secondary text-muted-foreground", - medium: "bg-info/15 text-info", - high: "bg-success/15 text-success", -}; - /** Whole numbers for steps; one decimal otherwise. */ function fmt( value: number, @@ -207,78 +195,40 @@ export function CyclePhaseCrosstab({ const { t } = useTranslations(); const nf = useFormatters(); const unitDisplay = useUnitDisplay(); - if (rows.length === 0) return null; - return ( -
-

- {t("cycle.insights.crosstab.description")} -

-
    - {rows.map((row) => { - const metricLabel = t( - METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey, - ); - const { + { + const metricLabel = t(METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey); + const { + unit, + abs: absConv, + delta: deltaConv, + } = rowUnits(row.display, unitDisplay, t); + // `delta` = lutealAvg − follicularAvg: positive = the vital runs + // higher in the luteal phase. + return { + key: row.metricKey, + metricKey: row.metricKey, + pairLabel: t("cycle.insights.crosstab.pairLabel", { + metric: metricLabel, + }), + up: row.delta >= 0, + deltaValue: fmt(deltaConv(row.delta), row.display, nf), + unit, + confidence: row.confidence, + detail: t("cycle.insights.crosstab.detail", { + metric: metricLabel, + lutealAvg: fmt(absConv(row.lutealAvg), row.display, nf), + follicularAvg: fmt(absConv(row.follicularAvg), row.display, nf), unit, - abs: absConv, - delta: deltaConv, - } = rowUnits(row.display, unitDisplay, t); - // `delta` = lutealAvg − follicularAvg: positive = the vital runs - // higher in the luteal phase. The number stays NEUTRAL — higher is - // good for steps but bad for resting HR, and this board is - // observational, so colouring it would imply a verdict the data - // doesn't support. The sign prefix + `data-direction` carry it. - const up = row.delta >= 0; - const deltaText = `${up ? "+" : ""}${fmt(deltaConv(row.delta), row.display, nf)} ${unit}`; - return ( -
  • -
    - - {t("cycle.insights.crosstab.pairLabel", { - metric: metricLabel, - })} - - - {deltaText} - - - {t(CONFIDENCE_KEY[row.confidence])} - -
    -

    - {t("cycle.insights.crosstab.detail", { - metric: metricLabel, - lutealAvg: fmt(absConv(row.lutealAvg), row.display, nf), - follicularAvg: fmt( - absConv(row.follicularAvg), - row.display, - nf, - ), - unit, - lutealDays: row.lutealDays, - follicularDays: row.follicularDays, - })} -

    -
  • - ); - })} -
-
+ lutealDays: row.lutealDays, + follicularDays: row.follicularDays, + }), + }; + })} + /> ); } diff --git a/src/components/insights/__tests__/metric-crosstab.test.tsx b/src/components/insights/__tests__/metric-crosstab.test.tsx new file mode 100644 index 000000000..7fd1f334a --- /dev/null +++ b/src/components/insights/__tests__/metric-crosstab.test.tsx @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import { Moon } from "lucide-react"; + +import { I18nProvider } from "@/lib/i18n/context"; +import { MetricCrosstab, type MetricCrosstabRow } from "../metric-crosstab"; + +function row(overrides: Partial = {}): MetricCrosstabRow { + return { + key: "a", + metricKey: "restingHeartRate", + pairLabel: "Resting heart rate", + up: true, + deltaValue: "2,5", + unit: "bpm", + confidence: "medium", + detail: "62 vs 59.5 bpm", + ...overrides, + }; +} + +function render(rows: MetricCrosstabRow[]) { + return renderToStaticMarkup( + + + , + ); +} + +describe("", () => { + it("renders nothing for no rows", () => { + expect(render([])).toBe(""); + }); + + it("prints the delta as given, signed, and never coloured", () => { + const html = render([row()]); + expect(html).toContain('data-slot="board"'); + expect(html).toContain('data-slot="board-row"'); + expect(html).toContain("+2,5 bpm"); + expect(html).toContain('data-direction="up"'); + expect(html).not.toMatch(/text-(success|destructive)/); + }); + + it("leaves a negative delta's own sign alone", () => { + const html = render([row({ up: false, deltaValue: "-1,2" })]); + expect(html).toContain("-1,2 bpm"); + expect(html).not.toContain("+-1,2"); + expect(html).toContain('data-direction="down"'); + }); + + it("states confidence through the grade chip", () => { + const html = render([row({ confidence: "high" })]); + expect(html).toMatch(/data-slot="score-pill"[^>]*data-tone="success"/); + expect(html).toContain("High confidence"); + }); + + it("draws the row icon when one is given", () => { + expect(render([row({ icon: Moon })])).toContain(" { + const html = render([row({ patternId: "p1", dismissed: true })]); + expect(html).not.toContain('data-slot="board-row"'); + }); +}); diff --git a/src/components/insights/metric-crosstab.tsx b/src/components/insights/metric-crosstab.tsx new file mode 100644 index 000000000..3211ff5d3 --- /dev/null +++ b/src/components/insights/metric-crosstab.tsx @@ -0,0 +1,140 @@ +"use client"; + +import type { LucideIcon } from "lucide-react"; + +import { CONFIDENCE_TONE, ScorePill } from "@/components/ui/score-pill"; +import { useTranslations } from "@/lib/i18n/context"; +import { + PatternDismissButton, + PatternDismissedNotice, + usePatternDismissalOverrides, +} from "@/components/insights/pattern-dismiss-action"; + +/** + * The "this vital on these days vs those days" board: one row per pair, the + * signed difference, a confidence chip, one detail line with both means and + * the day count. Three surfaces carry it — mood tag × metric, rated mood + * factor × metric, cycle phase × vital — and they were three copies of the + * same markup that had begun to differ (the cycle copy printed its numbers + * with a full stop in every language). + * + * The caller owns the meaning: it resolves the labels, converts the units and + * formats the numbers (`deltaValue` already in the reader's notation, signed + * when negative). This component owns the shape: the row, the neutral delta + * (a higher value is good on one row and bad on the next, so the number is + * never coloured; the sign and `data-direction` carry the direction), the + * confidence chip, and the optional per-row dismissal. + */ + +export type MetricCrosstabConfidence = "low" | "medium" | "high"; + +export interface MetricCrosstabRow { + /** React key. */ + key: string; + /** `data-metric` on the row. */ + metricKey: string; + pairLabel: string; + icon?: LucideIcon | null; + /** True when the difference is zero or positive. */ + up: boolean; + /** The difference in the reader's notation, with its minus sign. */ + deltaValue: string; + unit: string; + confidence: MetricCrosstabConfidence; + detail: string; + /** A dismissible pattern; omitted on boards that cannot be dismissed. */ + patternId?: string; + canonicalKey?: string; + dismissed?: boolean; +} + +const CONFIDENCE_KEY: Record = { + low: "insights.mood.influence.confidenceLow", + medium: "insights.mood.influence.confidenceMedium", + high: "insights.mood.influence.confidenceHigh", +}; + +export function MetricCrosstab({ + slot, + rowSlot, + description, + rows, +}: { + /** `data-slot` of the board. */ + slot: string; + /** `data-slot` of each row. */ + rowSlot: string; + description: string; + rows: readonly MetricCrosstabRow[]; +}) { + const { t } = useTranslations(); + const dismissal = usePatternDismissalOverrides(); + if (rows.length === 0) return null; + + return ( +
+

{description}

+
    + {rows.map((row) => { + const patternId = row.patternId; + if ( + patternId && + dismissal.isDismissed(patternId, row.dismissed === true) + ) { + return ( +
  • + dismissal.setDismissed(patternId, false)} + onSettled={() => dismissal.clearDismissed(patternId, false)} + /> +
  • + ); + } + const Icon = row.icon; + return ( +
  • +
    + {Icon ? ( +
    +

    {row.detail}

    +
  • + ); + })} +
+
+ ); +} diff --git a/src/components/insights/mood/mood-factor-metric-crosstab.tsx b/src/components/insights/mood/mood-factor-metric-crosstab.tsx index 7c9875098..412c15c3f 100644 --- a/src/components/insights/mood/mood-factor-metric-crosstab.tsx +++ b/src/components/insights/mood/mood-factor-metric-crosstab.tsx @@ -4,13 +4,8 @@ import { useFormatters, useTranslations } from "@/lib/i18n/context"; import type { Formatters } from "@/lib/format-locale"; import { useUnitDisplay } from "@/hooks/use-unit-display"; import { moodTagIcon } from "@/components/mood/mood-tag-icons"; -import { cn } from "@/lib/utils"; +import { MetricCrosstab } from "@/components/insights/metric-crosstab"; import type { MoodInfluenceConfidence } from "./mood-tag-influence"; -import { - PatternDismissButton, - PatternDismissedNotice, - usePatternDismissalOverrides, -} from "@/components/insights/pattern-dismiss-action"; /** * v1.14.0 — RATED-factor × vital crosstab card. @@ -72,18 +67,6 @@ const UNIT_KEY: Record = { mmHg: "insights.mood.factorCrosstab.unitMmHg", }; -const CONFIDENCE_KEY: Record = { - low: "insights.mood.influence.confidenceLow", - medium: "insights.mood.influence.confidenceMedium", - high: "insights.mood.influence.confidenceHigh", -}; - -const CONFIDENCE_CLASS: Record = { - low: "bg-secondary text-muted-foreground", - medium: "bg-info/15 text-info", - high: "bg-success/15 text-success", -}; - /** Whole numbers for steps; one decimal otherwise. */ function formatValue( value: number, @@ -101,120 +84,56 @@ export function MoodFactorMetricCrosstab({ const { t } = useTranslations(); const nf = useFormatters(); const unitDisplay = useUnitDisplay(); - const dismissal = usePatternDismissalOverrides(); - if (rows.length === 0) return null; - return ( -
-

- {t("insights.mood.factorCrosstab.description")} -

-
    - {rows.map((row) => { - const patternId = row.patternId; - if ( - patternId && - dismissal.isDismissed(patternId, row.dismissed === true) - ) { - return ( -
  • - dismissal.setDismissed(patternId, false)} - onSettled={() => dismissal.clearDismissed(patternId, false)} - /> -
  • - ); - } - const Icon = moodTagIcon(row.icon); - const factorLabel = t(row.labelKey); - const metricLabel = t( - METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey, - ); - const pairLabel = t("insights.mood.factorCrosstab.pairLabel", { + { + const factorLabel = t(row.labelKey); + const metricLabel = t(METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey); + // The weight ("kg") row follows the metric/imperial preference: the + // unit symbol and the values (averages and delta, all in kg, + // offset-free) convert together. Every other display is unitless to + // the preference and keeps its i18n label and raw value. + const isWeightRow = row.display === "kg"; + const unit = isWeightRow + ? unitDisplay.unitFor("WEIGHT") + : t(UNIT_KEY[row.display]); + const conv = (v: number) => + isWeightRow ? unitDisplay.toDisplay("WEIGHT", v) : v; + // `delta` = lowAvg − highAvg: positive = the vital runs higher on the + // low-factor (worse) days. + return { + key: `${row.metricKey}:${row.factor}`, + metricKey: row.metricKey, + pairLabel: t("insights.mood.factorCrosstab.pairLabel", { factor: factorLabel, metric: metricLabel, - }); - // v1.32.26 — the weight ("kg") row follows the metric/imperial - // preference: the unit symbol + the values (averages + delta, all in - // kg, offset-free) convert together. Every other display is unitless - // to the preference and keeps its i18n label + raw value. - const isWeightRow = row.display === "kg"; - const unit = isWeightRow - ? unitDisplay.unitFor("WEIGHT") - : t(UNIT_KEY[row.display]); - const conv = (v: number) => - isWeightRow ? unitDisplay.toDisplay("WEIGHT", v) : v; - // `delta` = lowAvg − highAvg: positive = the vital runs higher on the - // low-factor (worse) days. The number stays NEUTRAL — a higher value - // is good for steps but bad for resting HR, and this board is - // observational ("associations, not causes"), so colouring it - // green/red would imply a health verdict the data doesn't support. - // The sign prefix + `data-direction` carry the direction. - const up = row.delta >= 0; - const deltaText = `${up ? "+" : ""}${formatValue(conv(row.delta), row.display, nf)} ${unit}`; - return ( -
  • -
    - {Icon && ( -
    -

    - {t( - row.inverse - ? "insights.mood.factorCrosstab.detailInverse" - : "insights.mood.factorCrosstab.detail", - { - factor: factorLabel, - metric: metricLabel, - lowAvg: formatValue(conv(row.lowAvg), row.display, nf), - highAvg: formatValue(conv(row.highAvg), row.display, nf), - unit, - lowDays: row.lowDays, - }, - )} -

    -
  • - ); - })} -
-
+ }), + icon: moodTagIcon(row.icon), + up: row.delta >= 0, + deltaValue: formatValue(conv(row.delta), row.display, nf), + unit, + confidence: row.confidence, + detail: t( + row.inverse + ? "insights.mood.factorCrosstab.detailInverse" + : "insights.mood.factorCrosstab.detail", + { + factor: factorLabel, + metric: metricLabel, + lowAvg: formatValue(conv(row.lowAvg), row.display, nf), + highAvg: formatValue(conv(row.highAvg), row.display, nf), + unit, + lowDays: row.lowDays, + }, + ), + patternId: row.patternId, + canonicalKey: row.canonicalKey, + dismissed: row.dismissed, + }; + })} + /> ); } diff --git a/src/components/insights/mood/mood-tag-metric-crosstab.tsx b/src/components/insights/mood/mood-tag-metric-crosstab.tsx index 2bc485ff0..7762ea1a3 100644 --- a/src/components/insights/mood/mood-tag-metric-crosstab.tsx +++ b/src/components/insights/mood/mood-tag-metric-crosstab.tsx @@ -3,13 +3,8 @@ import { useFormatters, useTranslations } from "@/lib/i18n/context"; import type { Formatters } from "@/lib/format-locale"; import { moodTagIcon } from "@/components/mood/mood-tag-icons"; -import { cn } from "@/lib/utils"; +import { MetricCrosstab } from "@/components/insights/metric-crosstab"; import type { MoodInfluenceConfidence } from "./mood-tag-influence"; -import { - PatternDismissButton, - PatternDismissedNotice, - usePatternDismissalOverrides, -} from "@/components/insights/pattern-dismiss-action"; /** * v1.12.0 — tag × health-metric crosstab card. @@ -76,22 +71,6 @@ const UNIT_KEY: Record = { ms: "insights.mood.crosstab.unitMs", }; -const CONFIDENCE_KEY: Record = { - low: "insights.mood.influence.confidenceLow", - medium: "insights.mood.influence.confidenceMedium", - high: "insights.mood.influence.confidenceHigh", -}; - -// Confidence chips carry COLORED TEXT, so they ride the semantic feedback -// tokens (`--info` / `--success`) — these carry the Alucard light-mode -// overrides that clear AA on the white card. Raw `--dracula-*` stays bright -// green/cyan in light mode and fails AA for text. -const CONFIDENCE_CLASS: Record = { - low: "bg-secondary text-muted-foreground", - medium: "bg-info/15 text-info", - high: "bg-success/15 text-success", -}; - /** One decimal for hours/score, whole numbers for kcal. */ function formatValue( value: number, @@ -108,103 +87,43 @@ export function MoodTagMetricCrosstab({ }) { const { t } = useTranslations(); const nf = useFormatters(); - const dismissal = usePatternDismissalOverrides(); - if (rows.length === 0) return null; - return ( -
-

- {t("insights.mood.crosstab.description")} -

-
    - {rows.map((row) => { - const patternId = row.patternId; - if ( - patternId && - dismissal.isDismissed(patternId, row.dismissed === true) - ) { - return ( -
  • - dismissal.setDismissed(patternId, false)} - onSettled={() => dismissal.clearDismissed(patternId, false)} - /> -
  • - ); - } - const Icon = moodTagIcon(row.icon); - const tagLabel = row.label ?? t(row.labelKey); - const metricLabel = t( - METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey, - ); - const pairLabel = t("insights.mood.crosstab.pairLabel", { + { + const tagLabel = row.label ?? t(row.labelKey); + const metricLabel = t(METRIC_LABEL_KEY[row.metricKey] ?? row.metricKey); + const unit = t(UNIT_KEY[row.display]); + return { + key: `${row.metricKey}:${row.tag}`, + metricKey: row.metricKey, + pairLabel: t("insights.mood.crosstab.pairLabel", { tag: tagLabel, metric: metricLabel, - }); - const unit = t(UNIT_KEY[row.display]); - const up = row.delta >= 0; - const deltaText = `${up ? "+" : ""}${formatValue(row.delta, row.display, nf)} ${unit}`; - return ( -
  • -
    - {Icon && ( -
    -

    - {t( - row.mode === "nextDay" - ? "insights.mood.crosstab.detailNextDay" - : "insights.mood.crosstab.detailSameDay", - { - withAvg: formatValue(row.withAvg, row.display, nf), - withoutAvg: formatValue(row.withoutAvg, row.display, nf), - unit, - withDays: row.withDays, - }, - )} -

    -
  • - ); - })} -
-
+ }), + icon: moodTagIcon(row.icon), + up: row.delta >= 0, + deltaValue: formatValue(row.delta, row.display, nf), + unit, + confidence: row.confidence, + detail: t( + row.mode === "nextDay" + ? "insights.mood.crosstab.detailNextDay" + : "insights.mood.crosstab.detailSameDay", + { + withAvg: formatValue(row.withAvg, row.display, nf), + withoutAvg: formatValue(row.withoutAvg, row.display, nf), + unit, + withDays: row.withDays, + }, + ), + patternId: row.patternId, + canonicalKey: row.canonicalKey, + dismissed: row.dismissed, + }; + })} + /> ); }