diff --git a/renderer/carbone-service.js b/renderer/carbone-service.js index fdb8541..f61f920 100644 --- a/renderer/carbone-service.js +++ b/renderer/carbone-service.js @@ -709,6 +709,67 @@ router.post('/templates/:id/render', async (req, res) => { } }); +// POST /api/carbone/templates/:id/preview - Render template with data -> HTML preview +router.post('/templates/:id/preview', async (req, res) => { + const templateId = req.params.id; + const registry = loadRegistry(); + const template = registry.find(t => t.id === templateId); + + if (!template) return res.status(404).json({ error: 'Template not found' }); + if (!fs.existsSync(template.path)) return res.status(404).json({ error: 'Template file missing' }); + + const candidateData = req.body.candidateData || req.body; + const referenceDate = req.body.referenceDate || req.body.generation_date; + const carboneData = transformCandidateData(candidateData, referenceDate); + + try { + // Step 1: Merge data into DOCX with Carbone + const docxBuffer = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('Carbone merge timed out')), 30000); + carbone.render(template.path, carboneData, {}, (err, result) => { + clearTimeout(timer); + if (err) reject(err); else resolve(result); + }); + }); + + // Step 2: Convert to HTML with LibreOffice + const tmpDir = path.join(__dirname, 'output'); + fs.mkdirSync(tmpDir, { recursive: true }); + const tmpId = uuidv4(); + const tmpDocx = path.join(tmpDir, `${tmpId}.docx`); + const tmpHtml = path.join(tmpDir, `${tmpId}.html`); + + fs.writeFileSync(tmpDocx, docxBuffer); + + const { execFile } = require('child_process'); + const htmlPath = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('LibreOffice conversion timed out')), 60000); + execFile('soffice', [ + '--headless', '--invisible', '--nocrashreport', '--nodefault', + '--nologo', '--nofirststartwizard', '--norestore', + '--convert-to', 'html', '--outdir', tmpDir, tmpDocx + ], { timeout: 60000 }, (err) => { + clearTimeout(timer); + if (err) reject(err); + else if (!fs.existsSync(tmpHtml)) reject(new Error('HTML was not generated')); + else resolve(tmpHtml); + }); + }); + + const htmlBuffer = fs.readFileSync(htmlPath); + + // Clean up temp files + try { fs.unlinkSync(tmpDocx); } catch {} + try { fs.unlinkSync(tmpHtml); } catch {} + + res.setHeader('Content-Type', 'text/html'); + res.send(htmlBuffer); + } catch (err) { + console.error('Preview error:', err.message); + res.status(500).json({ error: 'Preview failed: ' + err.message }); + } +}); + // DELETE /api/carbone/templates/:id - Delete a template router.delete('/templates/:id', (req, res) => { const templateId = req.params.id; diff --git a/renderer/carbone-templates/433a0593-d3cf-414e-8763-ab5f87cbc75c.docx b/renderer/carbone-templates/433a0593-d3cf-414e-8763-ab5f87cbc75c.docx index 0b2d90b..c55c03a 100644 Binary files a/renderer/carbone-templates/433a0593-d3cf-414e-8763-ab5f87cbc75c.docx and b/renderer/carbone-templates/433a0593-d3cf-414e-8763-ab5f87cbc75c.docx differ diff --git a/renderer/carbone-templates/registry.json b/renderer/carbone-templates/registry.json index 702942a..95b13e0 100644 --- a/renderer/carbone-templates/registry.json +++ b/renderer/carbone-templates/registry.json @@ -6,6 +6,6 @@ "originalName": "master_cv_template.docx", "path": "/root/workspace/cv-app/renderer/carbone-templates/433a0593-d3cf-414e-8763-ab5f87cbc75c.docx", "uploadedAt": "2026-07-16T09:03:29.601Z", - "updatedAt": "2026-07-17T09:07:36.233Z" + "updatedAt": "2026-07-17T13:05:53.142Z" } ] \ No newline at end of file diff --git a/static/carbone.js b/static/carbone.js index d4c0143..ce11082 100644 --- a/static/carbone.js +++ b/static/carbone.js @@ -96,7 +96,7 @@ async function loadCarboneTemplates() { } // ============================================================ -// RENDER SAMPLE CV +// RENDER SAMPLE CV (download PDF) // ============================================================ async function renderSampleCV(templateId) { toast('Generating sample CV PDF...'); @@ -137,6 +137,49 @@ async function renderSampleCV(templateId) { } } +// ============================================================ +// PREVIEW TEMPLATE (inline HTML preview) +// ============================================================ +async function previewTemplate(templateId) { + toast('Generating preview...'); + + try { + // Get first candidate for sample data + const candidatesResp = await fetch('/api/candidates?limit=1'); + const candidatesData = await candidatesResp.json(); + let cvData = null; + + if (candidatesData.candidates && candidatesData.candidates.length > 0) { + const fullResp = await fetch('/api/candidates/' + candidatesData.candidates[0].id); + cvData = await fullResp.json(); + } + + if (!cvData) { + toast('Upload a CV first to preview', 'error'); + return; + } + + const resp = await fetch(CARBONE_API + '/templates/' + templateId + '/preview', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ candidateData: cvData }) + }); + + if (!resp.ok) { + const err = await resp.json(); + throw new Error(err.error || 'Preview failed'); + } + + const html = await resp.text(); + // Show the HTML in a modal with an iframe + const body = ''; + showModal(body, 'Template Preview', 'large'); + toast('Preview generated'); + } catch (e) { + toast('Preview failed: ' + e.message, 'error'); + } +} + // ============================================================ // DELETE TEMPLATE // ============================================================ @@ -250,7 +293,8 @@ async function loadOnlyofficeTemplates() {
- + +
diff --git a/static/index.html b/static/index.html index f54510d..b7ed968 100644 --- a/static/index.html +++ b/static/index.html @@ -140,6 +140,6 @@ - + \ No newline at end of file