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>
78 lines
2.4 KiB
TypeScript
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;
|
|
}
|