(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 '
' + c.icon + '
' + c.value + '
' + c.label + '
'; }).join(''); grid.querySelectorAll('.kpi-card').forEach(function (el) { el.addEventListener('click', function () { var kpi = el.dataset.kpi || null; if (!kpi) return; state.activeFilters.kpi = (state.activeFilters.kpi === kpi) ? null : kpi; state.activeFilters.chart = null; state.page = 1; renderAll(); }); }); } // ===================== CHARTS ===================== function renderCharts(full) { Chart.defaults.font.family = "'Inter', sans-serif"; Chart.defaults.color = COLORS.textSecondary; renderTrendChart(full); if (state.mode === 'calculator') { renderCalcPerfChart(full); renderScoreDonut(full); renderPdfDonut(full); } else if (state.mode === 'guide') { renderGuidePerfChart(full); renderSourceDonut(full); } else if (state.mode === 'clicks') { renderClickAssetChart(full); renderClickReferrerDonut(full); renderClickDeviceDonut(full); } else { renderContactServiceChart(full); renderSourceDonut(full); } } function destroy(key) { if (state.charts[key]) { try { state.charts[key].destroy(); } catch(e) {} } } function renderTrendChart(full) { var days = lastNDays(30); var counts = days.map(function (d) { return full.filter(function (r) { var ts = r.timestamp || r.startedAt || ''; return ts && isSameDay(new Date(ts), d); }).length; }); destroy('trend'); var label = state.mode === 'clicks' ? 'Clicks' : 'Leads'; state.charts.trend = new Chart(document.getElementById('trendChart'), { type: 'line', data: { labels: days.map(function (d) { return d.getDate() + '/' + (d.getMonth() + 1); }), datasets: [{ label: label, data: counts, borderColor: COLORS.brown, backgroundColor: 'rgba(122,75,46,0.08)', fill: true, tension: 0.35, pointRadius: function (ctx) { return ctx.raw > 0 ? 4 : 0; }, pointBackgroundColor: COLORS.brown, pointBorderColor: '#fff', pointBorderWidth: 1.5, borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } }, scales: { x: { grid: { display: false }, ticks: { maxTicksLimit: 10 } }, y: { beginAtZero: true, grid: { color: COLORS.border }, ticks: { precision: 0, stepSize: 1 } } } } }); } function renderCalcPerfChart(full) { destroy('perf'); state.charts.perf = barChart(groupCount(full, function (r) { return r.calculator || 'Unknown'; }), function (label) { applyChartFilter('magnet', label); }); } function renderGuidePerfChart(full) { destroy('perf'); state.charts.perf = barChart(groupCount(full, function (r) { return r.guide || 'Unknown'; }), function (label) { applyChartFilter('magnet', label); }); } function renderContactServiceChart(full) { destroy('perf'); state.charts.perf = barChart(groupCount(full, function (r) { return r.service || 'General'; }), function (label) { applyChartFilter('magnet', label); }); } function renderClickAssetChart(full) { destroy('perf'); var grouped = groupCount(full, function (r) { return r.assetLabel || r.asset || 'Unknown'; }); state.charts.perf = barChart(grouped, function (label) { applyChartFilter('magnet', assetLabelToKey(label)); }); } function barChart(dataObj, onClick) { var labels = Object.keys(dataObj); var values = labels.map(function (l) { return dataObj[l]; }); var maxVal = Math.max.apply(null, values.concat([1])); return new Chart(document.getElementById('perfChart'), { type: 'bar', data: { labels: labels, datasets: [{ data: values, backgroundColor: COLORS.brownLight, borderRadius: 6, maxBarThickness: 22 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { anchor: 'end', align: 'end', color: COLORS.brownDark, font: { weight: 700, size: 12 }, formatter: function (v) { return v; } } }, layout: { padding: { right: 22 } }, scales: { x: { beginAtZero: true, max: maxVal + Math.ceil(maxVal * 0.25), grid: { color: COLORS.border }, ticks: { precision: 0 } }, y: { grid: { display: false } } }, onClick: function (evt, els) { if (els.length) onClick(labels[els[0].index]); } } }); } function renderScoreDonut(full) { var bands = ['Critical', 'Significant', 'Specific', 'Blind Spots']; var byBand = {}; bands.forEach(function (b) { byBand[b] = 0; }); full.forEach(function (r) { if (r.scoreBand) byBand[r.scoreBand] = (byBand[r.scoreBand] || 0) + 1; }); destroy('donut1'); state.charts.donut1 = donutChart('donut1Chart', byBand, [COLORS.error, COLORS.warning, COLORS.brownLight, COLORS.success], function (label) { applyChartFilter('scoreBand', label); }); } function renderSourceDonut(full) { destroy('donut1'); state.charts.donut1 = donutChart('donut1Chart', groupCount(full, function (r) { return r.source || 'Website'; }), [COLORS.brown, COLORS.success, COLORS.warning, COLORS.brownLight], function (label) { applyChartFilter('source', label); }); } function renderPdfDonut(full) { destroy('donut2'); state.charts.donut2 = donutChart('donut2Chart', groupCount(full, function (r) { return r.pdfStatus || 'Failed'; }), [COLORS.success, COLORS.error], function () {}); } function renderClickReferrerDonut(full) { destroy('donut1'); state.charts.donut1 = donutChart('donut1Chart', groupCount(full, function (r) { return r.referrerSource || 'Direct'; }), [COLORS.brown, COLORS.success, COLORS.warning, COLORS.brownLight, COLORS.error], function (label) { applyChartFilter('source', label); }); } function renderClickDeviceDonut(full) { destroy('donut2'); state.charts.donut2 = donutChart('donut2Chart', groupCount(full, function (r) { return r.deviceType || 'Other'; }), [COLORS.brown, COLORS.brownLight, COLORS.success, COLORS.warning], function (label) { applyChartFilter('device', label); }); } function donutChart(canvasId, dataObj, palette, onClick) { var labels = Object.keys(dataObj).filter(function(k) { return dataObj[k] > 0; }); var values = labels.map(function (l) { return dataObj[l]; }); var total = values.reduce(function (a, b) { return a + b; }, 0) || 1; return new Chart(document.getElementById(canvasId), { type: 'doughnut', data: { labels: labels, datasets: [{ data: values, backgroundColor: labels.map(function(_, i) { return palette[i % palette.length]; }), borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: '62%', plugins: { legend: { position: 'bottom', labels: { boxWidth: 10, padding: 12, font: { size: 11.5 }, generateLabels: function (chart) { return chart.data.labels.map(function (label, i) { var v = chart.data.datasets[0].data[i]; var color = chart.data.datasets[0].backgroundColor[i]; return { text: label + ' — ' + v, fillStyle: color, strokeStyle: color, index: i }; }); } } }, datalabels: { color: '#fff', font: { weight: 700, size: 12 }, formatter: function (v) { return v > 0 ? Math.round((v / total) * 100) + '%' : ''; }, display: function (ctx) { return ctx.dataset.data[ctx.dataIndex] > 0; } } }, onClick: function (evt, els) { if (els.length) onClick(labels[els[0].index]); } } }); } function applyChartFilter(type, value) { var current = state.activeFilters.chart; state.activeFilters.chart = (current && current.type === type && current.value === value) ? null : { type: type, value: value }; state.activeFilters.kpi = null; state.page = 1; renderAll(); } // ===================== TABLE ===================== function renderTable(filtered) { var sorted = sortRows(filtered.slice(), state.sort); var cols; if (state.mode === 'calculator') { cols = [ { key: 'timestamp', label: 'Date', fmt: formatDate }, { key: 'email', label: 'Email' }, { key: 'calculator', label: 'Calculator' }, { key: 'score', label: 'Score' }, { key: 'scoreBand', label: 'Score Band', fmt: badgeScoreBand }, { key: 'source', label: 'Source' }, { key: 'emailStatus', label: 'Email', fmt: badgeStatus }, { key: 'pdfStatus', label: 'PDF', fmt: badgeStatus } ]; } else if (state.mode === 'guide') { cols = [ { key: 'timestamp', label: 'Date', fmt: formatDate }, { key: 'name', label: 'Name' }, { key: 'email', label: 'Email' }, { key: 'guide', label: 'Guide' }, { key: 'source', label: 'Source' } ]; } else if (state.mode === 'clicks') { cols = [ { key: 'timestamp', label: 'Date', fmt: formatDate }, { key: 'assetLabel', label: 'Asset' }, { key: 'referrerSource', label: 'Source', fmt: badgeReferrer }, { key: 'deviceType', label: 'Device', fmt: badgeDevice }, { key: 'referrer', label: 'Referrer URL', fmt: function (v) { return v ? '' + escapeHtml(v.length > 40 ? v.substring(0, 40) + '…' : v) + '' : ''; } } ]; } else { cols = [ { key: 'timestamp', label: 'Date', fmt: formatDate }, { key: 'name', label: 'Name' }, { key: 'email', label: 'Email' }, { key: 'company', label: 'Company' }, { key: 'service', label: 'Service' }, { key: 'source', label: 'Source' } ]; } document.getElementById('leadsThead').innerHTML = '' + cols.map(function (c) { var sortedCls = state.sort.key === c.key ? 'sorted' : ''; var arrow = state.sort.key === c.key ? (state.sort.dir === 'asc' ? '▲' : '▼') : '▲'; return '' + c.label + '' + arrow + ''; }).join('') + ''; document.querySelectorAll('#leadsThead th').forEach(function (th) { th.addEventListener('click', function () { var key = th.dataset.key; if (state.sort.key === key) { state.sort.dir = state.sort.dir === 'asc' ? 'desc' : 'asc'; } else { state.sort = { key: key, dir: 'desc' }; } renderTable(filtered); }); }); var totalPages = Math.max(1, Math.ceil(sorted.length / state.pageSize)); state.page = Math.min(state.page, totalPages); var pageRows = sorted.slice((state.page - 1) * state.pageSize, state.page * state.pageSize); document.getElementById('emptyState').style.display = sorted.length ? 'none' : 'block'; document.getElementById('leadsTable').style.display = sorted.length ? '' : 'none'; document.getElementById('leadsTbody').innerHTML = pageRows.map(function (row, i) { return '' + cols.map(function (c) { var val = row[c.key]; return '' + (c.fmt ? c.fmt(val) : escapeHtml(String(val === null || val === undefined || val === '' ? '—' : val))) + ''; }).join('') + ''; }).join(''); document.querySelectorAll('#leadsTbody tr').forEach(function (tr) { tr.addEventListener('click', function () { openDrawer(sorted[Number(tr.dataset.idx)]); }); }); var rowLabel = state.mode === 'clicks' ? ' click' : state.mode === 'contact' ? ' enquir' + (sorted.length === 1 ? 'y' : 'ies') : ' lead'; document.getElementById('resultCount').textContent = sorted.length + (state.mode === 'contact' ? '' : (sorted.length === 1 ? rowLabel : rowLabel + 's')); renderPagination(totalPages, sorted); } function renderPagination(totalPages, sorted) { var el = document.getElementById('pagination'); if (!sorted.length) { el.innerHTML = ''; return; } var start = (state.page - 1) * state.pageSize + 1; var end = Math.min(state.page * state.pageSize, sorted.length); el.innerHTML = 'Showing ' + start + '–' + end + ' of ' + sorted.length + '
'; var prev = document.getElementById('prevPage'), next = document.getElementById('nextPage'); if (prev) prev.addEventListener('click', function () { state.page--; renderTable(applyFilters(getModeData())); }); if (next) next.addEventListener('click', function () { state.page++; renderTable(applyFilters(getModeData())); }); } function sortRows(rows, sort) { return rows.sort(function (a, b) { var va = a[sort.key], vb = b[sort.key]; var tsKey = sort.key === 'timestamp' || sort.key === 'startedAt'; if (tsKey) { va = va ? new Date(va).getTime() : 0; vb = vb ? new Date(vb).getTime() : 0; } if (typeof va === 'string') va = va.toLowerCase(); if (typeof vb === 'string') vb = vb.toLowerCase(); if (va < vb) return sort.dir === 'asc' ? -1 : 1; if (va > vb) return sort.dir === 'asc' ? 1 : -1; return 0; }); } // ===================== DRAWER ===================== function openDrawer(row) { var fields; if (state.mode === 'calculator') { fields = [['Email', row.email], ['Calculator', row.calculator], ['Score', row.score], ['Score Band', row.scoreBand], ['PDF', row.pdf], ['PDF Status', row.pdfStatus], ['Email Status', row.emailStatus], ['Email Sent', row.emailSentTime ? formatDate(row.emailSentTime) : ''], ['Submitted', formatDate(row.timestamp)]]; } else if (state.mode === 'guide') { fields = [['Name', row.name], ['Email', row.email], ['Guide', row.guide], ['Source', row.source], ['Downloaded', formatDate(row.timestamp)]]; } else if (state.mode === 'clicks') { fields = [['Asset', row.assetLabel], ['Referrer Source', row.referrerSource], ['Full Referrer URL', row.referrer], ['Device / Browser', row.device], ['Email (if captured)', row.email], ['Clicked At', formatDate(row.timestamp)]]; } else { fields = [['Name', row.name], ['Email', row.email], ['Company', row.company], ['Service', row.service], ['Message', row.message], ['Source', row.source], ['Submitted', formatDate(row.timestamp)]]; } document.getElementById('drawerBody').innerHTML = fields .filter(function (f) { return f[1] !== undefined && f[1] !== null && String(f[1]).trim() !== ''; }) .map(function (f) { return '
' + escapeHtml(f[0]) + '
' + escapeHtml(String(f[1])) + '
'; }).join(''); document.getElementById('drawer').classList.add('open'); document.getElementById('drawerOverlay').classList.add('open'); } function closeDrawer() { document.getElementById('drawer').classList.remove('open'); document.getElementById('drawerOverlay').classList.remove('open'); } // ===================== FORMATTERS ===================== function badgeScoreBand(band) { var map = { 'Critical': 'badge-error', 'Significant': 'badge-warning', 'Specific': 'badge-neutral', 'Blind Spots': 'badge-success' }; return '' + escapeHtml(band || '—') + ''; } function badgeStatus(status) { var s = String(status || '').toLowerCase(); var cls = (s === 'sent' || s === 'generated') ? 'badge-success' : (s === 'pending') ? 'badge-warning' : (s === 'failed') ? 'badge-error' : 'badge-neutral'; return '' + escapeHtml(status || '—') + ''; } function badgeReferrer(source) { var map = { 'LinkedIn': 'badge-success', 'Instagram': 'badge-warning', 'Facebook': 'badge-success', 'Twitter/X': 'badge-neutral', 'Google': 'badge-neutral', 'Direct': 'badge-neutral' }; return '' + escapeHtml(source || 'Direct') + ''; } function badgeDevice(type) { var map = { 'Mobile': 'badge-warning', 'Desktop': 'badge-success', 'Tablet': 'badge-neutral' }; return '' + escapeHtml(type || '—') + ''; } function formatDate(iso) { if (!iso) return '—'; var d = new Date(iso); if (isNaN(d.getTime())) return escapeHtml(String(iso)); return d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }) + ', ' + d.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' }); } function formatTime(d) { return d.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' }); } function renderClock() { document.getElementById('currentDate').textContent = new Date().toLocaleDateString('en-IN', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' }); } // ===================== UTILITIES ===================== function uniq(arr) { var seen = {}; return arr.filter(function (v) { if (!v || seen[v]) return false; seen[v] = 1; return true; }); } function isSameDay(a, b) { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); } function lastNDays(n) { var out = []; var d = new Date(); for (var i = n - 1; i >= 0; i--) { var dd = new Date(d); dd.setDate(d.getDate() - i); out.push(dd); } return out; } function groupCount(arr, keyFn) { var out = {}; arr.forEach(function (r) { var k = keyFn(r) || 'Unknown'; out[k] = (out[k] || 0) + 1; }); return out; } function escapeHtml(str) { return String(str).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } // ===================== ICONS ===================== function iconStack() { return ''; } function iconBolt() { return ''; } function iconCalendar() { return ''; } function iconMail() { return ''; } function iconDoc() { return ''; } function iconLinkedIn() { return ''; } function iconInstagram(){ return ''; } })();