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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user