mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
feat(documents): review and extract lab values on the document itself
The server chain existed end to end (extract stages PENDING facts, per-fact
edit clears the low-confidence gate, confirm is the only write into the
structured stores) but the document detail offered no control over any of
it: it counted observation facts and linked to Labs. A user whose automatic
staging run was skipped had to re-upload the same PDF through Labs > Scan a
report.
- The detail sheet gains a staged-facts block with honest states: pending
facts open an inline review (approve/reject per fact; a low-confidence
fact is checked and saved first via the existing per-fact edit); a
lab-filed, already-read document with nothing staged offers "Extract lab
values" over its stored text; otherwise nothing renders.
- The extract route gains { mode: "stored" }: the same text-structuring
pass the automatic worker runs, over the document's own content index.
No re-upload, no second read of the original; full gauntlet including
the shared document-AI bucket with charge-at-dispatch honesty; 422
notIndexed when no stored text exists.
- A manual "Read with AI" / "Read again" now continues into the same
lab auto-staging the background worker performs; the response reports
labFactsStaged and the toast names the values waiting for review.
- Unchecked values are rejected and the copy says so; an unverified
low-confidence fact receives no decision and stays pending rather than
being silently discarded. Confirmation remains the only write into
Labs, conditions, and medications; approved facts carry the
committedRecordId link back to the source document (existing model, no
schema change).
- The "lab values from this document" link now counts APPROVED facts
only — it claims values that actually exist in Labs.
- Copy in all six locales; OpenAPI regenerated for the new extract mode
and the index response field.
This commit is contained in:
1 parent
8e16df6894
commit
fd104dd0ac
20 files changed
+1681
-64
No files matched your search
@@ -0,0 +1,154 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import type {
|
||||
ExtractedFactDto,
|
||||
InboundDocumentDetailDto,
|
||||
} from "@/lib/validations/inbound-documents";
|
||||
|
||||
import { DocumentFactsSection } from "../document-facts-review";
|
||||
|
||||
/**
|
||||
* The staged-facts block's honest states, pinned by static render:
|
||||
*
|
||||
* - a document with PENDING facts shows the review action (the control the
|
||||
* detail view never had — its absence forced a re-upload through Labs);
|
||||
* - a lab-filed, already-read document with nothing staged offers the
|
||||
* stored-text extract (recovery for a skipped automatic run);
|
||||
* - a delegate inside somebody else's record sees neither (staging and
|
||||
* confirming write to the owner's record);
|
||||
* - a fully confirmed document renders nothing — the surface stays calm.
|
||||
*/
|
||||
|
||||
function render(node: React.ReactNode) {
|
||||
const queryClient = new QueryClient();
|
||||
return renderToStaticMarkup(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nProvider initialLocale="en">{node}</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
function fact(overrides: Partial<ExtractedFactDto> = {}): ExtractedFactDto {
|
||||
return {
|
||||
id: "fact-1",
|
||||
factType: "OBSERVATION",
|
||||
status: "PENDING",
|
||||
confidence: 0.95,
|
||||
needsReview: false,
|
||||
data: {
|
||||
label: "Hemoglobin",
|
||||
code: null,
|
||||
codeSystem: null,
|
||||
value: 13.9,
|
||||
valueText: null,
|
||||
unit: "g/dL",
|
||||
referenceLow: 12,
|
||||
referenceHigh: 16,
|
||||
referenceText: "12-16",
|
||||
effectiveDate: "2026-08-01",
|
||||
},
|
||||
provenance: {
|
||||
sourceText: "Hemoglobin 13.9 g/dL",
|
||||
anchored: true,
|
||||
sourceOffset: 10,
|
||||
page: 0,
|
||||
confidence: 0.95,
|
||||
},
|
||||
committedRecordId: null,
|
||||
committedRecordType: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function doc(
|
||||
overrides: Partial<InboundDocumentDetailDto> = {},
|
||||
): InboundDocumentDetailDto {
|
||||
return {
|
||||
id: "doc-1",
|
||||
kind: "LAB_RESULT",
|
||||
title: "Blood panel",
|
||||
filename: "panel.pdf",
|
||||
mimeType: "application/pdf",
|
||||
byteSize: 1000,
|
||||
status: "EXTRACTED",
|
||||
providerType: "anthropic",
|
||||
reportDate: "2026-08-01",
|
||||
documentDate: null,
|
||||
errorReason: null,
|
||||
factCount: 1,
|
||||
pendingCount: 1,
|
||||
conditionLinks: [],
|
||||
encounterLinks: [],
|
||||
servingClass: "inline",
|
||||
hasContentIndex: true,
|
||||
contentIndexSource: "vision",
|
||||
hasThumbnail: false,
|
||||
createdAt: "2026-08-01T10:00:00.000Z",
|
||||
updatedAt: "2026-08-01T10:00:00.000Z",
|
||||
facts: [fact()],
|
||||
summary: null,
|
||||
summaryGeneratedAt: null,
|
||||
summaryState: "NONE",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const baseProps = {
|
||||
canManage: true,
|
||||
aiEnabled: true,
|
||||
labsModuleEnabled: true,
|
||||
};
|
||||
|
||||
describe("<DocumentFactsSection>", () => {
|
||||
it("shows the review action for a lab document with pending facts", () => {
|
||||
const html = render(<DocumentFactsSection doc={doc()} {...baseProps} />);
|
||||
expect(html).toContain('data-slot="document-facts-review-open"');
|
||||
expect(html).toContain("Review extracted value");
|
||||
});
|
||||
|
||||
it("offers the stored-text extract for a read lab document with nothing staged", () => {
|
||||
const html = render(
|
||||
<DocumentFactsSection
|
||||
doc={doc({ status: "STORED", facts: [], factCount: 0 })}
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain('data-slot="document-facts-extract"');
|
||||
expect(html).toContain("Extract lab values");
|
||||
// Honest about the mechanism: stored text, review-first, no auto-save.
|
||||
expect(html).toContain("nothing is saved automatically");
|
||||
});
|
||||
|
||||
it("withdraws the extract offer once facts were approved", () => {
|
||||
const html = render(
|
||||
<DocumentFactsSection
|
||||
doc={doc({
|
||||
facts: [fact({ status: "APPROVED", committedRecordId: "lab-1" })],
|
||||
})}
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
expect(html).not.toContain('data-slot="document-facts-extract"');
|
||||
expect(html).not.toContain('data-slot="document-facts-review-open"');
|
||||
});
|
||||
|
||||
it("renders nothing for a delegate inside somebody else's record", () => {
|
||||
const html = render(
|
||||
<DocumentFactsSection doc={doc()} {...baseProps} canManage={false} />,
|
||||
);
|
||||
expect(html).toBe("");
|
||||
});
|
||||
|
||||
it("renders nothing for an unread non-lab document", () => {
|
||||
const html = render(
|
||||
<DocumentFactsSection
|
||||
doc={doc({ kind: "DOCTOR_REPORT", facts: [], factCount: 0 })}
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
expect(html).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user