mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
Merge branch 'main' into fix/wipe-geo-truths
This commit is contained in:
93 files changed
+5417
-236
No files matched your search
@@ -194,6 +194,9 @@ function catalogueLabel(
|
||||
if (catalogue === "illnessSymptom") {
|
||||
return t("admin.section.backups.restoreSkippedIllnessSymptom");
|
||||
}
|
||||
if (catalogue === "moodTag") {
|
||||
return t("admin.section.backups.restoreSkippedMoodTag");
|
||||
}
|
||||
return t("admin.section.backups.restoreSkippedMoodFactor");
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,13 @@ function reading(id: string, value: number, takenAt: string): LabResultDto {
|
||||
unit: "mg/dL",
|
||||
referenceLow: 0,
|
||||
referenceHigh: 100,
|
||||
catalogReferenceLow: 0,
|
||||
catalogReferenceHigh: 100,
|
||||
sourceReferenceLow: null,
|
||||
sourceReferenceHigh: null,
|
||||
sourceReferenceText: null,
|
||||
referenceOrigin: "catalog",
|
||||
referenceDivergesFromCatalog: false,
|
||||
takenAt,
|
||||
source: "MANUAL",
|
||||
hasNote: false,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { CHART_HEIGHT_PX } from "@/lib/charts/constants";
|
||||
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
|
||||
import { formatLabValue } from "@/lib/labs/format-value";
|
||||
import { formatReferenceRange } from "@/lib/labs/reference-range";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
|
||||
import { RichChartTooltip, type RichTooltipRow } from "../charts/chart-tooltip";
|
||||
@@ -32,6 +33,15 @@ import type { LabResultDto } from "./types";
|
||||
* MUTED primary fill — never a danger colour — honouring the no-alarming-
|
||||
* colour rule. Every reading carries the same y-axis unit because a chart is
|
||||
* one biomarker only.
|
||||
*
|
||||
* A SECOND band draws the window a source report printed, when the readings in
|
||||
* view carry one. It is visually distinct from the catalog band — an outlined
|
||||
* band in the chart's secondary hue against the catalog's filled primary — and
|
||||
* both stay muted; the in/out verdict is the badge's job, never the chart's
|
||||
* colour. The source window is a per-reading fact, so the band is drawn from
|
||||
* the NEWEST reading in view that carries one and the legend dates it. When
|
||||
* the readings in view carry more than one distinct printed window the legend
|
||||
* says so rather than implying one window covered the whole series.
|
||||
*/
|
||||
|
||||
const RANGE_DAYS = [
|
||||
@@ -46,6 +56,26 @@ type RangeKey = (typeof RANGE_DAYS)[number]["key"];
|
||||
interface ChartPoint {
|
||||
timestamp: number;
|
||||
value: number;
|
||||
/** The window this reading was judged against, for the tooltip. */
|
||||
referenceLow: number | null;
|
||||
referenceHigh: number | null;
|
||||
referenceOrigin: LabResultDto["referenceOrigin"];
|
||||
sourceReferenceText: string | null;
|
||||
}
|
||||
|
||||
/** The source window drawn as the second band, resolved from the readings in view. */
|
||||
interface SourceBand {
|
||||
low: number | null;
|
||||
high: number | null;
|
||||
/** The reading the band was taken from. */
|
||||
takenAt: number;
|
||||
/** True when the readings in view printed more than one distinct window. */
|
||||
mixed: boolean;
|
||||
}
|
||||
|
||||
/** Identity of a printed window, for counting distinct ones. */
|
||||
function bandKey(low: number | null, high: number | null): string {
|
||||
return `${low ?? ""}|${high ?? ""}`;
|
||||
}
|
||||
|
||||
export function LabBiomarkerChart({
|
||||
@@ -87,9 +117,37 @@ export function LabBiomarkerChart({
|
||||
return filtered.map((r) => ({
|
||||
timestamp: new Date(r.takenAt).getTime(),
|
||||
value: r.value,
|
||||
referenceLow: r.referenceLow,
|
||||
referenceHigh: r.referenceHigh,
|
||||
referenceOrigin: r.referenceOrigin,
|
||||
sourceReferenceText: r.sourceReferenceText,
|
||||
}));
|
||||
}, [readings, range, nowMs]);
|
||||
|
||||
// The source band: the newest in-view reading whose report printed a window
|
||||
// with usable bounds. Readings without one contribute nothing — an absent
|
||||
// window is absent, not a reason to widen or narrow the band.
|
||||
const sourceBand = useMemo<SourceBand | null>(() => {
|
||||
const withWindow = points.filter(
|
||||
(p) =>
|
||||
p.referenceOrigin === "source" &&
|
||||
(p.referenceLow !== null || p.referenceHigh !== null),
|
||||
);
|
||||
if (withWindow.length === 0) return null;
|
||||
const newest = withWindow.reduce((a, b) =>
|
||||
b.timestamp > a.timestamp ? b : a,
|
||||
);
|
||||
const distinct = new Set(
|
||||
withWindow.map((p) => bandKey(p.referenceLow, p.referenceHigh)),
|
||||
);
|
||||
return {
|
||||
low: newest.referenceLow,
|
||||
high: newest.referenceHigh,
|
||||
takenAt: newest.timestamp,
|
||||
mixed: distinct.size > 1,
|
||||
};
|
||||
}, [points]);
|
||||
|
||||
// Y domain wraps both the data and the reference window so the band is
|
||||
// always visible even when every reading sits inside it.
|
||||
const yDomain = useMemo<[number, number]>(() => {
|
||||
@@ -97,15 +155,23 @@ export function LabBiomarkerChart({
|
||||
const candidates = [...values];
|
||||
if (lowerBound != null) candidates.push(lowerBound);
|
||||
if (upperBound != null) candidates.push(upperBound);
|
||||
// The source band has to fit too, or the second band is drawn off-canvas
|
||||
// and the chart silently shows one window while claiming two.
|
||||
if (sourceBand?.low != null) candidates.push(sourceBand.low);
|
||||
if (sourceBand?.high != null) candidates.push(sourceBand.high);
|
||||
if (candidates.length === 0) return [0, 1];
|
||||
const min = Math.min(...candidates);
|
||||
const max = Math.max(...candidates);
|
||||
const pad = (max - min || Math.abs(max) || 1) * 0.1;
|
||||
return [min - pad, max + pad];
|
||||
}, [points, lowerBound, upperBound]);
|
||||
}, [points, lowerBound, upperBound, sourceBand]);
|
||||
|
||||
const animate = !prefersReducedMotion();
|
||||
const primary = "var(--primary)";
|
||||
// A second muted hue from the chart palette. Distinct from the primary band
|
||||
// by hue AND by treatment (outline vs fill), so the two are told apart
|
||||
// without relying on colour alone.
|
||||
const secondary = "var(--chart-2)";
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
@@ -185,6 +251,42 @@ export function LabBiomarkerChart({
|
||||
strokeDasharray="4 4"
|
||||
/>
|
||||
) : null}
|
||||
{/* The source window — outlined, not filled, so it reads as a
|
||||
second band over the catalog one rather than a second fill
|
||||
competing with it. Same no-alarm-colour rule. */}
|
||||
{sourceBand &&
|
||||
sourceBand.low != null &&
|
||||
sourceBand.high != null ? (
|
||||
<ReferenceArea
|
||||
y1={sourceBand.low}
|
||||
y2={sourceBand.high}
|
||||
fill={secondary}
|
||||
fillOpacity={0.06}
|
||||
stroke={secondary}
|
||||
strokeOpacity={0.5}
|
||||
strokeDasharray="2 3"
|
||||
/>
|
||||
) : null}
|
||||
{sourceBand &&
|
||||
sourceBand.low != null &&
|
||||
sourceBand.high == null ? (
|
||||
<ReferenceLine
|
||||
y={sourceBand.low}
|
||||
stroke={secondary}
|
||||
strokeOpacity={0.6}
|
||||
strokeDasharray="2 3"
|
||||
/>
|
||||
) : null}
|
||||
{sourceBand &&
|
||||
sourceBand.high != null &&
|
||||
sourceBand.low == null ? (
|
||||
<ReferenceLine
|
||||
y={sourceBand.high}
|
||||
stroke={secondary}
|
||||
strokeOpacity={0.6}
|
||||
strokeDasharray="2 3"
|
||||
/>
|
||||
) : null}
|
||||
<Tooltip
|
||||
content={(props) => {
|
||||
const active = props.active ?? false;
|
||||
@@ -201,6 +303,22 @@ export function LabBiomarkerChart({
|
||||
color: primary,
|
||||
},
|
||||
];
|
||||
// Name the window THIS reading was judged against. Two
|
||||
// readings on one chart can carry different printed windows,
|
||||
// so the band alone cannot answer it per point.
|
||||
if (point.referenceOrigin === "source") {
|
||||
rows.push({
|
||||
name: t("labs.chart.sourceRangeLabel"),
|
||||
value:
|
||||
point.sourceReferenceText ??
|
||||
`${formatReferenceRange(
|
||||
point.referenceLow,
|
||||
point.referenceHigh,
|
||||
formatLabValue,
|
||||
)} ${unit}`.trim(),
|
||||
color: secondary,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<RichChartTooltip
|
||||
active
|
||||
@@ -221,6 +339,15 @@ export function LabBiomarkerChart({
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
{sourceBand ? (
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
{sourceBand.mixed
|
||||
? t("labs.chart.sourceBandMixed")
|
||||
: t("labs.chart.sourceBandLegend", {
|
||||
date: fmt.dateShortSmart(new Date(sourceBand.takenAt)),
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { apiGet, apiPost } from "@/lib/api/api-fetch";
|
||||
import { localizedApiError } from "@/lib/api/localized-error";
|
||||
import { parseReferenceRange } from "@/lib/labs/parse-reference-range";
|
||||
import { formatReferenceRange } from "@/lib/labs/reference-range";
|
||||
import { formatLabValue } from "@/lib/labs/format-value";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -118,6 +119,13 @@ export function LabForm({
|
||||
const [valueText, setValueText] = useState("");
|
||||
const [takenAt, setTakenAt] = useState(defaultTakenAtValue);
|
||||
const [note, setNote] = useState("");
|
||||
// The window the paper report printed for THIS reading. Optional and folded
|
||||
// away by default: most entries are typed against a catalog marker and never
|
||||
// need it, and a field nobody fills is a field in the way. One text input,
|
||||
// because that is how a report states it — "3,5 - 5,0", "< 5", "bis 5,0".
|
||||
// The bounds are derived from what is typed; what is typed is kept verbatim.
|
||||
const [sourceRangeOpen, setSourceRangeOpen] = useState(false);
|
||||
const [sourceRange, setSourceRange] = useState("");
|
||||
// v1.30.1 H3 — which submit button is in flight, so the OTHER one doesn't
|
||||
// also show a spinner. `!== null` replaces the old plain `submitting`
|
||||
// boolean for every disabled check below.
|
||||
@@ -193,11 +201,27 @@ export function LabForm({
|
||||
|
||||
setPendingAction(keepOpen ? "saveAndAddAnother" : "save");
|
||||
try {
|
||||
// Parse client-side so the user sees the derived window before saving,
|
||||
// and send BOTH: the bounds where they could be read, the text always.
|
||||
const printed = isQualitative
|
||||
? null
|
||||
: parseReferenceRange(sourceRange, selected?.unit);
|
||||
const created = await apiPost<LabResultDto>("/api/labs", {
|
||||
biomarkerId,
|
||||
...(isQualitative
|
||||
? { valueText: qualitativeValue }
|
||||
: { value: numericValue }),
|
||||
...(printed
|
||||
? {
|
||||
...(printed.low !== null
|
||||
? { sourceReferenceLow: printed.low }
|
||||
: {}),
|
||||
...(printed.high !== null
|
||||
? { sourceReferenceHigh: printed.high }
|
||||
: {}),
|
||||
sourceReferenceText: printed.text,
|
||||
}
|
||||
: {}),
|
||||
takenAt: new Date(takenAt).toISOString(),
|
||||
...(note.trim() ? { note: note.trim() } : {}),
|
||||
});
|
||||
@@ -221,6 +245,8 @@ export function LabForm({
|
||||
setValue("");
|
||||
setValueText("");
|
||||
setNote("");
|
||||
// The next analyte off the same report has its own printed window.
|
||||
setSourceRange("");
|
||||
setError(null);
|
||||
onSavedKeepOpen?.(created);
|
||||
// Return focus to wherever the next entry starts so the flow
|
||||
@@ -252,6 +278,20 @@ export function LabForm({
|
||||
)
|
||||
: "";
|
||||
|
||||
// What the typed string resolved to, shown back so the user can see whether
|
||||
// a window was read out of it before the reading is saved. When none was,
|
||||
// the hint says the text is kept as written rather than staying silent.
|
||||
const parsedSourceRange = parseReferenceRange(sourceRange, selected?.unit);
|
||||
const sourceRangeHint = parsedSourceRange
|
||||
? parsedSourceRange.low === null && parsedSourceRange.high === null
|
||||
? t("labs.form.sourceRangeUnparsed")
|
||||
: `${formatReferenceRange(
|
||||
parsedSourceRange.low,
|
||||
parsedSourceRange.high,
|
||||
formatLabValue,
|
||||
)} ${selected?.unit ?? ""}`.trim()
|
||||
: "";
|
||||
|
||||
const footerNode = (
|
||||
<>
|
||||
{onCancel ? (
|
||||
@@ -415,6 +455,39 @@ export function LabForm({
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{resultType === "numeric" ? (
|
||||
<div className="space-y-1.5">
|
||||
{sourceRangeOpen ? (
|
||||
<>
|
||||
<Label htmlFor="lab-sourceRange">
|
||||
{t("labs.form.sourceRange")}
|
||||
</Label>
|
||||
<Input
|
||||
id="lab-sourceRange"
|
||||
value={sourceRange}
|
||||
onChange={(e) => setSourceRange(e.target.value)}
|
||||
placeholder={t("labs.form.sourceRangePlaceholder")}
|
||||
maxLength={120}
|
||||
autoFocus
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{sourceRangeHint || t("labs.form.sourceRangeHint")}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-muted-foreground h-auto px-0 text-xs"
|
||||
onClick={() => setSourceRangeOpen(true)}
|
||||
>
|
||||
{t("labs.form.sourceRangeAdd")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="lab-note">{t("labs.form.note")}</Label>
|
||||
<Textarea
|
||||
|
||||
@@ -35,6 +35,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
import { ReferenceRangeBadge } from "./reference-range-badge";
|
||||
import { SourceRangeNote } from "./source-range-note";
|
||||
import type { LabResultDetailDto, LabResultDto } from "./types";
|
||||
|
||||
const NOTE_MAX_LENGTH = 2000;
|
||||
@@ -356,7 +357,13 @@ export function LabHistoryList({ readings }: { readings: LabResultDto[] }) {
|
||||
{formatDateShort(r.takenAt, true)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ReferenceRangeBadge status={r.rangeStatus} />
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<ReferenceRangeBadge status={r.rangeStatus} />
|
||||
<SourceRangeNote
|
||||
reading={r}
|
||||
className="text-muted-foreground text-xs"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
{r.hasNote ? t("labs.hasNote") : ""}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { applyOrder, useModuleListPrefs } from "@/lib/module-list-prefs";
|
||||
|
||||
import { LabTrendSparkline } from "./lab-trend-sparkline";
|
||||
import { ReferenceRangeBadge } from "./reference-range-badge";
|
||||
import { SourceRangeNote } from "./source-range-note";
|
||||
import type { LabResultDto, LabResultListResponse } from "./types";
|
||||
|
||||
interface MarkerGroup {
|
||||
@@ -316,6 +317,10 @@ export function LabList({ onAddFirst }: { onAddFirst?: () => void } = {}) {
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
<SourceRangeNote
|
||||
reading={group.latest}
|
||||
className="text-xs"
|
||||
/>
|
||||
<span className="text-xs">
|
||||
{formatDate(group.latest.takenAt)}
|
||||
</span>
|
||||
|
||||
@@ -76,6 +76,7 @@ function toCommitRow(row: OcrReviewRow): OcrCommitRowInput | null {
|
||||
takenAt: takenAt.toISOString(),
|
||||
...(row.referenceLow !== null ? { referenceLow: row.referenceLow } : {}),
|
||||
...(row.referenceHigh !== null ? { referenceHigh: row.referenceHigh } : {}),
|
||||
...(row.referenceText ? { referenceText: row.referenceText } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface OcrReviewRow {
|
||||
unit: string | null;
|
||||
referenceLow: number | null;
|
||||
referenceHigh: number | null;
|
||||
/** The reference range as printed on the scanned report, verbatim. */
|
||||
referenceText: string | null;
|
||||
/** ISO calendar day (YYYY-MM-DD) or null. */
|
||||
takenAt: string | null;
|
||||
confidence: OcrExtractedRowDto["confidence"];
|
||||
@@ -39,6 +41,7 @@ export function seedReviewRows(
|
||||
unit: row.unit,
|
||||
referenceLow: row.referenceLow,
|
||||
referenceHigh: row.referenceHigh,
|
||||
referenceText: row.referenceText,
|
||||
takenAt: row.takenAt ?? fallbackDate,
|
||||
confidence: row.confidence,
|
||||
biomarkerMatch: row.biomarkerMatch,
|
||||
|
||||
@@ -247,6 +247,32 @@ export function OcrRowEditor({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* The window as the report printed it. Editable, because the human is
|
||||
reviewing a transcription: the two bounds above are the derived
|
||||
reading of this string, and correcting the string is how a range the
|
||||
parser could not read ("bis 5,0", "negativ") still reaches the row
|
||||
instead of being dropped. */}
|
||||
{!isQualitative ? (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor={`${fieldId}-refText`} className="text-xs">
|
||||
{t("labs.ocr.refTextLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id={`${fieldId}-refText`}
|
||||
value={row.referenceText ?? ""}
|
||||
placeholder={t("labs.ocr.refTextPlaceholder")}
|
||||
maxLength={120}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
onChange({
|
||||
...row,
|
||||
referenceText: raw.trim() === "" ? null : raw,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{valueUnreadable ? (
|
||||
<p className="text-muted-foreground flex items-center gap-1.5 text-xs">
|
||||
<AlertCircle aria-hidden className="h-3.5 w-3.5" />
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { formatLabValue } from "@/lib/labs/format-value";
|
||||
import { formatReferenceRange } from "@/lib/labs/reference-range";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
import type { LabResultDto } from "./types";
|
||||
|
||||
/**
|
||||
* The reference window a reading's own report printed, shown wherever that
|
||||
* reading is shown.
|
||||
*
|
||||
* Renders nothing for a reading with no printed window — most of them — so it
|
||||
* costs no space on the common row. When the report DID state a window it
|
||||
* names it, verbatim where the report gave words; and when that window and the
|
||||
* catalog band disagree it names the catalog band too. A reader told only
|
||||
* "3,9–5,4" and a reader told only "3,5–5,0" draw different conclusions from
|
||||
* the same number, so a surface that shows one silently is showing a partial
|
||||
* answer.
|
||||
*
|
||||
* Neutral by construction: no tint, no icon, muted text. The in/out verdict is
|
||||
* the badge's job — this element states where the window came from, nothing
|
||||
* about whether the value is good news.
|
||||
*/
|
||||
export function SourceRangeNote({
|
||||
reading,
|
||||
className,
|
||||
}: {
|
||||
reading: Pick<
|
||||
LabResultDto,
|
||||
| "unit"
|
||||
| "referenceLow"
|
||||
| "referenceHigh"
|
||||
| "catalogReferenceLow"
|
||||
| "catalogReferenceHigh"
|
||||
| "sourceReferenceText"
|
||||
| "referenceOrigin"
|
||||
| "referenceDivergesFromCatalog"
|
||||
>;
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
if (reading.referenceOrigin !== "source") return null;
|
||||
|
||||
const printed =
|
||||
reading.sourceReferenceText ??
|
||||
`${formatReferenceRange(
|
||||
reading.referenceLow,
|
||||
reading.referenceHigh,
|
||||
formatLabValue,
|
||||
)} ${reading.unit}`.trim();
|
||||
|
||||
const catalog = formatReferenceRange(
|
||||
reading.catalogReferenceLow,
|
||||
reading.catalogReferenceHigh,
|
||||
formatLabValue,
|
||||
);
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
{t("labs.sourceRange.fromReport", { range: printed })}
|
||||
{reading.referenceDivergesFromCatalog && catalog
|
||||
? ` · ${t("labs.sourceRange.catalogDiffers", {
|
||||
range: `${catalog} ${reading.unit}`.trim(),
|
||||
})}`
|
||||
: null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ReferenceRangeStatus } from "@/lib/validations/labs";
|
||||
import type {
|
||||
ReferenceRangeOrigin,
|
||||
ReferenceRangeStatus,
|
||||
} from "@/lib/validations/labs";
|
||||
|
||||
/** A lab-result row as the list / create endpoints serialise it. */
|
||||
export interface LabResultDto {
|
||||
@@ -12,8 +15,25 @@ export interface LabResultDto {
|
||||
/** v1.18.9 — qualitative result text ("negativ" / …); null for a numeric row. */
|
||||
valueText: string | null;
|
||||
unit: string;
|
||||
/**
|
||||
* The window this reading is judged against and the one `rangeStatus` was
|
||||
* computed from: the range its own report printed when it carries one, the
|
||||
* catalog band otherwise. Render these and the range shown always matches
|
||||
* the verdict shown.
|
||||
*/
|
||||
referenceLow: number | null;
|
||||
referenceHigh: number | null;
|
||||
/** The catalog marker's band, so both windows can be drawn. */
|
||||
catalogReferenceLow: number | null;
|
||||
catalogReferenceHigh: number | null;
|
||||
/** The window the source report printed; the text verbatim, bounds when derivable. */
|
||||
sourceReferenceLow: number | null;
|
||||
sourceReferenceHigh: number | null;
|
||||
sourceReferenceText: string | null;
|
||||
/** Which window is in force. */
|
||||
referenceOrigin: ReferenceRangeOrigin;
|
||||
/** True when the report's window and the catalog band state different limits. */
|
||||
referenceDivergesFromCatalog: boolean;
|
||||
takenAt: string;
|
||||
source: string;
|
||||
hasNote: boolean;
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface OcrCommitRowInput {
|
||||
unit?: string;
|
||||
referenceLow?: number;
|
||||
referenceHigh?: number;
|
||||
referenceText?: string;
|
||||
takenAt: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user