feat: dynamic data insights + remaining audit fixes

DATA FIXES:
- Dashboard insights now fully dynamic per date range
  - Revenue/orders/newcust/AOV trend (splits data in half, compares)
  - Channel concentration % computed from filtered data
  - Repeat revenue % with date-range label
  - All insights adapt when user changes date filter/preset
- Offer page charts now load from API (not hardcoded arrays)
  - Revenue chart, new customer chart, channel donut = live data
  - Hero stats (-84%, -42%, 85%) computed dynamically from API
  - ROI calculator AOV pulled from latest year's actual data

REMAINING AUDIT FIXES:
- Fix #4: 'Built by' section on offer page (name, bio, contact)
- Fix #9: Before/After comparison block on homepage
  - Side-by-side: current JV PDP flaws vs AI engine output
  - 8 specific ✗/✓ comparison points
- Fix #6: Before/after serves as instant proof (no 90s wait)

All 10 audit fixes now implemented.
This commit is contained in:
2026-03-02 22:16:29 +08:00
parent ebe1dd5c14
commit 3cd296f6bf
3 changed files with 222 additions and 56 deletions

View File

@@ -387,18 +387,69 @@ function buildExec(fm){
`<div class="kpi"><div class="kpi-label">${k.l}</div><div class="kpi-value ${k.c}">${k.v}</div><div class="kpi-sub">${k.s}</div></div>`
).join('');
// Insights
const latestYear = fm.filter(r=>r.YearMonth>='2025-01');
const prevYear = fm.filter(r=>r.YearMonth>='2024-01'&&r.YearMonth<='2024-12');
const lyRev = latestYear.reduce((s,r)=>s+r.revenue,0);
const pyRev = prevYear.reduce((s,r)=>s+r.revenue,0);
const yoyChange = pyRev > 0 ? ((lyRev - pyRev) / pyRev * 100) : 0;
// Dynamic range-aware insights
// Split into halves for trend comparison
const half = Math.floor(fm.length / 2);
const firstHalf = fm.slice(0, half);
const secondHalf = fm.slice(half);
const fhRev = firstHalf.reduce((s,r)=>s+r.revenue,0);
const shRev = secondHalf.reduce((s,r)=>s+r.revenue,0);
const revTrend = fhRev > 0 ? ((shRev - fhRev) / fhRev * 100) : 0;
const fhOrders = firstHalf.reduce((s,r)=>s+r.orders,0);
const shOrders = secondHalf.reduce((s,r)=>s+r.orders,0);
const orderTrend = fhOrders > 0 ? ((shOrders - fhOrders) / fhOrders * 100) : 0;
const fhNew = firstHalf.reduce((s,r)=>s+r.newCustomers,0);
const shNew = secondHalf.reduce((s,r)=>s+r.newCustomers,0);
const newTrend = fhNew > 0 ? ((shNew - fhNew) / fhNew * 100) : 0;
const fhAOV = fhOrders > 0 ? fhRev / fhOrders : 0;
const shAOV = shOrders > 0 ? shRev / shOrders : 0;
const aovTrend = fhAOV > 0 ? ((shAOV - fhAOV) / fhAOV * 100) : 0;
document.getElementById('execInsights').innerHTML = `
<div class="insight ${avgMargin >= 55 ? 'good' : 'warn'}"><strong>Avg Margin ${pct(avgMargin)}:</strong> ${avgMargin >= 55 ? 'Healthy margins across the product range.' : 'Margins are under pressure — review product costs and pricing.'}</div>
<div class="insight"><strong>Returning customer revenue:</strong> ${pct(repeatRevPct)} of revenue comes from repeat buyers. ${repeatRevPct > 40 ? 'Strong loyalty base to leverage.' : 'Opportunity to improve retention.'}</div>
<div class="insight ${avgItems < 2 ? 'warn' : 'good'}"><strong>Items per order: ${fmt(avgItems,1)}.</strong> ${avgItems < 2 ? 'Most orders are single-item. Bundling and cross-sell could lift AOV.' : 'Good cross-sell rate.'}</div>
`;
// Channel concentration for this date range
const chFilt = filterArr(RAW.channelMonthly);
const chTotals = {};
chFilt.forEach(r => { chTotals[r.ReferrerSource] = (chTotals[r.ReferrerSource]||0) + r.orders; });
const chTotal = Object.values(chTotals).reduce((a,b)=>a+b,0);
const googleOrg = (chTotals['Organic']||0) + (chTotals['Google Adwords']||0);
const googlePct = chTotal > 0 ? googleOrg / chTotal * 100 : 0;
const socialPct = chTotal > 0 ? ((chTotals['Facebook']||0) / chTotal * 100) : 0;
const rangeLabel = FSTART && FEND ? `${FSTART} to ${FEND}` : FSTART ? `${FSTART} onwards` : FEND ? `up to ${FEND}` : 'all time';
const halfLabel1 = firstHalf.length ? `${firstHalf[0].YearMonth}${firstHalf[firstHalf.length-1].YearMonth}` : '';
const halfLabel2 = secondHalf.length ? `${secondHalf[0].YearMonth}${secondHalf[secondHalf.length-1].YearMonth}` : '';
let insightsHtml = '';
// Revenue trend
if(fm.length >= 4) {
insightsHtml += `<div class="insight ${revTrend >= 0 ? 'good' : 'warn'}"><strong>Revenue ${revTrend >= 0 ? '↑' : '↓'} ${pct(Math.abs(revTrend))}:</strong> ${gbp(shRev)} in ${halfLabel2} vs ${gbp(fhRev)} in ${halfLabel1}. ${revTrend < -10 ? 'Declining trend in this range.' : revTrend > 10 ? 'Growing in this range.' : 'Relatively flat.'}</div>`;
}
// New customer trend
if(fm.length >= 4 && fhNew > 0) {
insightsHtml += `<div class="insight ${newTrend >= 0 ? 'good' : 'warn'}"><strong>New customers ${newTrend >= 0 ? '↑' : '↓'} ${pct(Math.abs(newTrend))}:</strong> ${fmt(shNew)} new customers in second half vs ${fmt(fhNew)} in first half of this range.</div>`;
}
// AOV trend
if(fm.length >= 4) {
insightsHtml += `<div class="insight ${aovTrend >= 0 ? 'good' : 'warn'}"><strong>AOV ${aovTrend >= 0 ? '↑' : '↓'} ${pct(Math.abs(aovTrend))}:</strong> ${gbp(shAOV)} vs ${gbp(fhAOV)}. ${aovTrend > 5 ? 'Customers are spending more per order.' : aovTrend < -5 ? 'Average basket value declining.' : 'AOV holding steady.'}</div>`;
}
// Margins
insightsHtml += `<div class="insight ${avgMargin >= 55 ? 'good' : 'warn'}"><strong>Avg margin ${pct(avgMargin)}:</strong> ${avgMargin >= 55 ? 'Healthy margins across the product range.' : 'Margins under pressure — review product costs and pricing.'}</div>`;
// Returning revenue
insightsHtml += `<div class="insight"><strong>Returning customer revenue:</strong> ${pct(repeatRevPct)} of revenue comes from repeat buyers (${rangeLabel}). ${repeatRevPct > 60 ? 'Extremely loyal base — but dependency on existing customers may mask acquisition weakness.' : repeatRevPct > 40 ? 'Strong loyalty base to leverage.' : 'Opportunity to improve retention.'}</div>`;
// Channel concentration
if(chTotal > 0) {
insightsHtml += `<div class="insight ${googlePct > 80 ? 'warn' : ''}"><strong>Channel concentration:</strong> ${pct(googlePct)} of orders from Google channels (Organic + Ads) in this range. ${socialPct < 1 ? 'Social commerce is under ' + pct(socialPct) + ' — virtually zero discovery from social channels.' : 'Social contributing ' + pct(socialPct) + '.'}</div>`;
}
// Items per order
insightsHtml += `<div class="insight ${avgItems < 2 ? 'warn' : 'good'}"><strong>Items per order: ${fmt(avgItems,1)}.</strong> ${avgItems < 2 ? 'Most orders are single-item. Bundling and cross-sell could lift AOV.' : 'Good cross-sell rate.'}</div>`;
document.getElementById('execInsights').innerHTML = insightsHtml;
// Revenue + Orders chart
destroyChart('revOrdersChart');

View File

@@ -599,6 +599,19 @@ input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:20px;heigh
<div class="faq-item" onclick="toggleFaq(this)"><div class="faq-q">How is this different from hiring an agency?<span class="arrow">+</span></div><div class="faq-a"><div class="faq-a-inner">An agency charges monthly for services and you own nothing when you leave. This is a one-time infrastructure build — you own the server, the workflows, the content, and the integrations. The £500/mo covers hosting and maintenance only. If you cancel, you keep the infrastructure and can self-host.</div></div></div>
</section>
<!-- ══════════════ BUILT BY ══════════════ -->
<div style="max-width:680px;margin:48px auto;padding:28px 32px;background:rgba(99,102,241,.04);border:1px solid rgba(99,102,241,.15);border-radius:14px;display:flex;gap:20px;align-items:center">
<div style="flex-shrink:0;width:64px;height:64px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#06b6d4);display:flex;align-items:center;justify-content:center;font-size:28px;font-weight:700;color:#fff">O</div>
<div>
<div style="font-size:16px;font-weight:700;color:var(--text)">Omair · <a href="https://quikcue.com" style="color:var(--accent)">QuikCue</a></div>
<div style="font-size:13px;color:var(--text2);margin-top:4px;line-height:1.5">I build AI infrastructure for ecommerce brands — automation, content engines, and data systems. This proposal was built using the same tools I'm proposing to install. Every demo, every chart, every number on this page is real.</div>
<div style="margin-top:8px;font-size:12px;display:flex;gap:12px">
<a href="mailto:omair@quikcue.com" style="color:var(--accent)">omair@quikcue.com</a>
<a href="https://quikcue.com" style="color:var(--text3)">quikcue.com</a>
</div>
</div>
</div>
<div class="page-footer">
Prepared for Just Vitamins · March 2026 · Confidential<br>
Data source: 728,018 validated orders · Nov 2005 — Jan 2026
@@ -644,59 +657,117 @@ const counterObs = new IntersectionObserver(e => {
}, {threshold:.5});
document.querySelectorAll('[data-count]').forEach(el => counterObs.observe(el));
// ══════ CHARTS ══════
// ══════ LOAD REAL DATA FROM API ══════
const cBase = {responsive:true,maintainAspectRatio:false,animation:{duration:600},plugins:{legend:{display:false}},scales:{x:{grid:{color:'rgba(26,37,69,.4)'},ticks:{color:'#5a6d94',font:{size:10}}},y:{grid:{color:'rgba(26,37,69,.4)'},ticks:{color:'#5a6d94',font:{size:10}}}}};
// Revenue chart
new Chart(document.getElementById('revChart'), {
fetch('/api/dashboard/data').then(r=>r.json()).then(data => {
const m = data.monthly.sort((a,b)=>a.YearMonth.localeCompare(b.YearMonth));
// Aggregate by year (2018+)
const yearly = {};
m.forEach(r => {
const y = r.YearMonth.substring(0,4);
if(parseInt(y) < 2018) return;
if(!yearly[y]) yearly[y] = {rev:0, orders:0, newCust:0};
yearly[y].rev += r.revenue;
yearly[y].orders += r.orders;
yearly[y].newCust += r.newCustomers;
});
const years = Object.keys(yearly).sort().filter(y => y !== '2026');
const peakYear = years.reduce((a,b) => yearly[a].rev > yearly[b].rev ? a : b);
const latestYear = years[years.length-1];
const peakNewYear = years.reduce((a,b) => yearly[a].newCust > yearly[b].newCust ? a : b);
// Update hero stats dynamically
const newDecline = yearly[peakNewYear] && yearly[latestYear] ? Math.round((yearly[latestYear].newCust - yearly[peakNewYear].newCust) / yearly[peakNewYear].newCust * 100) : -84;
const revDecline = yearly[peakYear] && yearly[latestYear] ? Math.round((yearly[latestYear].rev - yearly[peakYear].rev) / yearly[peakYear].rev * 100) : -42;
document.querySelectorAll('[data-count]').forEach(el => {
if(el.closest('.red')) el.dataset.count = newDecline;
if(el.closest('.amber')) el.dataset.count = revDecline;
});
// Channel data
const ch = data.channelMonthly || [];
const chTotals = {};
// Use last 24 months for channel %
const recent = m.filter(r => r.YearMonth >= (parseInt(latestYear)-1)+'-01');
const recentYMs = new Set(recent.map(r=>r.YearMonth));
ch.filter(r => recentYMs.has(r.YearMonth)).forEach(r => {
chTotals[r.ReferrerSource] = (chTotals[r.ReferrerSource]||0) + r.orders;
});
const chTotal = Object.values(chTotals).reduce((a,b)=>a+b,0);
const googleOrgPct = chTotal > 0 ? Math.round(((chTotals['Organic']||0) + (chTotals['Google Adwords']||0)) / chTotal * 100) : 85;
// Update the 85% stat dynamically
const greyVal = document.querySelector('.alert-stat.grey .val');
if(greyVal) greyVal.textContent = googleOrgPct + '%';
const channelH2 = document.querySelector('h2.stitle');
if(channelH2 && channelH2.textContent.includes('Channel Dependency'))
channelH2.textContent = googleOrgPct + '% Channel Dependency';
// Revenue chart — from real data
new Chart(document.getElementById('revChart'), {
type:'line',
data:{
labels:['2018','2019','2020','2021','2022','2023','2024','2025'],
labels: years,
datasets:[{
data:[1654002,1545674,1820963,1775174,1514505,1378510,1244661,1047850],
data: years.map(y => Math.round(yearly[y].rev)),
borderColor:'#6366f1',backgroundColor:'rgba(99,102,241,.1)',fill:true,tension:.4,pointRadius:4,pointBackgroundColor:'#6366f1',
pointBorderColor:'#0d1322',pointBorderWidth:2,borderWidth:2.5
}]
},
options:{...cBase,scales:{...cBase.scales,y:{...cBase.scales.y,ticks:{...cBase.scales.y.ticks,callback:v=>'£'+Math.round(v/1000)+'k'}}},
plugins:{...cBase.plugins,tooltip:{callbacks:{label:ctx=>'£'+ctx.raw.toLocaleString()}}}}
});
});
// New customer chart
new Chart(document.getElementById('custChart'), {
// New customer chart — from real data
const custColors = years.map(y => {
if(y === peakNewYear) return '#10b981cc';
return yearly[y].newCust < yearly[peakNewYear].newCust * 0.5 ? '#ef4444cc' :
yearly[y].newCust < yearly[peakNewYear].newCust * 0.75 ? '#f59e0bcc' : '#6366f1cc';
});
new Chart(document.getElementById('custChart'), {
type:'bar',
data:{
labels:['2018','2019','2020','2021','2022','2023','2024','2025'],
labels: years,
datasets:[{
data:[20219,11874,24666,20309,16404,9869,6338,3941],
backgroundColor:['#6366f1cc','#6366f1cc','#10b981cc','#6366f1cc','#f59e0bcc','#ef4444cc','#ef4444cc','#ef4444cc'],
data: years.map(y => yearly[y].newCust),
backgroundColor: custColors,
borderRadius:6,borderSkipped:false
}]
},
options:{...cBase,plugins:{...cBase.plugins,tooltip:{callbacks:{label:ctx=>ctx.raw.toLocaleString()+' new customers'}}}}
});
});
// Channel donut
new Chart(document.getElementById('chChart'), {
// Channel donut — from real data
const chEntries = Object.entries(chTotals).sort((a,b)=>b[1]-a[1]);
const chColors = ['#6366f1','#818cf8','#a855f7','#06b6d4','#64748b','#ef4444','#f59e0b','#10b981'];
new Chart(document.getElementById('chChart'), {
type:'doughnut',
data:{
labels:['Organic','Google Ads','Webgains','Email','Bing','Social/Other'],
labels: chEntries.map(e => e[0]),
datasets:[{
data:[50.5,31.2,10.4,5.6,2.2,0.1],
backgroundColor:['#6366f1','#818cf8','#a855f7','#06b6d4','#64748b','#ef4444'],
data: chEntries.map(e => parseFloat((e[1]/chTotal*100).toFixed(1))),
backgroundColor: chColors.slice(0, chEntries.length),
borderWidth:0,spacing:2
}]
},
options:{responsive:true,maintainAspectRatio:false,cutout:'65%',
plugins:{legend:{position:'bottom',labels:{color:'#5a6d94',font:{size:10},padding:12,usePointStyle:true,pointStyleWidth:8}},
tooltip:{callbacks:{label:ctx=>ctx.label+': '+ctx.raw+'%'}}}}
});
});
// Update ROI calculator with real AOV
const latestAOV = yearly[latestYear] ? yearly[latestYear].rev / yearly[latestYear].orders : 35.02;
window._realAOV = latestAOV;
calcROI();
}).catch(err => console.warn('Offer charts: using static fallback', err));
// ══════ ROI CALCULATOR ══════
function calcROI() {
const n = parseInt(document.getElementById('roiSlider').value);
document.getElementById('sliderVal').textContent = n;
const aov = 35.02; // 2025 actual (source-linked)
const aov = window._realAOV || 35.02; // live from API, fallback to 2025 actual
const cohortReturnRate = 0.576; // 12-month cohort return rate (verified from 728K orders)
const avgRepeats = 2; // conservative avg repeat purchases in year 1
const ltv = aov * (1 + cohortReturnRate * avgRepeats);

View File

@@ -68,6 +68,50 @@
</div>
</section>
<!-- ═══ BEFORE / AFTER ═══ -->
<section class="demo" id="before-after" style="padding-top:2rem">
<span class="badge" style="background:rgba(245,158,11,.15);color:#f59e0b">📊 WHAT THE AI ENGINE PRODUCES</span>
<h2 style="margin-bottom:.5rem">Before → After: Real Product Comparison</h2>
<p class="sub" style="margin-bottom:2rem">Same product. Left is the current justvitamins.co.uk page. Right is what the AI engine generates in 90 seconds.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:2rem">
<div style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;position:relative">
<div style="position:absolute;top:12px;left:12px;background:rgba(239,68,68,.15);color:#ef4444;font-size:10px;font-weight:700;padding:3px 10px;border-radius:4px;text-transform:uppercase">Current PDP</div>
<div style="margin-top:28px">
<div style="font-size:15px;font-weight:700;color:var(--text);margin-bottom:8px">Super Strength Vitamin D3 4000iu + K2</div>
<div style="font-size:12px;color:var(--text3);margin-bottom:16px">justvitamins.co.uk — actual product page</div>
<div style="display:flex;flex-direction:column;gap:8px;font-size:12px;color:var(--text2)">
<div style="display:flex;gap:8px;align-items:center"><span style="color:#ef4444"></span> Generic subtitle: "A powerful pairing"</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#ef4444"></span> Single product photo (pouch only)</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#ef4444"></span> No benefit-driven bullets (Feature→Benefit→Proof)</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#ef4444"></span> No trust signals (money-back, UK-made, certifications)</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#ef4444"></span> No price anchoring (daily cost, cost-per-dose)</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#ef4444"></span> No SEO meta description</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#ef4444"></span> No ad hooks or email sequences</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#ef4444"></span> Cookie consent modal covers half the page</div>
</div>
</div>
</div>
<div style="background:var(--card);border:1px solid rgba(16,185,129,.3);border-radius:12px;padding:24px;position:relative">
<div style="position:absolute;top:12px;left:12px;background:rgba(16,185,129,.15);color:#10b981;font-size:10px;font-weight:700;padding:3px 10px;border-radius:4px;text-transform:uppercase">AI-Generated PDP</div>
<div style="margin-top:28px">
<div style="font-size:15px;font-weight:700;color:var(--text);margin-bottom:8px">Same product — generated in ~90 seconds</div>
<div style="font-size:12px;color:var(--text3);margin-bottom:16px">Conversion-optimised by Gemini AI</div>
<div style="display:flex;flex-direction:column;gap:8px;font-size:12px;color:var(--text2)">
<div style="display:flex;gap:8px;align-items:center"><span style="color:#10b981"></span> 5 AI-generated product photos (lifestyle, scale, detail, banner)</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#10b981"></span> Feature → Benefit → Proof bullet structure</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#10b981"></span> Trust bar: money-back, UK-made, EFSA claims</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#10b981"></span> Price anchoring: "just 14p per day"</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#10b981"></span> Full FAQ accordion with objection handling</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#10b981"></span> SEO meta + Open Graph tags</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#10b981"></span> 3 ad hooks + 3 email sequences</div>
<div style="display:flex;gap:8px;align-items:center"><span style="color:#10b981"></span> 4 copy styles: Balanced, Premium, Direct, Medical-Safe</div>
</div>
</div>
</div>
</div>
<p style="text-align:center;color:var(--text3);font-size:12px">↑ Run Demo A above to see the full AI-generated PDP for any JustVitamins product</p>
</section>
<!-- ═══ DEMO B — Competitor X-Ray ═══ -->
<section class="demo" id="demo-b">
<span class="badge blue">🔍 DEMO B — LIVE</span>