Files
zouyun-amazon-za-daily-report/index.html
T
2026-08-24 17:25:06 +08:00

41 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Amazon - ZA daily tracker</title>
<style>
:root{--bg:#f4f7fb;--panel:#fff;--ink:#162033;--muted:#667085;--line:#dce3ec;--blue:#2f6fed;--gold:#d79b24;--orange:#e56d35;--soft:#eaf1ff;--shadow:0 8px 28px rgba(23,43,77,.08)}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.55 Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}main{max-width:1180px;margin:auto;padding:42px 24px 64px}.eyebrow{color:var(--blue);font-weight:700;letter-spacing:.08em;text-transform:uppercase}.hero h1{font-size:clamp(28px,4vw,46px);line-height:1.1;margin:8px 0 12px}.hero p{color:var(--muted);font-size:16px;margin:0;max-width:800px}.status{display:inline-flex;align-items:center;gap:8px;margin-top:18px;padding:7px 11px;border:1px solid var(--line);border-radius:999px;background:var(--panel);color:var(--muted)}.dot{width:8px;height:8px;border-radius:50%;background:var(--blue)}section{margin-top:30px}h2{font-size:22px;margin:0 0 12px}.summary{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}.card,.panel{background:var(--panel);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow)}.card{padding:18px}.card .label{color:var(--muted);font-weight:600}.card .value{font-size:28px;font-weight:750;margin-top:6px;font-variant-numeric:tabular-nums}.card .note{color:var(--muted);font-size:12px;margin-top:3px}.panel{padding:22px;margin-top:14px}.takeaway{border-left:4px solid var(--blue);padding-left:14px;color:var(--muted)}.takeaway strong{color:var(--ink)}.charts{display:grid;grid-template-columns:1fr 1fr;gap:14px}.chart{min-height:300px;overflow-x:auto}.chart-head{margin-bottom:16px}.chart-title{font-weight:750;font-size:16px}.chart-sub{color:var(--muted);font-size:12px}.bars{height:220px;display:flex;align-items:end;gap:14px;padding:20px 10px 28px;border-bottom:1px solid var(--line);position:relative}.bar-group{flex:1;height:100%;display:flex;align-items:end;justify-content:center;gap:6px;position:relative}.bar{min-width:8px;max-width:34px;width:33%;border-radius:6px 6px 0 0;position:relative}.bar span{position:absolute;top:-21px;left:50%;transform:translateX(-50%);font-size:11px;font-weight:700;white-space:nowrap}.bar-label{position:absolute;bottom:-25px;left:50%;transform:translateX(-50%);color:var(--muted);font-size:11px}.legend{display:flex;gap:14px;margin-top:13px;color:var(--muted);font-size:12px}.swatch{width:9px;height:9px;border-radius:2px;display:inline-block;margin-right:5px}.rate-bars .bar{width:52%;background:var(--orange)}.progress{height:14px;background:var(--soft);border-radius:999px;overflow:hidden;margin:18px 0 8px}.progress-fill{height:100%;background:var(--blue);border-radius:inherit}.progress-labels{display:flex;justify-content:space-between;color:var(--muted);font-size:12px}table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}th,td{padding:11px 9px;border-bottom:1px solid var(--line);text-align:right;white-space:nowrap}th:first-child,td:first-child{text-align:left}th{color:var(--muted);font-size:12px}tbody tr:hover{background:var(--soft)}tr.future{color:var(--muted);opacity:.65}.footer-note{color:var(--muted);font-size:12px;margin-top:18px}.error{padding:18px;border:1px solid #f0b7b7;background:#fff2f2;color:#912b2b;border-radius:12px}.login-shell{min-height:100vh;display:grid;place-items:center;padding:24px}.login-card{width:min(430px,100%);background:var(--panel);border:1px solid var(--line);border-radius:22px;padding:30px;box-shadow:var(--shadow)}.login-card h1{font-size:28px;margin:8px 0}.login-card p{color:var(--muted);margin:0 0 22px}.field{display:block;margin-top:14px;font-weight:700}.field input{display:block;width:100%;margin-top:7px;border:1px solid var(--line);border-radius:11px;padding:12px 13px;background:var(--bg);color:var(--ink);font:inherit;outline:none}.field input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(47,111,237,.14)}button{width:100%;margin-top:20px;border:0;border-radius:11px;padding:13px;background:var(--blue);color:white;font:inherit;font-weight:750;cursor:pointer}button:disabled{opacity:.65;cursor:wait}.login-error{min-height:22px;color:#c13a3a;font-size:13px;margin-top:10px}.access-note{font-size:12px!important;margin-top:18px!important}#report[hidden],#login[hidden]{display:none!important}@media(max-width:850px){.summary{grid-template-columns:1fr 1fr}.charts{grid-template-columns:1fr}}@media(max-width:520px){main{padding:26px 14px}.summary{grid-template-columns:1fr}.panel{padding:16px;overflow:auto}.card .value{font-size:24px}.login-card{padding:24px}}
.table-wrap{overflow-x:auto;padding:0}.table-wrap table{min-width:950px}.table-wrap th{position:sticky;top:0;background:var(--soft);z-index:1;text-transform:uppercase;letter-spacing:.03em}.table-wrap th,.table-wrap td{padding:12px 14px}.table-wrap tbody tr:nth-child(even):not(.future){background:rgba(47,111,237,.035)}.table-wrap tbody tr:hover{background:var(--soft)}.budget-panel{padding:0;max-width:620px;overflow:hidden}.budget-table{width:100%}.budget-table thead th{background:var(--blue);color:#fff;text-align:left;font-size:13px}.budget-table th,.budget-table td{padding:10px 14px;text-align:left}.budget-table tbody tr:nth-child(even){background:var(--soft)}.budget-table td:nth-child(n+2){font-variant-numeric:tabular-nums;font-weight:650}.rate-spike{background:rgba(229,109,53,.16);color:#b94f20;font-weight:800;border-radius:7px}.trend-svg{display:block;width:100%;height:260px;min-width:760px}.trend-grid{stroke:var(--line);stroke-width:1}.trend-axis{fill:var(--muted);font-size:10px}.trend-line{fill:none;stroke-width:3;stroke-linejoin:round;stroke-linecap:round}.trend-point{stroke:var(--panel);stroke-width:2}
@media(prefers-color-scheme:dark){:root{--bg:#0c1220;--panel:#121a2a;--ink:#eef3fb;--muted:#9ba9bd;--line:#28354a;--soft:#182743;--shadow:none}}
</style>
</head>
<body><div id="login" class="login-shell"><form id="loginForm" class="login-card" autocomplete="off"><div class="eyebrow">Restricted Report</div><h1>Enter access code</h1><p>Enter the report access code to continue.</p><label class="field">Access code<input id="accessCode" type="password" autocomplete="off" required autofocus></label><button id="loginButton" type="submit">Unlock report</button><div id="loginError" class="login-error" role="alert" aria-live="polite"></div><p class="access-note">Access is limited to people who have the code.</p></form></div><main id="report" hidden>
<header class="hero"><div class="eyebrow">Daily Delivery · ZA</div><h1>Amazon - ZA daily tracker</h1><div class="status"><span class="dot"></span><span id="range">Loading data…</span></div></header>
<section><h2>Key Metrics</h2><div class="summary" id="cards"></div></section>
<section><h2>August Budget Pacing</h2><div class="panel budget-panel"><table class="budget-table"><thead><tr><th>Aug</th><th>ZAR</th><th>USD</th></tr></thead><tbody id="budgetRows"></tbody></table></div></section>
<section><h2>Daily Delivery & Conversion</h2><div class="panel chart"><div class="chart-head"><div class="chart-title">Daily Spend Trend</div><div class="chart-sub">ZAR · Midas spend vs. payable spend</div></div><div id="spendTrend"></div><div class="legend"><span><i class="swatch" style="background:var(--blue)"></i>Midas spend (ZAR)</span><span><i class="swatch" style="background:var(--gold)"></i>Payable spend (ZAR)</span></div></div><div class="panel chart"><div class="chart-head"><div class="chart-title">E1+E2% Trend</div><div class="chart-sub">(Event1 + Event2) ÷ Install</div></div><div id="rateTrend"></div></div></section>
<section><h2>August Daily Detail</h2><div class="panel table-wrap"><table><thead><tr><th>Date</th><th>Install</th><th>Event1</th><th>Event2</th><th>Total</th><th>Event rate</th><th>Payable (ZAR)</th><th>Daily budget</th><th>FFR</th><th>Midas (USD)</th></tr></thead><tbody id="rows"></tbody></table><div class="footer-note">Definitions: Install includes delivery repostbacks only; Total = E1+E2; Payable spend = Total × 49.50 ZAR; FFR = Total ÷ daily cap. Event-rate changes of ≥5 percentage points versus the previous day are highlighted. Grey rows are after the data cutoff.</div></div></section>
<section><h2>Calendar Week Summary</h2><div class="panel table-wrap"><table><thead><tr><th>Calendar week (Mon–Sun)</th><th>Install</th><th>Event1+2</th><th>Event1+2%</th><th>Payable spend (ZAR)</th><th>Midas spend (USD)</th></tr></thead><tbody id="weeklyRows"></tbody></table><div class="footer-note">Payable spend is calculated in ZAR at Event1+2 × 49.50. The first and last weeks are clipped to August; the current week runs through the data cutoff.</div></div></section>
</main><script>
const fmt=n=>new Intl.NumberFormat('en-US').format(n), money=n=>'$'+new Intl.NumberFormat('en-US',{minimumFractionDigits:2,maximumFractionDigits:2}).format(n), zar=n=>'R '+new Intl.NumberFormat('en-US',{minimumFractionDigits:2,maximumFractionDigits:2}).format(n), pct=n=>n==null?'—':n.toFixed(2)+'%';
const label=d=>d.slice(5).replace('-','/');
function bars(el,data,series,max){const showValues=data.length<=12;el.style.minWidth=Math.max(0,data.length*48)+'px';el.innerHTML=data.map(d=>`<div class="bar-group">${series.map(s=>`<div class="bar" style="height:${Math.max(2,d[s.key]/max*100)}%;background:${s.color}">${showValues?`<span>${s.format(d[s.key])}</span>`:''}</div>`).join('')}<div class="bar-label">${label(d.date)}</div></div>`).join('')}
function lineChart(el,data,series,format){const w=Math.max(760,data.length*42),h=260,p={l:48,r:18,t:18,b:34},vals=data.flatMap(d=>series.map(s=>Number(d[s.key])||0)),max=Math.max(...vals,1),min=0,x=i=>p.l+i*(w-p.l-p.r)/Math.max(data.length-1,1),y=v=>h-p.b-(v-min)*(h-p.t-p.b)/(max-min||1),ticks=[0,.25,.5,.75,1];el.innerHTML=`<svg class="trend-svg" viewBox="0 0 ${w} ${h}" role="img">${ticks.map(t=>`<line class="trend-grid" x1="${p.l}" x2="${w-p.r}" y1="${y(max*t)}" y2="${y(max*t)}"/><text class="trend-axis" x="${p.l-7}" y="${y(max*t)+3}" text-anchor="end">${format(max*t)}</text>`).join('')}${data.map((d,i)=>`<text class="trend-axis" x="${x(i)}" y="${h-10}" text-anchor="middle">${label(d.date)}</text>`).join('')}${series.map(s=>`<polyline class="trend-line" stroke="${s.color}" points="${data.map((d,i)=>`${x(i)},${y(d[s.key]||0)}`).join(' ')}"/>${data.map((d,i)=>`<circle class="trend-point" fill="${s.color}" cx="${x(i)}" cy="${y(d[s.key]||0)}" r="3"><title>${d.date} · ${s.name}: ${format(d[s.key]||0)}</title></circle>`).join('')}`).join('')}</svg>`}
async function sha256(value){const bytes=new TextEncoder().encode(value);const digest=await crypto.subtle.digest('SHA-256',bytes);return [...new Uint8Array(digest)].map(x=>x.toString(16).padStart(2,'0')).join('')}
function showReport(){document.getElementById('login').hidden=true;document.getElementById('report').hidden=false;loadReport()}
async function loadReport(){fetch('data.json',{cache:'no-store'}).then(r=>{if(!r.ok)throw Error('data.json '+r.status);return r.json()}).then(data=>{
const t=data.totals,b=data.budget,all=data.daily,d=all.filter(x=>x.is_actual),fx=16.5,payinZar=49.5,monthlyBudgetZar=544500,dailyBudgetZar=18150,dailyCap=367,installCap=1740,usedBudgetZar=t.e1e2*payinZar,remainingBudgetZar=monthlyBudgetZar-usedBudgetZar;document.getElementById('range').textContent=`Aug 1–31, 2026 · Data through ${data.meta.data_cutoff}`;
const cards=[['Install',fmt(t.install),'Delivery repostbacks only · app opens excluded'],['E1 + E2',fmt(t.e1e2),`${fmt(t.event1)} E1 · ${fmt(t.event2)} E2`],['E1+E2%',pct(t.e1e2_rate),'Cumulative E1+E2 ÷ cumulative Install'],['Payable spend',zar(usedBudgetZar),'(E1 + E2) × 49.50 ZAR']];document.getElementById('cards').innerHTML=cards.map(x=>`<div class="card"><div class="label">${x[0]}</div><div class="value">${x[1]}</div><div class="note">${x[2]}</div></div>`).join('');
const budgetRows=[['Monthly Budget',zar(monthlyBudgetZar),money(monthlyBudgetZar/fx)],['Remain budget',zar(remainingBudgetZar),money(remainingBudgetZar/fx)],['left days','—',fmt(b.remaining_days)],['daily budget',zar(dailyBudgetZar),money(dailyBudgetZar/fx)],['Daily cap',fmt(dailyCap),fmt(dailyCap)],['install cap',fmt(installCap),fmt(installCap)],['Payin',zar(payinZar),money(payinZar/fx)]];document.getElementById('budgetRows').innerHTML=budgetRows.map(r=>`<tr><td>${r[0]}</td><td>${r[1]}</td><td>${r[2]}</td></tr>`).join('');
const spendDaily=d.map(x=>({...x,midas_spend_zar:x.midas_spend*fx,payable_spend_zar:x.e1e2*payinZar}));lineChart(document.getElementById('spendTrend'),spendDaily,[{key:'midas_spend_zar',name:'Midas spend',color:'var(--blue)'},{key:'payable_spend_zar',name:'Payable spend',color:'var(--gold)'}],v=>'R '+Math.round(v));
lineChart(document.getElementById('rateTrend'),d,[{key:'e1e2_rate',name:'E1+E2%',color:'var(--orange)'}],v=>v.toFixed(0)+'%');
document.getElementById('rows').innerHTML=all.map((x,i)=>{const prev=i?all[i-1]:null,spike=x.is_actual&&prev&&prev.is_actual&&Math.abs((x.e1e2_rate||0)-(prev.e1e2_rate||0))>=5;return `<tr class="${x.is_actual?'':'future'}"><td>${x.date}</td><td>${fmt(x.install)}</td><td>${fmt(x.event1)}</td><td>${fmt(x.event2)}</td><td>${fmt(x.e1e2)}</td><td class="${spike?'rate-spike':''}">${pct(x.e1e2_rate)}</td><td>${zar(x.e1e2*payinZar)}</td><td>${zar(dailyBudgetZar)}</td><td>${pct(x.e1e2/dailyCap*100)}</td><td>${money(x.midas_spend)}</td></tr>`}).join('');
document.getElementById('weeklyRows').innerHTML=data.weekly.map(w=>`<tr><td>${w.label}</td><td>${fmt(w.install)}</td><td>${fmt(w.e1e2)}</td><td>${pct(w.e1e2_rate)}</td><td>${zar(w.e1e2*payinZar)}</td><td>${money(w.midas_spend)}</td></tr>`).join('');
}).catch(e=>{document.getElementById('range').textContent='Failed to load data: '+e.message})}
document.getElementById('loginForm').addEventListener('submit',async e=>{e.preventDefault();const button=document.getElementById('loginButton'),error=document.getElementById('loginError');button.disabled=true;error.textContent='';try{const access=await fetch('access.json',{cache:'no-store'}).then(r=>{if(!r.ok)throw Error('access configuration unavailable');return r.json()}),code=document.getElementById('accessCode').value,codeHash=await sha256(access.salt+':'+code);if(codeHash===access.hash){sessionStorage.setItem('amazonZaCodeAccessV1','granted');showReport()}else{error.textContent='Incorrect access code.'}}catch(_){error.textContent='Unable to verify access. Please try again.'}finally{button.disabled=false}});
if(sessionStorage.getItem('amazonZaCodeAccessV1')==='granted')showReport();
</script></body></html>