/* Growth OS — helpers de formatação, matemática (spec §12) e gráficos */ (function(){ "use strict"; const R=window.React,h=R.createElement; const G=window.GOS; const monthName=k=>{const m=k.split("-")[1];const f=G.MONTHS.find(x=>x[0]===m);return f?f[1]:m;}; const monthLong=k=>{const[y,m]=k.split("-");const f=G.MONTHS.find(x=>x[0]===m);return`${f?f[1]:m}/${y.slice(2)}`;}; // aggregation mode per indicator const LAST=new Set(["ativos","frota","projAtivos","dividas"]); const AVG_TYPES=new Set(["percentual","indice","tempo"]); const AVG_KEYS=new Set(["cpl","cac","ticket","valorLoc","diariaMedia"]); function aggMode(ind){ if(LAST.has(ind.key))return"last"; if(AVG_TYPES.has(ind.type)||AVG_KEYS.has(ind.key))return"avg"; return"sum"; } function seriesVals(ind,months){ return months.map(k=>ind.series[k]).filter(v=>v!=null&&!isNaN(v)); } function agg(ind,months){ const vals=seriesVals(ind,months); if(!vals.length)return null; const m=aggMode(ind); if(m==="last")return vals[vals.length-1]; if(m==="avg")return round(vals.reduce((a,b)=>a+b,0)/vals.length, decs(ind)); return round(vals.reduce((a,b)=>a+b,0), decs(ind)); } function decs(ind){ if(ind.type==="percentual")return 1; if(ind.type==="indice")return ind.unit==="x"?1:0; if(ind.unit==="R$ mi")return 1; return 0; } function round(n,d=0){if(n==null)return null;const f=Math.pow(10,d);return Math.round(n*f)/f;} // spec §12 math function varAbs(cur,prev){if(cur==null||prev==null)return null;return cur-prev;} function varPct(cur,prev){if(cur==null||prev==null||prev===0)return null;return round((cur-prev)/prev*100,1);} function isPP(ind){return ind.type==="percentual";} // difference in pontos percentuais function delta(ind,cur,prev){ if(cur==null||prev==null)return{ok:false}; const abs=varAbs(cur,prev); if(prev===0)return{ok:true,abs,pct:null,nc:true}; if(isPP(ind))return{ok:true,abs:round(abs,1),pp:round(abs,1),pct:round((cur-prev)/prev*100,1)}; return{ok:true,abs,pct:varPct(cur,prev)}; } // direction goodness: is this delta good for the business? function goodness(ind,cur,prev){ const d=varAbs(cur,prev); if(d==null||d===0)return"flat"; const up=d>0; if(ind.dir==="maior")return up?"up":"down"; if(ind.dir==="menor")return up?"down":"up"; if(ind.dir==="faixa"&&ind.faixa){const[lo,hi]=ind.faixa;const inRange=cur>=lo&&cur<=hi;return inRange?"up":"warn";} return up?"up":"down"; } // trend over last N comparable months (spec §17) function trend(ind,months){ const vals=seriesVals(ind,months); if(vals.length<3)return{cls:"Sem dados",dir:"flat"}; const last3=vals.slice(-3); const diffs=[];for(let i=1;id>0),allDown=diffs.every(d=>d<0); const avg=last3.reduce((a,b)=>a+b,0)/3; const vol=Math.sqrt(last3.reduce((a,b)=>a+Math.pow(b-avg,2),0)/3)/(avg||1); if(allUp)return{cls:"Crescente",dir:"up"}; if(allDown)return{cls:"Decrescente",dir:"down"}; if(vol>0.18)return{cls:"Volátil",dir:"warn"}; return{cls:"Estável",dir:"flat"}; } // value formatting function fmtNum(n,d=0){if(n==null)return"—";return n.toLocaleString("pt-BR",{minimumFractionDigits:d,maximumFractionDigits:d});} function fmtMoneyBRL(n,compact=false){ if(n==null)return"—"; if(compact&&Math.abs(n)>=1000000)return"R$ "+round(n/1000000,2).toLocaleString("pt-BR",{maximumFractionDigits:2})+" mi"; if(compact&&Math.abs(n)>=1000)return"R$ "+round(n/1000,1).toLocaleString("pt-BR",{maximumFractionDigits:1})+" mil"; return"R$ "+fmtNum(n,0); } function fmtVal(ind,v,compact=false){ if(v==null)return"—"; switch(ind.type){ case"moeda": if(ind.unit==="R$ mi")return"R$ "+fmtNum(v,1)+" mi"; return fmtMoneyBRL(v,compact); case"percentual":return fmtNum(v,1)+"%"; case"indice":return ind.unit==="x"?fmtNum(v,1)+"×":fmtNum(v,0)+"%"; case"tempo":return fmtNum(v,0)+" "+(ind.unit||"dias"); default:return fmtNum(v,0); } } function unitLabel(ind){ if(ind.type==="moeda")return""; if(ind.type==="percentual"||ind.unit==="%")return""; if(ind.unit==="x")return""; return ind.unit||""; } /* ============ CHART: line ============ */ function LineChart(props){ const{months,values,goal,color="#f86537",h:H=220,cmpValues}=props; const W=680,pad={t:18,r:16,b:28,l:46}; const all=values.filter(v=>v!=null).concat(cmpValues?cmpValues.filter(v=>v!=null):[]).concat(goal!=null?[goal]:[]); const min=Math.min(...all),max=Math.max(...all); const range=(max-min)||1,lo=min-range*0.12,hi=max+range*0.12,span=hi-lo; const iw=W-pad.l-pad.r,ih=H-pad.t-pad.b; const x=i=>pad.l+(months.length<=1?iw/2:i/(months.length-1)*iw); const y=v=>pad.t+ih-((v-lo)/span)*ih; const [hover,setHover]=R.useState(null); const path=v=>v.map((val,i)=>val==null?null:`${i===0||v[i-1]==null?"M":"L"}${x(i).toFixed(1)},${y(val).toFixed(1)}`).filter(Boolean).join(" "); const area=(()=>{const pts=values.map((val,i)=>val==null?null:`${x(i).toFixed(1)},${y(val).toFixed(1)}`).filter(Boolean);if(!pts.length)return"";return`M${pts[0].split(",")[0]},${y(lo)} L`+pts.join(" L ")+` L${x(values.length-1).toFixed(1)},${y(lo)} Z`;})(); const ticks=4; return h("div",{className:"chart-wrap"}, h("svg",{viewBox:`0 0 ${W} ${H}`,style:{width:"100%",height:H},onMouseLeave:()=>setHover(null)}, // grid + y labels Array.from({length:ticks+1}).map((_,i)=>{const val=lo+span*(i/ticks);const yy=y(val);return h("g",{key:i}, h("line",{x1:pad.l,x2:W-pad.r,y1:yy,y2:yy,stroke:"rgba(0,0,0,.08)",strokeWidth:1}), h("text",{x:pad.l-8,y:yy+3,textAnchor:"end",fontSize:9,fill:"#86868b",fontFamily:"var(--mono)"},fmtCompactAxis(val)));}), goal!=null&&h("line",{x1:pad.l,x2:W-pad.r,y1:y(goal),y2:y(goal),stroke:"#c1c1c6",strokeWidth:1,strokeDasharray:"4 4"}), goal!=null&&h("text",{x:W-pad.r,y:y(goal)-5,textAnchor:"end",fontSize:8.5,fill:"#86868b",fontFamily:"var(--mono)"},"meta"), cmpValues&&h("path",{d:path(cmpValues),fill:"none",stroke:"#c1c1c6",strokeWidth:1.6,strokeDasharray:"3 3",opacity:.8}), area&&h("path",{d:area,fill:color,opacity:.08}), h("path",{d:path(values),fill:"none",stroke:color,strokeWidth:2.4,strokeLinejoin:"round",strokeLinecap:"round"}), values.map((val,i)=>val==null?null:h("circle",{key:i,cx:x(i),cy:y(val),r:hover===i?5:3.2,fill:hover===i?color:"#ffffff",stroke:color,strokeWidth:2,style:{cursor:"pointer",transition:"r .12s"},onMouseEnter:()=>setHover(i)})), months.map((k,i)=>h("text",{key:k,x:x(i),y:H-9,textAnchor:"middle",fontSize:9.5,fill:hover===i?"#1d1d1f":"#86868b",fontFamily:"var(--mono)"},monthName(k))) ), hover!=null&&values[hover]!=null&&h("div",{className:"chart-tip",style:{left:`${x(hover)/W*100}%`,top:`${y(values[hover])/H*100}%`,opacity:1}}, `${monthLong(months[hover])} · ${props.tipFmt?props.tipFmt(values[hover]):fmtNum(values[hover])}`) ); } function fmtCompactAxis(v){ if(Math.abs(v)>=1000000)return round(v/1000000,1)+"M"; if(Math.abs(v)>=1000)return round(v/1000,0)+"k"; return round(v,Math.abs(v)<10?1:0); } /* ============ CHART: sparkline ============ */ function Spark(props){ const{values,color="#f86537",good}=props; const clean=values.filter(v=>v!=null); if(clean.length<2)return null; const W=180,H=34,min=Math.min(...clean),max=Math.max(...clean),range=(max-min)||1; const x=i=>i/(values.length-1)*W; const y=v=>H-4-((v-min)/range)*(H-8); const col=good==="down"?"#E5837A":good==="up"?"#8FE07A":color; const d=values.map((v,i)=>v==null?null:`${i===0?"M":"L"}${x(i).toFixed(1)},${y(v).toFixed(1)}`).filter(Boolean).join(" "); return h("svg",{viewBox:`0 0 ${W} ${H}`,preserveAspectRatio:"none",style:{width:"100%",height:34}}, h("path",{d,fill:"none",stroke:col,strokeWidth:2,strokeLinejoin:"round",strokeLinecap:"round"}), h("circle",{cx:x(values.length-1),cy:y(clean[clean.length-1]),r:2.6,fill:col}) ); } window.GOSlib={ monthName,monthLong,agg,aggMode,seriesVals,decs,round, varAbs,varPct,delta,goodness,trend,isPP, fmtNum,fmtMoneyBRL,fmtVal,unitLabel, LineChart,Spark }; })();