Upgrade to Pro for $19.99/month. Get anomaly detection, trend warnings, cross-metric analysis, and day-of-week recommendations — all from your real data.
Enter the email you subscribed with. We'll verify your active subscription instantly.
✓ Pro verified! Your AI insights are unlocked.
Step 1 of 2
Select your POS system
Upload your sales export and get AI-powered insights backed by your actual numbers.
1
In Toast, go to Reports in the left sidebar
2
Click Sales › Sales by Day — this gives daily revenue, orders & guest counts
3
Set your date range (90+ days recommended), then click Export to CSV
4
Optional: Also export Item Selection Report under Sales → Menu for item-level data
💡 Revy auto-detects Toast’s date format — just upload the CSV as-is, no editing needed.
1
Go to squareup.com › Reports in the left menu
2
Click Sales Summary or Transactions for a full date range
3
Set your date range to Last 90 Days or custom, then click Export › CSV
4
Upload the downloaded CSV directly to Revy
💡 The Transactions export gives the most data. Sales Summary also works well.
1
Log into clover.com › go to Reports
2
Select Sales from the report type dropdown
3
Choose your date range (90 days or more for best insights), click Export
4
Select CSV format and download
💡 Use the Payments report if Sales export is unavailable — both formats are supported.
1
In Lightspeed Restaurant, go to Reports › Sales
2
Select Daily Sales Summary or Sales by Day
3
Set your date range, then click the Export button (top right) → CSV
4
Upload the CSV to Revy — no formatting required
💡 Lightspeed’s Business Insights export also works if you need more granular data.
1
Export any CSV or Excel file with a date column and a revenue/sales column
2
Revy auto-detects columns — common names like Date, Net Sales, Revenue, Total all work
3
Include as many columns as possible (orders, guests, items) for richer insights
4
90+ days of data gives the best anomaly detection and trend analysis
💡 Supported formats: .csv, .xlsx, .xls. Column names are flexible — Revy will figure it out.
Supported formats
Toast Sales by Day · Toast Item Selection · Square · Clover · Lightspeed · Any CSV or XLSX with date & revenue columns
Step 2 of 2
Upload your sales report
Revy auto-detects your format. No editing needed.
💡 Upload the largest date range available — filter down after upload.
↑
Drop your file here
or click to browse · CSV, XLSX, XLS
Analyzing file…
Reading file…
○ Parsing sales records
○ Calculating daily & weekly patterns
○ Detecting anomalies & trends
○ Running AI analysis
○ Running AI analysis
Sales Intelligence Report
Your restaurant intelligence report
Free plan: Charts & data always free. Upgrade to Pro to unlock AI insights, anomaly detection, and trend warnings.
📅 Date Range
→
Re-analyzing selected date range…
var state={pos:null,posName:null,allRows:[],fileMin:null,fileMax:null,filterStart:null,filterEnd:null,salesData:null,report:null,reportType:null,isPro:false,proEmail:null};
document.getElementById('yr').textContent=new Date().getFullYear();
function setCookie(n,v,d){var e=new Date();e.setTime(e.getTime()+(d*864e5));document.cookie=n+'='+encodeURIComponent(v)+';expires='+e.toUTCString()+';path=/;SameSite=Lax';}
function getCookie(n){var k=n+'=',ca=document.cookie.split(';');for(var i=0;i(i+1)*18)document.getElementById('ps-'+i).classList.add('done');}
}
function parseCSV(text){
var lines=text.trim().split('\n').filter(function(l){return l.trim();});
if(lines.length<2)return[];
var headers=lines[0].split(',').map(function(h){return h.replace(/"/g,'').trim().toLowerCase();});
return lines.slice(1).map(function(line){
var vals=[],cur='',inQ=false;
for(var i=0;i-1||keys.some(function(k){return k.includes('net sales');}))return'sales_by_day';
if(keys.some(function(k){return k.includes('item name')||k.includes('menu item');}))return'item_selection';
return'generic';
}
function parseToastDate(raw){
var s=String(raw).trim().replace(/[^0-9]/g,'');
if(s.length===8){var dt=new Date(parseInt(s.slice(0,4)),parseInt(s.slice(4,6))-1,parseInt(s.slice(6,8)));if(!isNaN(dt.getTime()))return dt;}
var dt2=new Date(raw);return isNaN(dt2.getTime())?null:dt2;
}
function attachDates(rows,rt){
return rows.map(function(row){
var parsed=null;
if(rt==='sales_by_day'){parsed=parseToastDate(row['yyyymmdd']||row['date']||row['business date']||'');}
else{var keys=Object.keys(row);for(var i=0;i0&&rev===0)rev=v;}});
if(rt!=='sales_by_day')orders=1;
if(rev>0)data.totalRevenue+=rev;
data.totalOrders+=orders||1;data.totalGuests+=guests;
var d=row._date;
if(d){
var dk=d.toISOString().slice(0,10);
data.byDay[dk]=(data.byDay[dk]||0)+rev;
var dow=DN[d.getDay()];data.byDOW[dow]=(data.byDOW[dow]||0)+rev;
var mk=d.toLocaleString('default',{month:'short'})+" '"+String(d.getFullYear()).slice(2);
data.byMonth[mk]=(data.byMonth[mk]||0)+rev;
data.dates.push(d);
data.dailyRows.push({date:d,dateKey:dk,rev:rev,orders:orders,guests:guests,avgCheck:orders>0?(rev/orders):0});
}
});
data.dailyRows.sort(function(a,b){return a.date-b.date;});
var dowC={};
data.dailyRows.forEach(function(r){var dow=DN[r.date.getDay()];if(!dowC[dow])dowC[dow]={rev:0,count:0,orders:0,guests:0};dowC[dow].rev+=r.rev;dowC[dow].count++;dowC[dow].orders+=r.orders;dowC[dow].guests+=r.guests;});
data.dowAvg={};
Object.keys(dowC).forEach(function(d){var c=dowC[d];data.dowAvg[d]={avg:c.rev/c.count,count:c.count,orders:c.orders/c.count,guests:c.guests/c.count};});
var ds=Object.entries(data.dowAvg).sort(function(a,b){return b[1].avg-a[1].avg;});
data.peakDOW=ds[0]?ds[0][0]:'N/A';data.slowDOW=ds[ds.length-1]?ds[ds.length-1][0]:'N/A';
var mid=Math.floor(data.dailyRows.length/2);
var a1=mid?data.dailyRows.slice(0,mid).reduce(function(s,r){return s+r.rev;},0)/mid:0;
var a2=(data.dailyRows.length-mid)?data.dailyRows.slice(mid).reduce(function(s,r){return s+r.rev;},0)/(data.dailyRows.length-mid):0;
data.trendPct=a1>0?((a2-a1)/a1*100):0;data.trendDir=data.trendPct>=0?'up':'down';
var revs=data.dailyRows.map(function(r){return r.rev;});
var mean=revs.reduce(function(s,v){return s+v;},0)/(revs.length||1);
var std=Math.sqrt(revs.reduce(function(s,v){return s+Math.pow(v-mean,2);},0)/(revs.length||1));
data.anomalies=data.dailyRows.filter(function(r){return Math.abs(r.rev-mean)>1.8*std;}).map(function(r){return{date:r.date,rev:r.rev,mean:mean,type:r.rev>mean?'spike':'drop'};});
var wkMap={};
data.dailyRows.forEach(function(r){var dow=r.date.getDay();var mon=new Date(r.date);mon.setDate(r.date.getDate()-((dow+6)%7));var wk=mon.toISOString().slice(0,10);if(!wkMap[wk])wkMap[wk]={rev:0,orders:0,guests:0,days:0,weekStart:mon};wkMap[wk].rev+=r.rev;wkMap[wk].orders+=r.orders;wkMap[wk].guests+=r.guests;wkMap[wk].days++;});
data.weeks=Object.entries(wkMap).sort(function(a,b){return a[0]0;}).slice(-3).reverse();
var uniq=[...new Set(data.dates.map(function(d){return d.toDateString();}))].length||1;
data.dailyAvg=(data.totalRevenue/uniq).toFixed(2);
data.avgCheck=data.totalOrders>0?(data.totalRevenue/data.totalOrders).toFixed(2):'0';
var topQ=data.dailyRows.slice().sort(function(a,b){return b.rev-a.rev;}).slice(0,Math.max(1,Math.floor(data.dailyRows.length*0.25)));
data.bestDayAvg=(topQ.reduce(function(s,r){return s+r.rev;},0)/topQ.length).toFixed(2);
var _gap=parseFloat(data.bestDayAvg)-parseFloat(data.dailyAvg);if(isNaN(_gap)||_gap<0)_gap=0;
data.dailyGap=_gap.toFixed(2);data.monthlyOpportunity=Math.round(_gap*30*0.5);
var _dn=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],_dc={};
topQ.forEach(function(r){var d=_dn[r.date.getDay()];_dc[d]=(_dc[d]||0)+1;});
data.bestDOWpattern=Object.entries(_dc).sort(function(a,b){return b[1]-a[1];}).slice(0,2).map(function(e){return e[0];}).join(' and ');
data.avgGuests=uniq>0?(data.totalGuests/uniq).toFixed(1):'0';
data.daysCovered=uniq;
if(data.dates.length>0){
var ts=data.dates.map(function(d){return d.getTime();});
data.startDate=new Date(Math.min.apply(null,ts));data.endDate=new Date(Math.max.apply(null,ts));
data.dateRangeLabel=data.startDate.toLocaleDateString('en-US',{month:'short',day:'numeric'})+' \u2013 '+data.endDate.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});
}else data.dateRangeLabel='All dates';
return data;
}
function buildPrompt(sd){
var DO=['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
var dowL=DO.filter(function(d){return sd.dowAvg[d];}).map(function(d){
var v=sd.dowAvg[d];
return d+': $'+v.avg.toFixed(0)+'/day avg ('+v.count+' days'+( v.orders>0?', '+v.orders.toFixed(1)+' avg orders':'')+')';
}).join('\n');
var wkL=sd.weeks.map(function(w,i){
var prev=i>0?sd.weeks[i-1]:null;
var wow=prev&&prev.rev>0?((w.rev-prev.rev)/prev.rev*100):null;
return 'Week of '+w.weekStart.toLocaleDateString('en-US',{month:'short',day:'numeric'})+': $'+w.rev.toFixed(0)+(wow!==null?' ('+(wow>=0?'+':'')+wow.toFixed(1)+'% WoW)':'');
}).join('\n');
var bst=sd.bestDays.slice(0,5).map(function(r){
var dow=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][r.date.getDay()];
return r.date.toLocaleDateString('en-US',{month:'short',day:'numeric'})+' ('+dow+'): $'+r.rev.toFixed(0);
}).join(', ');
var wst=sd.worstDays.slice(0,5).map(function(r){
var dow=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][r.date.getDay()];
return r.date.toLocaleDateString('en-US',{month:'short',day:'numeric'})+' ('+dow+'): $'+r.rev.toFixed(0);
}).join(', ');
var anomL=sd.anomalies.length?sd.anomalies.map(function(a){
var dow=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][a.date.getDay()];
return a.date.toLocaleDateString('en-US',{month:'short',day:'numeric'})+' ('+dow+'): $'+a.rev.toFixed(0)+' -- '+(a.type==='spike'?'SPIKE':'DROP')+' vs avg $'+a.mean.toFixed(0);
}).join('\n'):'None';
return 'You are a sharp restaurant business analyst. Analyze this data and respond with EXACTLY this format -- four sections with these exact headers, each with 4-5 bullet points starting with *. Every bullet must reference a specific date, dollar amount, or day name. No generic advice.\n\n##INSIGHTS##\n* [insight]\n\n##ANOMALIES##\n* [anomaly]\n\n##SPECIALS##\n* [special]\n\n##OPPORTUNITY##\n* [opportunity]\n\nDATA SUMMARY ('+sd.daysCovered+' days, '+sd.dateRangeLabel+'):\n- Total: $'+sd.totalRevenue.toFixed(0)+'\n- Daily Avg: $'+sd.dailyAvg+'\n- Best-Day Avg (top 25%): $'+sd.bestDayAvg+'\n- Daily Gap: $'+sd.dailyGap+'/day\n- Monthly Opportunity (close half gap): $'+sd.monthlyOpportunity+'\n- Avg Check: $'+sd.avgCheck+'\n- Orders: '+sd.totalOrders+', Guests: '+sd.totalGuests+'\n- Trend: '+Math.abs(sd.trendPct).toFixed(1)+'% '+(sd.trendDir==='up'?'up':'down')+'\n- Peak DOW: '+sd.peakDOW+' | Slow DOW: '+sd.slowDOW+'\n\nTOP 5 DAYS:\n'+bst+'\n\nWORST 5 DAYS:\n'+wst+'\n\nDOW AVERAGES:\n'+dowL+'\n\nWEEK-OVER-WEEK:\n'+wkL+'\n\nANOMALIES:\n'+anomL;
}
function extractJSON(raw){
if(!raw)return null;
// Try JSON first (legacy)
try{
var c=raw.replace(/```json|```/gi,'').trim();
var s=c.indexOf('{'),e=c.lastIndexOf('}');
if(s>-1&&e>-1){var parsed=JSON.parse(c.slice(s,e+1));if(parsed.insights)return parsed;}
}catch(err){}
// Parse section headers
var sections={insights:null,anomalies:null,specials:null,opportunity:null};
var keys=['INSIGHTS','ANOMALIES','SPECIALS','OPPORTUNITY'];
var mapped={INSIGHTS:'insights',ANOMALIES:'anomalies',SPECIALS:'specials',OPPORTUNITY:'opportunity'};
for(var i=0;i=firstHalf?'up':'down';
var peakAvg=sd.dowAvg[sd.peakDOW]?Math.round(sd.dowAvg[sd.peakDOW].avg).toLocaleString():'--';
var slowAvg=sd.dowAvg[sd.slowDOW]?Math.round(sd.dowAvg[sd.slowDOW].avg).toLocaleString():'--';
var slowCount=sd.dowAvg[sd.slowDOW]?sd.dowAvg[sd.slowDOW].count:sd.daysCovered/7;
var peakCount=sd.dowAvg[sd.peakDOW]?sd.dowAvg[sd.peakDOW].count:sd.daysCovered/7;
var upsellEst=Math.round(sd.totalOrders*0.3*5).toLocaleString();
var weeklyLift=Math.round(10*slowCount).toLocaleString();
var checkLift=Math.round(parseFloat(sd.avgCheck)*0.15).toLocaleString();
var monthlyFmt=parseInt(sd.monthlyOpportunity).toLocaleString();
var yearlyFmt=Math.round(sd.monthlyOpportunity*12).toLocaleString();
var gapFmt=parseFloat(sd.dailyGap).toLocaleString();
var anomList=sd.anomalies.length?sd.anomalies.slice(0,3).map(function(a){
var dow=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][a.date.getDay()];
return a.date.toLocaleDateString('en-US',{month:'short',day:'numeric'})+' ('+dow+') -- $'+Math.round(a.rev).toLocaleString()+', '+(a.type==='spike'?'spike':'drop')+' vs $'+Math.round(a.mean).toLocaleString()+' avg';
}):'';
return{
insights:(
'* Revenue ran $'+halfDiff+' '+halfDir+' in the second half of this period vs the first. '+(halfDir==='up'?'Momentum is building.':'Worth understanding what shifted.')+'\n'
+'* Best day: '+bstStr+'. Worst day: '+wstStr+'. The $'+Math.round((bst&&wst)?bst.rev-wst.rev:0).toLocaleString()+' gap between them is not luck -- it shows what this location can do when conditions are right.\n'
+'* '+sd.peakDOW+' ($'+peakAvg+' avg) vs '+sd.slowDOW+' ($'+slowAvg+' avg). A 4x difference between your best and worst day of week is a significant operational opportunity.\n'
+'* At $'+sd.avgCheck+' avg check, a $'+checkLift+' upsell on 1 in 4 tables adds ~$'+upsellEst+' over '+sd.daysCovered+' days without adding a single new customer.'
),
anomalies:(
(anomList.length?anomList.map(function(s){return '* '+s;}).join('\n')+'\n':'')
+'* Strongest days: '+sd.bestDays.slice(0,3).map(function(r){return r.date.toLocaleDateString('en-US',{month:'short',day:'numeric'})+' $'+Math.round(r.rev).toLocaleString();}).join(', ')+'\n'
+'* Weakest days: '+sd.worstDays.slice(0,3).map(function(r){return r.date.toLocaleDateString('en-US',{month:'short',day:'numeric'})+' $'+Math.round(r.rev).toLocaleString();}).join(', ')
),
specials:(
'* '+sd.slowDOW+' is your floor at $'+slowAvg+'/day. A targeted happy hour or limited special on '+sd.slowDOW+' evenings could realistically add $'+Math.round(parseFloat(sd.avgCheck)*3).toLocaleString()+'-$'+Math.round(parseFloat(sd.avgCheck)*6).toLocaleString()+' per week.\n'
+'* '+sd.peakDOW+' at $'+peakAvg+' carries the week. Make sure you are fully staffed, menu is tight, and the experience is consistent -- this is your brand day.\n'
+'* The gap between '+sd.peakDOW+' and '+sd.slowDOW+' is $'+(Math.round(Math.abs((sd.dowAvg[sd.peakDOW]?sd.dowAvg[sd.peakDOW].avg:0)-(sd.dowAvg[sd.slowDOW]?sd.dowAvg[sd.slowDOW].avg:0)))).toLocaleString()+'/day. Even moving '+sd.slowDOW+' halfway to your weekly average adds $'+weeklyLift+' back over this period.\n'
+'* Consider a consistent weekly promotion on '+sd.slowDOW+' -- same time, same offer, marketed in advance so regulars plan around it.'
),
opportunity:(
'* Your daily avg is $'+sd.dailyAvg+'. Your top-quartile days avg $'+sd.bestDayAvg+'. The $'+gapFmt+' gap is not a ceiling -- you have already cleared it '+Math.round(sd.dailyRows.length*0.25)+' times in this dataset.\n'
+'* Close half that gap and you add $'+monthlyFmt+'/month, $'+yearlyFmt+'/year. That is real money: an extra employee, a marketing budget, or margin you are not currently capturing.\n'
+'* '+sd.bestDOWpattern+' drive your best numbers. Understand exactly why -- is it volume, check size, or both -- then replicate those conditions on your slowest days.\n'
+'* At $'+sd.avgCheck+' avg check, your leverage is in check size, not just covers. One additional item per table on '+sd.slowDOW+' could close a meaningful portion of the gap.'
)
};
}
async function runAI(sd){
if(!state.isPro){renderInsights(null,false,true);return;}
document.getElementById('reanalyze-note').classList.add('show');
renderInsights(null,true,false);
try{var res=await fetch('/.netlify/functions/analyze',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt:buildPrompt(sd)})});if(!res.ok){var errBody=await res.text();console.error('analyze HTTP error:',res.status,errBody);throw new Error('HTTP '+res.status+': '+errBody);}var d=await res.json();var parsed=extractJSON(d.raw);if(!parsed)console.error('Parse failed, raw:',d.raw);state.report=parsed||fallbackReport(sd);}
catch(e){console.error('analyze error:',e.message||e);state.report=fallbackReport(sd);}
document.getElementById('reanalyze-note').classList.remove('show');
renderInsights(state.report,false,false);
}
async function processFile(file){
showScreen('processing');
document.getElementById('proc-title').textContent='Analyzing '+file.name+'\u2026';
setProgress(10,'Reading file\u2026');
var rows=[];
try{if(/\.csv$/i.test(file.name)){rows=parseCSV(await file.text());}else{var buf=await file.arrayBuffer();var wb=XLSX.read(buf,{type:'array'});var ws=wb.Sheets[wb.SheetNames[0]];rows=XLSX.utils.sheet_to_json(ws,{defval:''}).map(function(r){var lo={};Object.entries(r).forEach(function(e){lo[e[0].toLowerCase().trim()]=String(e[1]);});return lo;});}}catch(e){console.error(e);}
setProgress(25,'Detecting format\u2026');
var rt=detectReportType(rows);state.reportType=rt;
setProgress(40,'Parsing dates\u2026');
var dr=attachDates(rows,rt);
var vd=dr.map(function(r){return r._date;}).filter(Boolean);
if(vd.length){var ts=vd.map(function(d){return d.getTime();});state.fileMin=new Date(Math.min.apply(null,ts));state.fileMax=new Date(Math.max.apply(null,ts));state.filterStart=state.fileMin;state.filterEnd=state.fileMax;}
state.allRows=dr;
setProgress(60,'Detecting anomalies\u2026');
state.salesData=extractSalesData(dr,rt);
setProgress(78,'Running AI\u2026');
await runAI(state.salesData);
setProgress(100,'Complete');
setTimeout(function(){renderResults(state.salesData);showScreen('results');},300);
}
function filterRows(s,e){var st=s.getTime(),en=e.getTime()+86399999;return state.allRows.filter(function(r){return !r._date||(r._date.getTime()>=st&&r._date.getTime()<=en);});}
function fmtDate(d){return d?d.toISOString().slice(0,10):'';}
function parseDate(s){return s?new Date(s+'T00:00:00'):null;}
function onDateChange(){var s=parseDate(document.getElementById('date-start').value);var e=parseDate(document.getElementById('date-end').value);if(s&&e&&s<=e)applyDateFilter(s,e);}
function applyDateFilter(start,end){state.filterStart=start;state.filterEnd=end;updateDateCount();updatePresetHighlight();state.salesData=extractSalesData(filterRows(start,end),state.reportType);renderResults(state.salesData);runAI(state.salesData);}
function updateDateCount(){document.getElementById('date-count').textContent=Math.round((state.filterEnd-state.filterStart)/86400000)+1+' days';}
function buildPresets(){
var min=state.fileMin,max=state.fileMax;
var ps=[{label:'All time',s:min,e:max},{label:'Last 7d',s:new Date(Math.max(min.getTime(),max.getTime()-6*86400000)),e:max},{label:'Last 30d',s:new Date(Math.max(min.getTime(),max.getTime()-29*86400000)),e:max},{label:'Last 90d',s:new Date(Math.max(min.getTime(),max.getTime()-89*86400000)),e:max}];
var c=document.getElementById('presets');c.innerHTML='';
ps.forEach(function(p){var btn=document.createElement('button');btn.className='preset-btn';btn.textContent=p.label;btn.onclick=function(){applyDateFilter(p.s,p.e);document.getElementById('date-start').value=fmtDate(p.s);document.getElementById('date-end').value=fmtDate(p.e);};c.appendChild(btn);});
}
function updatePresetHighlight(){
var s=fmtDate(state.filterStart),mx=fmtDate(state.fileMax);
var starts=[fmtDate(state.fileMin),fmtDate(new Date(Math.max(state.fileMin.getTime(),state.fileMax.getTime()-6*86400000))),fmtDate(new Date(Math.max(state.fileMin.getTime(),state.fileMax.getTime()-29*86400000))),fmtDate(new Date(Math.max(state.fileMin.getTime(),state.fileMax.getTime()-89*86400000)))];
document.querySelectorAll('.preset-btn').forEach(function(btn,i){btn.classList.toggle('active',starts[i]===s&&mx===fmtDate(state.filterEnd));});
}
function $h(tag,cls,html,style){var el=document.createElement(tag);if(cls)el.className=cls;if(html)el.innerHTML=html;if(style)el.style.cssText=style;return el;}
function makeInsightCard(label,color,icon,content,loading,locked){
var wrap=$h('div','insight-wrap');
var inner=$h('div',locked?'insight-locked':'');
var card=$h('div','insight-card');card.style.borderTopColor=color;
card.appendChild($h('div','card-title tag',''+icon+' '+label+''));
var body=$h('div','');
if(loading){body.innerHTML=[100,85,92,70,88].map(function(w,i){return '';}).join('');}
else if(content){var bd=$h('div','insight-body');bd.textContent=content.replace(/^\* /gm,'\u2022 ');body.appendChild(bd);}
card.appendChild(body);inner.appendChild(card);
if(locked){var ov=$h('div','lock-overlay');ov.innerHTML='
🔒
Pro Insight
Upgrade to unlock '+label.toLowerCase()+' from your real data.
';var btn=$h('button','lock-btn','Unlock for $19.99/mo');btn.onclick=openModal;ov.appendChild(btn);inner.appendChild(ov);}
wrap.appendChild(inner);return wrap;
}
function renderInsights(report,loading,locked){
var right=document.getElementById('insight-right');right.innerHTML='';
right.appendChild(makeInsightCard('Tailored Insights','#F0624D','↗',report?report.insights:null,loading,locked));
right.appendChild(makeInsightCard('Anomaly Detection','#C0392B','⚠',report?report.anomalies:null,loading,locked));
var bg=document.getElementById('bottom-grid');bg.innerHTML='';
bg.appendChild(makeInsightCard('Day-of-Week Specials','#2D6A4F','▲',report?report.specials:null,loading,locked));
bg.appendChild(makeInsightCard('Revenue Opportunity','#1D3557','△',report?report.opportunity:null,loading,locked));
}
function renderResults(sd){
document.getElementById('res-tag').textContent=(state.posName||'')+' \u00b7 '+sd.dateRangeLabel+' \u00b7 '+sd.daysCovered+' days';
document.getElementById('res-date').textContent='Generated '+new Date().toLocaleDateString('en-US',{month:'long',day:'numeric',year:'numeric'});
if(state.fileMin&&state.fileMax){
var ds=document.getElementById('date-start'),de=document.getElementById('date-end');
ds.value=fmtDate(state.filterStart||state.fileMin);ds.min=fmtDate(state.fileMin);ds.max=fmtDate(state.fileMax);
de.value=fmtDate(state.filterEnd||state.fileMax);de.min=fmtDate(state.fileMin);de.max=fmtDate(state.fileMax);
buildPresets();updateDateCount();updatePresetHighlight();
document.getElementById('date-bar').style.display='flex';
}
var kr=document.getElementById('kpi-row');kr.innerHTML='';
function kpi(label,val,sub,trend){var cls=trend==='up'?'up':trend==='down'?'down':'neu';var arr=trend==='up'?'\u2191':trend==='down'?'\u2193':'\u2013';kr.appendChild($h('div','kpi','
'+label+'
'+val+'
'+arr+' '+sub+'
'));}
kpi('Total Revenue','$'+Math.round(sd.totalRevenue).toLocaleString(),sd.daysCovered+' days','neu');
kpi('Daily Average','$'+Math.round(sd.dailyAvg).toLocaleString(),'daily average',sd.trendDir==='up'?'up':'down');
kpi('Avg Check','$'+sd.avgCheck,'per order','neu');
kpi('Total Orders',sd.totalOrders.toLocaleString(),'across '+sd.daysCovered+' days','neu');
if(sd.totalGuests>0)kpi('Avg Guests/Day',sd.avgGuests,'per trading day','neu');
kpi('Trend',sd.trendDir==='up'?'\u25b2 Up':'\u25bc Down',Math.abs(sd.trendPct).toFixed(1)+'% vs first half of period',sd.trendDir==='up'?'up':'down');
var col=document.getElementById('chart-col');col.innerHTML='';
var DO=['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
var dd=DO.filter(function(d){return sd.dowAvg[d];}).map(function(d){return{d:d,v:sd.dowAvg[d].avg};});
if(dd.length){
var maxD=Math.max.apply(null,dd.map(function(x){return x.v;}));
var c1=$h('div','card');c1.appendChild($h('div','card-title tag','Avg revenue by day of week'));
dd.forEach(function(x){var hi=x.v===maxD;var b=$h('div','mini-bar');b.innerHTML='
';c2.appendChild(b);});
col.appendChild(c2);
}
renderInsights(state.report,false,!state.isPro);
var ws=document.getElementById('weekly-section');ws.innerHTML='';
if(sd.dailyRows&&sd.dailyRows.length){
var tc=$h('div','card','','margin-bottom:16px');tc.appendChild($h('div','card-title tag','Daily breakdown'));
var tbl=$h('table','weekly-table');
var hg=sd.totalGuests>0;
tbl.innerHTML='
Date
Day
Revenue
Orders
'+(hg?'
Guests
Avg Check
':'')+'
';
var tbody=document.createElement('tbody');
var maxR=Math.max.apply(null,sd.dailyRows.map(function(r){return r.rev;}));
var minR=Math.min.apply(null,sd.dailyRows.filter(function(r){return r.rev>0;}).map(function(r){return r.rev;}));
sd.dailyRows.slice().reverse().forEach(function(r){
var dow=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][r.date.getDay()];
var ds=r.date.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});
var isBest=r.rev===maxR,isWorst=r.rev===minR&&minR!==maxR;
var isAnom=sd.anomalies.some(function(a){return a.date.toDateString()===r.date.toDateString();});
var tr=document.createElement('tr');if(isAnom)tr.style.background='#FEF0EE';
tr.innerHTML='
':'');
tbody.appendChild(tr);
});
tbl.appendChild(tbody);tc.appendChild(tbl);
var FREE_ROWS=7;
if(!state.isPro&&sd.dailyRows.length>FREE_ROWS){
var wrap=$h('div','table-lock-wrap');
var visCard=$h('div','card','','margin-bottom:16px');
visCard.appendChild($h('div','card-title tag','Daily breakdown'));
var visTbl=$h('table','weekly-table');
visTbl.innerHTML=tbl.querySelector('thead').outerHTML;
var visTbody=document.createElement('tbody');
var allRows=Array.from(tbl.querySelectorAll('tbody tr'));
allRows.slice(0,FREE_ROWS).forEach(function(r){visTbody.appendChild(r.cloneNode(true));});
visTbl.appendChild(visTbody);
visCard.appendChild(visTbl);
wrap.appendChild(visCard);
var ov=$h('div','table-blur-overlay');
ov.innerHTML='
🔒
Pro unlocks all '+sd.dailyRows.length+' days
See every day, every anomaly.
';
wrap.appendChild(ov);
ws.appendChild(wrap);
} else {
ws.appendChild(tc);
}
}
var ms=document.getElementById('monthly-section');ms.innerHTML='';
var monthEntries=Object.entries(sd.byMonth).sort(function(a,b){
var mn=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
var pa=a[0].split(' '),pb=b[0].split(' ');
var ya=parseInt('20'+pa[1].replace("'",'')),yb=parseInt('20'+pb[1].replace("'",""));
var ma=mn.indexOf(pa[0]),mb2=mn.indexOf(pb[0]);
return ya!==yb?ya-yb:ma-mb2;
}).slice(-12);
if(monthEntries.length>1){
var mc=$h('div','card');mc.appendChild($h('div','card-title tag','Monthly revenue trend'));
var W=540,H=110,PL=6,PR=6,PT=14,PB=26;
var vals=monthEntries.map(function(e){return e[1];});
var labels=monthEntries.map(function(e){return e[0];});
var minV=Math.min.apply(null,vals),maxV=Math.max.apply(null,vals),rng=maxV-minV||1;
function gx(i){return PL+(i/(vals.length-1))*(W-PL-PR);}
function gy(v){return PT+(1-(v-minV)/rng)*(H-PT-PB);}
var pts=vals.map(function(v,i){return gx(i).toFixed(1)+','+gy(v).toFixed(1);}).join(' ');
var aB=gy(minV-rng*0.05);
var aPts=vals.map(function(v,i){return gx(i).toFixed(1)+','+gy(v).toFixed(1);}).join(' ')+' '+gx(vals.length-1).toFixed(1)+','+aB+' '+gx(0).toFixed(1)+','+aB;
var NS='http://www.w3.org/2000/svg';
var svg=document.createElementNS(NS,'svg');
svg.setAttribute('viewBox','0 0 '+W+' '+H);
svg.setAttribute('style','width:100%;height:'+H+'px;display:block;overflow:visible');
var poly=document.createElementNS(NS,'polygon');
poly.setAttribute('points',aPts);poly.setAttribute('fill','rgba(240,98,77,0.10)');svg.appendChild(poly);
var line=document.createElementNS(NS,'polyline');
line.setAttribute('points',pts);line.setAttribute('fill','none');line.setAttribute('stroke','#F0624D');
line.setAttribute('stroke-width','2');line.setAttribute('stroke-linejoin','round');line.setAttribute('stroke-linecap','round');
svg.appendChild(line);
var maxV2=Math.max.apply(null,vals),minV2=Math.min.apply(null,vals);
vals.forEach(function(v,i){
var isLast=(i===vals.length-1),isFirst=(i===0);
var isMax=(v===maxV2),isMin=(v===minV2);
var showVal=isFirst||isLast||isMax||isMin;
var dot=document.createElementNS(NS,'circle');
dot.setAttribute('cx',gx(i).toFixed(1));dot.setAttribute('cy',gy(v).toFixed(1));
dot.setAttribute('r',isLast||isMax?4:2.5);dot.setAttribute('fill','#F0624D');
dot.setAttribute('stroke','#fff');dot.setAttribute('stroke-width','1.5');
svg.appendChild(dot);
if(isFirst||isLast||i%Math.ceil(vals.length/4)===0){
var lbl=document.createElementNS(NS,'text');
lbl.setAttribute('x',gx(i).toFixed(1));lbl.setAttribute('y',H-4);
lbl.setAttribute('text-anchor',isFirst?'start':isLast?'end':'middle');
lbl.setAttribute('font-size','10');lbl.setAttribute('fill','#8A877F');
lbl.textContent=labels[i];svg.appendChild(lbl);
}
if(showVal){
var nowM=new Date();
var isPartial=isLast&&(nowM.getFullYear()===2000+parseInt(labels[i].split("'")[1])&&nowM.getMonth()===(['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'].indexOf(labels[i].split(' ')[0])));
var fmtV='$'+Math.round(v/1000*10)/10+'k'+(isPartial?' *':'');
var anchor=isFirst?'start':isLast?'end':'middle';
var xOff=isFirst?0:isLast?-4:0;
var yOff=isMax?-8:10;
var valLbl=document.createElementNS(NS,'text');
valLbl.setAttribute('x',(gx(i)+xOff).toFixed(1));
valLbl.setAttribute('y',(gy(v)+yOff).toFixed(1));
valLbl.setAttribute('text-anchor',anchor);
valLbl.setAttribute('font-size','10');
valLbl.setAttribute('fill',isMax?'#2D6A4F':isMin?'#C0392B':'#F0624D');
valLbl.setAttribute('font-weight','bold');
valLbl.textContent=fmtV;svg.appendChild(valLbl);
if(isPartial){
var noteLbl=document.createElementNS(NS,'text');
noteLbl.setAttribute('x',(gx(i)-4).toFixed(1));
noteLbl.setAttribute('y',(gy(v)-18).toFixed(1));
noteLbl.setAttribute('text-anchor','end');
noteLbl.setAttribute('font-size','9');
noteLbl.setAttribute('fill','#B0ADA6');
noteLbl.textContent='month in progress';svg.appendChild(noteLbl);
}
}
});
mc.appendChild(svg);ms.appendChild(mc);
}
}
dy>