// v-data.jsx — Fire & Dough vendor sample data (Lasso Vendor Dashboard, Milton Keynes)

// ─── Vendor profile ──────────────────────────────────────────────────
const VENDOR = {
  name: 'Fire & Dough',
  cuisine: 'Pizza · Wings · Loaded Fries',
  cuisineShort: 'Pizza',
  location: 'Milton Keynes',
  level: 7,
  levelLabel: 'Growth Level 7',
  xp: 14820,
  xpToNext: 18000,
  influence: 28,            // overall territory influence %
  initials: 'FD',
  topOpportunity: 'Bletchley',
  activeCampaign: 'Weekend Family Bundle',
  activeCampaignArea: 'Central MK',
  streak: 6,
};

// ─── Node / influence state palette ──────────────────────────────────
// grey=low data, blue=active base, green=strong opportunity,
// amber=competitive pressure, red=influence declining, purple=campaign running
const NODE_STATE = {
  grey:   { fill: '#9C8A7C', soft: 'rgba(156,138,124,0.16)', label: 'Low data',          ring: '#B6A595' },
  blue:   { fill: '#3E7CB0', soft: 'rgba(62,124,176,0.16)',  label: 'Active base',        ring: '#3E7CB0' },
  green:  { fill: '#2E8B4E', soft: 'rgba(46,139,78,0.18)',   label: 'Strong opportunity', ring: '#2E8B4E' },
  amber:  { fill: '#E0992A', soft: 'rgba(224,153,42,0.18)',  label: 'Competitive pressure',ring: '#E0992A' },
  red:    { fill: '#C13B1F', soft: 'rgba(193,59,31,0.16)',   label: 'Influence declining', ring: '#C13B1F' },
  purple: { fill: '#7A57C9', soft: 'rgba(122,87,201,0.18)',  label: 'Campaign running',    ring: '#7A57C9' },
};

function influenceLevel(inf) {
  if (inf < 10) return 'Cold';
  if (inf < 25) return 'Warming';
  if (inf < 45) return 'Active';
  return 'Stronghold';
}

// ─── Estates (stylised Milton Keynes map, x/y in % of map canvas) ─────
// Each: customers (active Lasso), interest (interested in vendor cuisine),
// opportunity 0-100, influence %, trend, competitor, recommended campaign + estimates.
const ESTATES = [
  { id:'olney',     name:'Olney',              x:58, y:6,  state:'grey',   customers:120,  interest:34,  opportunity:35, influence:4,  trendDir:'flat', trendPct:1,  aov:[16,22], peak:'18:30–20:00', competitor:'Low',    rec:'New menu item launch',         demand:[3,3,4,3,4,4,5] },
  { id:'stony',     name:'Stony Stratford',    x:20, y:22, state:'blue',   customers:480,  interest:138, opportunity:55, influence:29, trendDir:'up',   trendPct:5,  aov:[18,26], peak:'18:00–20:30', competitor:'Medium', rec:'Free delivery window',         demand:[8,9,9,11,12,12,14] },
  { id:'wolverton', name:'Wolverton',          x:33, y:17, state:'blue',   customers:720,  interest:206, opportunity:64, influence:34, trendDir:'up',   trendPct:7,  aov:[17,25], peak:'18:30–21:00', competitor:'Medium', rec:'Dormant customer reactivation',demand:[12,13,14,14,16,17,19] },
  { id:'newport',   name:'Newport Pagnell',    x:70, y:19, state:'amber',  customers:540,  interest:152, opportunity:58, influence:22, trendDir:'down', trendPct:6,  aov:[19,28], peak:'19:00–21:00', competitor:'High',   rec:'First-order discount',         demand:[16,15,15,14,13,13,12] },
  { id:'linford',   name:'Great Linford',      x:52, y:28, state:'blue',   customers:660,  interest:190, opportunity:61, influence:31, trendDir:'up',   trendPct:7,  aov:[18,26], peak:'18:30–20:30', competitor:'Medium', rec:'Lunchtime push',               demand:[11,12,13,14,15,15,17] },
  { id:'bradwell',  name:'Bradwell',           x:37, y:39, state:'grey',   customers:140,  interest:38,  opportunity:38, influence:6,  trendDir:'flat', trendPct:0,  aov:[15,21], peak:'18:30–20:00', competitor:'Low',    rec:'New menu item launch',         demand:[4,4,3,4,5,4,5] },
  { id:'central',   name:'Central MK',         x:48, y:46, state:'purple', customers:1240, interest:402, opportunity:72, influence:41, trendDir:'up',   trendPct:8,  aov:[20,30], peak:'18:00–21:30', competitor:'High',   rec:'Weekend family meal promo',    demand:[24,26,27,28,30,33,36], campaign:'Weekend Family Bundle' },
  { id:'shenley',   name:'Shenley Church End', x:27, y:53, state:'green',  customers:610,  interest:198, opportunity:84, influence:12, trendDir:'up',   trendPct:18, aov:[19,28], peak:'18:30–21:00', competitor:'Low',    rec:'First-order discount',         demand:[9,11,13,15,18,21,24] },
  { id:'fishermead',name:'Fishermead',         x:57, y:53, state:'amber',  customers:520,  interest:166, opportunity:67, influence:19, trendDir:'down', trendPct:4,  aov:[16,24], peak:'19:00–21:30', competitor:'High',   rec:'High-value customer boost',    demand:[15,15,14,14,13,14,13] },
  { id:'monkston',  name:'Monkston',           x:69, y:49, state:'blue',   customers:580,  interest:170, opportunity:59, influence:27, trendDir:'up',   trendPct:6,  aov:[19,27], peak:'18:30–20:30', competitor:'Medium', rec:'Payday campaign',              demand:[10,11,12,12,13,14,15] },
  { id:'tattenhoe', name:'Tattenhoe',          x:25, y:69, state:'green',  customers:430,  interest:140, opportunity:79, influence:9,  trendDir:'up',   trendPct:14, aov:[18,26], peak:'18:30–20:30', competitor:'Low',    rec:'First-order discount',         demand:[6,7,9,10,12,14,16] },
  { id:'emerson',   name:'Emerson Valley',     x:37, y:71, state:'red',    customers:500,  interest:148, opportunity:52, influence:24, trendDir:'down', trendPct:11, aov:[17,25], peak:'18:00–20:00', competitor:'High',   rec:'Dormant customer reactivation',demand:[18,17,16,15,14,13,11] },
  { id:'westcroft', name:'Westcroft',          x:22, y:64, state:'grey',   customers:180,  interest:50,  opportunity:41, influence:7,  trendDir:'flat', trendPct:1,  aov:[16,23], peak:'18:30–20:00', competitor:'Low',    rec:'New menu item launch',         demand:[5,5,6,5,6,6,7] },
  { id:'walnut',    name:'Walnut Tree',        x:60, y:61, state:'blue',   customers:470,  interest:142, opportunity:56, influence:26, trendDir:'up',   trendPct:4,  aov:[18,26], peak:'18:30–20:30', competitor:'Medium', rec:'Free delivery window',         demand:[9,9,10,10,11,11,12] },
  { id:'kingston',  name:'Kingston',           x:71, y:57, state:'amber',  customers:540,  interest:158, opportunity:63, influence:21, trendDir:'down', trendPct:3,  aov:[19,28], peak:'19:00–21:00', competitor:'High',   rec:'Lunchtime push',               demand:[14,14,13,14,13,12,13] },
  { id:'bletchley', name:'Bletchley',          x:44, y:82, state:'green',  customers:980,  interest:316, opportunity:91, influence:16, trendDir:'up',   trendPct:22, aov:[18,27], peak:'18:00–21:30', competitor:'Medium', rec:'First-order discount',         demand:[14,17,20,24,28,33,39] },
  { id:'woburn',    name:'Woburn Sands',       x:82, y:73, state:'green',  customers:360,  interest:118, opportunity:74, influence:11, trendDir:'up',   trendPct:16, aov:[19,28], peak:'18:30–20:30', competitor:'Low',    rec:'New menu item launch',         demand:[6,7,8,9,11,12,14] },
];

// ─── Your restaurant HQ — Stadium MK (Denbigh, south MK) ─────────────
const HQ = {
  name:'Fire & Dough', sub:'Stadium MK · Denbigh', x:53, y:78,
  radius:34,                       // delivery radius, % of map width
  ordersToday:142, driversOut:11, avgDeliver:'27 min',
};

// ─── Rival vendors on the map (anonymised competitive set) ───────────
// pressure: how hard they compete · cuisine clash with Fire & Dough
const RIVALS = [
  { id:'rv1', name:'Stone Fired Co.',   cuisine:'Wood-fired pizza', x:53, y:46, pressure:'High',   share:34, trendDir:'up',   note:'Market leader in Central MK. Aggressive weekend bundles.' },
  { id:'rv2', name:'The Wing Shack',    cuisine:'Wings · Chicken',  x:67, y:25, pressure:'High',   share:31, trendDir:'up',   note:'Owns Newport Pagnell. Strong on late-night orders.' },
  { id:'rv3', name:'Coal & Ember',      cuisine:'Pizza · Grill',    x:74, y:55, pressure:'Medium', share:25, trendDir:'flat', note:'Steady presence east of the city around Kingston.' },
  { id:'rv4', name:'Loaded Street Co.', cuisine:'Loaded fries',     x:61, y:58, pressure:'Medium', share:22, trendDir:'down', note:'Slipping in Fishermead — reactivation window open.' },
  { id:'rv5', name:'Crust & Co.',       cuisine:'Neapolitan pizza', x:39, y:78, pressure:'Medium', share:19, trendDir:'up',   note:'Rising fast in Bletchley. Watch this one.' },
  { id:'rv6', name:'Dough Bros',        cuisine:'Pizza · Pasta',    x:30, y:60, pressure:'Low',    share:12, trendDir:'flat', note:'Small west-side operator. Easy to out-market.' },
];

// Derived estimate helper (reach/orders/revenue/cost/roi) for a budget on an estate
function estimateFor(estate, budget) {
  const interest = estate.interest;
  const oppFactor = 0.55 + estate.opportunity / 180;            // 0.55–1.05
  const reach = Math.round(Math.min(interest, budget / 0.85) * oppFactor + interest * 0.35);
  const convBase = 0.085 + estate.opportunity / 1400;           // ~9–15%
  const orders = Math.round(reach * convBase);
  const aovMid = (estate.aov[0] + estate.aov[1]) / 2;
  const revenue = Math.round(orders * aovMid);
  const roiLow = +(revenue / Math.max(budget, 1) * 0.78).toFixed(1);
  const roiHigh = +(revenue / Math.max(budget, 1) * 1.12).toFixed(1);
  return { reach, orders, revenue, cost: budget, roiLow, roiHigh };
}

// ─── KPI tiles (overview) ────────────────────────────────────────────
const KPIS = [
  { id:'orders',   label:"Today's orders",      value:'142',    delta:'+12%', dir:'up',   sub:'vs. last Tue',        icon:'receipt' },
  { id:'revenue',  label:'Revenue today',       value:'£3,184', delta:'+9%',  dir:'up',   sub:'vs. last Tue',        icon:'pound' },
  { id:'aov',      label:'Avg order value',     value:'£22.40', delta:'+3%',  dir:'up',   sub:'7-day average',       icon:'tag' },
  { id:'repeat',   label:'Repeat customer rate',value:'47%',    delta:'+5pts',dir:'up',   sub:'last 30 days',        icon:'refresh' },
  { id:'active',   label:'Active campaigns',    value:'3',      delta:'1 ending', dir:'flat', sub:'2 areas live',    icon:'megaphone' },
  { id:'roi',      label:'Marketing ROI',       value:'3.8×',   delta:'+0.4×', dir:'up',   sub:'30-day blended',      icon:'trendUp' },
  { id:'health',   label:'Menu health score',   value:'78',     delta:'Good', dir:'up',    sub:'of 100',              icon:'utensils' },
  { id:'territory',label:'Territory influence',  value:'28%',   delta:'+3pts',dir:'up',    sub:'17 estates',          icon:'target' },
];

// ─── Menu (Fire & Dough) ─────────────────────────────────────────────
// margin: low/med/high · pop 0-100 · trend up/down/flat
const MENU = [
  { cat:'Pizzas', items:[
    { id:'m1', name:'The Forno Margherita',   price:9.50,  avail:true,  pop:74, margin:'high', photo:true,  mods:'full',    trendDir:'up',   trendPct:8,  best:false },
    { id:'m2', name:'Smoky Pepperoni Storm',  price:12.00, avail:true,  pop:96, margin:'high', photo:true,  mods:'full',    trendDir:'up',   trendPct:14, best:true  },
    { id:'m3', name:'Nduja & Hot Honey',      price:13.50, avail:true,  pop:68, margin:'high', photo:false, mods:'partial', trendDir:'up',   trendPct:21, best:false },
    { id:'m4', name:'Garden Veg Stonebake',   price:11.00, avail:false, pop:41, margin:'med',  photo:false, mods:'partial', trendDir:'down', trendPct:6,  best:false },
  ]},
  { cat:'Wings', items:[
    { id:'m5', name:'Buffalo Hot Wings (10)',  price:8.50,  avail:true,  pop:91, margin:'high', photo:true,  mods:'full',    trendDir:'up',   trendPct:11, best:true  },
    { id:'m6', name:'Korean BBQ Wings (10)',   price:9.00,  avail:true,  pop:79, margin:'high', photo:true,  mods:'full',    trendDir:'up',   trendPct:9,  best:false },
    { id:'m7', name:'Garlic Parm Wings (10)',  price:8.50,  avail:true,  pop:55, margin:'med',  photo:false, mods:'none',    trendDir:'flat', trendPct:1,  best:false },
  ]},
  { cat:'Loaded Fries', items:[
    { id:'m8', name:'Dirty Loaded Fries',      price:7.00,  avail:true,  pop:88, margin:'high', photo:true,  mods:'full',    trendDir:'up',   trendPct:18, best:true  },
    { id:'m9', name:'Buffalo Chicken Fries',   price:8.00,  avail:true,  pop:72, margin:'high', photo:false, mods:'partial', trendDir:'up',   trendPct:12, best:false },
    { id:'m10',name:'Cheese & Bacon Fries',    price:7.50,  avail:true,  pop:64, margin:'med',  photo:true,  mods:'full',    trendDir:'flat', trendPct:2,  best:false },
  ]},
  { cat:'Sides & Dips', items:[
    { id:'m11',name:'Halloumi Fries',          price:6.00,  avail:true,  pop:58, margin:'high', photo:false, mods:'none',    trendDir:'up',   trendPct:7,  best:false },
    { id:'m12',name:'Garlic Doughballs',       price:4.50,  avail:true,  pop:62, margin:'high', photo:true,  mods:'partial', trendDir:'up',   trendPct:5,  best:false },
    { id:'m13',name:'Dip Trio',                price:3.00,  avail:false, pop:38, margin:'high', photo:false, mods:'none',    trendDir:'down', trendPct:4,  best:false },
  ]},
  { cat:'Bundles', items:[
    { id:'m14',name:'Family Feast Bundle',     price:28.00, avail:true,  pop:83, margin:'high', photo:true,  mods:'full',    trendDir:'up',   trendPct:24, best:true  },
    { id:'m15',name:'Match Night for Two',     price:18.50, avail:true,  pop:70, margin:'high', photo:false, mods:'partial', trendDir:'up',   trendPct:10, best:false },
  ]},
];

const MENU_RECS = [
  { id:'r1', icon:'image',    text:'Add photos to 6 items', detail:'Items with photos sell up to 2.3× more. Nduja & Hot Honey is trending but has no photo.', action:'Add photos' },
  { id:'r2', icon:'rocket',   text:'Promote best sellers in nearby estates', detail:'Pepperoni Storm over-indexes in Bletchley. Push it where demand is rising.', action:'Promote' },
  { id:'r3', icon:'alert',    text:'2 items unavailable', detail:'Garden Veg Stonebake and Dip Trio are switched off. Re-enable or hide from menu.', action:'Review' },
  { id:'r4', icon:'layers',   text:'Bundle popular combinations', detail:'Loaded Fries + Wings are ordered together 41% of the time. Create a combo.', action:'Build bundle' },
  { id:'r5', icon:'flame',    text:'Launch campaign for high-margin items', detail:'Dirty Loaded Fries has 18% demand growth and high margin. Ideal campaign hero.', action:'Launch' },
];

// ─── Campaign types ──────────────────────────────────────────────────
const CAMPAIGN_TYPES = [
  { id:'firstorder', name:'First-order discount',          icon:'gift',     blurb:'Win new customers with an intro deal', best:'Cold & Warming areas' },
  { id:'freedeliv',  name:'Free delivery window',          icon:'truck',    blurb:'Lift conversion in price-sensitive areas', best:'Active areas' },
  { id:'lunch',      name:'Lunchtime push',                icon:'sun',      blurb:'Capture the 12–2pm weekday window', best:'Workplace estates' },
  { id:'family',     name:'Weekend family meal promo',     icon:'users',    blurb:'Bundle deals for Fri–Sun households', best:'Residential estates' },
  { id:'dormant',    name:'Dormant customer reactivation', icon:'refresh',  blurb:'Bring back customers gone quiet 30+ days', best:'Declining areas' },
  { id:'newitem',    name:'New menu item launch',          icon:'sparkle',  blurb:'Spotlight a new dish to local demand', best:'Low-data areas' },
  { id:'highvalue',  name:'High-value customer boost',     icon:'crown',    blurb:'Reward your top spenders with perks', best:'Stronghold areas' },
  { id:'payday',     name:'Payday campaign',               icon:'pound',    blurb:'Time a push to end-of-month spend', best:'Any area' },
];

// ─── Growth missions ─────────────────────────────────────────────────
const MISSIONS = [
  { id:'ms1', text:'Win 20 new customers in Bletchley',        area:'Bletchley',  progress:11, goal:20, xp:600, type:'firstorder' },
  { id:'ms2', text:'Reactivate 15 dormant customers in Wolverton', area:'Wolverton', progress:9, goal:15, xp:450, type:'dormant' },
  { id:'ms3', text:'Push loaded fries in Central MK this weekend', area:'Central MK', progress:1, goal:1, xp:300, type:'newitem', live:true },
  { id:'ms4', text:'Reach 40% influence in Fishermead',        area:'Fishermead', progress:19, goal:40, xp:800, unit:'%', type:'highvalue' },
];

// ─── Badges ──────────────────────────────────────────────────────────
const BADGES = [
  { id:'b1', name:'First Blood',      desc:'Launched your first campaign',         icon:'rocket',  earned:true },
  { id:'b2', name:'Estate Hopper',    desc:'Active in 10+ estates',                icon:'map',     earned:true },
  { id:'b3', name:'Streak Keeper',    desc:'6-week campaign streak',               icon:'flame',   earned:true },
  { id:'b4', name:'Reactivator',      desc:'Won back 100 dormant customers',       icon:'refresh', earned:true },
  { id:'b5', name:'Stronghold',       desc:'Reach 45% influence in any estate',    icon:'shield',  earned:false },
  { id:'b6', name:'City Wide',        desc:'Active customer base in all 17 estates', icon:'target',earned:false },
];

// ─── Territory progress (Cold → Warming → Active → Stronghold) ───────
const TERRITORY_TIERS = [
  { id:'cold',     name:'Cold',       color:'#9C8A7C', count:4 },
  { id:'warming',  name:'Warming',    color:'#3E7CB0', count:7 },
  { id:'active',   name:'Active',     color:'#E0992A', count:5 },
  { id:'strong',   name:'Stronghold', color:'#2E8B4E', count:1 },
];

// ─── Leaderboard (anonymised competitors) ────────────────────────────
const LEADERBOARD = [
  { rank:1, name:'Stone•••• Co.',     cuisine:'Pizza',          score:34, you:false },
  { rank:2, name:'The •••• Shack',    cuisine:'Wings',          score:31, you:false },
  { rank:3, name:'Fire & Dough',      cuisine:'Pizza · Wings',  score:28, you:true  },
  { rank:4, name:'Coal & •••••',      cuisine:'Pizza',          score:25, you:false },
  { rank:5, name:'•••• Street Food',  cuisine:'Loaded Fries',   score:22, you:false },
];

// ─── Active campaigns (overview / campaigns) ─────────────────────────
const ACTIVE_CAMPAIGNS = [
  { id:'c1', name:'Weekend Family Bundle', area:'Central MK', type:'family',     status:'live',   spend:180, budget:240, reach:402, orders:58, revenue:1392, roi:7.7, ends:'2 days left',
    offer:'£6 off bundles over £25', hero:'Family Feast Bundle', audience:'Households in Central MK', hasPhoto:true,  elapsed:5, total:7,  benchmarkRoi:4.5, newPct:42 },
  { id:'c2', name:'Pepperoni First Bite',  area:'Bletchley',  type:'firstorder', status:'live',   spend:95,  budget:160, reach:212, orders:24, revenue:540,  roi:5.7, ends:'5 days left',
    offer:'30% off first order',     hero:'Smoky Pepperoni Storm', audience:'New customers in Bletchley', hasPhoto:true, elapsed:3, total:8,  benchmarkRoi:4.5, newPct:88 },
  { id:'c3', name:'Wolverton Win-Back',    area:'Wolverton',  type:'dormant',    status:'ending', spend:140, budget:150, reach:188, orders:19, revenue:456,  roi:3.3, ends:'Ends today',
    offer:'£5 off, come back',       hero:'Dirty Loaded Fries', audience:'Lapsed 30+ days in Wolverton', hasPhoto:false, elapsed:7, total:7,  benchmarkRoi:4.0, newPct:8 },
];

// Full operational read for a campaign — funnel, daily series, customer split,
// items driven, and rule-based "areas for development".
function campaignOps(c){
  const h = strHash(c.id);
  const impressions = Math.round(c.reach*4.2);
  const views = c.reach;
  const conv = +(c.orders/views*100).toFixed(1);              // view → order %
  const benchConv = 12.0;
  const cpa = +(c.spend/c.orders).toFixed(2);
  const roas = +(c.revenue/c.spend).toFixed(1);
  const aov = +(c.revenue/c.orders).toFixed(2);
  const newCust = Math.round(c.orders*c.newPct/100);
  const repeatCust = c.orders-newCust;
  const redemptions = c.orders;
  const incremental = Math.round(c.revenue*0.62);             // est. incremental vs baseline
  const dailyBurn = +(c.spend/Math.max(1,c.elapsed)).toFixed(0);
  const projSpend = Math.min(c.budget, dailyBurn*c.total);
  const daysLeft = Math.max(0, c.total-c.elapsed);
  // daily series since launch (orders + revenue + spend), gentle ramp
  const series = Array.from({length:c.elapsed}, (_,i)=>{
    const t=(i+1)/c.elapsed; const wob=((strHash(c.id+i)%20)-10)/100;
    const o=Math.max(1, Math.round(c.orders/c.elapsed*(0.7+0.6*t)*(1+wob)));
    return { d:i+1, orders:o, rev:Math.round(o*aov), spend:Math.round(c.spend/c.elapsed) };
  });
  // items driven (top 4 from menu, biased to hero)
  const items = allMenuItems().map(it=>({ it, w: (it.name===c.hero?120:0)+it.pop*0.6+(strHash(c.id+it.id)%40) }))
    .sort((a,b)=>b.w-a.w).slice(0,4);
  const isum = items.reduce((s,r)=>s+r.w,0);
  const topItems = items.map(r=>({ name:r.it.name, share:Math.round(r.w/isum*100), orders:Math.round(r.w/isum*c.orders) }));

  // areas for development (rule-based, ordered by severity)
  const areas=[];
  const spentPct = c.spend/c.budget;
  if (c.status==='ending' || daysLeft<=1)
    areas.push({ sev:'high', icon:'clock', title:'Ending today — extend to hold momentum', detail:`This campaign has driven £${c.revenue.toLocaleString()} at ${roas}× ROAS. Going dark now loses the audience you've warmed up.`, action:'Extend 7 days', kind:'extend' });
  if (c.roi < c.benchmarkRoi)
    areas.push({ sev:'high', icon:'trendDown', title:`ROI ${c.roi}× trails the ${c.benchmarkRoi}× area benchmark`, detail:`CPA is £${cpa}. Tighten the audience or sharpen the offer to lift return before adding budget.`, action:'Adjust targeting', kind:'edit' });
  else
    areas.push({ sev:'low', icon:'trendUp', title:`Beating benchmark — ${c.roi}× vs ${c.benchmarkRoi}×`, detail:`Strong efficiency at £${cpa} CPA. This is a candidate to scale budget or clone into an adjacent estate.`, action:'Scale budget', kind:'budget' });
  if (!c.hasPhoto)
    areas.push({ sev:'med', icon:'image', title:'No hero photo on the offer', detail:`“${c.hero}” has no image in this campaign. Creatives with a photo convert roughly 2× better.`, action:'Add photo', kind:'photo' });
  if (conv < benchConv)
    areas.push({ sev:'med', icon:'filter', title:`View-to-order conversion ${conv}% below ${benchConv}% benchmark`, detail:'People are seeing the offer but not converting — test a lower minimum-spend or a stronger first-order hook.', action:'Test new offer', kind:'edit' });
  if (spentPct>0.85 && daysLeft>0)
    areas.push({ sev:'med', icon:'pound', title:`Budget ${Math.round(spentPct*100)}% spent with ${daysLeft} day${daysLeft>1?'s':''} left`, detail:`Burning £${dailyBurn}/day. Top up or the campaign pauses before it ends.`, action:'Top up budget', kind:'budget' });
  areas.push({ sev:'low', icon:'sparkle', title:'Expand to an adjacent high-opportunity estate', detail:`Demand for your cuisine is rising next door. Cloning this offer could reach a fresh warm audience.`, action:'Clone to new area', kind:'clone' });

  return { impressions, views, conv, benchConv, cpa, roas, aov, newCust, repeatCust, redemptions, incremental,
    dailyBurn, projSpend, daysLeft, series, topItems, areas };
}


// ─── Orders & Sales ──────────────────────────────────────────────────
// Live kitchen queue. status: new → cooking → ready → delivery/collection → completed
const ORDERS = [
  { id:'4821', name:'Aisha M.',  estate:'Central MK',          type:'delivery',   items:'Pepperoni Storm · Dirty Loaded Fries · Garlic Doughballs', qty:3, total:24.50, status:'new',       mins:1 },
  { id:'4820', name:'Tom R.',    estate:'Bletchley',           type:'delivery',   items:'Family Feast Bundle',                                     qty:1, total:28.00, status:'new',       mins:3 },
  { id:'4819', name:'Priya S.',  estate:'Shenley Church End',  type:'collection', items:'Buffalo Hot Wings (10) · Halloumi Fries',                  qty:2, total:14.50, status:'cooking',   mins:6,  prep:18 },
  { id:'4818', name:'Jordan K.', estate:'Wolverton',           type:'delivery',   items:'Korean BBQ Wings · Dirty Loaded Fries · Dip Trio',         qty:3, total:20.00, status:'cooking',   mins:9,  prep:18 },
  { id:'4817', name:'Megan T.',  estate:'Central MK',          type:'delivery',   items:'Forno Margherita · Nduja & Hot Honey',                    qty:2, total:23.00, status:'ready',     mins:12 },
  { id:'4816', name:'Sam W.',    estate:'Tattenhoe',           type:'collection', items:'Match Night for Two',                                     qty:1, total:18.50, status:'ready',     mins:14 },
  { id:'4815', name:'Leah B.',   estate:'Walnut Tree',         type:'delivery',   items:'Pepperoni Storm · Cheese & Bacon Fries',                  qty:2, total:19.50, status:'delivery',  mins:18 },
  { id:'4814', name:'Chris D.',  estate:'Fishermead',          type:'delivery',   items:'Dirty Loaded Fries · Buffalo Hot Wings (10)',             qty:2, total:17.00, status:'completed', mins:24 },
  { id:'4813', name:'Noor H.',   estate:'Bletchley',           type:'delivery',   items:'Garden Veg Stonebake · Garlic Doughballs',                qty:2, total:15.50, status:'completed', mins:31 },
];

// Hourly takings for today (trading 11:00–23:00). Sums to 142 orders / £3,184.
const SALES_HOURLY = [
  { label:'11', rev:64,  ord:3 },  { label:'12', rev:182, ord:9 },  { label:'13', rev:210, ord:10 },
  { label:'14', rev:96,  ord:5 },  { label:'15', rev:54,  ord:3 },  { label:'16', rev:88,  ord:4 },
  { label:'17', rev:240, ord:11 }, { label:'18', rev:402, ord:18 }, { label:'19', rev:486, ord:21 },
  { label:'20', rev:438, ord:19 }, { label:'21', rev:512, ord:22 }, { label:'22', rev:316, ord:14 },
  { label:'23', rev:96,  ord:3 },
];

const SALES_DAILY = [
  { label:'Wed', rev:2680, ord:121 }, { label:'Thu', rev:2940, ord:131 }, { label:'Fri', rev:3820, ord:168 },
  { label:'Sat', rev:4310, ord:189 }, { label:'Sun', rev:3560, ord:158 }, { label:'Mon', rev:2510, ord:113 },
  { label:'Tue', rev:3184, ord:142 },
];

// 30-day series (oldest → today), gentle weekend peaks
const SALES_MONTHLY = (()=>{
  const base = [2410,2280,2360,3680,4120,3390,2440, 2520,2400,2470,3760,4210,3510,2560,
                2610,2490,2580,3880,4360,3620,2680, 2700,2560,2680,3920,4310,3560,2510, 2940,3184];
  const today = new Date();
  return base.map((rev,i)=>{
    const d = new Date(today); d.setDate(today.getDate() - (base.length-1-i));
    return { label: String(d.getDate()), rev, ord: Math.round(rev/22.4), date:d };
  });
})();

// Top sellers today (by revenue)
const TOP_SELLERS = [
  { name:'Smoky Pepperoni Storm',   cat:'Pizzas',       qty:38, rev:456, trendDir:'up',   trendPct:14 },
  { name:'Dirty Loaded Fries',      cat:'Loaded Fries', qty:44, rev:308, trendDir:'up',   trendPct:18 },
  { name:'Buffalo Hot Wings (10)',  cat:'Wings',        qty:31, rev:264, trendDir:'up',   trendPct:11 },
  { name:'Family Feast Bundle',     cat:'Bundles',      qty:9,  rev:252, trendDir:'up',   trendPct:24 },
  { name:'The Forno Margherita',    cat:'Pizzas',       qty:22, rev:209, trendDir:'flat', trendPct:2 },
  { name:'Korean BBQ Wings (10)',   cat:'Wings',        qty:19, rev:171, trendDir:'up',   trendPct:9 },
];

// Fulfilment split today
const ORDER_MIX = [
  { label:'Delivery',   count:111, color:'#D85A14' },
  { label:'Collection', count:31,  color:'#3E7CB0' },
];

// Weekly payout statements (£). Lasso fee ≈ 15%.
const PAYOUTS = [
  { id:'PO-2418', period:'16–22 Jun', gross:3184,  fees:478,  net:2706,  status:'pending', date:'Due 23 Jun' },
  { id:'PO-2405', period:'9–15 Jun',  gross:21840, fees:3276, net:18564, status:'paid',    date:'16 Jun' },
  { id:'PO-2391', period:'2–8 Jun',   gross:19620, fees:2943, net:16677, status:'paid',    date:'9 Jun' },
  { id:'PO-2377', period:'26 May–1 Jun', gross:20180, fees:3027, net:17153, status:'paid', date:'2 Jun' },
];

// Per-range headline summary for the Orders & Sales screen
const SALES_SUMMARY = {
  today: { orders:142,  revenue:3184,  aov:22.40, prep:16, accept:98, ordDelta:'+12%', revDelta:'+9%',  aovDelta:'+3%',  acceptDelta:'+1pt',  sub:'vs. last Tue' },
  week:  { orders:1022, revenue:23004, aov:22.51, prep:17, accept:97, ordDelta:'+8%',  revDelta:'+11%', aovDelta:'+2%',  acceptDelta:'flat',  sub:'vs. prior 7 days' },
  month: { orders:4380, revenue:98120, aov:22.40, prep:17, accept:97, ordDelta:'+14%', revDelta:'+16%', aovDelta:'+1%',  acceptDelta:'+2pt',  sub:'vs. prior 30 days' },
};

// ─── Profit & performance ops (Overview) ─────────────────────────────
// Today's trading vs target, vs same time last week, vs yesterday.
// "pace" = % of the day's trading already elapsed (used for on-track maths).
const PROFIT = {
  pace: 0.78,                       // 78% through the trading day
  revenue:   { now:3184, target:4000, lastWeekToDate:2920, yesterdayToDate:2760, lastWeekFull:3760 },
  net:       { now:1592, target:2000, lastWeekToDate:1404, marginPct:50 },
  orders:    { now:142,  target:182,  lastWeekToDate:131,  yesterdayToDate:124 },
  aov:       { now:22.40, target:22.00, lastWeek:21.80 },
  // profit waterfall for today (gross → net)
  waterfall: [
    { label:'Gross sales',   value:3184,  kind:'start' },
    { label:'Lasso fee 15%', value:-478,  kind:'cost' },
    { label:'Food & packaging', value:-892, kind:'cost' },
    { label:'Delivery cost', value:-222,  kind:'cost' },
    { label:'Net profit',    value:1592,  kind:'end' },
  ],
};

// Operational performance scorecard — each vs a target band
const OPS_METRICS = [
  { id:'accept',  label:'Order acceptance',  value:98,  unit:'%',   target:95, dir:'up',   good:'high', vsLastWeek:'+1pt',  spark:[94,95,96,95,97,98,98] },
  { id:'prep',    label:'Avg prep time',     value:16,  unit:' min',target:18, dir:'down', good:'low',  vsLastWeek:'−1 min', spark:[19,18,18,17,17,16,16] },
  { id:'cancel',  label:'Cancellation rate', value:1.4, unit:'%',   target:2.5,dir:'down', good:'low',  vsLastWeek:'−0.3pt', spark:[2.1,1.9,1.8,1.7,1.6,1.5,1.4] },
  { id:'ontime',  label:'On-time delivery',  value:94,  unit:'%',   target:92, dir:'up',   good:'high', vsLastWeek:'+2pt',  spark:[90,91,92,92,93,93,94] },
];

// Delivery focus
const DELIVERY = {
  avgTime:27, lastWeekTime:29, target:28,
  onTime:94, late:6, driversOut:11, driversIdle:3,
  longestWait:'Woburn Sands · 38 min',
  // delivery time distribution buckets (min → % of orders)
  dist:[ {b:'<20', pct:18}, {b:'20–25', pct:31}, {b:'25–30', pct:28}, {b:'30–35', pct:15}, {b:'35+', pct:8} ],
};

// Reviews & reputation
const REVIEWS = {
  rating:4.6, count:1284, last30:212, responseRate:88, ratingDelta:'+0.2', target:4.5,
  dist:[ {s:5,pct:74}, {s:4,pct:17}, {s:3,pct:5}, {s:2,pct:2}, {s:1,pct:2} ],
  themes:[
    { tag:'Pizza quality', sentiment:'pos', count:148 },
    { tag:'Generous portions', sentiment:'pos', count:96 },
    { tag:'Fast delivery', sentiment:'pos', count:73 },
    { tag:'Cold on arrival', sentiment:'neg', count:21 },
    { tag:'Missing dip', sentiment:'neg', count:14 },
  ],
  recent:[
    { id:'rv1', name:'Aisha M.',  estate:'Central MK',         stars:5, when:'2h ago',  text:'Pepperoni Storm is unreal. Best pizza in MK, arrived hot and early.', item:'Smoky Pepperoni Storm', replied:false },
    { id:'rv2', name:'Tom R.',    estate:'Bletchley',          stars:5, when:'5h ago',  text:'Family bundle fed all of us with leftovers. Will order again.', item:'Family Feast Bundle', replied:true },
    { id:'rv3', name:'Priya S.',  estate:'Shenley Church End', stars:3, when:'Yesterday', text:'Wings were great but the dip was missing from the bag.', item:'Buffalo Hot Wings (10)', replied:false },
    { id:'rv4', name:'Jordan K.', estate:'Wolverton',          stars:4, when:'Yesterday', text:'Loaded fries on point. Delivery took a touch longer than quoted.', item:'Dirty Loaded Fries', replied:false },
    { id:'rv5', name:'Megan T.',  estate:'Central MK',         stars:5, when:'Yesterday', text:'Korean BBQ wings are addictive. Packaging kept everything hot.', item:'Korean BBQ Wings (10)', replied:false },
    { id:'rv6', name:'Sam W.',    estate:'Tattenhoe',          stars:2, when:'2 days ago', text:'Pizza arrived a bit cold this time — shame as it usually slaps.', item:'The Forno Margherita', replied:false },
    { id:'rv7', name:'Leah B.',   estate:'Walnut Tree',        stars:5, when:'2 days ago', text:'Doughballs are the best in town. Fast delivery too.', item:'Garlic Doughballs', replied:true },
    { id:'rv8', name:'Chris D.',  estate:'Fishermead',         stars:4, when:'3 days ago', text:'Solid as always. Would love a vegan option though.', item:'Garden Veg Stonebake', replied:false },
    { id:'rv9', name:'Noor H.',   estate:'Bletchley',          stars:5, when:'3 days ago', text:'Match Night for Two is great value. Sorted our Saturday.', item:'Match Night for Two', replied:false },
    { id:'rv10',name:'Dan P.',    estate:'Monkston',           stars:4, when:'4 days ago', text:'Halloumi fries were a nice surprise. Will get them again.', item:'Halloumi Fries', replied:true },
  ],
};

// Where you rank among local vendors (anonymised), by net revenue this month
const PERFORMANCE_RANK = { rank:3, of:11, area:'Milton Keynes', cuisine:'Pizza · Wings', up:1, percentile:82 };

// ─── Customer Insights (aggregated · anonymised) ─────────────────────
// New vs repeat vs dormant mix
const CUST_MIX = [
  { id:'new',     label:'New',     pct:31, count:1356, color:'#3E7CB0', delta:'+4pt',  note:'first order in 30 days' },
  { id:'repeat',  label:'Repeat',  pct:47, count:2056, color:'#2E8B4E', delta:'+5pt',  note:'2+ orders, active' },
  { id:'dormant', label:'Dormant', pct:22, count:962,  color:'#C13B1F', delta:'−2pt',  note:'no order in 30+ days' },
];
const CUST_TOTAL = 4374;

// Repeat-rate trend (last 8 weeks, %)
const REPEAT_TREND = [38,40,41,43,44,45,46,47];

// Reorder window — days between orders for repeat customers
const REORDER_WINDOW = [
  { b:'0–3 days', pct:9 }, { b:'4–7 days', pct:34 }, { b:'8–14 days', pct:31 },
  { b:'15–30 days', pct:18 }, { b:'30+ days', pct:8 },
];

// Basket size distribution (£)
const BASKET_DIST = [
  { b:'<£12', pct:11 }, { b:'£12–18', pct:24 }, { b:'£18–25', pct:37 }, { b:'£25–35', pct:21 }, { b:'£35+', pct:7 },
];

// Cuisine affinity in your delivery radius vs what your menu covers
const CUISINE_AFFINITY = [
  { name:'Pizza',        demand:92, onMenu:true },
  { name:'Wings',        demand:78, onMenu:true },
  { name:'Loaded fries', demand:71, onMenu:true },
  { name:'Burgers',      demand:64, onMenu:false },
  { name:'Desserts',     demand:58, onMenu:false },
  { name:'Vegan',        demand:41, onMenu:false },
];

// Order volume by day × time-of-day (heatmap). rows = days, cols = day-parts.
const DAYPARTS = ['Lunch','Aft','Early eve','Peak','Late'];
const ORDER_HEAT = [
  { day:'Mon', v:[14,6,22,38,12] },
  { day:'Tue', v:[16,7,26,44,14] },
  { day:'Wed', v:[18,8,28,46,16] },
  { day:'Thu', v:[20,9,32,52,19] },
  { day:'Fri', v:[26,11,44,78,38] },
  { day:'Sat', v:[30,14,52,92,46] },
  { day:'Sun', v:[28,12,46,74,30] },
];

// Customer value bands (LTV)
const VALUE_BANDS = [
  { band:'VIP',      spend:'£300+',   count:214,  pct:5,  color:'#7A57C9' },
  { band:'Loyal',    spend:'£120–300',count:712,  pct:16, color:'#2E8B4E' },
  { band:'Regular',  spend:'£40–120', count:1788, pct:41, color:'#3E7CB0' },
  { band:'Occasional',spend:'<£40',   count:1660, pct:38, color:'#9C8A7C' },
];

// ─── Item insight + estate-ranking helpers ───────────────────────────
function strHash(s){ let h=0; for(let i=0;i<s.length;i++){ h=(h*31+s.charCodeAt(i))|0; } return Math.abs(h); }
const _MS_DAY = 86400000;

// Full lifetime insight for a menu item, derived deterministically from its id.
function itemInsights(it){
  const h = strHash(it.id);
  const daysLive = it.isNew ? 6 + (h%10) : 90 + (h%560);          // ~3mo–~2yr
  const added = new Date(Date.now() - daysLive*_MS_DAY);
  const weeks = Math.max(2, Math.round(daysLive/7));
  const ordersPerWeek = Math.round((it.pop/100)*70 + 6 + (h%12));
  const lifeOrders = it.isNew ? (h%30)+4 : ordersPerWeek*weeks;
  const lifeRev = Math.round(lifeOrders*it.price);
  const attach = 28 + (h%34);                                     // % ordered with something else
  const reorder = 24 + (h%38);                                    // % from repeat customers
  const rating = +(4.1 + ((h%9)/10)).toFixed(1);
  const refundPct = +(((h%14)/10)).toFixed(1);
  // weekly orders series since launch (trend-aware), capped to last 12 pts
  const pts = Math.min(12, weeks);
  const dirMul = it.trendDir==='up'?1: it.trendDir==='down'?-1:0;
  const series = Array.from({length:pts}, (_,i)=>{
    const t = i/(pts-1||1);
    const base = ordersPerWeek*(0.7 + 0.3*t*(dirMul>0?1:0.6));
    const wobble = ((strHash(it.id+i)%24)-12)/100;
    const decay = dirMul<0 ? (1-0.35*t) : 1;
    return Math.max(2, Math.round(base*decay*(1+wobble)));
  });
  // top estates for this item (share of the item's orders)
  const ranked = [...ESTATES]
    .map(e=>({ e, w: e.interest*(0.6+((strHash(it.id+e.id)%50)/100)) }))
    .sort((a,b)=>b.w-a.w).slice(0,5);
  const wsum = ranked.reduce((s,r)=>s+r.w,0);
  const topEstates = ranked.map(r=>({ name:r.e.name, id:r.e.id, share:Math.round(r.w/wsum*100), orders:Math.round(r.w/wsum*lifeOrders) }));
  return { added, daysLive, lifeOrders, lifeRev, ordersPerWeek, attach, reorder, rating, refundPct, series, topEstates,
    avgWeek:Math.round(series.reduce((a,b)=>a+b,0)/series.length) };
}

// Flat list of every menu item with its category (for lookups)
function allMenuItems(){ return MENU.flatMap(c=>c.items.map(it=>({ ...it, cat:c.cat }))); }

// Top-selling items for a given estate (ranked), derived from cuisine pull.
function estateTopItems(estate, n=6){
  const items = allMenuItems();
  const scored = items.map(it=>{
    const h = strHash(it.id+estate.id);
    const score = it.pop*0.7 + (h%60) + (it.best?40:0) + (it.margin==='high'?10:0);
    return { it, score };
  }).sort((a,b)=>b.score-a.score).slice(0,n);
  const ssum = scored.reduce((s,r)=>s+r.score,0);
  const estOrders = Math.round(estate.customers*0.6);
  return scored.map((r,i)=>({
    rank:i+1, item:r.it,
    share: Math.round(r.score/ssum*100),
    orders: Math.round(r.score/ssum*estOrders),
    aov: +( (estate.aov[0]+estate.aov[1])/2 ).toFixed(2),
  }));
}

// Saved campaign templates (reusable settings, not running)
const CAMPAIGN_TEMPLATES = [
  { id:'t1', name:'First-order 30% off', typeId:'firstorder', estateId:'bletchley', budget:160, duration:7, mechanic:'percent', val:30, minOrder:15, newOnly:true, copy:{}, lastUsed:'Used in Bletchley' },
  { id:'t2', name:'Weekend family bundle', typeId:'family', estateId:'central', budget:240, duration:7, mechanic:'bundle', val:24, minOrder:0, newOnly:false, copy:{}, lastUsed:'Used in Central MK' },
];

// Real Milton Keynes coordinates [lat, lng] for the Leaflet basemap
const MK_CENTER = [52.035, -0.755];
const ESTATE_LL = {
  olney:[52.150,-0.700], stony:[52.060,-0.853], wolverton:[52.064,-0.812], newport:[52.087,-0.722],
  linford:[52.060,-0.762], bradwell:[52.046,-0.800], central:[52.040,-0.760], shenley:[52.028,-0.810],
  fishermead:[52.036,-0.742], monkston:[52.028,-0.700], tattenhoe:[52.000,-0.812], emerson:[52.006,-0.790],
  westcroft:[52.004,-0.824], walnut:[52.022,-0.712], kingston:[52.026,-0.690], bletchley:[51.995,-0.736],
  woburn:[52.015,-0.650],
};
const HQ_LL = [52.009,-0.733];
const RIVAL_LL = { rv1:[52.043,-0.755], rv2:[52.075,-0.720], rv3:[52.030,-0.685], rv4:[52.034,-0.745], rv5:[51.998,-0.745], rv6:[52.020,-0.800] };

Object.assign(window, {
  VENDOR, NODE_STATE, ESTATES, HQ, RIVALS, KPIS, MENU, MENU_RECS, CAMPAIGN_TYPES,
  MISSIONS, BADGES, TERRITORY_TIERS, LEADERBOARD, ACTIVE_CAMPAIGNS,
  ORDERS, SALES_HOURLY, SALES_DAILY, SALES_MONTHLY, TOP_SELLERS, ORDER_MIX, PAYOUTS, SALES_SUMMARY,
  PROFIT, OPS_METRICS, DELIVERY, REVIEWS, PERFORMANCE_RANK, CAMPAIGN_TEMPLATES,
  CUST_MIX, CUST_TOTAL, REPEAT_TREND, REORDER_WINDOW, BASKET_DIST, CUISINE_AFFINITY,
  DAYPARTS, ORDER_HEAT, VALUE_BANDS,
  influenceLevel, estimateFor, itemInsights, allMenuItems, estateTopItems, strHash, campaignOps,
  MK_CENTER, ESTATE_LL, HQ_LL, RIVAL_LL,
});


