Merge branch 'feat/v139-targets-and-links' into release/v1.39.0

This commit is contained in:
Marc-André Bombeck committed 2026-09-24 09:53:41 +02:00
commit e89f41710a
58 files changed
+2785 -214

No files matched your search

+86
View File
@@ -11,7 +11,9 @@ import { listTargetsBySource, replaceTargets } from "@/lib/links";
import type {
DocumentConditionLinkDto,
DocumentEncounterLinkDto,
DocumentVaccinationLinkDto,
} from "@/lib/validations/inbound-documents";
import { resolveCatalogEntry } from "@/lib/vaccinations/vaccine-catalog";
/**
* Load the condition links for a page of documents in ONE grouped query.
@@ -162,3 +164,87 @@ export async function replaceConditionLinks(
targetIds: episodeIds,
});
}
/**
* Load the VACCINATION links for one or more documents in two grouped
* queries: the links through the link service, then the doses' identity so
* the client can name each one from its own bundle.
*
* Tombstoned doses drop out, the same honest default the visit links take.
*/
export async function loadDocumentVaccinationLinks(
userId: string,
documentIds: string[],
): Promise<Map<string, DocumentVaccinationLinkDto[]>> {
const map = new Map<string, DocumentVaccinationLinkDto[]>();
if (documentIds.length === 0) return map;
const byDocument = await listTargetsBySource(prisma, {
userId,
sourceKind: "document",
sourceIds: documentIds,
targetKind: "vaccination",
});
const doseIds = [
...new Set([...byDocument.values()].flat().map((target) => target.id)),
];
if (doseIds.length === 0) return map;
const doses = await prisma.vaccinationRecord.findMany({
where: { id: { in: doseIds }, userId },
select: { id: true, antigenSlug: true, vaccineName: true },
});
const doseById = new Map(doses.map((dose) => [dose.id, dose]));
for (const [documentId, targets] of byDocument) {
map.set(
documentId,
targets.map((target) => {
const dose = doseById.get(target.id);
return {
vaccinationId: target.id,
occurredAt: target.date,
catalogSlug:
resolveCatalogEntry(dose?.antigenSlug ?? null)?.slug ?? null,
vaccineName: dose?.vaccineName ?? null,
};
}),
);
}
return map;
}
/**
* Narrow a client-sent dose-id list to the caller's LIVE doses. Same shape
* and same refusal as {@link narrowOwnedEncounterIds}.
*/
export async function narrowOwnedVaccinationIds(
userId: string,
vaccinationIds: string[],
): Promise<string[] | null> {
const unique = [...new Set(vaccinationIds)];
if (unique.length === 0) return [];
const owned = await prisma.vaccinationRecord.findMany({
where: { id: { in: unique }, userId, deletedAt: null },
select: { id: true },
});
if (owned.length !== unique.length) return null;
return unique;
}
/**
* Replace-set a document's vaccination links through the link service, in
* the `document → vaccination` direction of the table the dose's own form
* writes.
*/
export async function replaceVaccinationLinks(
tx: Prisma.TransactionClient,
userId: string,
documentId: string,
vaccinationIds: string[],
): Promise<void> {
await replaceTargets(tx, {
userId,
sourceKind: "document",
sourceId: documentId,
targetKind: "vaccination",
targetIds: vaccinationIds,
});
}
+3
View File
@@ -21,6 +21,7 @@ import { toIndexOutcome } from "@/lib/validations/inbound-documents";
import type {
DocumentConditionLinkDto,
DocumentEncounterLinkDto,
DocumentVaccinationLinkDto,
DocumentContentIndexSourceValue,
DocumentSummaryStateValue,
ExtractedFactDto,
@@ -275,6 +276,7 @@ export function serialiseDocumentDetail(
contentIndexSource: DocumentContentIndexSourceValue | null = null,
hasThumbnail = false,
encounterLinks: DocumentEncounterLinkDto[] = [],
vaccinationLinks: DocumentVaccinationLinkDto[] | null = [],
): InboundDocumentDetailDto {
const pendingCount = facts.filter((f) => f.status === "PENDING").length;
// `factCount` excludes REJECTED facts (a rejected fact is discarded, not part
@@ -324,6 +326,7 @@ export function serialiseDocumentDetail(
encounterLinks,
),
facts: facts.map(serialiseFact),
vaccinationLinks,
summary,
summaryGeneratedAt: doc.summaryGeneratedAt
? doc.summaryGeneratedAt.toISOString()