mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
Evolve the inbound-documents screen into a real document library on top of the store-first backend. - Upload stays store-only and always works: the form now files a document with an optional title, category, and document date in one step. Extraction is a separate, explicit action. - List becomes an organized library: documents grouped by date, with a title (filename fallback), category and status badges, and pending/fact counts. A toolbar adds title/filename search, a category filter, and a sort control (document date / date added / title, asc/desc). Keyset pagination loads more via the returned cursor through useInfiniteQuery. - Detail panel: view or download the original, edit metadata (rename, recategorise, set the document date), optionally run extraction, then the existing review-then-confirm flow. When no scan provider is configured, extraction surfaces a calm inline note instead of a hard error; the stored document is untouched. Discard stays. - Add a parameterised list key to the query-key factory so each filter/sort combination keeps its own cache bucket while staying under the documents prefix for invalidation. - Rename the surface to "Documents" across all six locales (title, subtitle, module label/description) and add the new toolbar, edit, extract, and date-group strings. Route, model, and module key stay internal. Add component and unit coverage for the grouped list, the detail/review surface, the metadata edit form, the provider-unsupported note, and the search-param/grouping/error-classification helpers.
126 lines
3.6 KiB
TypeScript
126 lines
3.6 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
|
|
import { ApiError } from "@/lib/api/api-fetch";
|
|
import type { InboundDocumentDto } from "@/lib/validations/inbound-documents";
|
|
import {
|
|
buildDocumentListSearch,
|
|
documentDateKey,
|
|
groupDocumentsByDate,
|
|
isProviderUnsupportedError,
|
|
} from "../library-utils";
|
|
|
|
function doc(
|
|
id: string,
|
|
over: Partial<InboundDocumentDto> = {},
|
|
): InboundDocumentDto {
|
|
return {
|
|
id,
|
|
kind: "OTHER",
|
|
title: null,
|
|
filename: `${id}.pdf`,
|
|
mimeType: "application/pdf",
|
|
byteSize: 1000,
|
|
status: "STORED",
|
|
providerType: null,
|
|
reportDate: null,
|
|
documentDate: null,
|
|
errorReason: null,
|
|
factCount: 0,
|
|
pendingCount: 0,
|
|
createdAt: "2026-06-20T08:00:00.000Z",
|
|
updatedAt: "2026-06-20T08:00:00.000Z",
|
|
...over,
|
|
};
|
|
}
|
|
|
|
describe("buildDocumentListSearch", () => {
|
|
it("emits the active filters, sort, order, and limit", () => {
|
|
const qs = buildDocumentListSearch(
|
|
{
|
|
q: "cardio",
|
|
kind: "LAB_RESULT",
|
|
from: "2026-01-01",
|
|
to: "2026-06-01",
|
|
sort: "title",
|
|
order: "asc",
|
|
},
|
|
null,
|
|
25,
|
|
);
|
|
const sp = new URLSearchParams(qs);
|
|
expect(sp.get("q")).toBe("cardio");
|
|
expect(sp.get("kind")).toBe("LAB_RESULT");
|
|
expect(sp.get("from")).toBe("2026-01-01");
|
|
expect(sp.get("to")).toBe("2026-06-01");
|
|
expect(sp.get("sort")).toBe("title");
|
|
expect(sp.get("order")).toBe("asc");
|
|
expect(sp.get("limit")).toBe("25");
|
|
expect(sp.has("cursor")).toBe(false);
|
|
});
|
|
|
|
it("omits empty filters and appends the keyset cursor when present", () => {
|
|
const qs = buildDocumentListSearch(
|
|
{ sort: "documentDate", order: "desc" },
|
|
"cursor-123",
|
|
);
|
|
const sp = new URLSearchParams(qs);
|
|
expect(sp.has("q")).toBe(false);
|
|
expect(sp.has("kind")).toBe(false);
|
|
expect(sp.get("sort")).toBe("documentDate");
|
|
expect(sp.get("order")).toBe("desc");
|
|
expect(sp.get("cursor")).toBe("cursor-123");
|
|
});
|
|
});
|
|
|
|
describe("documentDateKey", () => {
|
|
it("prefers the user filing date", () => {
|
|
expect(documentDateKey(doc("a", { documentDate: "2026-03-15" }))).toBe(
|
|
"2026-03-15",
|
|
);
|
|
});
|
|
|
|
it("falls back to the upload day from createdAt", () => {
|
|
expect(
|
|
documentDateKey(doc("a", { createdAt: "2026-06-20T08:00:00.000Z" })),
|
|
).toBe("2026-06-20");
|
|
});
|
|
});
|
|
|
|
describe("groupDocumentsByDate", () => {
|
|
it("buckets consecutive same-date documents, preserving server order", () => {
|
|
const groups = groupDocumentsByDate([
|
|
doc("a", { documentDate: "2026-06-20" }),
|
|
doc("b", { documentDate: "2026-06-20" }),
|
|
doc("c", { documentDate: "2026-06-19" }),
|
|
]);
|
|
expect(groups.map((g) => g.key)).toEqual(["2026-06-20", "2026-06-19"]);
|
|
expect(groups[0].documents.map((d) => d.id)).toEqual(["a", "b"]);
|
|
expect(groups[1].documents.map((d) => d.id)).toEqual(["c"]);
|
|
});
|
|
|
|
it("returns an empty array for no documents", () => {
|
|
expect(groupDocumentsByDate([])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("isProviderUnsupportedError", () => {
|
|
it("matches the extract 422 provider-unsupported signal", () => {
|
|
const err = new ApiError("no provider", 422, {
|
|
errorCode: "documents.inbound.providerUnsupported",
|
|
});
|
|
expect(isProviderUnsupportedError(err)).toBe(true);
|
|
});
|
|
|
|
it("rejects other API errors and non-errors", () => {
|
|
expect(
|
|
isProviderUnsupportedError(
|
|
new ApiError("rate", 429, {
|
|
errorCode: "documents.inbound.rateLimited",
|
|
}),
|
|
),
|
|
).toBe(false);
|
|
expect(isProviderUnsupportedError(new Error("boom"))).toBe(false);
|
|
expect(isProviderUnsupportedError(null)).toBe(false);
|
|
});
|
|
});
|