/* global Office, Excel, msal */ import { msalConfig, loginRequest, tokenRequest } from '../../msalConfig.js'; import html2canvas from 'html2canvas'; import jsPDF from 'jspdf'; // MSAL instance for user authentication (delegated flow) let msalInstance = null; let userToken = null; // In-memory buffer of diagnostic lines (kept even if DevTools closes). const DIAGNOSTIC_BUFFER_LIMIT = 1000; const diagBuffer = []; // restore persisted diagnostics when available try { const saved = localStorage.getItem('addinDiagnostics'); if (saved) { const arr = JSON.parse(saved); if (Array.isArray(arr)) { for (const l of arr) diagBuffer.push(l); } } } catch (e) { /* ignore */ } function pushDiagnosticsLine(line) { try { const iso = new Date().toISOString(); const msg = `${iso} - ${line}`; diagBuffer.push(msg); if (diagBuffer.length > DIAGNOSTIC_BUFFER_LIMIT) diagBuffer.splice(0, diagBuffer.length - DIAGNOSTIC_BUFFER_LIMIT); // persist a copy in localStorage so it survives crashes try { localStorage.setItem('addinDiagnostics', JSON.stringify(diagBuffer)); } catch (e) { /* ignore */ } } catch (e) { /* ignore */ } } // Simple UI logger that writes to #logArea (and console) function addLog(message) { try { const t = new Date().toISOString(); const line = `${t} - ${message}`; // console console.log(line); // in-page log const area = document && document.getElementById ? document.getElementById('logArea') : null; if (area) { const div = document.createElement('div'); div.textContent = line; area.appendChild(div); // keep last ~200 lines while (area.childNodes.length > 200) area.removeChild(area.firstChild); // scroll into view area.scrollTop = area.scrollHeight; } // also record into diagnostics buffer pushDiagnosticsLine(message); } catch (e) { try { console.log('addLog error', e); } catch (e2) {} } } // Show a fatal error message in the UI and disable interactions function showFatalError(message, details) { try { console.error('[fatal] ', message, details || ''); pushDiagnosticsLine('[FATAL] ' + message + ' | ' + (details ? (typeof details === 'string' ? details : JSON.stringify(details)) : '')); const el = document.getElementById('fatalError'); const detailsEl = document.getElementById('fatalErrorDetails'); if (el) el.style.display = 'block'; // Always show a short 'Could not be started' title (for clarity) if (detailsEl) detailsEl.textContent = details ? (typeof details === 'string' ? details : JSON.stringify(details)) : message; // Also ensure the primary text includes 'Could not be started' for better clarity const primary = el && el.querySelector ? el.querySelector('strong') : null; if (primary) { primary.textContent = "Sorry — we can't load the add-in right now."; // add a secondary clarification line for 'Could not be started' // (the HTML shows a header already) } // disable the create button if present const createBtn = document.getElementById('createBtn'); if (createBtn) { createBtn.disabled = true; createBtn.textContent = 'Unavailable'; } const spinner = document.getElementById('spinner'); if (spinner) spinner.style.display = 'none'; // reveal stored diagnostics in the UI (helpful when Console closes) try { const logArea = document.getElementById('logArea'); if (logArea) { // append last 200 diagnostic lines to the log area so user can copy const start = Math.max(0, diagBuffer.length - 200); for (let i = start; i < diagBuffer.length; i++) { const div = document.createElement('div'); div.textContent = diagBuffer[i]; logArea.appendChild(div); } logArea.scrollTop = logArea.scrollHeight; } } catch (e) { console.error('reveal diagnostics failed', e); } } catch (e) { console.error('Failed to render fatal error UI', e); } } // expose shorthand for msalConfig loggerCallback to call if (typeof window !== 'undefined') window.__addinLog = addLog; function formatError(err) { if (err instanceof Error) return err.message; return JSON.stringify(err, null, 2); } /** * Initialize MSAL on page load * Sets up single sign-on so user only signs in once */ async function initMsal() { try { addLog('Initializing MSAL...'); msalInstance = new msal.PublicClientApplication(msalConfig); // Check if user is already signed in (from localStorage) const accounts = msalInstance.getAllAccounts(); if (accounts.length > 0) { addLog(`User already signed in: ${accounts[0].username}`); // Try to acquire token silently await acquireTokenSilently(); } } catch (error) { addLog('MSAL initialization error: ' + formatError(error)); console.error('MSAL initialization error:', error); // Initialization failed — show a friendly fatal error to the user showFatalError("Sorry — we can't load the add-in right now.", error && (error.message || error)); // Re-throw so callers are aware throw error; } } /** * Acquire access token silently (with refresh if needed) * MSAL handles token refresh automatically */ async function acquireTokenSilently() { try { const response = await msalInstance.acquireTokenSilent(tokenRequest); userToken = response.accessToken; addLog('Token acquired silently (auto-refreshed if needed)'); return userToken; } catch (error) { if (error instanceof msal.InteractionRequiredAuthError) { addLog('Interactive sign-in required'); return acquireTokenInteractive(); } else { addLog('Silent token acquisition failed: ' + formatError(error)); throw error; } } } /** * Interactive sign-in (one-time, on first use) */ async function acquireTokenInteractive() { try { addLog('Prompting user to sign in...'); const response = await msalInstance.loginPopup(loginRequest); userToken = response.accessToken; addLog(`User signed in: ${response.account.username}`); return userToken; } catch (error) { addLog('Interactive login failed: ' + formatError(error)); throw new Error(`Sign-in failed: ${error.message}`); } } // Suppress Office.js debug handler prompt if (window.Office && window.Office.onReady) { window.Office.diagnostics = window.Office.diagnostics || {}; window.Office.diagnostics.isDebugModeEnabled = false; } Office.onReady(async () => { try { await initMsal(); const createBtn = document.getElementById('createBtn'); if (createBtn) createBtn.onclick = handleCreate; } catch (initErr) { // If initialization failed, show a friendly error — initMsal already calls showFatalError. console.error('Office.onReady initialization error:', initErr); showFatalError("Sorry — we can't load the add-in right now.", initErr && (initErr.message || initErr)); } // wire diagnostics buttons even if init partially failed try { setupDiagnosticsButtons(); } catch (e) {} }); // Global handlers for uncaught errors / rejections to surface a friendly error message window.addEventListener('error', (ev) => { try { const msg = ev && ev.message ? ev.message : String(ev || 'Unknown error'); pushDiagnosticsLine('[window.error] ' + msg + ' | ' + JSON.stringify(ev || {})); showFatalError("Sorry — we can't load the add-in right now.", msg); } catch (e) { console.error('global error handler failed', e); } }); window.addEventListener('unhandledrejection', (ev) => { try { const reason = ev && ev.reason ? ev.reason : ev; pushDiagnosticsLine('[unhandledrejection] ' + (reason && (reason.message || JSON.stringify(reason)))); showFatalError("Sorry — we can't load the add-in right now.", reason && (reason.message || JSON.stringify(reason))); } catch (e) { console.error('unhandledrejection handler failed', e); } }); // --- diagnostics exporter + UI wiring --- function getDiagnosticsText() { const header = []; header.push('Add-in diagnostics'); header.push('Time: ' + new Date().toISOString()); header.push('Location: ' + (location && location.href)); header.push('UserAgent: ' + (navigator && navigator.userAgent)); try { const savedAcct = msalInstance && typeof msalInstance.getAllAccounts === 'function' ? msalInstance.getAllAccounts() : null; header.push('MSAL accounts: ' + (savedAcct ? JSON.stringify(savedAcct.map(a => ({ username: a.username, homeAccountId: a.homeAccountId }))) : 'none')); } catch(e) {} const body = diagBuffer.join('\n'); return header.join('\n') + '\n\n' + body; } function setupDiagnosticsButtons() { try { const copyBtn = document.getElementById('copyDiagnostics'); const downloadBtn = document.getElementById('downloadDiagnostics'); const diagSaved = document.getElementById('diagSaved'); if (copyBtn) { copyBtn.onclick = async () => { try { const text = getDiagnosticsText(); if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); if (diagSaved) { diagSaved.style.display = 'inline'; diagSaved.textContent = 'Copied to clipboard'; setTimeout(() => { diagSaved.style.display = 'none'; }, 3500); } } else { const area = document.getElementById('logArea'); if (area) { area.focus(); } if (diagSaved) { diagSaved.style.display = 'inline'; diagSaved.textContent = 'Copy the logs manually'; setTimeout(() => { diagSaved.style.display = 'none'; }, 3500); } } } catch(e) { console.error('copy diagnostics failed', e); } }; } if (downloadBtn) { downloadBtn.onclick = () => { try { const text = getDiagnosticsText(); const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const filename = `add-in-diagnostics-${new Date().toISOString().replace(/[:.]/g,'-')}.txt`; a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); if (diagSaved) { diagSaved.style.display = 'inline'; diagSaved.textContent = 'Downloaded'; setTimeout(() => { diagSaved.style.display = 'none'; }, 3500); } } catch(e) { console.error('download diagnostics failed', e); } }; } } catch (e) { console.error('setupDiagnosticsButtons failed', e); } } async function handleCreate() { const statusEl = document.getElementById('statusMessage'); try { // Ensure user is authenticated and token is valid if (!userToken) { statusEl.textContent = 'Signing in...'; await acquireTokenSilently(); } statusEl.textContent = 'Reading Job info from workbook...'; const { jobNumber, jobFullName } = await getJobInfoFromWorkbook(); statusEl.textContent = `Using Job: ${jobNumber} - ${jobFullName}`; statusEl.textContent = 'Reading Managers Info sheet...'; const managersHtml = await readManagersInfoAsHtml(); // show preview const preview = document.getElementById('preview'); preview.style.display = 'block'; preview.innerHTML = managersHtml; statusEl.textContent = 'Generating PDF...'; const pdfBlob = await generatePdfFromHtml(preview); statusEl.textContent = 'PDF generated.'; const timestamp = getTimestamp(); const pdfFileName = `${jobNumber} - ${jobFullName} - ${timestamp}.pdf`; const createBtn = document.getElementById('createBtn'); const spinner = document.getElementById('spinner'); if (createBtn) { createBtn.disabled = true; createBtn.textContent = 'Working...'; } if (spinner) spinner.style.display = 'inline-block'; statusEl.textContent = 'Sending PDF to backend for upload...'; // convert blob to base64 const arrayBuffer = await pdfBlob.arrayBuffer(); const uint8 = new Uint8Array(arrayBuffer); let binary = ''; for (let i = 0; i < uint8.byteLength; i++) binary += String.fromCharCode(uint8[i]); const base64 = btoa(binary); // Backend endpoint - adjust if your backend uses HTTP fallback const backendEndpoint = `${location.protocol}//localhost:3001/api/upload-pdf`; console.log('Attempting to POST PDF to backend:', backendEndpoint); console.log('Has user token?', !!userToken); if (userToken) console.log('userToken (truncated):', userToken.substring(0, 60) + '...'); // Prepare payload const payload = JSON.stringify({ jobNumber, jobFullName, fileName: pdfFileName, pdfBase64: base64 }); // Use XMLHttpRequest to show upload progress to the user const xhr = new XMLHttpRequest(); xhr.open('POST', backendEndpoint, true); xhr.setRequestHeader('Content-Type', 'application/json'); if (userToken) xhr.setRequestHeader('Authorization', `Bearer ${userToken}`); xhr.upload.onprogress = function (ev) { if (ev.lengthComputable) { const pct = Math.round((ev.loaded / ev.total) * 100); statusEl.textContent = `Uploading PDF... ${pct}%`; } else { statusEl.textContent = 'Uploading PDF...'; } }; const respText = await new Promise((resolve, reject) => { xhr.onerror = function (e) { reject(new Error('Network error during upload')); }; xhr.onload = function () { resolve(xhr.responseText); }; xhr.send(payload); }); let result; try { result = JSON.parse(respText || '{}'); } catch (parseErr) { console.error('Failed to parse JSON response from backend:', parseErr, 'raw:', respText); throw new Error('Invalid JSON response from backend'); } // If server returned an error structure, throw if (!result || (!result.uploadedItem && !result.shareLink)) { console.error('Unexpected backend response:', result); throw new Error('Upload failed: ' + JSON.stringify(result)); } const shareEl = document.getElementById('shareLink'); // Show detailed success info if (result.shareLink) shareEl.innerHTML = `${result.shareLink}`; if (result.uploadedItem) { const info = result.uploadedItem; const details = `Uploaded: ${info.name} (${info.size} bytes) — ${info.webUrl || ''}`; statusEl.textContent = '✅ PDF uploaded successfully!'; // append a smaller detail line const small = document.createElement('div'); small.style.fontSize = '90%'; small.style.marginTop = '6px'; small.textContent = details; statusEl.appendChild(small); } else { statusEl.textContent = '✅ Upload complete.'; } if (spinner) spinner.style.display = 'none'; if (createBtn) { createBtn.disabled = false; createBtn.textContent = '📁 Create folder & upload PDF'; } } catch (err) { console.error('Create error', err); const statusEl = document.getElementById('statusMessage'); statusEl.textContent = 'Error: ' + formatError(err); // restore UI state const createBtn = document.getElementById('createBtn'); const spinner = document.getElementById('spinner'); if (spinner) spinner.style.display = 'none'; if (createBtn) { createBtn.disabled = false; createBtn.textContent = '📁 Create folder & upload PDF'; } } } // Read job info from 'Job Sheet' as the first data row. // Structure assumed similar to original script: header row starts at A3 with column names. // If not found, fallback to 'Test' + timestamp. async function getJobInfoFromWorkbook() { try { return Excel.run(async (context) => { const sheet = context.workbook.worksheets.getItemOrNullObject('Job Sheet'); await context.sync(); if (sheet.isNullObject) { // fallback const ts = getTimestamp(); return { jobNumber: `Test`, jobFullName: `Test ${ts}` }; } const headerStart = sheet.getRange("A3"); const headerRegion = headerStart.getSurroundingRegion(); headerRegion.load("values, rowCount, columnCount"); await context.sync(); const headers = headerRegion.values && headerRegion.values[0] ? headerRegion.values[0] : []; if (!headers || headers.length === 0) { const ts = getTimestamp(); return { jobNumber: `Test`, jobFullName: `Test ${ts}` }; } // Get first data row below header const dataRange = headerRegion.getOffsetRange(1, 0).getResizedRange(1, headerRegion.columnCount - 1); dataRange.load("values"); await context.sync(); const row = (dataRange.values && dataRange.values[0]) ? dataRange.values[0] : []; const normalize = h => h ? h.toString().trim().toLowerCase() : ""; const idxJobNumber = headers.findIndex(h => normalize(h) === "job_number" || normalize(h) === "job number"); const idxJobName = headers.findIndex(h => normalize(h) === "job_name" || normalize(h) === "job name" || normalize(h) === "job_name"); const jobNumber = (idxJobNumber >= 0 && row[idxJobNumber]) ? String(row[idxJobNumber]).trim() : null; const jobFullName = (idxJobName >= 0 && row[idxJobName]) ? String(row[idxJobName]).trim() : null; if (jobNumber && jobFullName) return { jobNumber, jobFullName }; // fallback const ts = getTimestamp(); return { jobNumber: jobNumber || `Test`, jobFullName: jobFullName || `Test ${ts}` }; }); } catch (e) { const ts = getTimestamp(); return { jobNumber: 'Test', jobFullName: `Test ${ts}` }; } } function getTimestamp() { const now = new Date(); const pad = (n) => n.toString().padStart(2, '0'); const mm = pad(now.getMonth() + 1); const dd = pad(now.getDate()); const yyyy = now.getFullYear(); const hh = pad(now.getHours()); const min = pad(now.getMinutes()); return `${mm}-${dd}-${yyyy}_${hh}${min}`; } async function readManagersInfoAsHtml() { return Excel.run(async (context) => { const sheet = context.workbook.worksheets.getItemOrNullObject('Managers Info'); await context.sync(); if (sheet.isNullObject) throw new Error('Worksheet \"Managers Info\" not found.'); const range = sheet.getUsedRange(); range.load(['values', 'rowCount', 'columnCount']); await context.sync(); const values = range.values; if (!values || values.length === 0) throw new Error('No data in Managers Info sheet.'); // build HTML table let html = ''; for (let r = 0; r < values.length; r++) { html += ''; for (let c = 0; c < values[r].length; c++) { const cell = values[r][c] !== null && values[r][c] !== undefined ? values[r][c] : ''; if (r === 0) html += ``; else html += ``; } html += ''; } html += '
${escapeHtml(cell)}${escapeHtml(cell)}
'; return html; }); } function escapeHtml(s) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return String(s).replace(/[&<>"']/g, (m) => map[m]); } async function generatePdfFromHtml(element) { // Use html2canvas and jsPDF (imported as modules, not global) const canvas = await html2canvas(element, { scale: 2 }); const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF({ unit: 'pt', format: 'a4' }); const pageWidth = pdf.internal.pageSize.getWidth(); const imgWidth = pageWidth - 40; // margins const imgProps = canvas.width ? (canvas.height / canvas.width) : 1; const imgHeight = imgWidth * imgProps; pdf.addImage(imgData, 'PNG', 20, 20, imgWidth, imgHeight); const blob = pdf.output('blob'); return blob; }