Files
pn-new-crm/src/app/api/v1/admin/templates/preview/route.ts
Matt 67d7e6e3d5
Some checks failed
Build & Push Docker Images / build-and-push (push) Has been cancelled
Build & Push Docker Images / deploy (push) Has been cancelled
Build & Push Docker Images / lint (push) Has been cancelled
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00

78 lines
2.4 KiB
TypeScript

import { NextResponse } from 'next/server';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { parseBody } from '@/lib/api/route-helpers';
import { errorResponse, ValidationError } from '@/lib/errors';
import { generatePdf } from '@/lib/pdf/generate';
import {
validateTipTapDocument,
tipTapToPdfmeTemplate,
buildContentInputsFromDoc,
substituteVariables,
type TipTapNode,
} from '@/lib/pdf/tiptap-to-pdfme';
import { previewAdminTemplateSchema } from '@/lib/validators/document-templates';
/**
* POST /api/v1/admin/templates/preview
*
* Generates a preview PDF from a TipTap JSON content block.
* Returns { data: { pdfBase64: string } } — the client can render this
* in an <iframe src="data:application/pdf;base64,..."> or open in a new tab.
*
* Body:
* content: TipTap JSON document
* sampleData?: Record<string, string> — variable substitutions
*/
export const POST = withAuth(
withPermission('documents', 'manage', async (req, _ctx) => {
try {
const body = await parseBody(req, previewAdminTemplateSchema);
const doc = body.content as unknown as TipTapNode;
const sampleData = body.sampleData ?? {};
// Validate content nodes
const unsupported = validateTipTapDocument(doc);
if (unsupported.length > 0) {
throw new ValidationError(
`Content contains unsupported node types: ${unsupported.join(', ')}`,
);
}
// Substitute variables in text nodes
const substitutedDoc = substituteInDoc(doc, sampleData);
// Convert to pdfme template + inputs
const template = tipTapToPdfmeTemplate(substitutedDoc);
const inputs = buildContentInputsFromDoc(substitutedDoc, template);
const pdfBytes = await generatePdf(template, inputs);
const pdfBase64 = Buffer.from(pdfBytes).toString('base64');
return NextResponse.json({ data: { pdfBase64 } });
} catch (error) {
return errorResponse(error);
}
}),
);
/**
* Deeply substitutes {{variable}} tokens in all text nodes of a TipTap doc.
*/
function substituteInDoc(
node: TipTapNode,
data: Record<string, string>,
): TipTapNode {
if (node.type === 'text' && node.text) {
return { ...node, text: substituteVariables(node.text, data) };
}
if (node.content) {
return {
...node,
content: node.content.map((child) => substituteInDoc(child, data)),
};
}
return node;
}