feat: Carbone Word template integration with DOCX merge and LibreOffice PDF conversion
This commit is contained in:
151
static/carbone.js
Normal file
151
static/carbone.js
Normal file
@@ -0,0 +1,151 @@
|
||||
// Carbone Word Template Manager - Frontend
|
||||
const CARBONE_API = 'http://localhost:8771/api/carbone';
|
||||
|
||||
// ============================================================
|
||||
// NAVIGATION
|
||||
// ============================================================
|
||||
// Load carbone data when the page is shown via a MutationObserver
|
||||
const carboneNavObserver = new MutationObserver(() => {
|
||||
if (document.getElementById('page-carbone') && document.getElementById('page-carbone').classList.contains('active')) {
|
||||
loadCarboneTemplates();
|
||||
}
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const carbonePage = document.getElementById('page-carbone');
|
||||
if (carbonePage) carboneNavObserver.observe(carbonePage, { attributes: true, attributeFilter: ['class'] });
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// DOWNLOAD MASTER TEMPLATE
|
||||
// ============================================================
|
||||
function downloadMasterTemplate() {
|
||||
window.open(CARBONE_API + '/master-template', '_blank');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UPLOAD CUSTOM TEMPLATE
|
||||
// ============================================================
|
||||
const carboneUploadZone = document.getElementById('carbone-upload-zone');
|
||||
if (carboneUploadZone) {
|
||||
carboneUploadZone.addEventListener('dragover', e => { e.preventDefault(); carboneUploadZone.classList.add('dragover'); });
|
||||
carboneUploadZone.addEventListener('dragleave', () => carboneUploadZone.classList.remove('dragover'));
|
||||
carboneUploadZone.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
carboneUploadZone.classList.remove('dragover');
|
||||
if (e.dataTransfer.files.length) uploadCarboneTemplate(e.dataTransfer.files[0]);
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadCarboneTemplate(file) {
|
||||
if (!file) return;
|
||||
const name = document.getElementById('carbone-template-name').value || file.name.replace(/\.[^.]+$/, '');
|
||||
const status = document.getElementById('carbone-upload-status');
|
||||
status.innerHTML = '<div class="loading"><span class="spinner"></span> Uploading template...</div>';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('template', file);
|
||||
formData.append('name', name);
|
||||
|
||||
try {
|
||||
const resp = await fetch(CARBONE_API + '/templates/upload', { method: 'POST', body: formData });
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
status.innerHTML = '<div class="toast success" style="position:relative">Template uploaded successfully!</div>';
|
||||
toast('Template uploaded: ' + name);
|
||||
document.getElementById('carbone-template-name').value = '';
|
||||
loadCarboneTemplates();
|
||||
} else {
|
||||
status.innerHTML = '<div class="toast error" style="position:relative">Upload failed: ' + (data.error || 'Unknown error') + '</div>';
|
||||
}
|
||||
} catch (e) {
|
||||
status.innerHTML = '<div class="toast error" style="position:relative">Upload failed: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LIST TEMPLATES
|
||||
// ============================================================
|
||||
async function loadCarboneTemplates() {
|
||||
const list = document.getElementById('carbone-templates-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '<div class="loading">Loading...</div>';
|
||||
|
||||
try {
|
||||
const resp = await fetch(CARBONE_API + '/templates');
|
||||
const data = await resp.json();
|
||||
if (!data.templates || !data.templates.length) {
|
||||
list.innerHTML = '<p class="text-muted">No custom templates uploaded yet. Download the master template, customize it in Word, and upload it here.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = data.templates.map(t => `
|
||||
<div class="skill-row">
|
||||
<div>
|
||||
<strong>${t.name}</strong>
|
||||
<div class="text-muted text-sm">Uploaded: ${new Date(t.uploadedAt).toLocaleDateString()} · ${t.originalName}</div>
|
||||
</div>
|
||||
<div class="flex gap-8">
|
||||
<button class="btn btn-sm btn-outline" onclick="renderSampleCV('${t.id}')">Generate Sample CV</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteCarboneTemplate('${t.id}')">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
list.innerHTML = '<p class="text-muted">Error loading templates: ' + e.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// RENDER SAMPLE CV
|
||||
// ============================================================
|
||||
async function renderSampleCV(templateId) {
|
||||
toast('Generating sample CV PDF...');
|
||||
|
||||
// Get first candidate for sample data
|
||||
try {
|
||||
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 generate a sample', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await fetch(CARBONE_API + '/templates/' + templateId + '/render', {
|
||||
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 || 'Render failed');
|
||||
}
|
||||
|
||||
const blob = await resp.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank');
|
||||
toast('Sample CV generated');
|
||||
} catch (e) {
|
||||
toast('Render failed: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DELETE TEMPLATE
|
||||
// ============================================================
|
||||
async function deleteCarboneTemplate(templateId) {
|
||||
if (!confirm('Delete this template?')) return;
|
||||
try {
|
||||
await fetch(CARBONE_API + '/templates/' + templateId, { method: 'DELETE' });
|
||||
toast('Template deleted');
|
||||
loadCarboneTemplates();
|
||||
} catch (e) {
|
||||
toast('Delete failed: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
<li><a href="#" class="nav-link" data-page="candidates">Candidates</a></li>
|
||||
<li><a href="#" class="nav-link" data-page="upload">Upload CV</a></li>
|
||||
<li><a href="#" class="nav-link" data-page="templates">Templates</a></li>
|
||||
<li><a href="#" class="nav-link" data-page="carbone">Word Templates</a></li>
|
||||
<li><a href="#" class="nav-link" data-page="requirements">Requirements</a></li>
|
||||
<li><a href="#" class="nav-link" data-page="generated">Generated CVs</a></li>
|
||||
<li><a href="#" class="nav-link" data-page="chat">AI Chat</a></li>
|
||||
@@ -76,7 +77,38 @@
|
||||
<div id="templates-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- REQUIREMENTS -->
|
||||
<!-- CARBONE WORD TEMPLATES -->
|
||||
<div id="page-carbone" class="page">
|
||||
<h2 style="margin-bottom:20px">Word Templates (Carbone)</h2>
|
||||
<div class="card mb-16">
|
||||
<div class="card-header"><span class="card-title">How It Works</span></div>
|
||||
<div class="text-sm text-muted">
|
||||
1. Download the master template (.docx) with all data tags<br>
|
||||
2. Open in Microsoft Word, customize fonts/colors/layout, save<br>
|
||||
3. Upload your customized .docx back here<br>
|
||||
4. Generate CVs by merging candidate data with your template -> PDF
|
||||
</div>
|
||||
<button class="btn mt-16" onclick="downloadMasterTemplate()">Download Master Template (.docx)</button>
|
||||
</div>
|
||||
<div class="card mb-16">
|
||||
<div class="card-header"><span class="card-title">Upload Custom Template</span></div>
|
||||
<div class="form-group">
|
||||
<label>Template Name</label>
|
||||
<input type="text" id="carbone-template-name" placeholder="e.g. Corporate Blue, Minimalist Professional">
|
||||
</div>
|
||||
<div class="upload-zone" id="carbone-upload-zone" onclick="document.getElementById('carbone-file-input').click()">
|
||||
<p style="font-size:16px; margin-bottom:8px">Drop your .docx file here or click to browse</p>
|
||||
<p class="text-muted text-sm">Must be a .docx file with Carbone tags from the master template</p>
|
||||
</div>
|
||||
<input type="file" id="carbone-file-input" accept=".docx,.doc,.odt,.rtf" style="display:none" onchange="uploadCarboneTemplate(this.files[0])">
|
||||
<div id="carbone-upload-status" style="margin-top:12px"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Saved Templates</span></div>
|
||||
<div id="carbone-templates-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="page-requirements" class="page">
|
||||
<h2 style="margin-bottom:20px">Requirement Requests</h2>
|
||||
<div class="flex mb-16 gap-8">
|
||||
@@ -113,5 +145,6 @@
|
||||
<script src="/static/app.js"></script>
|
||||
<script src="/static/builder.js"></script>
|
||||
<script src="/static/designer.js"></script>
|
||||
<script src="/static/carbone.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user