<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>FlatPay — Call Dashboard</title>

<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>

<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>

<script src="https://unpkg.com/recharts/umd/Recharts.js"></script>

<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

<style>

*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}

body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f7f6f4;color:#1a1a1a}

input,button,select{font-family:inherit;font-size:13px;outline:none}

input,select{padding:6px 10px;border:1px solid #e4e2dc;border-radius:8px;background:#fff;color:#1a1a1a}

button{cursor:pointer;padding:6px 14px;border:1px solid #e4e2dc;border-radius:8px;background:#fff;color:#1a1a1a;transition:background .15s}

button:hover{background:#f0efea}

button:disabled{opacity:.4;cursor:not-allowed}

table{width:100%;border-collapse:collapse;font-size:12px}

th{padding:8px 12px;font-weight:500;color:#999;font-size:11px;border-bottom:1px solid #e4e2dc;white-space:nowrap;text-align:left;background:#fafafa}

td{padding:8px 12px;border-bottom:1px solid #f0efe9;color:#1a1a1a}

tr:last-child td{border-bottom:none}

.nr{text-align:right!important}

@keyframes spin{to{transform:rotate(360deg)}}

@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}

.spin{display:inline-block;width:16px;height:16px;border:2px solid #e4e2dc;border-top-color:#2a6080;border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle}

::-webkit-scrollbar{width:5px;height:5px}

::-webkit-scrollbar-thumb{background:#d3d1c7;border-radius:3px}

</style>

</head>

<body>

<div id="root"></div>

<script type="text/babel">

const {useState,useEffect,useRef,useCallback,useMemo}=React;

const {LineChart,Line,BarChart,Bar,XAxis,YAxis,Tooltip,ResponsiveContainer,PieChart,Pie,Cell,CartesianGrid,Legend}=Recharts;


// ── Config ────────────────────────────────────────────────────────────────

const PROXY="https://rough-frost-3472.olfr.workers.dev";

const Q={agents:22,overview:16,contactLists:23,agentGroups:24,map:4,agents6:6};

const FP={black:"#1a1a1a",white:"#fff",lavender:"#c8c8e0",blue:"#b8d8e8",pink:"#f0c8d0",mint:"#b8e0d0",yellow:"#f0e8c0",peach:"#f0d0b8",lavenDark:"#5a5a8a",blueDark:"#2a6080",pinkDark:"#8a3a50",mintDark:"#1a6050",yellowDark:"#7a6820",peachDark:"#8a4820",pageBg:"#f7f6f4",sideBg:"#1a1a1a",sideText:"#e8e8e8",sideActive:"#383838",border:"#e4e2dc"};

const CARD_PALETTES=[

  {bg:FP.blue,fg:FP.blueDark},{bg:FP.mint,fg:FP.mintDark},{bg:FP.lavender,fg:FP.lavenDark},

  {bg:FP.peach,fg:FP.peachDark},{bg:FP.pink,fg:FP.pinkDark},{bg:FP.yellow,fg:FP.yellowDark},

];

const ALL_TEAMS=["Team Krishna","Team Kaan","Team Marcus","Team Alberto","MCC Marketing"];

const TEAM_COLORS={"Team Krishna":FP.blue,"Team Kaan":FP.mint,"Team Marcus":FP.lavender,"Team Alberto":FP.peach,"MCC Marketing":FP.pink};

const TEAM_TEXT={"Team Krishna":FP.blueDark,"Team Kaan":FP.mintDark,"Team Marcus":FP.lavenDark,"Team Alberto":FP.peachDark,"MCC Marketing":FP.pinkDark};

const PRESETS=[["today","Today"],["yesterday","Yesterday"],["week","This week"],["lastweek","Last week"],["month","This month"],["lastmonth","Last month"],["custom","Custom"]];


// ── Date utilities ────────────────────────────────────────────────────────

const pad=n=>String(n).padStart(2,"0");

const fmtD=d=>`${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;

function getRange(preset,cFrom,cTo){

  const n=new Date(),t=fmtD(n);

  if(preset==="today") return{from:t+" 00:00",to:t+" 23:59"};

  if(preset==="yesterday"){const y=new Date(n);y.setDate(n.getDate()-1);const yd=fmtD(y);return{from:yd+" 00:00",to:yd+" 23:59"};}

  if(preset==="week"){const day=n.getDay()||7;const m=new Date(n);m.setDate(n.getDate()-day+1);return{from:fmtD(m)+" 00:00",to:t+" 23:59"};}

  if(preset==="lastweek"){const day=n.getDay()||7;const lm=new Date(n);lm.setDate(n.getDate()-day-6);const ls=new Date(n);ls.setDate(n.getDate()-day);return{from:fmtD(lm)+" 00:00",to:fmtD(ls)+" 23:59"};}

  if(preset==="month"){const m1=new Date(n.getFullYear(),n.getMonth(),1);return{from:fmtD(m1)+" 00:00",to:t+" 23:59"};}

  if(preset==="lastmonth"){const lm1=new Date(n.getFullYear(),n.getMonth()-1,1);const lme=new Date(n.getFullYear(),n.getMonth(),0);return{from:fmtD(lm1)+" 00:00",to:fmtD(lme)+" 23:59"};}

  if(preset==="custom"&&cFrom&&cTo) return{from:cFrom+" 00:00",to:cTo+" 23:59"};

  return{from:t+" 00:00",to:t+" 23:59"};

}

function getPrevRange(from,to){

  const ps=new Date(from.replace(" ","T")),pe=new Date(to.replace(" ","T"));

  const diff=pe-ps;

  const prevE=new Date(ps-60000),prevS=new Date(prevE-diff);

  return{from:fmtD(prevS)+" 00:00",to:fmtD(prevE)+" 23:59"};

}

function getPeriodLabel(p){return{today:"vs yesterday",yesterday:"vs day before",week:"vs same days last week",lastweek:"vs week before",month:"vs comparable days last month",lastmonth:"vs month before"}[p]||"vs prev period";}


// ── Redash API ────────────────────────────────────────────────────────────

async function rQuery(qid,params,key){

  const r=await fetch(`${PROXY}/redash/api/queries/${qid}/results`,{

    method:"POST",

    headers:{"Content-Type":"application/json","X-Redash-Key":key},

    body:JSON.stringify({parameters:params||{},max_age:600})

  });

  const d=await r.json();

  if(!r.ok) throw new Error(d.message||"Query failed");

  if(d.job) return pollJob(d.job.id,key);

  return d.query_result?.data?.rows||[];

}

async function pollJob(jid,key){

  for(let i=0;i<40;i++){

    await new Promise(r=>setTimeout(r,800));

    const r=await fetch(`${PROXY}/redash/api/jobs/${jid}`,{headers:{"X-Redash-Key":key}});

    const d=await r.json();

    if(d.job.status===3){

      const qr=await fetch(`${PROXY}/redash/api/query_results/${d.job.query_result_id}`,{headers:{"X-Redash-Key":key}});

      const qd=await qr.json();

      return qd.query_result?.data?.rows||[];

    }

    if(d.job.status>=4) return[];

  }

  return[];

}


// ── Formatters ────────────────────────────────────────────────────────────

const fn=n=>n==null?"—":Number(n).toLocaleString();

const fp=(n,d=1)=>n==null?"—":parseFloat(n).toFixed(d)+"%";

const fmtTalk=m=>{if(!m)return"0m";const dy=Math.floor(m/480),h=Math.floor((m%480)/60),mn=Math.round(m%60);return[dy>0?dy+"d":"",h>0?h+"h":"",mn>0?mn+"m":""].filter(Boolean).join(" ")||"0m";};

const chg=(a,b)=>b===0?0:Math.round((a-b)/b*100*10)/10;

const safePct=(a,b)=>b>0?Math.round(a/b*1000)/10:0;


// ── Small UI components ───────────────────────────────────────────────────

function Spin(){return <span className="spin"/>;}

function ChgBdg({cur,prev}){const d=chg(cur,prev);if(!prev) return null;return <span style={{fontSize:10,fontWeight:500,padding:"1px 5px",borderRadius:5,background:d>=0?"#dcfce7":"#fee2e2",color:d>=0?"#166534":"#991b1b",whiteSpace:"nowrap",marginLeft:4}}>{d>=0?"↑":"↓"}{Math.abs(d)}%</span>;}

function Bdg({c="lavender",children,sm}){const m={lavender:{bg:FP.lavender,fg:FP.lavenDark},blue:{bg:FP.blue,fg:FP.blueDark},pink:{bg:FP.pink,fg:FP.pinkDark},mint:{bg:FP.mint,fg:FP.mintDark},yellow:{bg:FP.yellow,fg:FP.yellowDark},peach:{bg:FP.peach,fg:FP.peachDark},green:{bg:"#dcfce7",fg:"#166534"},gray:{bg:"#ebebeb",fg:"#555"}};const s=m[c]||m.lavender;return <span style={{fontSize:sm?10:11,fontWeight:500,padding:sm?"1px 5px":"2px 8px",borderRadius:6,background:s.bg,color:s.fg,display:"inline-block",whiteSpace:"nowrap"}}>{children}</span>;}

function KCard({label,value,bg,fg,trend,sub,perBdr,loading}){return <div style={{background:bg,borderRadius:14,padding:"1rem"}}>

  <div style={{fontSize:11,color:fg,opacity:.7,marginBottom:5,fontWeight:500}}>{label}</div>

  <div style={{fontSize:20,fontWeight:500,color:fg,marginBottom:2}}>{loading?<Spin/>:value}</div>

  {perBdr!=null&&<div style={{fontSize:11,color:fg,opacity:.7,marginBottom:3}}>{loading?"…":perBdr} per BDR</div>}

  <div style={{display:"flex",alignItems:"center",gap:6,flexWrap:"wrap"}}>

    {sub&&<span style={{fontSize:10,color:fg,opacity:.65}}>{sub}</span>}

    {trend!=null&&!loading&&<span style={{fontSize:10,fontWeight:500,padding:"1px 5px",borderRadius:5,background:"rgba(0,0,0,0.09)",color:fg}}>{trend>=0?"↑":"↓"}{Math.abs(trend)}%</span>}

  </div>

</div>;}

function Card({title,children,action,noPad}){return <div style={{background:FP.white,border:`1px solid ${FP.border}`,borderRadius:14,padding:noPad?"0":"1rem",marginBottom:12,overflow:noPad?"hidden":"visible"}}>

  {(title||action)&&<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:10,padding:noPad?"1rem 1rem 0":0}}>

    {title&&<div style={{fontSize:12,fontWeight:500,color:"#666"}}>{title}</div>}

    {action}

  </div>}

  {children}

</div>;}

function PBtn({active,onClick,children}){return <button onClick={onClick} style={{fontSize:11,padding:"4px 11px",borderRadius:20,border:`1px solid ${active?FP.black:FP.border}`,background:active?FP.black:FP.white,color:active?FP.white:"#888",cursor:"pointer",fontWeight:active?500:400,whiteSpace:"nowrap"}}>{children}</button>;}

const TT=({active,payload,label})=>{if(!active||!payload?.length)return null;return <div style={{background:FP.white,border:`1px solid ${FP.border}`,borderRadius:10,padding:"8px 12px",fontSize:11}}><div style={{fontWeight:500,marginBottom:4}}>{label}</div>{payload.map((p,i)=><div key={i} style={{color:p.color||"#333"}}>{p.name}: <strong>{typeof p.value==="number"?Number(p.value).toLocaleString():p.value}</strong></div>)}</div>;};


// ── Team filter ───────────────────────────────────────────────────────────

function TeamFilter({sel,setSel,allTeams}){

  const teams=allTeams||ALL_TEAMS;

  return <div style={{display:"flex",alignItems:"center",gap:6,flexWrap:"wrap"}}>

    <span style={{fontSize:12,color:"#888",fontWeight:500}}>Teams:</span>

    <button onClick={()=>setSel([...teams])} style={{fontSize:11,padding:"3px 10px",borderRadius:20,border:`1px solid ${FP.border}`,background:"#f5f5f5",color:"#666",cursor:"pointer"}}>All</button>

    <button onClick={()=>setSel([])} style={{fontSize:11,padding:"3px 10px",borderRadius:20,border:`1px solid ${FP.border}`,background:"#f5f5f5",color:"#666",cursor:"pointer"}}>None</button>

    {teams.map(t=>{const on=sel.includes(t),c=TEAM_COLORS[t]||FP.blue,fg=TEAM_TEXT[t]||FP.blueDark;return <button key={t} onClick={()=>setSel(on?sel.filter(x=>x!==t):[...sel,t])} style={{fontSize:11,padding:"3px 10px",borderRadius:20,border:`1px solid ${on?fg:FP.border}`,background:on?c:"#f5f5f5",color:on?fg:"#888",cursor:"pointer",fontWeight:on?500:400}}>{t}</button>;})}

  </div>;

}


// ── Date bar ─────────────────────────────────────────────────────────────

function DateBar({preset,setPreset,cFrom,setCFrom,cTo,setCTo}){return <div style={{display:"flex",alignItems:"center",gap:6,flexWrap:"wrap"}}>

  {PRESETS.map(([p,l])=><PBtn key={p} active={preset===p} onClick={()=>setPreset(p)}>{l}</PBtn>)}

  {preset==="custom"&&<><input type="date" value={cFrom} onChange={e=>setCFrom(e.target.value)} style={{fontSize:11,padding:"3px 8px",borderRadius:8,border:`1px solid ${FP.border}`}}/><span style={{fontSize:11,color:"#aaa"}}>→</span><input type="date" value={cTo} onChange={e=>setCTo(e.target.value)} style={{fontSize:11,padding:"3px 8px",borderRadius:8,border:`1px solid ${FP.border}`}}/></>}

</div>;}


function FilterBar({preset,setPreset,cFrom,setCFrom,cTo,setCTo,teamSel,setTeamSel,allTeams}){return <div style={{display:"flex",flexDirection:"column",gap:10,marginBottom:"1rem"}}>

  <DateBar preset={preset} setPreset={setPreset} cFrom={cFrom} setCFrom={setCFrom} cTo={cTo} setCTo={setCTo}/>

  <TeamFilter sel={teamSel} setSel={setTeamSel} allTeams={allTeams}/>

</div>;}


// ── WoW line chart ────────────────────────────────────────────────────────

function WowLine({data,cur,prev,color,prevColor,fmt}){return <ResponsiveContainer width="100%" height={130}>

  <LineChart data={data} margin={{top:5,right:8,bottom:0,left:0}}>

    <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0"/>

    <XAxis dataKey="d" tick={{fontSize:10}}/><YAxis tick={{fontSize:9}} width={36} tickFormatter={fmt||(v=>v>=1000?Math.round(v/1000)+"k":v)}/>

    <Tooltip content={TT}/>

    <Legend iconSize={8} formatter={v=><span style={{fontSize:9}}>{v}</span>}/>

    <Line type="monotone" dataKey={prev} name="Prev" stroke={prevColor} strokeWidth={1.5} strokeDasharray="4 3" dot={{r:2}} activeDot={{r:4}}/>

    <Line type="monotone" dataKey={cur} name="Current" stroke={color} strokeWidth={2} dot={{r:2}} activeDot={{r:4}}/>

  </LineChart>

</ResponsiveContainer>;}


// ── Aggregate overview rows ───────────────────────────────────────────────

function aggRows(rows){

  return rows.reduce((a,r)=>({

    calls:a.calls+(parseInt(r["Total calls"])||0),

    answered:a.answered+(parseInt(r["Answered calls"])||0),

    booked:a.booked+(parseInt(r["Booked"])||0),

    finalized:a.finalized+(parseInt(r["Finalized"])||0),

    voicemails:a.voicemails+(parseInt(r["Voicemails"])||0),

  }),{calls:0,answered:0,booked:0,finalized:0,voicemails:0});

}


function aggAgentRows(rows,teamSel){

  return rows.filter(r=>!teamSel||teamSel.length===0||teamSel.includes(r.team));

}


// ── OVERVIEW TAB ─────────────────────────────────────────────────────────

function Overview({apiKey,...fp}){

  const {preset,setPreset,cFrom,setCFrom,cTo,setCTo,teamSel,setTeamSel}=fp;

  const [curRows,setCurRows]=useState([]);

  const [prevRows,setPrevRows]=useState([]);

  const [agentRows,setAgentRows]=useState([]);

  const [ldg,setLdg]=useState(false);


  const load=useCallback(async()=>{

    setLdg(true);

    try{

      const r=getRange(preset,cFrom,cTo),pr=getPrevRange(r.from,r.to);

      const p={from:r.from,to:r.to,"Week/Month":"Week",city:"Not selected",district_code:"Not selected"};

      const pp={...p,from:pr.from,to:pr.to};

      const [cr,prr,ar]=await Promise.all([

        rQuery(Q.overview,p,apiKey),

        rQuery(Q.overview,pp,apiKey),

        rQuery(Q.agents,{from:r.from,to:r.to},apiKey),

      ]);

      setCurRows(cr);setPrevRows(prr);setAgentRows(ar);

    }catch(e){console.error(e);}

    setLdg(false);

  },[apiKey,preset,cFrom,cTo]);


  useEffect(()=>{load();},[load]);


  const cur=aggRows(curRows),prev=aggRows(prevRows);

  const filteredAgents=aggAgentRows(agentRows,teamSel);

  const activeBDRs=new Set(filteredAgents.filter(r=>parseInt(r.total_calls)>0).map(r=>r.agent)).size;

  const gr=cur.calls>0?(cur.booked/cur.calls*100).toFixed(2):"0";

  const ac=cur.answered>0?(cur.booked/cur.answered*100).toFixed(2):"0";

  const grP=prev.calls>0?(prev.booked/prev.calls*100).toFixed(2):"0";

  const acP=prev.answered>0?(prev.booked/prev.answered*100).toFixed(2):"0";

  const talkMins=filteredAgents.reduce((s,r)=>s+(parseFloat(r.total_talk_mins)||0),0);


  // Build WoW data from rows

  const wowData=curRows.map((r,i)=>({

    d:r.Period||("P"+(i+1)),

    calls_cur:parseInt(r["Total calls"])||0,

    calls_prev:parseInt(prevRows[i]?.["Total calls"])||0,

    ans_cur:parseInt(r["Answered calls"])||0,

    ans_prev:parseInt(prevRows[i]?.["Answered calls"])||0,

    book_cur:parseInt(r["Booked"])||0,

    book_prev:parseInt(prevRows[i]?.["Booked"])||0,

    reach_cur:safePct(parseInt(r["Answered calls"]),parseInt(r["Total calls"])),

    reach_prev:safePct(parseInt(prevRows[i]?.["Answered calls"]),parseInt(prevRows[i]?.["Total calls"])),

    gross_cur:safePct(parseInt(r["Booked"]),parseInt(r["Total calls"])),

    gross_prev:safePct(parseInt(prevRows[i]?.["Booked"]),parseInt(prevRows[i]?.["Total calls"])),

  }));


  return <div>

    <FilterBar preset={preset} setPreset={setPreset} cFrom={cFrom} setCFrom={setCFrom} cTo={cTo} setCTo={setCTo} teamSel={teamSel} setTeamSel={setTeamSel}/>

    {ldg&&<div style={{textAlign:"center",padding:"1rem",color:"#888",fontSize:12}}><Spin/> Loading data…</div>}

    <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fit,minmax(120px,1fr))",gap:10,marginBottom:"1.25rem"}}>

      <KCard label="Calls" value={fn(cur.calls)} bg={FP.blue} fg={FP.blueDark} trend={chg(cur.calls,prev.calls)} sub={getPeriodLabel(preset)} perBdr={activeBDRs>0?Math.round(cur.calls/activeBDRs).toLocaleString():null} loading={ldg}/>

      <KCard label="Talk time" value={fmtTalk(talkMins)} bg={FP.mint} fg={FP.mintDark} loading={ldg}/>

      <KCard label="Answered" value={fn(cur.answered)} bg={FP.lavender} fg={FP.lavenDark} trend={chg(cur.answered,prev.answered)} perBdr={activeBDRs>0?Math.round(cur.answered/activeBDRs).toLocaleString():null} loading={ldg}/>

      <KCard label="Reach %" value={fp(safePct(cur.answered,cur.calls))} bg={FP.yellow} fg={FP.yellowDark} trend={chg(safePct(cur.answered,cur.calls),safePct(prev.answered,prev.calls))} loading={ldg}/>

      <KCard label="Bookings" value={fn(cur.booked)} bg={FP.peach} fg={FP.peachDark} trend={chg(cur.booked,prev.booked)} perBdr={activeBDRs>0?Math.round(cur.booked/activeBDRs).toLocaleString():null} loading={ldg}/>

      <KCard label="Gross conv." value={gr+"%"} bg={FP.pink} fg={FP.pinkDark} trend={chg(parseFloat(gr),parseFloat(grP))} loading={ldg}/>

      <KCard label="Ans. conv." value={ac+"%"} bg="#e8e0f8" fg={FP.lavenDark} trend={chg(parseFloat(ac),parseFloat(acP))} loading={ldg}/>

      <KCard label="Active BDRs" value={activeBDRs} bg="#f0f0f0" fg="#444" sub={`of ${filteredAgents.length} total`} loading={ldg}/>

    </div>

    {wowData.length>0&&<>

      <div style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:10,marginBottom:12}}>

        <Card title="Calls"><WowLine data={wowData} cur="calls_cur" prev="calls_prev" color={FP.blueDark} prevColor={FP.blue}/></Card>

        <Card title="Answered"><WowLine data={wowData} cur="ans_cur" prev="ans_prev" color={FP.mintDark} prevColor={FP.mint}/></Card>

        <Card title="Bookings"><WowLine data={wowData} cur="book_cur" prev="book_prev" color={FP.lavenDark} prevColor={FP.lavender}/></Card>

      </div>

      <div style={{display:"grid",gridTemplateColumns:"repeat(2,1fr)",gap:10,marginBottom:12}}>

        <Card title="Reach %"><WowLine data={wowData} cur="reach_cur" prev="reach_prev" color={FP.yellowDark} prevColor={FP.yellow} fmt={v=>v+"%"}/></Card>

        <Card title="Gross conv. %"><WowLine data={wowData} cur="gross_cur" prev="gross_prev" color={FP.peachDark} prevColor={FP.peach} fmt={v=>v+"%"}/></Card>

      </div>

    </>}

    <Card title={`Period breakdown · ${getPeriodLabel(preset)}`} noPad>

      <div style={{overflowX:"auto"}}>

        <table>

          <thead><tr><th>Period</th><th className="nr">Calls</th><th className="nr">Δ</th><th className="nr">Answered</th><th className="nr">Reach %</th><th className="nr">Bookings</th><th className="nr">Δ</th><th className="nr">Finalized</th><th className="nr">Voicemails</th></tr></thead>

          <tbody>{curRows.map((r,i)=>{const p2=prevRows[i]||{};const cc=parseInt(r["Total calls"])||0,pc=parseInt(p2["Total calls"])||0,cb=parseInt(r["Booked"])||0,pb=parseInt(p2["Booked"])||0;return(

            <tr key={i}>

              <td style={{fontWeight:500}}>{r.Period||("P"+(i+1))}</td>

              <td className="nr">{fn(cc)}</td><td className="nr"><ChgBdg cur={cc} prev={pc}/></td>

              <td className="nr">{fn(r["Answered calls"])}</td>

              <td className="nr">{fp(safePct(parseInt(r["Answered calls"]),cc))}</td>

              <td className="nr"><Bdg c="lavender">{fn(cb)}</Bdg></td>

              <td className="nr"><ChgBdg cur={cb} prev={pb}/></td>

              <td className="nr">{fn(r["Finalized"])}</td>

              <td className="nr">{fn(r["Voicemails"])}</td>

            </tr>

          );})}

          {!ldg&&curRows.length===0&&<tr><td colSpan={9} style={{textAlign:"center",color:"#aaa",padding:"2rem"}}>No data for this period</td></tr>}

          </tbody>

        </table>

      </div>

    </Card>

  </div>;

}


// ── AGENTS TAB ────────────────────────────────────────────────────────────

function Agents({apiKey,...fp}){

  const {preset,setPreset,cFrom,setCFrom,cTo,setCTo,teamSel,setTeamSel}=fp;

  const [rows,setRows]=useState([]);

  const [prevRows,setPrevRows]=useState([]);

  const [expanded,setExpanded]=useState(null);

  const [sort,setSort]=useState("booked");

  const [ldg,setLdg]=useState(false);


  const load=useCallback(async()=>{

    setLdg(true);

    try{

      const r=getRange(preset,cFrom,cTo),pr=getPrevRange(r.from,r.to);

      const [cr,prr]=await Promise.all([

        rQuery(Q.agents,{from:r.from,to:r.to},apiKey),

        rQuery(Q.agents,{from:pr.from,to:pr.to},apiKey),

      ]);

      setRows(cr);setPrevRows(prr);

    }catch(e){console.error(e);}

    setLdg(false);

  },[apiKey,preset,cFrom,cTo]);


  useEffect(()=>{load();},[load]);


  const filtered=rows.filter(r=>teamSel.length===0||teamSel.includes(r.team));

  const sortKey={booked:"booked",total:"total_calls",reach:"reach_pct",gross:"gross_pct",ansCon:"answered_conv_pct"}[sort]||"booked";

  const sorted=[...filtered].sort((a,b)=>(parseFloat(b[sortKey])||0)-(parseFloat(a[sortKey])||0));

  const prevMap={};prevRows.forEach(r=>{prevMap[r.agent]={total:parseInt(r.total_calls)||0,booked:parseInt(r.booked)||0};});


  return <div>

    <FilterBar preset={preset} setPreset={setPreset} cFrom={cFrom} setCFrom={setCFrom} cTo={cTo} setCTo={setCTo} teamSel={teamSel} setTeamSel={setTeamSel}/>

    {ldg&&<div style={{textAlign:"center",padding:"1rem",color:"#888",fontSize:12}}><Spin/> Loading agent data…</div>}

    <Card title={`${sorted.length} agents · click to expand`} action={<div style={{display:"flex",gap:4,flexWrap:"wrap"}}>{[["booked","Bookings"],["total","Calls"],["reach","Reach"],["gross","Gross"],["ansCon","Ans. conv."]].map(([s,l])=><PBtn key={s} active={sort===s} onClick={()=>setSort(s)}>{l}</PBtn>)}</div>} noPad>

      <div style={{overflowX:"auto"}}>

        <table>

          <thead><tr><th>Agent</th><th>Team</th><th className="nr">Calls</th><th className="nr">Δ</th><th className="nr">Talk time</th><th className="nr">Answered</th><th className="nr">Reach %</th><th className="nr">Bookings</th><th className="nr">Δ</th><th className="nr">Gross %</th><th className="nr">Ans. conv.</th></tr></thead>

          <tbody>{sorted.map((r,i)=>{

            const ag=r.agent||"—",tc=TEAM_COLORS[r.team]||FP.blue,tfg=TEAM_TEXT[r.team]||FP.blueDark,isExp=expanded===ag;

            const prev=prevMap[ag]||{};

            const talkMins=parseFloat(r.total_talk_mins)||0;

            return <>

              <tr key={ag} onClick={()=>setExpanded(isExp?null:ag)} style={{cursor:"pointer",background:isExp?FP.blue+"22":"transparent"}}>

                <td style={{fontWeight:500}}>{isExp?"▾ ":""}{ag}</td>

                <td><span style={{fontSize:11,fontWeight:500,padding:"2px 8px",borderRadius:6,background:tc,color:tfg}}>{r.team||"—"}</span></td>

                <td className="nr">{fn(r.total_calls)}</td>

                <td className="nr"><ChgBdg cur={parseInt(r.total_calls)||0} prev={prev.total||0}/></td>

                <td className="nr">{fmtTalk(talkMins)}</td>

                <td className="nr">{fn(r.answered)}</td>

                <td className="nr">{fp(r.reach_pct)}</td>

                <td className="nr"><Bdg c="mint">{fn(r.booked)}</Bdg></td>

                <td className="nr"><ChgBdg cur={parseInt(r.booked)||0} prev={prev.booked||0}/></td>

                <td className="nr">{fp(r.gross_pct,2)}</td>

                <td className="nr">{fp(r.answered_conv_pct,2)}</td>

              </tr>

              {isExp&&<tr key={ag+"_x"}><td colSpan={11} style={{padding:"0 12px 12px 36px",background:"#f8f8fc"}}>

                <div style={{fontSize:11,color:"#888",fontWeight:500,margin:"10px 0 6px"}}>Period summary</div>

                <div style={{display:"grid",gridTemplateColumns:"repeat(4,1fr)",gap:8}}>

                  {[["Total calls",fn(r.total_calls)],["Avg talk",((parseFloat(r.avg_talk_secs)||0).toFixed(0))+"s"],["Finalized",fn(r.finalized)],["Fin. %",fp(r.finalized_pct)]].map(([l,v])=>(

                    <div key={l} style={{background:"#f0f0f8",borderRadius:8,padding:"8px 10px"}}>

                      <div style={{fontSize:10,color:"#888",marginBottom:2}}>{l}</div>

                      <div style={{fontSize:14,fontWeight:500}}>{v}</div>

                    </div>

                  ))}

                </div>

              </td></tr>}

            </>;

          })}

          {!ldg&&sorted.length===0&&<tr><td colSpan={11} style={{textAlign:"center",color:"#aaa",padding:"2rem"}}>No data — try adjusting the filters</td></tr>}

          </tbody>

        </table>

      </div>

    </Card>

  </div>;

}


// ── TEAMS TAB ─────────────────────────────────────────────────────────────

function Teams({apiKey,...fp}){

  const {preset,setPreset,cFrom,setCFrom,cTo,setCTo,teamSel,setTeamSel}=fp;

  const [rows,setRows]=useState([]);

  const [ldg,setLdg]=useState(false);

  const [sel,setSel]=useState(null);


  const load=useCallback(async()=>{

    setLdg(true);

    try{const r=getRange(preset,cFrom,cTo);const cr=await rQuery(Q.agents,{from:r.from,to:r.to},apiKey);setRows(cr);}

    catch(e){console.error(e);}

    setLdg(false);

  },[apiKey,preset,cFrom,cTo]);


  useEffect(()=>{load();},[load]);


  const filtered=rows.filter(r=>teamSel.length===0||teamSel.includes(r.team));

  const teamMap={};

  filtered.forEach(r=>{

    const t=r.team||"Unknown";

    if(!teamMap[t]) teamMap[t]={name:t,agents:[],total:0,answered:0,booked:0,talkMins:0};

    teamMap[t].agents.push(r.agent);

    teamMap[t].total+=parseInt(r.total_calls)||0;

    teamMap[t].answered+=parseInt(r.answered)||0;

    teamMap[t].booked+=parseInt(r.booked)||0;

    teamMap[t].talkMins+=parseFloat(r.total_talk_mins)||0;

  });

  const teams=Object.values(teamMap).map(t=>({...t,

    activeBDRs:t.agents.filter(ag=>rows.find(r=>r.agent===ag&&parseInt(r.total_calls)>0)).length,

    reach:safePct(t.answered,t.total).toFixed(1),

    gross:t.total>0?(t.booked/t.total*100).toFixed(2):"0",

    ac:t.answered>0?(t.booked/t.answered*100).toFixed(2):"0",

  }));

  const maxBook=Math.max(...teams.map(x=>x.booked),1);

  const displayAgents=sel?filtered.filter(r=>r.team===sel):filtered;


  return <div>

    <FilterBar preset={preset} setPreset={setPreset} cFrom={cFrom} setCFrom={setCFrom} cTo={cTo} setCTo={setCTo} teamSel={teamSel} setTeamSel={setTeamSel}/>

    {ldg&&<div style={{textAlign:"center",padding:"1rem",color:"#888",fontSize:12}}><Spin/> Loading team data…</div>}

    <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fit,minmax(200px,1fr))",gap:10,marginBottom:"1.25rem"}}>

      {teams.map((t,i)=>{

        const c=TEAM_COLORS[t.name]||Object.values(TEAM_COLORS)[i%5];

        const fg=TEAM_TEXT[t.name]||Object.values(TEAM_TEXT)[i%5];

        const active=sel===t.name;

        return <div key={t.name} onClick={()=>setSel(active?null:t.name)} style={{background:c,border:active?`2px solid ${fg}`:`1px solid ${FP.border}`,borderRadius:14,padding:"1rem",cursor:"pointer"}}>

          <div style={{fontSize:13,fontWeight:500,color:fg,marginBottom:8}}>{t.name}</div>

          <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:6,marginBottom:8}}>

            <div><div style={{fontSize:10,color:fg,opacity:.65}}>Calls</div><div style={{fontSize:16,fontWeight:500,color:fg}}>{fn(t.total)}</div><div style={{fontSize:10,color:fg,opacity:.6}}>{t.activeBDRs>0?Math.round(t.total/t.activeBDRs).toLocaleString():"—"} per BDR</div></div>

            <div><div style={{fontSize:10,color:fg,opacity:.65}}>Bookings</div><div style={{fontSize:16,fontWeight:500,color:fg}}>{fn(t.booked)}</div><div style={{fontSize:10,color:fg,opacity:.6}}>{t.activeBDRs>0?Math.round(t.booked/t.activeBDRs).toLocaleString():"—"} per BDR</div></div>

            <div><div style={{fontSize:10,color:fg,opacity:.65}}>Talk time</div><div style={{fontSize:13,fontWeight:500,color:fg}}>{fmtTalk(t.talkMins)}</div><div style={{fontSize:10,color:fg,opacity:.6}}>{fmtTalk(t.activeBDRs>0?t.talkMins/t.activeBDRs:0)} per BDR</div></div>

            <div><div style={{fontSize:10,color:fg,opacity:.65}}>Active BDRs</div><div style={{fontSize:16,fontWeight:500,color:fg}}>{t.activeBDRs}</div></div>

          </div>

          <div style={{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:4,marginBottom:10}}>

            {[["Reach",t.reach+"%"],["Gross",t.gross+"%"],["Ans.conv.",t.ac+"%"]].map(([l,v])=>(

              <div key={l} style={{background:"rgba(0,0,0,0.07)",borderRadius:7,padding:"4px 6px",textAlign:"center"}}>

                <div style={{fontSize:9,color:fg,opacity:.65}}>{l}</div>

                <div style={{fontSize:12,fontWeight:500,color:fg}}>{v}</div>

              </div>

            ))}

          </div>

          <div style={{height:5,background:"rgba(0,0,0,0.08)",borderRadius:3,overflow:"hidden"}}><div style={{height:"100%",width:(t.booked/maxBook*100)+"%",background:fg,borderRadius:3,opacity:.6}}/></div>

        </div>;

      })}

    </div>

    <Card title={sel?`${sel} — agents`:"All agents"} action={sel&&<PBtn onClick={()=>setSel(null)}>← All teams</PBtn>} noPad>

      <div style={{overflowX:"auto"}}>

        <table>

          <thead><tr><th>Agent</th><th>Team</th><th className="nr">Calls</th><th className="nr">Talk time</th><th className="nr">Answered</th><th className="nr">Reach %</th><th className="nr">Bookings</th><th className="nr">Gross %</th><th className="nr">Ans. conv.</th></tr></thead>

          <tbody>{displayAgents.map((r,i)=>{const tc=TEAM_COLORS[r.team]||FP.blue,tfg=TEAM_TEXT[r.team]||FP.blueDark;return(

            <tr key={i}><td style={{fontWeight:500}}>{r.agent||"—"}</td>

            <td><span style={{fontSize:11,fontWeight:500,padding:"2px 8px",borderRadius:6,background:tc,color:tfg}}>{r.team||"—"}</span></td>

            <td className="nr">{fn(r.total_calls)}</td><td className="nr">{fmtTalk(parseFloat(r.total_talk_mins)||0)}</td>

            <td className="nr">{fn(r.answered)}</td><td className="nr">{fp(r.reach_pct)}</td>

            <td className="nr"><Bdg c="mint">{fn(r.booked)}</Bdg></td>

            <td className="nr">{fp(r.gross_pct,2)}</td><td className="nr">{fp(r.answered_conv_pct,2)}</td>

            </tr>

          );})}

          {!ldg&&displayAgents.length===0&&<tr><td colSpan={9} style={{textAlign:"center",color:"#aaa",padding:"2rem"}}>No data</td></tr>}

          </tbody>

        </table>

      </div>

    </Card>

  </div>;

}


// ── CONTACT LISTS TAB ─────────────────────────────────────────────────────

function ContactLists({apiKey,...fp}){

  const {preset,setPreset,cFrom,setCFrom,cTo,setCTo,teamSel,setTeamSel}=fp;

  const [rows,setRows]=useState([]);

  const [ldg,setLdg]=useState(false);

  const [sort,setSort]=useState("calls");

  const [expanded,setExpanded]=useState(null);


  const load=useCallback(async()=>{

    setLdg(true);

    try{const cr=await rQuery(Q.contactLists,{},apiKey);setRows(cr);}

    catch(e){console.error(e);}

    setLdg(false);

  },[apiKey]);


  useEffect(()=>{load();},[load]);


  const sortKey={calls:"call_attempts",contacts:"contacts_total"}[sort]||"call_attempts";

  const sorted=[...rows].sort((a,b)=>(parseInt(b[sortKey])||0)-(parseInt(a[sortKey])||0));


  return <div>

    <FilterBar preset={preset} setPreset={setPreset} cFrom={cFrom} setCFrom={setCFrom} cTo={cTo} setCTo={setCTo} teamSel={teamSel} setTeamSel={setTeamSel}/>

    {ldg&&<div style={{textAlign:"center",padding:"1rem",color:"#888",fontSize:12}}><Spin/> Loading contact lists…</div>}

    <Card title={`${sorted.length} active contact lists`} action={<div style={{display:"flex",gap:4}}>{[["calls","Calls"],["contacts","Contacts"]].map(([s,l])=><PBtn key={s} active={sort===s} onClick={()=>setSort(s)}>{l}</PBtn>)}</div>} noPad>

      <div style={{overflowX:"auto"}}>

        <table>

          <thead><tr><th>List name</th><th>Campaign</th><th className="nr">Contacts</th><th className="nr">Added</th><th className="nr">Remaining</th><th className="nr">Calls</th><th className="nr">% Called</th><th className="nr">Starts</th><th className="nr">Ends</th></tr></thead>

          <tbody>{sorted.map((r,i)=>{

            const isExp=expanded===i,pct=parseFloat(r.pct_called)||0;

            return <>

              <tr key={i} onClick={()=>setExpanded(isExp?null:i)} style={{cursor:"pointer",background:isExp?FP.blue+"22":"transparent"}}>

                <td style={{fontWeight:500,maxWidth:200,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{isExp?"▾ ":""}{r.list_name||"—"}</td>

                <td style={{color:"#888"}}>{r.campaign||"—"}</td>

                <td className="nr">{fn(r.contacts_total)}</td>

                <td className="nr">{fn(r.contacts_added)}</td>

                <td className="nr">{fn(r.contacts_remaining)}</td>

                <td className="nr">{fn(r.call_attempts)}</td>

                <td className="nr">

                  <div style={{display:"flex",alignItems:"center",gap:6,justifyContent:"flex-end"}}>

                    <div style={{width:50,height:5,background:"#f0f0f0",borderRadius:3,overflow:"hidden"}}><div style={{height:"100%",width:pct+"%",background:FP.blueDark,borderRadius:3}}/></div>

                    <span>{pct.toFixed(1)}%</span>

                  </div>

                </td>

                <td className="nr" style={{fontSize:11,color:"#888"}}>{r.calling_starts?new Date(r.calling_starts).toLocaleDateString():"—"}</td>

                <td className="nr" style={{fontSize:11,color:"#888"}}>{r.calling_ends?new Date(r.calling_ends).toLocaleDateString():"—"}</td>

              </tr>

              {isExp&&<tr key={i+"_x"}><td colSpan={9} style={{padding:"0 12px 12px 36px",background:"#f8f8fc"}}>

                <div style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:8,marginTop:10}}>

                  {[["Contacts total",fn(r.contacts_total)],["Contacts remaining",fn(r.contacts_remaining)],["% called",pct.toFixed(1)+"%"],["Call attempts",fn(r.call_attempts)],["Expiry",r.expiry_date?new Date(r.expiry_date).toLocaleDateString():"—"],["Campaign",r.campaign||"—"]].map(([l,v])=>(

                    <div key={l} style={{background:"#f0f0f8",borderRadius:8,padding:"8px 10px"}}>

                      <div style={{fontSize:10,color:"#888",marginBottom:2}}>{l}</div>

                      <div style={{fontSize:13,fontWeight:500}}>{v}</div>

                    </div>

                  ))}

                </div>

              </td></tr>}

            </>;

          })}

          {!ldg&&sorted.length===0&&<tr><td colSpan={9} style={{textAlign:"center",color:"#aaa",padding:"2rem"}}>No active contact lists found</td></tr>}

          </tbody>

        </table>

      </div>

    </Card>

  </div>;

}


// ── MAP VIEW ──────────────────────────────────────────────────────────────

function MapView({apiKey,...fp}){

  const {preset,setPreset,cFrom,setCFrom,cTo,setCTo,teamSel,setTeamSel}=fp;

  const svgRef=useRef(null);

  const [rows,setRows]=useState([]);

  const [metric,setMetric]=useState("calls");

  const [ldg,setLdg]=useState(false);

  const [hov,setHov]=useState(null);

  const [hovXY,setHovXY]=useState({x:0,y:0});


  const METRICS={calls:{label:"Calls",get:d=>d.answered_outbound_calls||0,fmt:fn,lo:"#e8f4fb",hi:"#1a4a6a"},};


  useEffect(()=>{

    setLdg(true);

    rQuery(Q.map,{},apiKey).then(r=>{setRows(r);setLdg(false);}).catch(()=>setLdg(false));

  },[apiKey]);


  const CENTROIDS=[["EC1",-0.0979,51.5219],["EC2",-0.0892,51.5177],["WC1",-0.1207,51.5228],["WC2",-0.1244,51.5122],["N1",-0.0992,51.5376],["N4",-0.1090,51.5721],["N5",-0.1009,51.5653],["N7",-0.1224,51.5588],["N8",-0.1247,51.5886],["N15",-0.0743,51.5788],["N16",-0.0765,51.5619],["N22",-0.0983,51.5987],["NW1",-0.1422,51.5387],["NW2",-0.2158,51.5572],["NW3",-0.1757,51.5570],["NW5",-0.1472,51.5573],["NW6",-0.1937,51.5461],["NW10",-0.2640,51.5327],["E1",-0.0591,51.5159],["E2",-0.0623,51.5264],["E3",-0.0270,51.5266],["E5",-0.0562,51.5617],["E6",0.0451,51.5264],["E7",0.0249,51.5497],["E8",-0.0577,51.5424],["E10",-0.0143,51.5694],["E11",0.0228,51.5697],["E13",0.0291,51.5215],["E14",-0.0169,51.5031],["E15",0.0008,51.5434],["E16",0.0243,51.5072],["E17",-0.0215,51.5848],["W1",-0.1440,51.5143],["W2",-0.1820,51.5133],["W5",-0.2987,51.5097],["W6",-0.2275,51.4915],["W9",-0.1909,51.5226],["W10",-0.2150,51.5218],["W12",-0.2283,51.5026],["SW1",-0.1357,51.4975],["SW2",-0.1154,51.4567],["SW4",-0.1340,51.4566],["SW6",-0.1985,51.4733],["SW8",-0.1258,51.4750],["SW9",-0.1121,51.4652],["SW11",-0.1618,51.4637],["SW12",-0.1464,51.4456],["SW15",-0.2309,51.4590],["SW16",-0.1223,51.4231],["SW17",-0.1598,51.4235],["SW18",-0.1939,51.4473],["SW19",-0.2019,51.4226],["SE1",-0.0924,51.5011],["SE4",-0.0303,51.4562],["SE5",-0.0775,51.4690],["SE6",-0.0214,51.4398],["SE8",-0.0228,51.4796],["SE10",-0.0038,51.4821],["SE11",-0.1065,51.4902],["SE13",-0.0178,51.4608],["SE15",-0.0600,51.4726],["SE16",-0.0521,51.4975],["SE17",-0.0881,51.4839],["SE19",-0.0777,51.4127],["SE22",-0.0673,51.4538],["SE25",-0.0479,51.3966],["CR0",-0.0853,51.3726],["CR2",-0.0793,51.3471],["CR4",-0.1660,51.4041],["CR7",-0.0836,51.4001],["BR1",0.0227,51.4113],["BR2",0.0208,51.3803],["BR3",-0.0163,51.4023],["HA1",-0.3350,51.5797],["HA3",-0.3195,51.5850],["EN1",-0.0720,51.6578],["EN2",-0.0947,51.6698],["KT1",-0.3063,51.4119],["KT2",-0.2898,51.4201],["SM1",-0.2000,51.3678],["SM4",-0.1948,51.3958],["TW1",-0.3260,51.4467],["TW3",-0.3681,51.4680],["TW7",-0.3390,51.4804],["DA1",0.2155,51.4446],["DA6",0.1358,51.4423],["IG1",0.0920,51.5582],["IG11",0.1085,51.5332],["RM1",0.2056,51.5790],["RM7",0.1514,51.5666]];


  // Build data map from Q4 rows + fallback to centroids

  const areaData=useMemo(()=>{

    const m={};

    rows.forEach(r=>{if(r.london_postal_area) m[r.london_postal_area]={calls:parseInt(r.answered_outbound_calls)||0};});

    // Q4 returns broad areas (E,EC,N,NW,SE,SW,W,WC) — distribute to districts proportionally using seed

    const seed=c=>{let h=0;for(let x of c)h=(h*31+x.charCodeAt(0))&0xffff;return h/65535;};

    CENTROIDS.forEach(([code])=>{

      if(!m[code]){

        const area=Object.keys(m).find(a=>code.startsWith(a))||null;

        if(area){const s=seed(code);m[code]={calls:Math.round((m[area].calls||0)*s*0.15)};}

        else m[code]={calls:0};

      }

    });

    return m;

  },[rows]);


  useEffect(()=>{

    if(!svgRef.current||Object.keys(areaData).length===0) return;

    const W=580,H=500,svg=d3.select(svgRef.current);

    svg.selectAll("*").remove();

    const proj=d3.geoMercator().center([-0.09,51.505]).scale(36000).translate([W/2,H/2]);

    const allVals=CENTROIDS.map(([c])=>(areaData[c]?.calls||0));

    const maxV=Math.max(...allVals,1);

    const colorSc=d3.scaleSequential().domain([0,maxV]).interpolator(d3.interpolate("#e8f4fb","#1a4a6a"));

    const pts=CENTROIDS.map(([code,lon,lat])=>{const p=proj([lon,lat]);return{code,px:p[0],py:p[1],calls:areaData[code]?.calls||0};});

    svg.append("rect").attr("width",W).attr("height",H).attr("fill","#f0f4f8").attr("rx",12);

    const del=d3.Delaunay.from(pts,p=>p.px,p=>p.py);

    const vor=del.voronoi([0,0,W,H]);

    svg.selectAll(".vc").data(pts).join("path").attr("class","vc")

      .attr("d",(_,i)=>vor.renderCell(i)).attr("fill",p=>colorSc(p.calls)).attr("stroke","#fff").attr("stroke-width",1.2).style("cursor","pointer")

      .on("mouseenter",(ev,p)=>{const r=svgRef.current.getBoundingClientRect();setHov(p);setHovXY({x:ev.clientX-r.left,y:ev.clientY-r.top});d3.select(ev.currentTarget).attr("stroke","#1a1a1a").attr("stroke-width",2.5).raise();})

      .on("mousemove",ev=>{const r=svgRef.current.getBoundingClientRect();setHovXY({x:ev.clientX-r.left,y:ev.clientY-r.top});})

      .on("mouseleave",ev=>{setHov(null);d3.select(ev.currentTarget).attr("stroke","#fff").attr("stroke-width",1.2);});

    svg.append("path").datum([[-0.50,51.49],[-0.30,51.48],[-0.13,51.499],[-0.07,51.503],[0.0,51.503],[0.18,51.498],[0.30,51.492]])

      .attr("d",d3.line().x(d=>proj(d)[0]).y(d=>proj(d)[1]).curve(d3.curveCatmullRom))

      .attr("fill","none").attr("stroke","rgba(180,210,240,0.8)").attr("stroke-width",6);

    pts.filter(p=>p.calls>100).forEach(p=>{svg.append("text").attr("x",p.px).attr("y",p.py+3.5).attr("text-anchor","middle").attr("font-size",8).attr("font-weight","600").attr("fill","rgba(10,20,40,0.6)").attr("pointer-events","none").text(p.code);});

    const grad=svg.append("defs").append("linearGradient").attr("id","lg");

    grad.append("stop").attr("offset","0%").attr("stop-color","#e8f4fb");

    grad.append("stop").attr("offset","100%").attr("stop-color","#1a4a6a");

    svg.append("rect").attr("x",W-112).attr("y",H-22).attr("width",100).attr("height",8).attr("fill","url(#lg)").attr("rx",4);

    svg.append("text").attr("x",W-112).attr("y",H-25).attr("font-size",8).attr("fill","rgba(0,0,0,0.45)").text("Low");

    svg.append("text").attr("x",W-12).attr("y",H-25).attr("font-size",8).attr("fill","rgba(0,0,0,0.45)").attr("text-anchor","end").text("High");

  },[areaData]);


  const top8=[...CENTROIDS].map(([c])=>({code:c,calls:areaData[c]?.calls||0})).sort((a,b)=>b.calls-a.calls).slice(0,8);

  const maxV2=Math.max(...top8.map(d=>d.calls),1);


  return <div>

    <FilterBar preset={preset} setPreset={setPreset} cFrom={cFrom} setCFrom={setCFrom} cTo={cTo} setCTo={setCTo} teamSel={teamSel} setTeamSel={setTeamSel}/>

    <div style={{marginBottom:"1rem",fontSize:12,color:"#888"}}>Showing answered outbound calls by London postcode district (all-time data from Query 4). {ldg&&<Spin/>}</div>

    <div style={{display:"grid",gridTemplateColumns:"3fr 2fr",gap:12}}>

      <Card>

        <div style={{position:"relative"}}>

          <svg ref={svgRef} viewBox="0 0 580 500" style={{display:"block",width:"100%"}}/>

          {hov&&<div style={{position:"absolute",left:Math.min(hovXY.x+12,370),top:Math.max(hovXY.y-60,4),background:FP.white,border:`1px solid ${FP.border}`,borderRadius:10,padding:"10px 14px",fontSize:12,pointerEvents:"none",zIndex:10,minWidth:140,boxShadow:"0 4px 12px rgba(0,0,0,0.08)"}}>

            <div style={{fontWeight:600,fontSize:14,marginBottom:6}}>{hov.code}</div>

            <div style={{color:"#999",fontSize:10}}>Answered calls</div>

            <div style={{fontWeight:500,fontSize:16}}>{fn(hov.calls)}</div>

          </div>}

        </div>

      </Card>

      <Card title="Top 8 districts">

        {top8.map((d,i)=>{

          const w=Math.round(d.calls/maxV2*100);

          const col=d3.scaleSequential().domain([0,maxV2]).interpolator(d3.interpolate("#e8f4fb","#1a4a6a"))(d.calls);

          return <div key={d.code} style={{marginBottom:10}}>

            <div style={{display:"flex",justifyContent:"space-between",marginBottom:3,fontSize:12}}>

              <span style={{fontWeight:500}}>{d.code}</span><span style={{color:"#888"}}>{fn(d.calls)}</span>

            </div>

            <div style={{height:7,background:"#f0f0f0",borderRadius:4,overflow:"hidden"}}><div style={{height:"100%",width:w+"%",background:col,borderRadius:4}}/></div>

          </div>;

        })}

        <div style={{marginTop:12,paddingTop:10,borderTop:`1px solid ${FP.border}`,fontSize:11,color:"#aaa",textAlign:"center"}}>{CENTROIDS.length} postcode districts · hover for details</div>

      </Card>

    </div>

  </div>;

}


// ── FLATPAY LOGO ──────────────────────────────────────────────────────────

function Logo(){return <svg width={110} height={24} viewBox="0 0 110 24" fill="none">

  <rect x="0" y="7" width="3.5" height="17" rx="1" fill="#fff" opacity=".9"/>

  <rect x="6" y="3" width="3.5" height="21" rx="1" fill="#fff"/>

  <rect x="12" y="10" width="3.5" height="14" rx="1" fill="#fff" opacity=".9"/>

  <text x="21" y="18" fontFamily="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" fontSize="14" fontWeight="500" fill="#fff" letterSpacing="-0.2">Flatpay</text>

</svg>;}


// ── LOGIN ─────────────────────────────────────────────────────────────────

function Login({onLogin}){

  const [key,setKey]=useState("");

  const [err,setErr]=useState("");

  const [ldg,setLdg]=useState(false);

  async function doLogin(){

    if(!key){setErr("Please enter your Redash API key.");return;}

    setLdg(true);setErr("");

    try{

      // Validate key by fetching agent groups

      const r=await rQuery(Q.agentGroups,{},key);

      onLogin(key);

    }catch(e){setErr("Could not connect: "+e.message);setLdg(false);}

  }

  return <div style={{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"100vh",padding:"1rem"}}>

    <div style={{background:FP.white,border:`1px solid ${FP.border}`,borderRadius:16,padding:"2rem",width:"100%",maxWidth:400}}>

      <div style={{marginBottom:"1.5rem"}}>

        <svg width={110} height={24} viewBox="0 0 110 24" fill="none" style={{marginBottom:12}}>

          <rect x="0" y="7" width="3.5" height="17" rx="1" fill={FP.black} opacity=".9"/>

          <rect x="6" y="3" width="3.5" height="21" rx="1" fill={FP.black}/>

          <rect x="12" y="10" width="3.5" height="14" rx="1" fill={FP.black} opacity=".9"/>

          <text x="21" y="18" fontFamily="-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif" fontSize="14" fontWeight="500" fill={FP.black} letterSpacing="-0.2">Flatpay</text>

        </svg>

        <div style={{fontSize:18,fontWeight:500,marginBottom:4}}>Call Dashboard</div>

        <div style={{fontSize:13,color:"#888"}}>Enter your Redash API key to connect</div>

      </div>

      <div style={{marginBottom:12}}>

        <label style={{display:"block",fontSize:12,color:"#666",marginBottom:4}}>Redash API key</label>

        <input type="password" value={key} onChange={e=>setKey(e.target.value)} placeholder="Your API key" style={{width:"100%"}} onKeyDown={e=>e.key==="Enter"&&doLogin()}/>

      </div>

      {err&&<div style={{fontSize:12,color:"#991b1b",marginBottom:8}}>{err}</div>}

      <button onClick={doLogin} disabled={ldg} style={{width:"100%",padding:"9px",background:FP.black,color:FP.white,border:"none",borderRadius:10,fontWeight:500,cursor:ldg?"not-allowed":"pointer",fontSize:13}}>

        {ldg?<><Spin/> Connecting…</>:"Connect"}

      </button>

      <p style={{fontSize:11,color:"#bbb",marginTop:"1rem",textAlign:"center"}}>Your key is used only to query Redash and is never stored.</p>

    </div>

  </div>;

}


// ── APP ───────────────────────────────────────────────────────────────────

const TABS=[["overview","Overview"],["agents","Agents"],["teams","Teams"],["lists","Contact lists"],["map","Map view"]];

function App(){

  const [apiKey,setApiKey]=useState(null);

  const [tab,setTab]=useState("overview");

  const [preset,setPreset]=useState("week");

  const [cFrom,setCFrom]=useState("");

  const [cTo,setCTo]=useState("");

  const [teamSel,setTeamSel]=useState([...ALL_TEAMS]);


  if(!apiKey) return <Login onLogin={k=>{setApiKey(k);}}/>;


  const shared={preset,setPreset,cFrom,setCFrom,cTo,setCTo,teamSel,setTeamSel};

  const tabContent={

    overview:<Overview apiKey={apiKey} {...shared}/>,

    agents:<Agents apiKey={apiKey} {...shared}/>,

    teams:<Teams apiKey={apiKey} {...shared}/>,

    lists:<ContactLists apiKey={apiKey} {...shared}/>,

    map:<MapView apiKey={apiKey} {...shared}/>,

  };


  return <div style={{display:"flex",minHeight:"100vh"}}>

    <div style={{width:185,background:FP.sideBg,padding:"1.25rem 1rem",display:"flex",flexDirection:"column",flexShrink:0,position:"fixed",top:0,bottom:0,overflowY:"auto"}}>

      <div style={{marginBottom:"2rem",padding:"0 0.25rem"}}><Logo/></div>

      {TABS.map(([t,l])=><button key={t} onClick={()=>setTab(t)} style={{display:"flex",alignItems:"center",gap:8,width:"100%",textAlign:"left",padding:"8px 12px",border:"none",borderRadius:8,background:tab===t?FP.sideActive:"none",fontSize:13,color:tab===t?FP.white:FP.sideText,cursor:"pointer",marginBottom:2,fontWeight:tab===t?500:400,opacity:tab===t?1:0.75}}>{l}</button>)}

      <div style={{marginTop:"auto"}}>

        <button onClick={()=>setApiKey(null)} style={{fontSize:11,color:"#555",border:"none",background:"none",cursor:"pointer",padding:"4px 0",textAlign:"left"}}>Disconnect</button>

      </div>

    </div>

    <div style={{flex:1,marginLeft:185,display:"flex",flexDirection:"column",minWidth:0}}>

      <div style={{padding:"0.875rem 1.5rem",borderBottom:`1px solid ${FP.border}`,background:FP.white,display:"flex",alignItems:"center",justifyContent:"space-between",gap:12,position:"sticky",top:0,zIndex:10}}>

        <div style={{fontSize:15,fontWeight:500}}>{TABS.find(([t])=>t===tab)?.[1]}</div>

        <div style={{fontSize:11,color:"#aaa"}}>{getPeriodLabel(preset)}</div>

      </div>

      <div style={{flex:1,padding:"1.25rem",background:FP.pageBg}}>{tabContent[tab]}</div>

    </div>

  </div>;

}


ReactDOM.createRoot(document.getElementById("root")).render(<App/>);

</script>

</body>

</html>