44 lines
15 KiB
HTML
44 lines
15 KiB
HTML
<!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)}.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="on"><div class="eyebrow">Restricted Report</div><h1>Sign in to view</h1><p>Enter your authorized email and password.</p><label class="field">Email<input id="email" type="email" autocomplete="username" required></label><label class="field">Password<input id="password" type="password" autocomplete="current-password" required></label><button id="loginButton" type="submit">Sign in</button><div id="loginError" class="login-error" role="alert" aria-live="polite"></div><p class="access-note">Access is limited to authorized email addresses.</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 id="budgetText" class="takeaway"></div><div class="summary" id="budgetCards" style="margin-top:14px"></div><div class="panel"><div class="chart-title">Monthly Budget Utilization</div><div class="chart-sub">Payable spend = (E1 + E2) × $3, converted at 16.50 ZAR/USD</div><div class="progress"><div id="budgetProgress" class="progress-fill"></div></div><div class="progress-labels"><span id="budgetUsed"></span><span id="budgetTotal"></span></div></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">USD · 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</span><span><i class="swatch" style="background:var(--gold)"></i>Payable spend</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 (USD)</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 × $3; 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</th><th>Midas spend</th></tr></thead><tbody id="weeklyRows"></tbody></table><div class="footer-note">Amounts are in USD. 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), 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>`}
|
||
const ACCESS={emails:['a91dc567bcdb6d054cf121d7e4016454ee3d316e3b452b851ae4d3087bc99bb2'],salt:'amazon-za-delivery-2026-08',password:'2344c6c9cf72430cafbf48e014d7f721a53484f766f1455f0c12232515ec61e9'};
|
||
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);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',money(t.payable_spend),`Midas spend ${money(t.midas_spend)}`]];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 budgetCards=[['Monthly budget','R '+fmt(b.monthly_budget_zar),money(b.monthly_budget_usd)],['Budget used','R '+fmt(b.used_budget_zar),`${pct(b.used_rate)} used · MTD expected FFR ${pct(b.mtd_expected_ffr)}`],['Budget remaining','R '+fmt(b.remaining_budget_zar),money(b.remaining_budget_usd)],['Required daily run rate','R '+fmt(b.required_daily_budget_zar),`${fmt(b.required_daily_events)} E1+E2 / day`]];document.getElementById('budgetCards').innerHTML=budgetCards.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('');
|
||
document.getElementById('budgetText').innerHTML=`<strong>Budget consumption is below even pacing through the current date.</strong> R ${fmt(b.used_budget_zar)} has been used versus an expected R ${fmt(b.expected_budget_to_date_zar)}. With ${b.remaining_days} days remaining, the required daily run rate is R ${fmt(b.required_daily_budget_zar)}, above the current daily budget of R ${fmt(b.daily_budget_zar)}.`;
|
||
document.getElementById('budgetProgress').style.width=Math.min(b.used_rate,100)+'%';document.getElementById('budgetUsed').textContent=`Used ${pct(b.used_rate)} · R ${fmt(b.used_budget_zar)}`;document.getElementById('budgetTotal').textContent=`Total budget R ${fmt(b.monthly_budget_zar)}`;
|
||
lineChart(document.getElementById('spendTrend'),d,[{key:'midas_spend',name:'Midas spend',color:'var(--blue)'},{key:'payable_spend',name:'Payable spend',color:'var(--gold)'}],v=>'$'+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>${money(x.payable_spend)}</td><td>R ${fmt(x.daily_budget_zar)}</td><td>${pct(x.ffr_rate)}</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>${money(w.payable_spend)}</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 email=document.getElementById('email').value.trim().toLowerCase(),password=document.getElementById('password').value;const [emailHash,passwordHash]=await Promise.all([sha256(email),sha256(ACCESS.salt+':'+password)]);if(ACCESS.emails.includes(emailHash)&&passwordHash===ACCESS.password){sessionStorage.setItem('amazonZaAccess','granted');showReport()}else{error.textContent='Incorrect email or password.'}}catch(_){error.textContent='This browser cannot complete verification. Please use a modern browser.'}finally{button.disabled=false}});
|
||
if(sessionStorage.getItem('amazonZaAccess')==='granted')showReport();
|
||
</script></body></html>
|