Add searchable and flattened export, history re-export and diagnostics self-test

issueAndArchive keeps an issued invoice even when rendering or archiving
fails (Issued - not archived, with a Render & archive action). The export
menu offers a searchable PDF or a 300 DPI image-only PDF whose metadata is
the invoice number alone; flatten failures offer Retry or searchable export
and never silently lower the resolution. History re-exports the original
as issued by default and warns when a re-render's layout fingerprint
differs. The render client gains a FIFO assemble lane that previews cannot
supersede. A Diagnostics panel runs fixtures in the real worker and
compares against goldens generated from Node.
This commit is contained in:
2026-10-04 06:11:23 +05:30
parent dff03ea210
commit b6d999b3e8
30 changed files with 2727 additions and 154 deletions
+5 -23
View File
@@ -1,4 +1,3 @@
import { save } from "@tauri-apps/plugin-dialog";
import pkg from "../../package.json";
import { parsePrefs, type RenderPrefsV1 } from "../pdf/model/prefs";
import { buildRenderModel, type RenderModel } from "../pdf/model/build";
@@ -57,6 +56,11 @@ export async function renderInvoicePdf(
prefs: RenderPrefsV1 = prefsForInvoice(invoice, settings),
): Promise<RenderResult> {
const model = await buildModelForInvoice(invoice, settings, bank);
return renderModelWithRetry(model, prefs);
}
/** Renders a ready model in the shared client. */
export async function renderModelWithRetry(model: RenderModel, prefs: RenderPrefsV1): Promise<RenderResult> {
// The live preview shares the client; if it takes the worker first this render is superseded, so go again.
for (let attempt = 0; ; attempt++) {
try {
@@ -66,25 +70,3 @@ export async function renderInvoicePdf(
}
}
}
/** Asks for a save location and writes the bytes there; null when the dialog is cancelled. */
export async function savePdfBytes(bytes: Uint8Array, fileName: string): Promise<string | null> {
const suggested = fileName.replace(/[^A-Za-z0-9._-]+/g, "-") || "invoice";
const path = await save({
defaultPath: `${suggested}.pdf`,
filters: [{ name: "PDF document", extensions: ["pdf"] }],
});
if (!path) return null;
await api.writeExportFile(path, bytes);
return path;
}
export async function exportInvoicePdf(
invoice: Invoice,
settings: Settings,
bank: BankAccount | null,
prefs?: RenderPrefsV1,
): Promise<string | null> {
const { bytes } = await renderInvoicePdf(invoice, settings, bank, prefs);
return savePdfBytes(bytes, invoice.number);
}