Files
cv-app/static/carbone.js

152 lines
6.0 KiB
JavaScript

// Carbone Word Template Manager - Frontend
// Use same host as the page, just different port
const CARBONE_API = window.location.protocol + '//' + window.location.hostname + ':8771/api/carbone';
// ============================================================
// NAVIGATION
// ============================================================
// Load carbone templates when the templates page is shown
const carboneNavObserver = new MutationObserver(() => {
if (document.getElementById('page-templates') && document.getElementById('page-templates').classList.contains('active')) {
loadCarboneTemplates();
}
});
document.addEventListener('DOMContentLoaded', () => {
const templatesPage = document.getElementById('page-templates');
if (templatesPage) carboneNavObserver.observe(templatesPage, { 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');
}
}