// The export: one workbook of the lines a person approved. It reads `documents_line`, which only an // approval writes (target.ts), so there is no filter here that a later change could forget: a line // that is pending or rejected is not in the table this reads. // // The columns are fixed and in English on purpose. The file is an interchange format -- what a // product's own import or a spreadsheet formula refers to by name -- and a header that changed with // the reviewer's language would break both. A product that needs another layout (a customer's ERP // import, say) adds its own exporter beside this one; that belongs to the customer's module. import { conflict } from "@conusai-kit/core" import { getFile } from "@conusai-kit/files" import { documents, lines } from "../contract/index.js" import type { DocumentScope, DocumentsDeps } from "./lines.js" import { buildWorkbook, type Cell } from "./xlsx.js" export const WORKBOOK_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" const PAGE = 200 export const EXPORT_HEADER = ["Label", "Value", "Page", "Source quote", "Edited by a reviewer"] as const /** `report.final.pdf` becomes `report.final-approved.xlsx`; nothing a path or a header could misread. */ export function exportFilename(source: string): string { const stem = source.replace(/\.[A-Za-z0-9]{1,5}$/u, "") // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what is removed. const safe = stem.replace(/[\u0000-\u001f\u007f"\\/:*?<>|]/gu, "_").trim() return `${safe.length > 0 ? safe : "document"}-approved.xlsx` } export async function exportApproved( deps: DocumentsDeps, scope: DocumentScope, fileId: string, ): Promise<{ filename: string; contentType: string; bytes: Uint8Array; lineCount: number }> { const file = await getFile(deps.files, scope, fileId) const document = ( await deps.data .forScope(scope) .of(documents) .list({ where: [{ field: "fileId", op: "eq", value: file.fileId }], limit: 1 }) ).items[0] if (!document) throw conflict("this document has not been read yet") const rows: Cell[][] = [[...EXPORT_HEADER]] let cursor: string | undefined do { const page = await deps.data .forScope(scope) .of(lines) .list({ where: [{ field: "documentId", op: "eq", value: document.id }], order: [ { field: "page", direction: "asc" }, { field: "position", direction: "asc" }, ], limit: PAGE, ...(cursor === undefined ? {} : { cursor }), }) for (const line of page.items) { rows.push([line.label, line.value, line.page, line.quote, line.edited ? "yes" : "no"]) } cursor = page.hasMore ? page.cursor : undefined } while (cursor !== undefined) // An empty workbook would read as "the document had nothing in it", which is not what was // decided: nobody has approved anything yet. if (rows.length === 1) throw conflict("no line of this document has been approved") return { filename: exportFilename(file.filename), contentType: WORKBOOK_CONTENT_TYPE, bytes: buildWorkbook("Approved lines", rows), lineCount: rows.length - 1, } }