L14 Abrechnungsübersicht: Services für Einträge, Aufstellung, Abrechnen/Stornieren, Meilensteine und PDF
syncBillingCandidates (idempotent, Event-Hook für work_order.released_for_billing und report.approved), Aufstellung ohne Preise (nur freigegebene Zeiten, Fahrzeit getrennt, Pausen informativ, Anfahrten-Zählung, Material), markBilled mit eingefrorenem Snapshot und Positionszuordnung, voidBilling mit Grund und neuem offenen Eintrag (billed → released_for_billing nur über applyTransition-Option billingVoid), Meilenstein-Flow mit Events, Job billing-pdf (Dokument other/backoffice_only), Sync-Op milestone.reach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
/* eslint-disable @next/next/no-head-element -- standalone print document for Chromium, not a Next.js page */
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import type { BillingStatement } from "@/lib/billing/statement";
|
||||
import { DOCUMENT_THEME, documentFooterLine } from "@/lib/document-brand";
|
||||
import { StatementDocument, statementCss, type Translate } from "@/components/billing/statement-document";
|
||||
|
||||
/**
|
||||
* L14 billing sheet PDF (React SSR → static HTML → src/server/pdf/render.ts). Same markup as the
|
||||
* detail/print pages (`StatementDocument`), themed with the Craftvia document CD.
|
||||
*/
|
||||
|
||||
const esc = (s: string) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
|
||||
export function renderBillingHtml(input: { statement: BillingStatement; t: Translate; locale: string; timeZone: string; fontDataUri?: string | null }) {
|
||||
const { statement: s, t } = input;
|
||||
const th = DOCUMENT_THEME;
|
||||
const font = input.fontDataUri ? `"CraftviaInter",${th.bodyFont}` : th.bodyFont;
|
||||
const css =
|
||||
`${input.fontDataUri ? `@font-face{font-family:"CraftviaInter";src:url(${input.fontDataUri}) format("truetype");font-weight:100 900;}` : ""}` +
|
||||
`@page{size:A4;}*{box-sizing:border-box;}html,body{margin:0;padding:0;background:${th.pageBackground};}` +
|
||||
statementCss({ text: th.text, muted: th.textMuted, accent: th.accent, rule: th.rule, headBg: th.tableHeaderBackground, warn: th.accentStrong, font }).replace(/font-size:13px/, "font-size:9.5pt");
|
||||
const html =
|
||||
"<!doctype html>" +
|
||||
renderToStaticMarkup(
|
||||
<html lang={input.locale}>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<title>{`${t("sheet.title")} ${s.workOrder.number}`}</title>
|
||||
<style dangerouslySetInnerHTML={{ __html: css }} />
|
||||
</head>
|
||||
<body>
|
||||
<StatementDocument s={s} t={t} locale={input.locale} timeZone={input.timeZone} />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
const small = `font-family:${th.bodyFont};font-size:7pt;color:${th.textMuted};width:100%;padding:0 16mm;display:flex;justify-content:space-between;gap:6mm;`;
|
||||
const created = new Intl.DateTimeFormat(input.locale, { timeZone: input.timeZone, dateStyle: "medium", timeStyle: "short" }).format(new Date(s.record.billedAt ?? s.generatedAt));
|
||||
const headerHtml = `<div style="${small}"><span>${esc(s.tenant.name)}</span><span>${esc(t("sheet.title"))} ${esc(s.workOrder.number)} · ${esc(t(`kind.${s.record.kind}`))}</span></div>`;
|
||||
const page = esc(t("pdf.page", { page: "__P__", pages: "__N__" }))
|
||||
.replace("__P__", '<span class="pageNumber"></span>')
|
||||
.replace("__N__", '<span class="totalPages"></span>');
|
||||
const footerHtml =
|
||||
`<div style="${small}"><span>${esc(t("pdf.recordId"))}: ${esc(s.record.id)} · ${esc(t("pdf.created"))}: ${esc(created)}<br/>${esc(documentFooterLine())}</span>` +
|
||||
`<span style="white-space:nowrap">${page}</span></div>`;
|
||||
return { html, headerHtml, footerHtml };
|
||||
}
|
||||
Reference in New Issue
Block a user