// 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 = '
Uploading template...
'; 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 = '
Template uploaded successfully!
'; toast('Template uploaded: ' + name); document.getElementById('carbone-template-name').value = ''; loadCarboneTemplates(); } else { status.innerHTML = '
Upload failed: ' + (data.error || 'Unknown error') + '
'; } } catch (e) { status.innerHTML = '
Upload failed: ' + e.message + '
'; } } // ============================================================ // LIST TEMPLATES // ============================================================ async function loadCarboneTemplates() { const list = document.getElementById('carbone-templates-list'); if (!list) return; list.innerHTML = '
Loading...
'; try { const resp = await fetch(CARBONE_API + '/templates'); const data = await resp.json(); if (!data.templates || !data.templates.length) { list.innerHTML = '

No custom templates uploaded yet. Download the master template, customize it in Word, and upload it here.

'; return; } list.innerHTML = data.templates.map(t => `
${t.name}
Uploaded: ${new Date(t.uploadedAt).toLocaleDateString()} · ${t.originalName}
`).join(''); } catch (e) { list.innerHTML = '

Error loading templates: ' + e.message + '

'; } } // ============================================================ // 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'); } } // ============================================================ // TAGS HELP - View in app // ============================================================ async function viewTagsHelp() { try { const resp = await fetch(CARBONE_API + '/help'); const data = await resp.json(); let html = '
'; for (const section of data.sections) { html += `

${section.title}

`; if (section.note) html += `

${section.note}

`; html += ''; for (const t of section.tags) { html += ``; } html += '
TagDescription
${escapeHtml(t.tag)}${escapeHtml(t.desc)}
'; } html += '
'; html += '
'; showModal(html, 'Carbone Tags Reference'); } catch (e) { toast('Failed to load help: ' + e.message, 'error'); } } // ============================================================ // TAGS HELP - Download .pdf // ============================================================ function downloadHelpPdf() { window.open(CARBONE_API + '/help-pdf', '_blank'); } // ============================================================ // ONLYOFFICE INLINE EDITOR // ============================================================ const ONLYOFFICE_API = window.location.protocol + '//' + window.location.hostname + ':8771/api/onlyoffice'; let onlyofficeEditorInstance = null; // Check ONLYOFFICE status when templates page loads async function checkOnlyofficeStatus() { const badge = document.getElementById('onlyoffice-status'); const list = document.getElementById('onlyoffice-templates-list'); if (!badge) return; try { const resp = await fetch(ONLYOFFICE_API + '/health'); const data = await resp.json(); if (data.onlyoffice === 'healthy') { badge.className = 'badge badge-green'; badge.textContent = 'online'; } else { badge.className = 'badge badge-red'; badge.textContent = data.onlyoffice === 'offline' ? 'offline' : data.onlyoffice; if (list) list.innerHTML = '

ONLYOFFICE server is not running. Start it with: cd /root/workspace/cv-app && docker compose up -d onlyoffice-server

'; } } catch (e) { badge.className = 'badge badge-red'; badge.textContent = 'error'; if (list) list.innerHTML = '

Cannot connect to ONLYOFFICE service. Make sure the Carbone+ONLYOFFICE backend is running.

'; } } // Load templates list for ONLYOFFICE editing async function loadOnlyofficeTemplates() { const list = document.getElementById('onlyoffice-templates-list'); if (!list) return; // Check if ONLYOFFICE is online first try { const healthResp = await fetch(ONLYOFFICE_API + '/health'); const health = await healthResp.json(); if (health.onlyoffice !== 'healthy') { list.innerHTML = '

ONLYOFFICE server is ' + health.onlyoffice + '. Start the container to enable inline editing.

'; return; } } catch (e) { list.innerHTML = '

Cannot reach ONLYOFFICE service.

'; return; } // Load templates from Carbone registry try { const resp = await fetch(CARBONE_API + '/templates'); const data = await resp.json(); if (!data.templates || !data.templates.length) { list.innerHTML = '

No templates uploaded yet. Upload a .docx template first to edit it inline.

'; return; } list.innerHTML = data.templates.map(t => `
${t.name}
${t.originalName} · Updated: ${t.updatedAt ? new Date(t.updatedAt).toLocaleString() : new Date(t.uploadedAt).toLocaleString()}
`).join(''); } catch (e) { list.innerHTML = '

Error: ' + e.message + '

'; } } // Force save the document function forceSaveOnlyoffice() { if (onlyofficeEditorInstance) { // The ONLYOFFICE editor's onSave event will trigger the callback // which saves the file to our backend toast('Saving... changes will be stored automatically'); } } // Close the editor and clean up function closeOnlyofficeEditor() { if (onlyofficeEditorInstance) { try { onlyofficeEditorInstance.destroyEditor(); } catch (e) { console.error('Error closing editor:', e); } onlyofficeEditorInstance = null; } closeModal(); // Reload the page to clear ONLYOFFICE's in-memory state. // Save the active page so the user returns to Templates automatically sessionStorage.setItem('returnPage', 'templates'); // Wait 2 seconds for the save callback (status=2) to complete // on our backend before reloading the page setTimeout(() => { location.reload(); }, 2000); } // Open ONLYOFFICE editor in a modal async function openOnlyofficeEditor(templateId, templateName) { // If there's a previous editor instance still lingering, destroy it first if (onlyofficeEditorInstance) { try { onlyofficeEditorInstance.destroyEditor(); } catch (e) {} onlyofficeEditorInstance = null; } try { // Get the JWT-signed editor config from our backend // Add cache-buster to prevent browser caching the old config const cacheBuster = Date.now(); const resp = await fetch(ONLYOFFICE_API + '/' + templateId + '/config?cb=' + cacheBuster); if (!resp.ok) { const err = await resp.json(); throw new Error(err.error || 'Failed to get editor config'); } const data = await resp.json(); const config = data.config; const onlyofficeUrl = data.onlyofficeUrl; // Build the editor modal with a unique container ID each time // so ONLYOFFICE never reuses a cached DOM element const containerId = 'oo-editor-' + Date.now(); const body = `

Editing: ${templateName}

Changes are saved automatically. Click "Save" to force-save, or "Close Editor" to return to the template list.

`; showModal(body, '', 'xlarge'); // Load ONLYOFFICE API script - force fresh load each time // Remove old script to force ONLYOFFICE to reload API.js from server const oldScript = document.getElementById('onlyoffice-api-script'); if (oldScript) oldScript.remove(); // Reload with cache-buster so ONLYOFFICE gets fresh API state const scriptBuster = Date.now(); const scriptUrl = onlyofficeUrl + '/web-apps/apps/api/documents/api.js?cb=' + scriptBuster; await new Promise((resolve, reject) => { const script = document.createElement('script'); script.id = 'onlyoffice-api-script'; script.src = scriptUrl; script.onload = resolve; script.onerror = () => reject(new Error('Failed to load ONLYOFFICE API from ' + scriptUrl)); document.head.appendChild(script); }); // Initialize the ONLYOFFICE editor if (typeof DocsAPI === 'undefined' || !DocsAPI.DocEditor) { throw new Error('ONLYOFFICE API not loaded. Check that the server is running at ' + onlyofficeUrl); } onlyofficeEditorInstance = new DocsAPI.DocEditor(containerId, { ...config, width: '100%', height: '800px', events: { onAppReady: function() { console.log('ONLYOFFICE editor ready'); }, onDocumentReady: function() { console.log('Document loaded in editor'); // Inject CSS into ONLYOFFICE iframe to hide the logo try { var iframe = document.getElementById(containerId).querySelector('iframe'); if (iframe && iframe.contentDocument) { var style = iframe.contentDocument.createElement('style'); style.textContent = '#header-logo { display: none !important; }'; iframe.contentDocument.head.appendChild(style); } } catch(e) { console.log('Could not inject CSS into iframe:', e); } }, onSave: function(event) { toast('Document saved successfully'); // Reload the templates list loadOnlyofficeTemplates(); }, onError: function(event) { console.error('ONLYOFFICE error:', event); toast('Editor error: ' + (event.data?.errorDescription || 'unknown'), 'error'); }, }, }); } catch (e) { toast('Failed to open editor: ' + e.message, 'error'); } } document.addEventListener('DOMContentLoaded', () => { const templatesPage = document.getElementById('page-templates'); if (templatesPage) { // Check on initial load (page may already be active from sessionStorage restore) if (templatesPage.classList.contains('active')) { checkOnlyofficeStatus(); loadOnlyofficeTemplates(); } // Also watch for future tab switches const observer = new MutationObserver(() => { if (templatesPage.classList.contains('active')) { checkOnlyofficeStatus(); loadOnlyofficeTemplates(); } }); observer.observe(templatesPage, { attributes: true, attributeFilter: ['class'] }); } });