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>
This commit is contained in:
77
src/app/api/v1/admin/templates/preview/route.ts
Normal file
77
src/app/api/v1/admin/templates/preview/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user