Files
panel/src/app/api/templates/generate/route.js
RKWojs c0d357efdd feat: Add document template management functionality
- Created migration script to add `docx_templates` table with necessary fields and indexes.
- Implemented API routes for uploading, fetching, and deleting document templates.
- Developed document generation feature using selected templates and project data.
- Added UI components for template upload and listing, including a modal for document generation.
- Integrated document generation into the project view page, allowing users to generate documents based on selected templates.
- Enhanced error handling and user feedback for template operations.
2025-12-16 09:50:19 +01:00

209 lines
6.7 KiB
JavaScript

import { NextRequest, NextResponse } from "next/server";
import PizZip from "pizzip";
import Docxtemplater from "docxtemplater";
import { readFile, writeFile } from "fs/promises";
import path from "path";
import db from "@/lib/db";
import { formatDate, formatCoordinates } from "@/lib/utils";
export async function POST(request) {
try {
const { templateId, projectId, customData } = await request.json();
if (!templateId || !projectId) {
return NextResponse.json(
{ error: "templateId and projectId are required" },
{ status: 400 }
);
}
// Get template
const template = db.prepare(`
SELECT * FROM docx_templates WHERE template_id = ? AND is_active = 1
`).get(templateId);
if (!template) {
return NextResponse.json(
{ error: "Template not found" },
{ status: 404 }
);
}
// Get project data
const project = db.prepare(`
SELECT
p.*,
c.contract_number,
c.customer_contract_number,
c.customer,
c.investor
FROM projects p
LEFT JOIN contracts c ON p.contract_id = c.contract_id
WHERE p.project_id = ?
`).get(projectId);
if (!project) {
return NextResponse.json(
{ error: "Project not found" },
{ status: 404 }
);
}
// Get project contacts
const contacts = db.prepare(`
SELECT
pc.*,
ct.name,
ct.phone,
ct.email,
ct.company,
ct.contact_type
FROM project_contacts pc
JOIN contacts ct ON pc.contact_id = ct.contact_id
WHERE pc.project_id = ?
ORDER BY pc.is_primary DESC, ct.name
`).all(projectId);
// Load template file
const templatePath = path.join(process.cwd(), "public", template.file_path);
const templateContent = await readFile(templatePath);
// Load the docx file as a binary
const zip = new PizZip(templateContent);
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
});
// Prepare data for template
const templateData = {
// Project basic info
project_name: project.project_name || "",
project_number: project.project_number || "",
address: project.address || "",
city: project.city || "",
plot: project.plot || "",
district: project.district || "",
unit: project.unit || "",
investment_number: project.investment_number || "",
wp: project.wp || "",
coordinates: project.coordinates || "",
notes: project.notes || "",
// Processed fields (extracted/transformed data)
investment_number_short: project.investment_number ? project.investment_number.split('-').pop() : "",
project_number_short: project.project_number ? project.project_number.split('-').pop() : "",
project_name_upper: project.project_name ? project.project_name.toUpperCase() : "",
project_name_lower: project.project_name ? project.project_name.toLowerCase() : "",
city_upper: project.city ? project.city.toUpperCase() : "",
customer_upper: project.customer ? project.customer.toUpperCase() : "",
// Contract info
contract_number: project.contract_number || "",
customer_contract_number: project.customer_contract_number || "",
customer: project.customer || "",
investor: project.investor || "",
// Dates
finish_date: project.finish_date ? formatDate(project.finish_date) : "",
completion_date: project.completion_date ? formatDate(project.completion_date) : "",
today_date: formatDate(new Date()),
// Project type and status
project_type: project.project_type || "",
project_status: project.project_status || "",
// Financial
wartosc_zlecenia: project.wartosc_zlecenia ? project.wartosc_zlecenia.toString() : "",
// Contacts
contacts: contacts.map(contact => ({
name: contact.name || "",
phone: contact.phone || "",
email: contact.email || "",
company: contact.company || "",
contact_type: contact.contact_type || "",
is_primary: contact.is_primary ? "Tak" : "Nie"
})),
// Primary contact
primary_contact: contacts.find(c => c.is_primary)?.name || "",
primary_contact_phone: contacts.find(c => c.is_primary)?.phone || "",
primary_contact_email: contacts.find(c => c.is_primary)?.email || "",
// Duplicate fields for repeated use (common fields that users might want to repeat)
project_name_1: project.project_name || "",
project_name_2: project.project_name || "",
project_name_3: project.project_name || "",
project_number_1: project.project_number || "",
project_number_2: project.project_number || "",
customer_1: project.customer || "",
customer_2: project.customer || "",
address_1: project.address || "",
address_2: project.address || "",
city_1: project.city || "",
city_2: project.city || "",
wartosc_zlecenia_1: project.wartosc_zlecenia ? project.wartosc_zlecenia.toString() : "",
wartosc_zlecenia_2: project.wartosc_zlecenia ? project.wartosc_zlecenia.toString() : "",
};
// Merge custom data (custom data takes precedence over project data)
if (customData && typeof customData === 'object') {
Object.assign(templateData, customData);
}
// Set the template variables
doc.setData(templateData);
try {
// Render the document
doc.render();
} catch (error) {
console.error("Template rendering error:", error);
// Check if it's a duplicate tags error
if (error.name === 'TemplateError' && error.properties?.id === 'duplicate_open_tag') {
return NextResponse.json(
{
error: "Template contains duplicate placeholders. Each placeholder (like {{project_name}}) can only be used once in the template. Please modify your DOCX template to use unique placeholders or remove duplicates.",
details: `Duplicate tag found: ${error.properties?.xtag || 'unknown'}`
},
{ status: 400 }
);
}
return NextResponse.json(
{ error: "Failed to render template. Please check template syntax and ensure all placeholders are properly formatted." },
{ status: 400 }
);
}
// Get the generated document
const buf = doc.getZip().generate({
type: "nodebuffer",
compression: "DEFLATE",
});
// Generate filename
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, "-");
const sanitizedTemplateName = template.template_name.replace(/[^a-zA-Z0-9]/g, "_");
const sanitizedProjectName = project.project_name.replace(/[^a-zA-Z0-9]/g, "_");
const filename = `${sanitizedTemplateName}_${sanitizedProjectName}_${timestamp}.docx`;
// Return the file as a downloadable response
return new NextResponse(buf, {
headers: {
"Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"Content-Disposition": `attachment; filename="${filename}"`,
},
});
} catch (error) {
console.error("Template generation error:", error);
return NextResponse.json(
{ error: "Failed to generate document" },
{ status: 500 }
);
}
}