(function () { 'use strict'; var SPREADSHEET_ID = '1x0LgDmrwmPEaXt6cTj471Iagy8nMTuWS9mOaEUwNZlI'; // Asset IDs as they appear in the Click Tracker sheet var ASSET_LABELS = { 'cto': 'Fractional CTO Calculator', 'gcc': 'GCC Calculator', 'team': 'Managed Teams Calculator', 'cto-guide': 'Fractional CTO Guide', 'gcc-guide': 'GCC Guide', 'team-guide': 'Managed Teams Guide', 'cto-pdf': 'CTO PDF Report', 'gcc-pdf': 'GCC PDF Report', 'team-pdf': 'Managed Teams PDF Report' }; var state = { mode: 'calculator', raw: { calculatorLeads: [], calcProgress: [], guideLeads: [], clickData: [], contactLeads: [] }, activeFilters: { date: 'all', source: 'all', magnet: 'all', search: '', kpi: null, chart: null }, sort: { key: 'timestamp', dir: 'desc' }, page: 1, pageSize: 8, charts: {}, loaded: false }; var COLORS = { brown: '#7A4B2E', brownDark: '#4E342E', brownLight: '#C7A17A', beige: '#F1EAE0', border: '#E8DED2', success: '#6FA287', warning: '#E0A458', error: '#C97064', textSecondary: '#6D625C' }; document.addEventListener('DOMContentLoaded', function () { if (window.Chart && window.ChartDataLabels) { Chart.register(ChartDataLabels); } renderClock(); bindStaticEvents(); loadData(); }); // ===================== DATA FETCHING (public gviz JSONP) ===================== function fetchSheetGviz(sheetName, extraParam) { return new Promise(function (resolve, reject) { var settled = false; window.google = window.google || {}; window.google.visualization = window.google.visualization || {}; window.google.visualization.Query = window.google.visualization.Query || {}; window.google.visualization.Query.setResponse = function (resp) { if (settled) return; settled = true; resolve(resp); }; var src = 'https://docs.google.com/spreadsheets/d/' + SPREADSHEET_ID + '/gviz/tq?tqx=out:json&sheet=' + encodeURIComponent(sheetName) + (extraParam || '') + '&_=' + Date.now(); var script = document.createElement('script'); script.src = src; script.onerror = function () { if (!settled) { settled = true; reject(new Error('Load failed: ' + sheetName)); } }; document.body.appendChild(script); setTimeout(function () { if (script.parentNode) document.body.removeChild(script); if (!settled) { settled = true; reject(new Error('Timeout: ' + sheetName)); } }, 10000); }); } function parseGvizDate(v) { if (typeof v !== 'string' || v.indexOf('Date(') !== 0) return null; var parts = v.replace('Date(', '').replace(')', '').split(',').map(Number); return new Date(parts[0], parts[1], parts[2], parts[3] || 0, parts[4] || 0, parts[5] || 0); } // Parse gviz response using detected column labels function gvizToRows(resp) { if (!resp || !resp.table) return []; var cols = resp.table.cols.map(function (c) { return c.label || ''; }); return (resp.table.rows || []).map(function (r) { var obj = {}; cols.forEach(function (label, i) { if (!label) return; var cell = r.c ? r.c[i] : null; var val = cell ? cell.v : null; var d = parseGvizDate(val); obj[label] = d ? d.toISOString() : val; }); return obj; }); } // Parse using hardcoded column order (for sheets whose headers are not gviz-readable) function gvizToRowsFixed(resp, colNames) { if (!resp || !resp.table) return []; return (resp.table.rows || []).map(function (r) { var obj = {}; colNames.forEach(function (label, i) { var cell = r.c ? r.c[i] : null; var val = cell ? cell.v : null; var d = parseGvizDate(val); obj[label] = d ? d.toISOString() : val; }); return obj; }); } // ===================== HELPERS ===================== function parseReferrerSource(ref) { if (!ref || ref === '' || ref === 'direct' || ref === '-') return 'Direct'; var lower = String(ref).toLowerCase(); if (lower.indexOf('linkedin') !== -1) return 'LinkedIn'; if (lower.indexOf('instagram') !== -1) return 'Instagram'; if (lower.indexOf('twitter') !== -1 || lower.indexOf('x.com') !== -1) return 'Twitter/X'; if (lower.indexOf('google') !== -1) return 'Google'; if (lower.indexOf('facebook') !== -1 || lower.indexOf('fb.com') !== -1) return 'Facebook'; try { var url = new URL(ref); return url.hostname.replace('www.', ''); } catch (e) { return 'Other'; } } function simplifyDevice(d) { d = (d || '').toLowerCase(); if (d.indexOf('mobile') !== -1 || d.indexOf('android') !== -1 || d.indexOf('iphone') !== -1) return 'Mobile'; if (d.indexOf('tablet') !== -1 || d.indexOf('ipad') !== -1) return 'Tablet'; if (d.indexOf('windows') !== -1 || d.indexOf('macintosh') !== -1 || d.indexOf('mac os x') !== -1 || d.indexOf('linux') !== -1) return 'Desktop'; return 'Other'; } function assetLabelToKey(label) { var keys = Object.keys(ASSET_LABELS); for (var i = 0; i < keys.length; i++) { if (ASSET_LABELS[keys[i]] === label) return keys[i]; } return label; } // ===================== LOAD DATA ===================== function loadData(isRetry) { document.getElementById('refreshBtn').classList.add('spin'); document.getElementById('lastUpdated').textContent = 'Refreshing…'; // Fetch all 5 sheets sequentially (gviz uses a shared global callback) // Sheets with purpose+header rows use &headers=2 so data starts at row 3 // Sheets where range=A2:Z works cleanly use that instead fetchSheetGviz('calculator-leads', '&headers=2') .then(function (r1) { return fetchSheetGviz('Calculator Progress', '&range=A2:Z').then(function (r2) { return fetchSheetGviz('Guide Leads', '&headers=2').then(function (r3) { return fetchSheetGviz('Click Tracker', '&range=A2:Z').then(function (r4) { return fetchSheetGviz('Contact Leads-MD site', '&headers=2').then(function (r5) { return [r1, r2, r3, r4, r5]; }); }); }); }); }) .then(function (results) { // Calculator Leads — use fixed column names (headers=2 gives messy first-col name) var calcCols = ['Timestamp', 'Email', 'Calculator', 'Score', 'Score Band', 'PDF', 'Source', 'Email Status', 'Email Sent Time', 'Debug Log']; state.raw.calculatorLeads = gvizToRowsFixed(results[0], calcCols) .filter(function (r) { return r.Email; }) .map(function (r) { var pdf = r.PDF || ''; return { timestamp: r.Timestamp || '', email: r.Email || '', calculator: r.Calculator || '', score: r.Score !== undefined && r.Score !== null ? Number(r.Score) : null, scoreBand: r['Score Band'] || '', pdf: pdf, pdfStatus: pdf ? 'Generated' : 'Failed', source: r.Source || 'Website', emailStatus: r['Email Status'] || 'Pending', emailSentTime: r['Email Sent Time'] || '' }; }); // Calculator Progress — gviz reads columns correctly with range=A2:Z state.raw.calcProgress = gvizToRows(results[1]) .filter(function (r) { return r['Session ID']; }) .map(function (r) { return { sessionId: r['Session ID'] || '', calculator: r.Calculator || '', startedAt: r['Started At'] || '', currentStep: r['Current Step'] !== null ? Number(r['Current Step']) : 0, totalSteps: r['Total Steps'] !== null ? Number(r['Total Steps']) : 0, lastUpdated: r['Last Updated'] || '', status: r.Status || '' }; }); // Guide Leads — fixed column names var guideCols = ['Timestamp', 'Name', 'Email', 'Guide', 'Source']; state.raw.guideLeads = gvizToRowsFixed(results[2], guideCols) .filter(function (r) { return r.Email; }) .map(function (r) { return { timestamp: r.Timestamp || '', name: r.Name || '', email: r.Email || '', guide: r.Guide || '', source: r.Source || 'Website' }; }); // Click Tracker — gviz reads correctly with range=A2:Z state.raw.clickData = gvizToRows(results[3]) .filter(function (r) { return r.Timestamp || r.Asset; }) .map(function (r) { var asset = String(r.Asset || '').trim().toLowerCase(); var ref = String(r.Referrer || '').trim(); var device = String(r['Device / Browser'] || '').trim(); return { timestamp: r.Timestamp || '', asset: asset, assetLabel: ASSET_LABELS[asset] || (asset ? asset : 'Unknown'), referrer: ref, referrerSource: parseReferrerSource(ref), device: device, deviceType: simplifyDevice(device), email: r.Email || '' }; }); // Contact Leads — fixed column names var contactCols = ['Timestamp', 'Name', 'Company', 'Email', 'Service', 'Message', 'Source']; state.raw.contactLeads = gvizToRowsFixed(results[4], contactCols) .filter(function (r) { return r.Email || r.Name; }) .map(function (r) { return { timestamp: r.Timestamp || '', name: r.Name || '', company: r.Company || '', email: r.Email || '', service: r.Service || '', message: r.Message || '', source: r.Source || 'Website' }; }); state.loaded = true; document.getElementById('lastUpdated').textContent = 'Last updated ' + formatTime(new Date()); document.getElementById('loadingState').style.display = 'none'; updateTopnavSub(); populateMagnetFilter(); setSwitchUI(state.mode, false); renderAll(); }) .catch(function (err) { console.error('Dashboard load error:', err); if (!isRetry) { setTimeout(function () { loadData(true); }, 1500); return; } document.getElementById('lastUpdated').textContent = 'Refresh failed — try again'; document.getElementById('loadingState').textContent = 'Could not load dashboard data. Click Refresh to retry.'; document.getElementById('loadingState').style.display = state.loaded ? 'none' : 'block'; }) .finally(function () { document.getElementById('refreshBtn').classList.remove('spin'); }); } function updateTopnavSub() { var calcCount = state.raw.calculatorLeads.length; var guideCount = state.raw.guideLeads.length; var contactCount = state.raw.contactLeads.length; var clickCount = state.raw.clickData.length; var startedCount = state.raw.calcProgress.length; document.getElementById('topnavSub').textContent = (calcCount + guideCount + contactCount) + ' leads · ' + startedCount + ' calc sessions · ' + clickCount + ' clicks tracked'; } // ===================== UI BINDING ===================== function bindStaticEvents() { document.getElementById('refreshBtn').addEventListener('click', function () { loadData(false); }); document.querySelectorAll('.segmented-option').forEach(function (btn) { btn.addEventListener('click', function () { setSwitchUI(btn.dataset.mode, true); }); }); document.getElementById('dateFilter').addEventListener('change', function (e) { state.activeFilters.date = e.target.value; state.page = 1; renderAll(); }); document.getElementById('sourceFilter').addEventListener('change', function (e) { state.activeFilters.source = e.target.value; state.page = 1; renderAll(); }); document.getElementById('magnetFilter').addEventListener('change', function (e) { state.activeFilters.magnet = e.target.value; state.page = 1; renderAll(); }); document.getElementById('searchInput').addEventListener('input', function (e) { state.activeFilters.search = e.target.value.toLowerCase(); state.page = 1; renderAll(); }); document.getElementById('clearFiltersBtn').addEventListener('click', function () { state.activeFilters = { date: 'all', source: 'all', magnet: 'all', search: '', kpi: null, chart: null }; document.getElementById('dateFilter').value = 'all'; document.getElementById('sourceFilter').value = 'all'; document.getElementById('magnetFilter').value = 'all'; document.getElementById('searchInput').value = ''; state.page = 1; renderAll(); }); document.getElementById('drawerClose').addEventListener('click', closeDrawer); document.getElementById('drawerOverlay').addEventListener('click', closeDrawer); } function setSwitchUI(mode, userInitiated) { state.mode = mode; document.querySelectorAll('.segmented-option').forEach(function (b) { b.classList.toggle('active', b.dataset.mode === mode); }); var idx = { calculator: 0, guide: 1, clicks: 2, contact: 3 }[mode] || 0; document.querySelector('.segmented-thumb').style.transform = idx ? 'translateX(' + (idx * 100) + '%)' : 'translateX(0)'; // donut2: hide only for guide & contact (no secondary donut needed) document.getElementById('donut2Card').style.display = (mode === 'guide' || mode === 'contact') ? 'none' : ''; if (mode === 'calculator') { document.getElementById('perfChartTitle').textContent = 'Assessment Performance'; document.getElementById('perfChartSub').textContent = 'Which calculator people are using most.'; document.getElementById('donut1Title').textContent = 'Score Distribution'; document.getElementById('donut1Sub').textContent = 'How prospects scored — Critical means they need help most urgently.'; document.getElementById('donut2Title').textContent = 'PDF Status'; document.getElementById('donut2Sub').textContent = 'Whether each lead\'s personalised report was successfully created.'; } else if (mode === 'guide') { document.getElementById('perfChartTitle').textContent = 'Guide Performance'; document.getElementById('perfChartSub').textContent = 'Which guide people are downloading most.'; document.getElementById('donut1Title').textContent = 'Lead Source'; document.getElementById('donut1Sub').textContent = 'Where these guide leads are coming from.'; } else if (mode === 'clicks') { document.getElementById('perfChartTitle').textContent = 'Clicks by Asset'; document.getElementById('perfChartSub').textContent = 'Which tracked links are getting the most traction.'; document.getElementById('donut1Title').textContent = 'Referrer Source'; document.getElementById('donut1Sub').textContent = 'Where your top-of-funnel traffic is arriving from.'; document.getElementById('donut2Title').textContent = 'Device / Browser'; document.getElementById('donut2Sub').textContent = 'What type of device visitors use when clicking your links.'; } else if (mode === 'contact') { document.getElementById('perfChartTitle').textContent = 'Service Interest'; document.getElementById('perfChartSub').textContent = 'Which service people are enquiring about most.'; document.getElementById('donut1Title').textContent = 'Lead Source'; document.getElementById('donut1Sub').textContent = 'How contact form leads are finding Sudarshan.'; } // Update source filter options var sourceFilter = document.getElementById('sourceFilter'); if (mode === 'clicks') { sourceFilter.innerHTML = ''; } else { sourceFilter.innerHTML = ''; } sourceFilter.value = 'all'; state.activeFilters.source = 'all'; state.activeFilters.kpi = null; state.activeFilters.chart = null; state.page = 1; populateMagnetFilter(); if (userInitiated && state.loaded) renderAll(); } function populateMagnetFilter() { var sel = document.getElementById('magnetFilter'); var items, label; if (state.mode === 'calculator') { items = uniq(state.raw.calculatorLeads.map(function (r) { return r.calculator; })); label = 'calculators'; } else if (state.mode === 'guide') { items = uniq(state.raw.guideLeads.map(function (r) { return r.guide; })); label = 'guides'; } else if (state.mode === 'clicks') { items = uniq(state.raw.clickData.map(function (r) { return r.asset; })); label = 'assets'; } else { items = uniq(state.raw.contactLeads.map(function (r) { return r.service; })); label = 'services'; } sel.innerHTML = '' + items.filter(Boolean).map(function (i) { var displayLabel = state.mode === 'clicks' ? (ASSET_LABELS[i] || i) : i; return ''; }).join(''); sel.value = 'all'; state.activeFilters.magnet = 'all'; } function getModeData() { var map = { calculator: 'calculatorLeads', guide: 'guideLeads', clicks: 'clickData', contact: 'contactLeads' }; return state.raw[map[state.mode]] || []; } function applyFilters(list) { var f = state.activeFilters; var now = new Date(); return list.filter(function (row) { var ts = row.timestamp || row.startedAt || ''; var d = ts ? new Date(ts) : null; if (f.date !== 'all' && d) { if (f.date === 'today' && !isSameDay(d, now)) return false; if (f.date === 'yesterday') { var y = new Date(now); y.setDate(y.getDate() - 1); if (!isSameDay(d, y)) return false; } if (f.date === '7d' && (now - d) > 7 * 86400000) return false; if (f.date === '30d' && (now - d) > 30 * 86400000) return false; if (f.date === 'month' && (d.getMonth() !== now.getMonth() || d.getFullYear() !== now.getFullYear())) return false; } if (f.source !== 'all') { var rowSource = state.mode === 'clicks' ? row.referrerSource : row.source; if (rowSource !== f.source) return false; } var magnetVal = state.mode === 'calculator' ? row.calculator : state.mode === 'guide' ? row.guide : state.mode === 'clicks' ? row.asset : row.service; if (f.magnet !== 'all' && magnetVal !== f.magnet) return false; if (f.search) { var hay = ''; if (state.mode === 'calculator') hay = [row.email, row.calculator, row.scoreBand].join(' '); else if (state.mode === 'guide') hay = [row.name, row.email, row.guide].join(' '); else if (state.mode === 'clicks') hay = [row.asset, row.assetLabel, row.referrer, row.referrerSource, row.device].join(' '); else hay = [row.name, row.email, row.company, row.service].join(' '); if (hay.toLowerCase().indexOf(f.search) === -1) return false; } if (f.kpi) { if (f.kpi === 'today' && !(d && isSameDay(d, now))) return false; if (f.kpi === '7d' && !(d && (now - d) <= 7 * 86400000)) return false; if (f.kpi === '30d' && !(d && (now - d) <= 30 * 86400000)) return false; if (f.kpi === 'month' && !(d && d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear())) return false; if (f.kpi === 'LinkedIn') { var src = state.mode === 'clicks' ? row.referrerSource : row.source; if (src !== 'LinkedIn') return false; } if (f.kpi === 'Instagram') { var src2 = state.mode === 'clicks' ? row.referrerSource : row.source; if (src2 !== 'Instagram') return false; } if (f.kpi.indexOf('emailStatus:') === 0 && row.emailStatus !== f.kpi.split(':')[1]) return false; if (f.kpi.indexOf('pdfStatus:') === 0 && row.pdfStatus !== f.kpi.split(':')[1]) return false; } if (f.chart) { if (f.chart.type === 'scoreBand' && row.scoreBand !== f.chart.value) return false; if (f.chart.type === 'source' && (state.mode === 'clicks' ? row.referrerSource : row.source) !== f.chart.value) return false; if (f.chart.type === 'magnet' && magnetVal !== f.chart.value) return false; if (f.chart.type === 'device' && row.deviceType !== f.chart.value) return false; } return true; }); } function renderAll() { var full = getModeData(); var filtered = applyFilters(full); renderSummaryBanner(full); renderKpis(full); renderCharts(full); renderTable(filtered); } // ===================== SUMMARY BANNER ===================== function renderSummaryBanner(full) { var el = document.getElementById('summaryText'); var now = new Date(); if (!full.length) { var emptyMsgs = { calculator: 'No calculator leads yet. Once someone completes an assessment, their result will appear here automatically.', guide: 'No guide downloads yet. Once someone unlocks a guide, their details will show up here automatically.', clicks: 'No clicks tracked yet. Once someone clicks a tracked link, it will appear here automatically.', contact: 'No contact form submissions yet. They will show up here once someone reaches out.' }; el.innerHTML = emptyMsgs[state.mode] || ''; return; } var today = full.filter(function (r) { return (r.timestamp || r.startedAt) && isSameDay(new Date(r.timestamp || r.startedAt), now); }).length; if (state.mode === 'calculator') { var sent = full.filter(function (r) { return r.emailStatus && r.emailStatus.toLowerCase() === 'sent'; }).length; var pdfOk = full.filter(function (r) { return r.pdfStatus === 'Generated'; }).length; var topCalc = topKey(groupCount(full, function (r) { return r.calculator; })); var started = state.raw.calcProgress.length; var convRate = started ? Math.round((full.length / started) * 100) : null; el.innerHTML = '' + full.length + ' calculator lead' + (full.length === 1 ? '' : 's') + ' completed so far' + (today ? ', ' + today + ' today' : '') + (started ? ' — from ' + started + ' sessions started (' + (convRate || 0) + '% completion)' : '') + '. ' + pdfOk + '/' + full.length + ' reports generated, ' + sent + '/' + full.length + ' emails sent.' + (topCalc ? ' Top calculator: ' + topCalc + '.' : ''); } else if (state.mode === 'guide') { var topGuide = topKey(groupCount(full, function (r) { return r.guide; })); el.innerHTML = '' + full.length + ' guide download' + (full.length === 1 ? '' : 's') + ' so far' + (today ? ', ' + today + ' today' : '') + '.' + (topGuide ? ' Most requested: ' + topGuide + '.' : ''); } else if (state.mode === 'clicks') { var liCount = full.filter(function (r) { return r.referrerSource === 'LinkedIn'; }).length; var topAsset = topKey(groupCount(full, function (r) { return r.assetLabel; })); var totalLeads = state.raw.calculatorLeads.length + state.raw.guideLeads.length; var convRateClick = full.length ? Math.round((totalLeads / full.length) * 100) : 0; el.innerHTML = '' + full.length + ' top-of-funnel click' + (full.length === 1 ? '' : 's') + ' tracked so far' + (today ? ', ' + today + ' today' : '') + '.' + (liCount ? ' ' + liCount + ' from LinkedIn.' : '') + (topAsset ? ' Most clicked: ' + topAsset + '.' : '') + (convRateClick ? ' Click-to-lead rate: ' + convRateClick + '%.' : ''); } else { var topService = topKey(groupCount(full, function (r) { return r.service; })); el.innerHTML = '' + full.length + ' contact form submission' + (full.length === 1 ? '' : 's') + ' so far' + (today ? ', ' + today + ' today' : '') + '.' + (topService ? ' Most enquiries about: ' + topService + '.' : ''); } } function topKey(obj) { var keys = Object.keys(obj).filter(function(k) { return k && k !== 'Unknown'; }); if (!keys.length) return null; return keys.reduce(function (best, k) { return obj[k] > obj[best] ? k : best; }, keys[0]); } // ===================== KPI CARDS ===================== function renderKpis(full) { var now = new Date(); var grid = document.getElementById('kpiGrid'); var cards = []; function count(pred) { return full.filter(pred).length; } var today = function (r) { var ts = r.timestamp || r.startedAt; return ts && isSameDay(new Date(ts), now); }; var week = function (r) { var ts = r.timestamp || r.startedAt; return ts && (now - new Date(ts)) <= 7 * 86400000; }; var month = function (r) { var ts = r.timestamp || r.startedAt; return ts && new Date(ts).getMonth() === now.getMonth() && new Date(ts).getFullYear() === now.getFullYear(); }; if (state.mode === 'calculator') { var started = state.raw.calcProgress.length; cards = [ { label: 'Completed Leads', value: full.length, kpi: null, icon: iconStack() }, { label: 'Sessions Started', value: started, kpi: null, icon: iconBolt() }, { label: "Today's Leads", value: count(today), kpi: 'today', icon: iconCalendar() }, { label: 'This Month', value: count(month), kpi: 'month', icon: iconCalendar() }, { label: 'Emails Sent', value: count(function (r) { return r.emailStatus && r.emailStatus.toLowerCase() === 'sent'; }), kpi: 'emailStatus:Sent', icon: iconMail() }, { label: 'PDF Delivered', value: count(function (r) { return r.pdfStatus === 'Generated'; }), kpi: 'pdfStatus:Generated', icon: iconDoc() } ]; } else if (state.mode === 'guide') { cards = [ { label: 'Total Guide Leads', value: full.length, kpi: null, icon: iconStack() }, { label: "Today's Leads", value: count(today), kpi: 'today', icon: iconBolt() }, { label: 'This Week', value: count(week), kpi: '7d', icon: iconCalendar() }, { label: 'This Month', value: count(month), kpi: 'month', icon: iconCalendar() } ]; } else if (state.mode === 'clicks') { cards = [ { label: 'Total Clicks', value: full.length, kpi: null, icon: iconStack() }, { label: "Today's Clicks", value: count(today), kpi: 'today', icon: iconBolt() }, { label: 'This Week', value: count(week), kpi: '7d', icon: iconCalendar() }, { label: 'This Month', value: count(month), kpi: 'month', icon: iconCalendar() }, { label: 'From LinkedIn', value: count(function (r) { return r.referrerSource === 'LinkedIn'; }), kpi: 'LinkedIn', icon: iconLinkedIn() }, { label: 'From Facebook', value: count(function (r) { return r.referrerSource === 'Facebook'; }), kpi: null, icon: iconInstagram() } ]; } else { cards = [ { label: 'Total Enquiries', value: full.length, kpi: null, icon: iconStack() }, { label: "Today's Enquiries", value: count(today), kpi: 'today', icon: iconBolt() }, { label: 'This Week', value: count(week), kpi: '7d', icon: iconCalendar() }, { label: 'This Month', value: count(month), kpi: 'month', icon: iconCalendar() } ]; } grid.innerHTML = cards.map(function (c) { var active = c.kpi && state.activeFilters.kpi === c.kpi; return '