// MaxAMove App — Main React Application

const { useState, useEffect } = React;

// Sidebar restructure: 5 essentials always visible up top under "Daily",
// everything else lives under a collapsible "More" group. Settings stays bottom.
// Nothing removed — every nav item still here, just re-grouped for daily speed.
const NAV_GROUPS = [
  { id:'daily', label:'Daily', icon:'bolt', items:[
    {id:'dashboard',label:'Dashboard',icon:'dashboard'},
    {id:'leads',label:'Leads',icon:'leads-target',badge:'newLeads'},
    {id:'jobs',label:'Jobs',icon:'truck',badge:'activeJobs'},
    {id:'onjob',label:'On Job',icon:'mobile',badge:'onJobCount'},
    {id:'estimates',label:'Estimates',icon:'clipboard'}]},
  { id:'more', label:'More', icon:'menu', items:[
    {id:'invoices',label:'Invoices',icon:'receipt'},
    {id:'bol',label:'Bill of Lading',icon:'file-text'},
    {id:'customers',label:'Customers',icon:'users'},
    {id:'crew',label:'Crew & Fleet',icon:'crew'},
    {id:'financials',label:'P&L & Profit',icon:'dollar-bill'},
    {id:'reports',label:'Reports',icon:'chart-bar'}]},
  { id:'admin', label:'Admin', icon:'gear', items:[
    {id:'settings',label:'Settings',icon:'gear'}]},
];

const ALL_ITEMS = NAV_GROUPS.flatMap(g => g.items);
const groupOf = v => NAV_GROUPS.find(g => g.items.find(i => i.id === v))?.id || 'overview';

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{"companyName":"MaxAMove LLC","tagline":"Maximizing your move, minimizing the stress"}/*EDITMODE-END*/;

function mkEstState() {
  return {
    step:1, status:'draft',
    // Empty until first save — saveEstimate() calls generateEstimateNumber() which
    // produces the proper auto-incremented MM-YYYY-NNNN number from the database.
    estimateNum:'',
    customer:{firstName:'',lastName:'',email:'',phone:'',currentAddress:'',contactPref:'email',source:'',notes:''},
    move:{moveDate:'',moveSize:'',fromAddress:'',fromFloor:'ground',fromElevator:'yes',toAddress:'',toFloor:'ground',toElevator:'yes',distance:'',packing:'none',specialInstructions:''},
    inventory:[],
    pricing:{
      movers:2,hours:4,hourlyRate:125,
      // Service type: 'full_service' (with truck + crew) or 'labor_only' (just hands).
      // The pricing wizard reads from a different tier table per type and the customer-facing
      // estimate displays "Service: Labor Only Move" / "Full Service Local Move" accordingly.
      serviceType:'full_service',
      // All-inclusive pricing — these line items are kept in the data shape for legacy
      // estimates but default to 0/empty for new ones so they don't add phantom charges.
      truckSize:'medium',truckFee:0,fuelRate:0,
      insuranceType:'none',insuranceFee:'',insuranceValue:'',insurancePct:0,
      packingFee:'',discount:'',discountType:'flat',internalNotes:'',
      // Trip Fee — first-class line item, always shown and editable per estimate.
      // Defaults to $50 (also configurable in Settings → Pricing). Click "Waive" to zero it out
      // for a customer when it's a deal-breaker.
      tripFee:50,
      tripFeeWaived:false,
      // Not-to-Exceed price ceiling — defaults ON, defaults to estimated hours + 1.
      // Customer sees this as a price-cap guarantee. nteHours: null = auto-track (hours+1);
      // type a number to lock a manual NTE value. Toggle nteEnabled OFF to hide entirely.
      nteEnabled:true,
      nteHours:null,
      // Specific crew members assigned to this estimate (array of crew_member.id strings).
      // When set, the estimator uses sum of each member's effective lbs/hr instead of the generic.
      crewIds: [],
      // Add-on surcharges selected for this estimate (each: {id, name, qty, price, type, unit})
      surcharges: [],
      // Estimate accuracy tracking — Phase 1 (capture only; analytics dashboard later).
      // systemEstimateHours: snapshot of what the algorithm suggested (frozen on first calc; never changes).
      // hoursOffset: your manual delta from the system estimate (preserved when inventory changes).
      // actualBillableHours: filled in by on-job clock when the job completes.
      systemEstimateHours: null,
      hoursOffset: null,
      actualBillableHours: null,
      // ── Deposit settings (per-estimate override) ──
      // Default OFF until Stripe is wired up — the portal's card form is not a real
      // charge yet, so collecting deposits would mislead customers. Flip default back
      // to true after the Stripe integration ships.
      depositRequired: false,          // owner can flip on per-estimate
      depositMode: 'pct',              // 'pct' or 'flat'
      depositValue: 20,                // 20% or $20 depending on mode (will pull from settings on first load)
      // ── Tentative reservation toggle ──
      // When true, customer portal shows TWO buttons: "Reserve Tentatively" (no deposit, lead → tentative)
      // and "Confirm & Book" (full booking with deposit, lead → booked + job auto-created).
      // When false (default), only the standard Confirm & Book button shows.
      allowTentative: false,
      // Set on accept; tracks WHICH path the customer took: 'guaranteed' or 'tentative'.
      reservationType: null,
    },
  };
}

// Quick-search modal — Cmd+K (Mac) or Ctrl+K (Win) opens it. Type to live-search
// across leads, customers, and estimates. Click any result to jump straight there.
// Eliminates "where did I save that?" hunting.
function QuickSearch({ open, onClose, onNavigate }) {
  const [q, setQ] = useState('');
  const [results, setResults] = useState({ leads: [], customers: [], estimates: [] });
  const [loading, setLoading] = useState(false);
  const inputRef = React.useRef(null);

  useEffect(() => {
    if (open) { setTimeout(() => inputRef.current?.focus(), 50); setQ(''); }
  }, [open]);

  useEffect(() => {
    if (!open || !q.trim() || !window.sb) { setResults({ leads: [], customers: [], estimates: [] }); return; }
    const term = q.trim();
    const pattern = `%${term}%`;
    setLoading(true);
    Promise.all([
      window.sb.from('leads').select('id, name, phone, email, stage').or(`name.ilike.${pattern},phone.ilike.${pattern},email.ilike.${pattern}`).limit(5),
      window.sb.from('customers').select('id, name, phone, email').or(`name.ilike.${pattern},phone.ilike.${pattern},email.ilike.${pattern}`).limit(5),
      window.sb.from('estimates').select('id, number, status, customers(name)').or(`number.ilike.${pattern}`).limit(5),
    ]).then(([l, c, e]) => {
      setResults({ leads: l.data || [], customers: c.data || [], estimates: e.data || [] });
      setLoading(false);
    }).catch(() => setLoading(false));
  }, [q, open]);

  if (!open) return null;
  const totalCount = results.leads.length + results.customers.length + results.estimates.length;
  const go = (view, id) => {
    if (id && view === 'estimates') { try { localStorage.setItem('mam_estimate_to_load', id); } catch {} }
    onNavigate(view); onClose();
  };

  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(15,23,42,.5)', zIndex: 9998, display: 'flex', justifyContent: 'center', alignItems: 'flex-start', paddingTop: '12vh' }}>
      <div onClick={e => e.stopPropagation()} style={{ width: '92%', maxWidth: 560, background: 'var(--card)', borderRadius: 14, boxShadow: '0 20px 60px rgba(15,23,42,.25)', overflow: 'hidden' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '16px 20px', borderBottom: '1px solid var(--border)' }}>
          <Icon name="search" size={18} color="var(--muted)"/>
          <input ref={inputRef} value={q} onChange={e => setQ(e.target.value)}
            onKeyDown={e => { if (e.key === 'Escape') onClose(); }}
            placeholder="Search leads, customers, estimates…"
            style={{ flex: 1, border: 'none', outline: 'none', fontSize: 16, fontFamily: 'inherit', background: 'transparent', color: 'var(--text)' }}/>
          <span style={{ fontSize: 10, color: 'var(--muted)', fontWeight: 700, padding: '3px 7px', background: 'var(--bg)', borderRadius: 5, border: '1px solid var(--border)' }}>ESC</span>
        </div>
        <div style={{ maxHeight: '60vh', overflowY: 'auto', padding: q ? 8 : 0 }}>
          {!q && (
            <div style={{ padding: 28, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>Type to search across leads, customers, and estimates.</div>
          )}
          {q && loading && <div style={{ padding: 16, color: 'var(--muted)', fontSize: 13 }}>Searching…</div>}
          {q && !loading && totalCount === 0 && <div style={{ padding: 24, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>No matches. Try a name, phone, email, or estimate #.</div>}
          {results.leads.length > 0 && (
            <div>
              <div style={{ padding: '8px 12px', fontSize: 10, fontWeight: 800, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.08em' }}>Leads</div>
              {results.leads.map(l => (
                <button key={l.id} onClick={() => go('leads')} style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '10px 12px', borderRadius: 8, background: 'transparent', border: 'none', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit' }}
                  onMouseEnter={e => e.currentTarget.style.background = 'var(--bg)'}
                  onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                  <Icon name="leads-target" size={16} color="var(--accent)"/>
                  <div style={{ flex: 1 }}><div style={{ fontWeight: 700, fontSize: 13 }}>{l.name}</div><div style={{ fontSize: 11, color: 'var(--muted)' }}>{l.phone || l.email || ''} · {l.stage}</div></div>
                </button>
              ))}
            </div>
          )}
          {results.customers.length > 0 && (
            <div>
              <div style={{ padding: '8px 12px', fontSize: 10, fontWeight: 800, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.08em' }}>Customers</div>
              {results.customers.map(c => (
                <button key={c.id} onClick={() => go('customers')} style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '10px 12px', borderRadius: 8, background: 'transparent', border: 'none', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit' }}
                  onMouseEnter={e => e.currentTarget.style.background = 'var(--bg)'}
                  onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
                  <Icon name="user" size={16} color="var(--accent)"/>
                  <div style={{ flex: 1 }}><div style={{ fontWeight: 700, fontSize: 13 }}>{c.name}</div><div style={{ fontSize: 11, color: 'var(--muted)' }}>{c.phone || c.email || ''}</div></div>
                </button>
              ))}
            </div>
          )}
          {results.estimates.length > 0 && (
            <div>
              <div style={{ padding: '8px 12px', fontSize: 10, fontWeight: 800, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.08em' }}>Estimates</div>
              {results.estimates.map(e => (
                <button key={e.id} onClick={() => go('estimates', e.id)} style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '10px 12px', borderRadius: 8, background: 'transparent', border: 'none', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit' }}
                  onMouseEnter={ev => ev.currentTarget.style.background = 'var(--bg)'}
                  onMouseLeave={ev => ev.currentTarget.style.background = 'transparent'}>
                  <Icon name="clipboard" size={16} color="var(--accent)"/>
                  <div style={{ flex: 1 }}><div style={{ fontWeight: 700, fontSize: 13 }}>#{e.number}</div><div style={{ fontSize: 11, color: 'var(--muted)' }}>{e.customers?.name || 'Customer'} · {e.status}</div></div>
                </button>
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function EstimatesModule() {
  // Initial state — pull from a stashed lead if present (sync), else blank.
  // The "load existing estimate by ID" path runs async in a useEffect below.
  const initialState = (() => {
    try {
      const raw = localStorage.getItem('mam_lead_for_estimate');
      if (raw) {
        const lead = JSON.parse(raw);
        localStorage.removeItem('mam_lead_for_estimate');
        const blank = mkEstState();
        return {
          ...blank,
          customer: { ...blank.customer,
            firstName: lead.firstName || '',
            lastName: lead.lastName || '',
            email: lead.email || '',
            phone: lead.phone || '',
            source: lead.source || '',
            notes: lead.notes || '',
          },
          move: { ...blank.move,
            moveDate: lead.moveDate || '',
            fromAddress: lead.fromAddress || '',
            toAddress: lead.toAddress || '',
            moveSize: lead.size || '',
          },
          _leadId: lead.leadId || null,
        };
      }
    } catch (e) {}
    return mkEstState();
  })();

  const [state, setState] = useState(initialState);
  const [estimateId, setEstimateId] = useState(null);

  // Load-existing-estimate path: when the leads drawer or estimates list opens an
  // existing estimate, it stashes the UUID in localStorage. We pull it on mount,
  // fetch the full row from Supabase, and hydrate state. Setting estimateId means
  // the auto-save will treat further edits as UPDATES (no dupes).
  useEffect(() => {
    const loadId = localStorage.getItem('mam_estimate_to_load');
    if (!loadId || !window.loadEstimate) return;
    localStorage.removeItem('mam_estimate_to_load');
    window.loadEstimate(loadId).then(loaded => {
      if (!loaded) return;
      setState({
        step: 1,
        status: loaded.status || 'draft',
        estimateNum: loaded.estimateNum || '',
        customer: loaded.customer || {},
        move: loaded.move || {},
        inventory: loaded.inventory || [],
        pricing: loaded.pricing || {},
        _leadId: loaded.leadId || null,
      });
      setEstimateId(loaded.id);
    }).catch(err => console.warn('[load estimate] failed', err));
  }, []);
  const [savedAt, setSavedAt] = useState(null);
  const [saveStatus, setSaveStatus] = useState('idle'); // idle | saving | saved | error
  const [saveError, setSaveError] = useState('');
  const [savedEstimates, setSavedEstimates] = useState([]);
  const [showList, setShowList] = useState(false);
  const [trucks, setTrucks] = useState([]);
  const [linkCopied, setLinkCopied] = useState(false);

  // Load saved estimates list + trucks + settings (for default deposit) on mount
  useEffect(() => {
    if (window.listEstimates) {
      window.listEstimates().then(list => setSavedEstimates(list)).catch(() => {});
    }
    if (window.listTrucks) {
      window.listTrucks().then(list => setTrucks(list)).catch(() => {});
    }
    // Pull defaults (deposit % + trip fee) from settings if user hasn't already overridden
    if (window.loadSettings) {
      window.loadSettings().then(s => {
        setState(prev => {
          let nextPricing = prev.pricing;
          // Seed deposit % only if it still matches the un-touched default
          if (nextPricing && nextPricing.depositValue === 20 && nextPricing.depositMode === 'pct') {
            nextPricing = { ...nextPricing, depositValue: Number(s.depositPct) || 20 };
          }
          // Seed trip fee only if it still matches the un-touched default of 50
          if (nextPricing && nextPricing.tripFee === 50 && s.defaultTripFee != null) {
            nextPricing = { ...nextPricing, tripFee: Number(s.defaultTripFee) };
          }
          return nextPricing === prev.pricing ? prev : { ...prev, pricing: nextPricing };
        });
      }).catch(() => {});
    }
  }, []);

  async function copyEstimateLink() {
    if (!estimateId) {
      alert('Estimate must be saved first. Fill in customer info — it auto-saves.');
      return;
    }
    const origin = (typeof window !== 'undefined' && window.location && window.location.origin) || '';
    const url = `${origin}/portal.html?est=${estimateId}`;
    try {
      await navigator.clipboard.writeText(url);
      setLinkCopied(true);
      setTimeout(() => setLinkCopied(false), 2500);
    } catch (e) {
      // Fallback for older browsers / non-HTTPS contexts
      window.prompt('Copy this estimate link and paste wherever you want to send it:', url);
    }
  }

  // Debounced auto-save to Supabase.
  // CRITICAL: For the FIRST save, require BOTH a name AND contact info (email or phone).
  //   Without contact info, getOrCreateCustomer can't dedupe → creates a NEW customer
  //   on every keystroke. Combined with the auto-create-lead logic, typing a name with
  //   no email caused 27 orphan customers + 27 orphan leads to be created in 5 minutes.
  // Once an estimateId exists, subsequent saves are updates (no dupes), so the gate relaxes.
  useEffect(() => {
    if (!window.saveEstimate) return;
    const cust = state.customer || {};
    const hasName = ((cust.firstName || '').trim() || (cust.lastName || '').trim());
    const hasContact = ((cust.email || '').trim() || (cust.phone || '').trim());
    if (estimateId) {
      // Already saved → updates are safe; fire on any change with at least a name
      if (!hasName) return;
    } else {
      // First save → require name AND contact info to prevent orphan dupes
      if (!hasName || !hasContact) return;
    }

    const timer = setTimeout(async () => {
      try {
        setSaveStatus('saving');
        const saved = await window.saveEstimate(state, { id: estimateId, leadId: state._leadId });
        setEstimateId(saved.id);
        if (saved.estimateNum && saved.estimateNum !== state.estimateNum) {
          setState(s => ({ ...s, estimateNum: saved.estimateNum }));
        }
        setSavedAt(new Date());
        setSaveStatus('saved');
        if (window.listEstimates) {
          window.listEstimates().then(list => setSavedEstimates(list)).catch(() => {});
        }
      } catch (e) {
        setSaveStatus('error');
        setSaveError(e.message);
      }
    }, 1500);
    return () => clearTimeout(timer);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [state]);

  const u = (f, v) => setState(s => ({ ...s, [f]: v }));
  const go = step => setState(s => ({ ...s, step }));
  const tot = (() => { try { return '$' + calcTotals(state.pricing, state.move).total.toFixed(2) } catch { return '$0.00' } })();
  const STEPS = [
    {id:1,label:'Customer',icon:'user'},{id:2,label:'Move',icon:'truck'},
    {id:3,label:'Inventory',icon:'box'},{id:4,label:'Pricing',icon:'dollar-bill'},
    {id:5,label:'Estimate',icon:'clipboard'},{id:6,label:'Invoice',icon:'receipt'}
  ];

  function startNew() {
    if (estimateId && !confirm('Start a fresh estimate? Your current one is saved.')) return;
    setEstimateId(null);
    setState(mkEstState());
    setSavedAt(null); setSaveStatus('idle');
  }

  async function openEstimate(id) {
    try {
      const est = await window.loadEstimate(id);
      setEstimateId(est.id);
      setState({
        step: 1,
        status: est.status,
        estimateNum: est.estimateNum,
        customer: est.customer,
        move: est.move,
        inventory: est.inventory,
        pricing: est.pricing,
        _leadId: est.leadId,
      });
      setShowList(false);
      setSavedAt(new Date(est.updatedAt || est.createdAt));
      setSaveStatus('saved');
    } catch (e) {
      setSaveError(e.message);
    }
  }

  // Capacity calc for Step 3 warning
  const totalCuft = window.calcInventoryCubicFeet ? window.calcInventoryCubicFeet(state.inventory) : 0;
  const totalLbs = window.calcInventoryWeight ? window.calcInventoryWeight(state.inventory) : 0;
  const activeTrucks = trucks.filter(t => t.status !== 'out-of-service' && t.capacityCuft);
  const capacityCheck = window.suggestTruckForCubicFeet
    ? window.suggestTruckForCubicFeet(totalCuft, activeTrucks)
    : { fits: null, exceeds: [], utilization: 0 };

  // Save status indicator text
  const saveAge = savedAt ? Math.floor((Date.now() - savedAt.getTime()) / 1000) : null;
  const saveText = saveStatus === 'saving' ? 'Saving…' :
                   saveStatus === 'saved' ? (saveAge != null ? `Saved ${saveAge < 5 ? 'just now' : saveAge < 60 ? saveAge + 's ago' : Math.floor(saveAge/60) + 'm ago'}` : 'Saved') :
                   saveStatus === 'error' ? (saveError || 'Save error') :
                   estimateId ? '' : 'Draft (not yet saved)';

  return (
    <div style={{display:'flex',flexDirection:'column',height:'100%',minHeight:0}}>
      <div style={{background:'var(--card)',borderBottom:'1px solid var(--border)',padding:'12px 20px',display:'flex',alignItems:'center',justifyContent:'space-between',flexShrink:0,gap:12,flexWrap:'wrap'}}>
        <div style={{display:'flex',gap:6,flexWrap:'wrap',alignItems:'center'}}>
          {STEPS.map(s=>{
            const active=state.step===s.id,done=state.step>s.id;
            return <button key={s.id} onClick={()=>go(s.id)} style={{padding:'6px 13px',borderRadius:20,border:'none',background:active?'var(--accent)':done?'var(--accent-light)':'var(--bg)',color:active?'#fff':done?'var(--accent-dark)':'var(--muted)',fontWeight:active?800:600,fontSize:12,cursor:'pointer',display:'flex',alignItems:'center',gap:5}}>
              {done&&!active ? <Icon name="check" size={14}/> : <Icon name={s.icon} size={14}/>}<span>{s.label}</span>
            </button>;
          })}
        </div>
        <div style={{display:'flex',gap:8,alignItems:'center',flexWrap:'wrap'}}>
          <span style={{fontSize:11,color:saveStatus==='error'?'#b91c1c':saveStatus==='saving'?'#0ea5e9':'var(--muted)',fontWeight:600,display:'inline-flex',alignItems:'center',gap:5}}>
            {saveStatus==='saving' && <Icon name="save" size={12}/>}
            {saveStatus==='saved' && <Icon name="check" size={12}/>}
            {saveStatus==='error' && <Icon name="alert" size={12}/>}
            {saveText}
          </span>
          <span style={{fontWeight:900,fontSize:16,background:'linear-gradient(135deg,#14C6BF,#3DCC7E)',WebkitBackgroundClip:'text',WebkitTextFillColor:'transparent',fontFamily:'Poppins'}}>{tot}</span>
          <StatusBadge status={state.status}/>
          {state.step>1&&<button onClick={()=>go(state.step-1)} style={btnStyle('secondary')}>← Back</button>}
          {state.step<6&&<button onClick={()=>go(state.step+1)} style={{...btnStyle('primary'),background:'var(--accent)'}}>Continue →</button>}
          <button onClick={copyEstimateLink} disabled={!estimateId}
            title={!estimateId ? 'Auto-saves once you fill in customer info' : 'Copy a shareable link to send the customer'}
            style={{...btnStyle('secondary'), fontSize:11,
              background: linkCopied ? 'var(--accent-light)' : (estimateId ? 'linear-gradient(135deg,#14C6BF,#3DCC7E)' : 'var(--bg)'),
              color: linkCopied ? 'var(--accent-dark)' : (estimateId ? '#fff' : 'var(--muted)'),
              border: linkCopied ? '1.5px solid var(--accent)' : 'none',
              cursor: !estimateId ? 'not-allowed' : 'pointer',
              fontWeight: 700,
            }}>
            <span style={{display:'inline-flex',alignItems:'center',gap:5}}>
              {linkCopied ? <><Icon name="check" size={12}/> Link copied!</> : <><Icon name="paperclip" size={12}/> Copy link</>}
            </span>
          </button>
          <button onClick={()=>setShowList(true)} style={{...btnStyle('secondary'),fontSize:11,display:'inline-flex',alignItems:'center',gap:5}}><Icon name="folder" size={12}/> Saved ({savedEstimates.length})</button>
          <button onClick={startNew} style={{...btnStyle('secondary'),fontSize:11}}>+ New</button>
        </div>
      </div>
      <div style={{flex:1,overflowY:'auto',padding:'24px 28px'}} className="pg">
        <div style={{maxWidth:860,margin:'0 auto'}}>
          {state.step===1&&<StepCustomer data={state.customer} onChange={v=>u('customer',v)}/>}
          {state.step===2&&<StepMove data={state.move} onChange={v=>u('move',v)} pricing={state.pricing} onPricingChange={v=>u('pricing',v)}/>}
          {state.step===3&&(
            <>
              <CapacityBanner totalCuft={totalCuft} totalLbs={totalLbs} check={capacityCheck} hasTrucks={activeTrucks.length > 0}/>
              <StepInventory items={state.inventory} onChange={v=>u('inventory',v)} moveData={state.move}/>
            </>
          )}
          {state.step===4&&<StepPricing data={state.pricing} onChange={v=>u('pricing',v)} moveData={state.move} inventory={state.inventory}/>}
          {state.step===5&&<EstimatePreview state={state} onStatusChange={v=>u('status',v)}/>}
          {state.step===6&&<InvoiceView state={state} onStatusChange={v=>u('status',v)}/>}
        </div>
      </div>

      {/* Saved estimates picker modal */}
      {showList && (
        <div onClick={()=>setShowList(false)} style={{position:'fixed',inset:0,background:'rgba(0,0,0,.5)',zIndex:200,display:'flex',alignItems:'center',justifyContent:'center',padding:20}}>
          <div onClick={e=>e.stopPropagation()} style={{background:'#fff',borderRadius:16,padding:24,maxWidth:640,width:'100%',maxHeight:'85vh',overflowY:'auto'}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}>
              <h2 style={{fontSize:18,fontWeight:900,fontFamily:'Poppins'}}>Saved estimates</h2>
              <button onClick={()=>setShowList(false)} style={{background:'none',border:'none',fontSize:22,cursor:'pointer',color:'var(--muted)'}}>×</button>
            </div>
            {savedEstimates.length === 0 ? <div style={{color:'var(--muted)',fontSize:13}}>No saved estimates yet. Start filling in customer info — it will auto-save.</div>
              : <div style={{display:'flex',flexDirection:'column',gap:6}}>
                  {savedEstimates.map(e => {
                    const fullName = `${e.customer?.firstName||''} ${e.customer?.lastName||''}`.trim() || '—';
                    const total = (() => { try { return calcTotals(e.pricing, e.move).total; } catch { return 0; } })();
                    return (
                      <button key={e.id} onClick={()=>openEstimate(e.id)} style={{textAlign:'left',padding:12,borderRadius:9,border:'1px solid var(--border)',background:e.id===estimateId?'var(--accent-light)':'var(--card)',cursor:'pointer',fontFamily:'inherit',width:'100%'}}>
                        <div style={{display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:10}}>
                          <div>
                            <div style={{fontWeight:800,fontSize:13}}>{e.estimateNum} <span style={{color:'var(--muted)',fontWeight:500,fontSize:12}}>· {fullName}</span></div>
                            <div style={{fontSize:11,color:'var(--muted)',marginTop:2}}>{e.move?.fromAddress||'—'} → {e.move?.toAddress||'—'} · {e.move?.moveDate ? fmtShortDate(e.move.moveDate) : 'no date'}</div>
                          </div>
                          <div style={{textAlign:'right'}}>
                            <div style={{fontWeight:800,color:'var(--accent)',fontSize:13}}>${total.toFixed(0)}</div>
                            <div style={{fontSize:10,fontWeight:700,textTransform:'uppercase',color:'var(--muted)'}}>{e.status}</div>
                          </div>
                        </div>
                      </button>
                    );
                  })}
                </div>
            }
          </div>
        </div>
      )}
    </div>
  );
}

function CapacityBanner({totalCuft, totalLbs, check, hasTrucks}) {
  if (totalCuft <= 0) return null;
  const overCapacity = check.fits == null && check.exceeds.length > 0;
  const fits = check.fits;
  const utilization = check.utilization || 0;
  const utilColor = utilization > 0.95 ? '#dc2626' : utilization > 0.85 ? '#f59e0b' : '#16a34a';

  if (!hasTrucks) {
    return (
      <div style={{padding:14,marginBottom:18,background:'#eff6ff',border:'1.5px solid #bfdbfe',borderRadius:12,fontSize:13,color:'#1e40af',display:'flex',alignItems:'center',gap:8,flexWrap:'wrap'}}>
        <Icon name="box" size={16}/> <span><b>{totalCuft.toFixed(0)} cu ft</b> · <b>{totalLbs.toLocaleString()} lbs</b> total inventory.</span>
        <span style={{marginLeft:8,opacity:.8}}>Add trucks in <b>Settings → Trucks</b> to see capacity warnings.</span>
      </div>
    );
  }

  if (overCapacity) {
    const biggest = check.exceeds[check.exceeds.length-1];
    return (
      <div style={{padding:14,marginBottom:18,background:'#fef2f2',border:'1.5px solid #fecaca',borderRadius:12,fontSize:13,color:'#991b1b',display:'flex',alignItems:'flex-start',gap:8}}>
        <Icon name="alert" size={16} style={{flexShrink:0,marginTop:2}}/>
        <div>
          <div><b>{totalCuft.toFixed(0)} cu ft</b> exceeds the largest available truck ({biggest?.name || '?'} — {biggest?.capacityCuft?.toLocaleString() || '?'} cu ft).
          Consider splitting the move across two trips, or add a larger truck in <b>Settings → Trucks</b>.</div>
          <div style={{fontSize:12,color:'#78350f',marginTop:4}}>Total weight: {totalLbs.toLocaleString()} lbs.</div>
        </div>
      </div>
    );
  }

  if (fits) {
    return (
      <div style={{padding:14,marginBottom:18,background:utilization > 0.85 ? '#fff7ed' : '#f0fdf4',border:`1.5px solid ${utilization > 0.85 ? '#fed7aa' : '#bbf7d0'}`,borderRadius:12,fontSize:13,color:utilization > 0.85 ? '#9a3412' : '#166534'}}>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',gap:10,flexWrap:'wrap'}}>
          <div style={{display:'flex',alignItems:'center',gap:8}}>
            <Icon name="box" size={16}/>
            <span><b>{totalCuft.toFixed(0)} cu ft</b> · {totalLbs.toLocaleString()} lbs · fits in <b>{fits.name}</b> ({fits.capacityCuft.toLocaleString()} cu ft).</span>
          </div>
          <div style={{fontSize:12,fontWeight:700,color:utilColor}}>{Math.round(utilization*100)}% full</div>
        </div>
        <div style={{height:6,marginTop:8,background:'rgba(0,0,0,.06)',borderRadius:3,overflow:'hidden'}}>
          <div style={{height:'100%',width:Math.min(100,utilization*100)+'%',background:utilColor,transition:'width .3s'}}/>
        </div>
      </div>
    );
  }
  return null;
}

function MobileSubDrawer({group,currentView,onNav,onClose}) {
  if(!group)return null;
  return (
    <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:120,background:'rgba(0,0,0,.5)'}}>
      <div onClick={e=>e.stopPropagation()} style={{position:'absolute',bottom:'calc(66px + env(safe-area-inset-bottom,0px))',left:0,right:0,background:'var(--sidebar)',borderRadius:'20px 20px 0 0',padding:'16px 12px 20px'}}>
        <div style={{width:36,height:4,background:'rgba(255,255,255,.2)',borderRadius:2,margin:'0 auto 16px'}}/>
        <div style={{fontSize:10,fontWeight:800,color:'rgba(255,255,255,.4)',textTransform:'uppercase',letterSpacing:'.1em',marginBottom:10,paddingLeft:10,display:'flex',alignItems:'center',gap:6}}><Icon name={group.icon} size={12}/> {group.label}</div>
        {group.items.map(item=>(
          <button key={item.id} onClick={()=>{onNav(item.id);onClose();}}
            style={{width:'100%',display:'flex',alignItems:'center',gap:12,padding:'12px 14px',borderRadius:10,border:'none',background:currentView===item.id?'rgba(20,198,191,.2)':'rgba(255,255,255,.05)',color:currentView===item.id?'#3DCC7E':'rgba(255,255,255,.8)',cursor:'pointer',fontSize:14,fontWeight:currentView===item.id?700:500,marginBottom:4}}>
            <Icon name={item.icon} size={20}/>{item.label}
          </button>
        ))}
      </div>
    </div>
  );
}

function UserBadge({me, open, onToggle, onClose, onNavigate}) {
  const displayName = me?.fullName || me?.email?.split('@')[0] || 'Loading…';
  const initials = (me?.fullName || me?.email || '?').split(/\s+/).filter(Boolean).slice(0,2).map(s=>s[0]).join('').toUpperCase();
  const roleLabel = {owner:'Owner', staff:'Staff', crew:'Crew', customer:'Customer', viewer:'Viewer'}[me?.role] || '';
  const roleColor = {owner:'#0a9e98', staff:'#0ea5e9', crew:'#f97316', customer:'#22c55e', viewer:'#64748b'}[me?.role] || '#64748b';

  async function handleSignOut() {
    if (!confirm('Sign out of MaxAMove?')) return;
    try { await window.signOut(); } catch(e) { alert('Sign out failed: ' + e.message); }
  }

  return (
    <div style={{position:'relative'}}>
      <button
        onClick={onToggle}
        style={{display:'flex',alignItems:'center',gap:8,padding:'4px 10px 4px 4px',background:open?'var(--accent-light)':'transparent',border:'1px solid var(--border)',borderRadius:20,cursor:'pointer',fontFamily:'inherit'}}
      >
        <div style={{width:28,height:28,borderRadius:'50%',background:'linear-gradient(135deg,#14C6BF,#3DCC7E)',color:'#fff',display:'flex',alignItems:'center',justifyContent:'center',fontWeight:800,fontSize:11,fontFamily:'Poppins',flexShrink:0}}>{initials}</div>
        <div style={{display:'flex',flexDirection:'column',alignItems:'flex-start',lineHeight:1.1}}>
          <span style={{fontSize:12,fontWeight:700,color:'var(--text)'}}>{displayName}</span>
          {roleLabel && <span style={{fontSize:9,fontWeight:700,color:roleColor,letterSpacing:'.05em',textTransform:'uppercase'}}>{roleLabel}</span>}
        </div>
        <span style={{fontSize:9,color:'var(--muted)',marginLeft:2}}>▾</span>
      </button>
      {open && (
        <>
          <div onClick={onClose} style={{position:'fixed',inset:0,zIndex:90}}/>
          <div style={{position:'absolute',top:'calc(100% + 6px)',right:0,background:'#fff',border:'1px solid var(--border)',borderRadius:12,boxShadow:'0 10px 30px rgba(0,0,0,.12)',minWidth:200,zIndex:100,overflow:'hidden'}}>
            <div style={{padding:'12px 14px',borderBottom:'1px solid var(--border)'}}>
              <div style={{fontWeight:700,fontSize:13}}>{displayName}</div>
              <div style={{fontSize:11,color:'var(--muted)',marginTop:2}}>{me?.email}</div>
            </div>
            <button onClick={()=>{onNavigate('settings');onClose();}} style={menuItemStyle}>
              <Icon name="user" size={15}/><span>My profile & settings</span>
            </button>
            <button onClick={handleSignOut} style={{...menuItemStyle,color:'#b91c1c',borderTop:'1px solid var(--border)'}}>
              <Icon name="arrow-right" size={15}/><span>Sign out</span>
            </button>
          </div>
        </>
      )}
    </div>
  );
}
const menuItemStyle = {display:'flex',alignItems:'center',gap:10,width:'100%',padding:'10px 14px',border:'none',background:'transparent',fontSize:13,fontWeight:600,color:'var(--text)',cursor:'pointer',fontFamily:'inherit',textAlign:'left'};

function App() {
  const [view,setView]=useState(()=>localStorage.getItem('app_v5')||'dashboard');
  const [leads,setLeads]=useState([]);
  const [jobs,setJobs]=useState([]);
  const [tweaks,setTweaks]=useState(()=>{try{return {...TWEAK_DEFAULTS,...JSON.parse(localStorage.getItem('app_tweaks')||'{}')};}catch{return TWEAK_DEFAULTS;}});
  const [showTweaks,setShowTweaks]=useState(false);
  const [collapsed,setCollapsed]=useState(false);
  // Default open: 'daily' group (5 essentials) + admin (always visible). 'more' stays
  // collapsed by default — one tap to expand. The currently-active group also auto-opens.
  const [openGroups,setOpenGroups]=useState(()=>{
    const active=groupOf(localStorage.getItem('app_v5')||'dashboard');
    return Array.from(new Set(['daily','admin',active]));
  });
  const [mobileDrawer,setMobileDrawer]=useState(null);
  const [installPrompt,setInstallPrompt]=useState(null);
  const [showInstall,setShowInstall]=useState(false);
  const [me,setMe]=useState(null);
  const [userMenuOpen,setUserMenuOpen]=useState(false);
  // Quick-search modal state — Cmd+K (Mac) / Ctrl+K (Win) opens it. `/` also opens
  // when the user isn't typing in another input. Esc closes.
  const [quickSearchOpen,setQuickSearchOpen]=useState(false);
  useEffect(() => {
    const handler = (e) => {
      const isMod = e.metaKey || e.ctrlKey;
      if (isMod && e.key.toLowerCase() === 'k') { e.preventDefault(); setQuickSearchOpen(true); return; }
      if (e.key === '/' && !['INPUT','TEXTAREA','SELECT'].includes(document.activeElement?.tagName)) { e.preventDefault(); setQuickSearchOpen(true); }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, []);
  const [dataLoading,setDataLoading]=useState(true);

  // Load the signed-in user's profile so we can show name + role in the topbar
  useEffect(()=>{
    if(typeof window.loadMyProfile==='function'){
      window.loadMyProfile().then(p=>setMe(p)).catch(()=>setMe(null));
    }
  },[]);

  // Refresh helpers — called by child views after they make Supabase changes
  async function refreshLeads(){
    try{ const l = await window.listLeads(); setLeads(l); }
    catch(e){ console.warn('[App] refreshLeads', e.message); }
  }
  async function refreshJobs(){
    try{ const j = await window.listJobs(); setJobs(j); }
    catch(e){ console.warn('[App] refreshJobs', e.message); }
  }
  // Initial data load from Supabase
  useEffect(()=>{
    if(!window.listLeads || !window.listJobs) return;
    setDataLoading(true);
    Promise.all([refreshLeads(), refreshJobs()]).finally(()=>setDataLoading(false));
  },[]);

  // Note: leads & jobs no longer written to localStorage — they live in Supabase.
  useEffect(()=>{localStorage.setItem('app_v5',view)},[view]);
  useEffect(()=>{localStorage.setItem('app_tweaks',JSON.stringify(tweaks))},[tweaks]);

  useEffect(()=>{
    const h=e=>{e.preventDefault();setInstallPrompt(e);setShowInstall(true)};
    window.addEventListener('beforeinstallprompt',h);
    return()=>window.removeEventListener('beforeinstallprompt',h);
  },[]);

  useEffect(()=>{
    // Service worker managed by shell
  },[]);

  useEffect(()=>{
    const h=e=>{
      if(e.data?.type==='__activate_edit_mode')setShowTweaks(true);
      if(e.data?.type==='__deactivate_edit_mode')setShowTweaks(false);
    };
    window.addEventListener('message',h);
    window.parent.postMessage({type:'__edit_mode_available'},'*');
    return()=>window.removeEventListener('message',h);
  },[]);

  // ── Customer-portal accept queue drainer (Slice 6) ──
  // Customers sign on portal.html → portal pushes to localStorage `crm_portal_accepts` queue.
  // This effect polls the queue and, for each pending accept, writes the result to Supabase:
  //   1. Update estimate: signed_at, signature_data, status='accepted'
  //   2. Move linked lead → 'booked' (via lead.estimateUuid match or customer match)
  //   3. Auto-create a job from the estimate
  //   4. Mark queue item as processed so we don't re-fire
  // Refreshes leads + jobs after each drain so UI updates.
  const [acceptToast,setAcceptToast]=useState(null);
  useEffect(() => {
    let cancelled = false;
    const drain = async () => {
      let q = [];
      try { q = JSON.parse(localStorage.getItem('crm_portal_accepts') || '[]'); } catch {}
      const pending = q.filter(a => a && !a.processed);
      if (!pending.length) return;
      for (const a of pending) {
        if (cancelled) return;
        try {
          // 1. Find the matching estimate by number (e.g. MM-2026-0717)
          const { data: estRow } = await window.sb
            .from('estimates')
            .select('id, customer_id, lead_id, pricing, move_data')
            .eq('number', a.estimateNum)
            .maybeSingle();
          if (!estRow) {
            console.warn('[accept-drain] No estimate row for', a.estimateNum);
            a.processed = true; continue;
          }
          // 2. Determine reservation type: 'tentative' (soft hold, no deposit, no job)
          //    or 'guaranteed' (full booking, deposit, auto-create job). Default = guaranteed.
          const isTentative = a.reservationType === 'tentative';
          const mergedPricing = {
            ...(estRow.pricing || {}),
            customerSig: a.customerSig,
            customerAcceptedAt: a.acceptedAt,
            reservationType: isTentative ? 'tentative' : 'guaranteed',
            depositPaid: isTentative ? 0 : (a.depositPaid || 0),
            depositMethod: isTentative ? null : (a.depositMethod || null),
            // Persist signature-bypass flag from owner override so the customer portal hides the signature block.
            ...(a.signatureRequired === false ? { signatureRequired: false } : {}),
          };
          await window.sb.from('estimates').update({
            status: 'signed',
            signed_at: a.acceptedAt,
            signature_data: a.customerSig,
            pricing: mergedPricing,
          }).eq('id', estRow.id);
          // 3. Move linked lead to the right stage based on reservation type
          if (estRow.lead_id) {
            await window.sb.from('leads').update({ stage: isTentative ? 'tentative' : 'booked' }).eq('id', estRow.lead_id);
          }
          // 4. Auto-create a job ONLY for guaranteed bookings. Tentative holds wait
          //    for the customer to upgrade (or for Gavin to manually confirm).
          if (!isTentative && typeof window.createJob === 'function' && estRow.customer_id) {
            const { data: existingJob } = await window.sb
              .from('jobs').select('id').eq('estimate_id', estRow.id).maybeSingle();
            if (!existingJob) {
              try {
                await window.createJob({
                  customerId: estRow.customer_id,
                  estimateUuid: estRow.id,
                  scheduledDate: estRow.move_data?.moveDate || new Date().toISOString().slice(0,10),
                  fromAddress: estRow.move_data?.fromAddress,
                  toAddress: estRow.move_data?.toAddress,
                  size: estRow.move_data?.moveSize,
                  // Carry the assigned crew from the estimate onto the job so each
                  // crew member sees the upcoming move in their Crew HQ portal.
                  // Crew lives in pricing.crewIds (the wizard's crew picker).
                  crewIds: Array.isArray(estRow.pricing?.crewIds) ? estRow.pricing.crewIds : [],
                  status: 'pending',
                  value: a.total || (estRow.pricing && estRow.pricing.total) || 0,
                });
              } catch (jobErr) {
                console.warn('[accept-drain] createJob failed:', jobErr);
              }
            }
          }
          a.processed = true;
          if (!cancelled) setAcceptToast(isTentative
            ? `${a.customer?.firstName || 'Customer'} tentatively reserved ${a.estimateNum}`
            : `${a.customer?.firstName || 'Customer'} confirmed booking ${a.estimateNum}`);
        } catch (err) {
          console.warn('[accept-drain] failed for', a.estimateNum, err);
          // leave processed:false so we retry next tick
        }
      }
      try { localStorage.setItem('crm_portal_accepts', JSON.stringify(q)); } catch {}
      // Refresh CRM views
      if (typeof refreshLeads === 'function') refreshLeads().catch(() => {});
      if (typeof refreshJobs === 'function') refreshJobs().catch(() => {});
    };
    drain();
    const iv = setInterval(drain, 4000);
    const onFocus = () => drain();
    window.addEventListener('focus', onFocus);
    return () => { cancelled = true; clearInterval(iv); window.removeEventListener('focus', onFocus); };
  }, []);

  useEffect(()=>{if(acceptToast){const t=setTimeout(()=>setAcceptToast(null),6000);return()=>clearTimeout(t);}},[acceptToast]);

  const updateTweak=(k,v)=>{const n={...tweaks,[k]:v};setTweaks(n);window.parent.postMessage({type:'__edit_mode_set_keys',edits:n},'*')};
  const navigate=v=>{setView(v);const g=groupOf(v);setOpenGroups(prev=>prev.includes(g)?prev:[...prev,g]);setMobileDrawer(null)};
  const toggleGroup=id=>setOpenGroups(prev=>prev.includes(id)?prev.filter(g=>g!==id):[...prev,id]);

  const newLeads=leads.filter(l=>l.stage==='new').length;
  const activeJobs=jobs.filter(j=>j.status!=='completed').length;
  // On-job count: jobs that have been clocked in but not completed
  const onJobCount=(()=>{try{const all=JSON.parse(localStorage.getItem('crm_job_states'))||{};return Object.values(all).filter(s=>s&&s.started&&!s.completed).length;}catch{return 0;}})();
  const badges={newLeads,activeJobs,onJobCount};
  const pipelineVal=jobs.filter(j=>j.status!=='completed').reduce((s,j)=>s+j.value,0);
  const curItem=ALL_ITEMS.find(i=>i.id===view)||{label:'Dashboard',icon:'dashboard'};
  const curGroup=NAV_GROUPS.find(g=>g.items.find(i=>i.id===view));
  const fullscreen=view==='estimates';

  const renderView=()=>{
    switch(view){
      case 'dashboard':return <Dashboard leads={leads} jobs={jobs} onNavigate={navigate}/>;
      case 'leads':return <LeadsPipeline leads={leads} onLeadsChange={refreshLeads} onNavigate={navigate}/>;
      case 'jobs':return <JobsBoard jobs={jobs} onJobsChange={refreshJobs} onNavigate={navigate}/>;
      case 'onjob':return <OnJobView jobs={jobs} onJobsChange={refreshJobs} onNavigate={navigate}/>;
      case 'estimates':return <EstimatesModule/>;
      case 'invoices':return <InvoicesView jobs={jobs}/>;
      case 'bol':return <BOLView jobs={jobs}/>;
      case 'customers':return <CustomersView/>;
      case 'crew':return <CrewView/>;
      case 'financials':return <FinancialsView jobs={jobs}/>;
      case 'reports':return <ReportsView leads={leads} jobs={jobs}/>;
      case 'settings':return <SettingsView/>;
      default:return <Dashboard leads={leads} jobs={jobs} onNavigate={navigate}/>;
    }
  };

  return (
    <div style={{display:'flex',height:'100vh',overflow:'hidden',flexDirection:'column'}}>
      {/* Mobile header */}
      <div className="mobile-hdr">
        <img src="uploads/logo_tight.png" style={{height:32,filter:'brightness(0) invert(1)'}} alt="MaxAMove"/>
        <div style={{flex:1,color:'rgba(255,255,255,.7)',fontSize:12,fontWeight:700,textAlign:'center',display:'flex',alignItems:'center',justifyContent:'center',gap:6}}><Icon name={curItem.icon} size={15}/> {curItem.label}</div>
        <button
          onClick={()=>navigate('settings')}
          title={me?.fullName || 'Settings'}
          style={{width:34,height:34,borderRadius:'50%',background:'linear-gradient(135deg,#14C6BF,#3DCC7E)',color:'#fff',border:'none',display:'flex',alignItems:'center',justifyContent:'center',fontWeight:800,fontSize:11,fontFamily:'Poppins',cursor:'pointer',flexShrink:0}}>
          {(me?.fullName || me?.email || '?').split(/\s+/).filter(Boolean).slice(0,2).map(s=>s[0]).join('').toUpperCase()}
        </button>
      </div>

      <div style={{display:'flex',flex:1,overflow:'hidden'}}>
        {/* Sidebar */}
        <div className="desktop-sidebar" style={{width:collapsed?62:264,background:'var(--sidebar)',display:'flex',flexDirection:'column',flexShrink:0,transition:'width .2s',overflow:'hidden'}}>
          {/* Logo */}
          <div onClick={()=>setCollapsed(c=>!c)} style={{padding:collapsed?'14px 8px':'22px 20px 18px',borderBottom:'1px solid rgba(255,255,255,.07)',display:'flex',alignItems:'center',cursor:'pointer',flexShrink:0,minHeight:collapsed?64:108,justifyContent:collapsed?'center':'flex-start',flexDirection:'column',gap:10}}>
            <img src="uploads/logo_tight.png"
              style={{height:collapsed?36:64,width:collapsed?36:'100%',objectFit:'contain',objectPosition:'left center',filter:'brightness(0) invert(1)',transition:'all .2s',flexShrink:0}}
              alt="MaxAMove"/>
            {!collapsed&&<div style={{width:'100%',height:2,background:'linear-gradient(90deg,#14C6BF,#3DCC7E,transparent)',borderRadius:2,opacity:.6}}/>}
          </div>
          {/* Nav */}
          <nav style={{flex:1,padding:'6px 8px',overflowY:'auto'}}>
            {NAV_GROUPS.map(grp=>{
              const isOpen=openGroups.includes(grp.id)||collapsed;
              const badge=grp.items.reduce((s,i)=>s+(badges[i.badge]||0),0);
              return (
                <div key={grp.id}>
                  <div className={'grp-header'+(isOpen?' open':'')} onClick={()=>toggleGroup(grp.id)} title={collapsed?grp.label:''}>
                    <Icon name={grp.icon} size={14} style={{opacity:.7}}/>
                    <span className="sidebar-text" style={{flex:1}}>{grp.label}</span>
                    {!collapsed&&badge>0&&<span style={{background:'linear-gradient(135deg,#14C6BF,#3DCC7E)',color:'#fff',borderRadius:10,padding:'1px 7px',fontSize:9,fontWeight:800}}>{badge}</span>}
                    {!collapsed&&<span style={{fontSize:9,opacity:.4}}>{isOpen?'▾':'▸'}</span>}
                  </div>
                  {isOpen&&grp.items.map(item=>{
                    const active=view===item.id;
                    const b=badges[item.badge];
                    return (
                      <button key={item.id} className={'nav-item'+(active?' active':'')} onClick={()=>navigate(item.id)} title={collapsed?item.label:''}>
                        <Icon name={item.icon} size={16} color={active?'var(--accent)':'currentColor'}/>
                        <span className="sidebar-text">{item.label}</span>
                        {b>0&&<span className="nav-badge sidebar-text">{b}</span>}
                      </button>
                    );
                  })}
                </div>
              );
            })}
          </nav>
          {/* Footer */}
          <div className="sidebar-text" style={{padding:'12px 16px',borderTop:'1px solid rgba(255,255,255,.07)',flexShrink:0}}>
            <div style={{color:'rgba(255,255,255,.3)',fontSize:9,letterSpacing:'.08em',textTransform:'uppercase',marginBottom:3}}>Pipeline Value</div>
            <div style={{background:'linear-gradient(135deg,#14C6BF,#3DCC7E)',WebkitBackgroundClip:'text',WebkitTextFillColor:'transparent',fontWeight:900,fontSize:19,fontFamily:'Poppins'}}>${pipelineVal.toLocaleString()}</div>
            <div style={{color:'rgba(255,255,255,.28)',fontSize:10,marginTop:1}}>{activeJobs} jobs · {newLeads} new leads</div>
          </div>
          {showInstall&&(
            <div style={{padding:'8px 7px',borderTop:'1px solid rgba(255,255,255,.07)',flexShrink:0}}>
              <button onClick={async()=>{if(installPrompt){installPrompt.prompt();const r=await installPrompt.userChoice;if(r.outcome==='accepted')setShowInstall(false)}}}
                style={{width:'100%',padding:collapsed?'9px':'9px 11px',borderRadius:8,border:'1.5px solid rgba(20,198,191,.4)',background:'rgba(20,198,191,.1)',color:'var(--accent)',cursor:'pointer',fontFamily:'inherit',fontWeight:700,fontSize:11,display:'flex',alignItems:'center',gap:8,justifyContent:collapsed?'center':'flex-start'}}>
                <Icon name="install" size={14}/><span className="sidebar-text">Install App</span>
              </button>
            </div>
          )}
        </div>

        {/* Main content */}
        <div style={{flex:1,display:'flex',flexDirection:'column',overflow:'hidden',minWidth:0}}>
          <div className="desktop-topbar" style={{background:'var(--card)',borderBottom:'none',display:'flex',flexDirection:'column',flexShrink:0}}>
            {/* Gradient accent line */}
            <div style={{height:3,background:'linear-gradient(90deg,#14C6BF,#3DCC7E)',flexShrink:0}}/>
            <div style={{padding:'0 24px',height:52,display:'flex',alignItems:'center',justifyContent:'space-between',borderBottom:'1px solid var(--border)'}}>
              <div style={{display:'flex',alignItems:'center',gap:10}}>
                <div style={{display:'flex',alignItems:'center',gap:6}}>
                  <span style={{fontSize:11,color:'var(--muted)',fontWeight:600,letterSpacing:'.04em',textTransform:'uppercase'}}>{curGroup?.label}</span>
                  <span style={{color:'var(--border)',fontSize:12}}>›</span>
                </div>
                <span style={{fontSize:15,fontWeight:900,fontFamily:'Poppins',color:'var(--text)',display:'inline-flex',alignItems:'center',gap:8}}><Icon name={curItem.icon} size={18} color="var(--accent)"/> {curItem.label}</span>
              </div>
              <div style={{display:'flex',gap:14,fontSize:12,color:'var(--muted)',alignItems:'center'}}>
                <button onClick={()=>setQuickSearchOpen(true)} title="Quick search (⌘K / Ctrl+K)"
                  style={{display:'flex',alignItems:'center',gap:8,padding:'6px 12px',background:'var(--bg)',border:'1px solid var(--border)',borderRadius:8,color:'var(--muted)',cursor:'pointer',fontFamily:'inherit',fontSize:12,fontWeight:500}}>
                  <Icon name="search" size={14}/><span>Search</span>
                  <span style={{fontSize:10,fontWeight:700,padding:'2px 6px',background:'#fff',border:'1px solid var(--border)',borderRadius:4,marginLeft:4}}>⌘K</span>
                </button>
                <span style={{display:'inline-flex',alignItems:'center',gap:5}}><Icon name="leads-target" size={14}/> <b style={{color:'var(--text)'}}>{newLeads}</b> new</span>
                <span style={{display:'inline-flex',alignItems:'center',gap:5}}><Icon name="truck" size={14}/> <b style={{color:'var(--text)'}}>{activeJobs}</b> active</span>
                <span style={{color:'var(--accent-dark)',fontWeight:700,fontSize:13,display:'inline-flex',alignItems:'center',gap:5}}><Icon name="dollar-bill" size={14}/> ${pipelineVal.toLocaleString()}</span>
                <UserBadge me={me} open={userMenuOpen} onToggle={()=>setUserMenuOpen(v=>!v)} onClose={()=>setUserMenuOpen(false)} onNavigate={navigate}/>
              </div>
            </div>
          </div>
          {fullscreen
            ?<div style={{flex:1,overflow:'hidden',display:'flex',flexDirection:'column'}}>{renderView()}</div>
            :<div className="main-scroll" style={{flex:1,overflowY:'auto',padding:'22px 24px'}}><div className="pg">{renderView()}</div></div>
          }
        </div>
      </div>

      {/* Mobile bottom nav */}
      <div className="bottom-nav">
        <div className="bnav-inner">
          {NAV_GROUPS.map(grp=>{
            const hasActive=grp.items.some(i=>i.id===view);
            const hasBadge=grp.items.some(i=>badges[i.badge]>0);
            return (
              <button key={grp.id} className={'bnav-btn'+(hasActive?' active':'')}
                onClick={()=>grp.items.length===1?navigate(grp.items[0].id):setMobileDrawer(mobileDrawer?.id===grp.id?null:grp)}>
                <span style={{position:'relative',display:'inline-flex'}}>
                  <Icon name={grp.icon} size={20}/>
                  {hasBadge&&<span style={{position:'absolute',top:-2,right:-4,width:7,height:7,background:'#3DCC7E',borderRadius:'50%'}}/>}
                </span>
                <span style={{fontSize:9,fontWeight:700,letterSpacing:'.03em',textTransform:'uppercase'}}>{grp.label}</span>
              </button>
            );
          })}
        </div>
      </div>

      {mobileDrawer&&<MobileSubDrawer group={mobileDrawer} currentView={view} onNav={navigate} onClose={()=>setMobileDrawer(null)}/>}

      {/* Quick-search modal — Cmd+K opens it */}
      <QuickSearch open={quickSearchOpen} onClose={()=>setQuickSearchOpen(false)} onNavigate={navigate}/>

      {/* Accept toast */}
      {acceptToast&&(
        <div onClick={()=>{setAcceptToast(null);navigate('jobs');}} style={{position:'fixed',top:'calc(16px + env(safe-area-inset-top,0px))',left:'50%',transform:'translateX(-50%)',background:'linear-gradient(135deg,#14C6BF,#3DCC7E)',color:'#fff',padding:'14px 22px',borderRadius:14,fontWeight:700,fontSize:14,boxShadow:'0 10px 40px rgba(20,198,191,.35)',zIndex:9999,cursor:'pointer',display:'flex',alignItems:'center',gap:12,maxWidth:'90vw'}}>
          <Icon name="party" size={24} color="#fff"/>
          <div>
            <div style={{fontWeight:900,fontSize:13,fontFamily:'Poppins'}}>New booking!</div>
            <div style={{fontSize:12,opacity:.95,fontWeight:500}}>{acceptToast}</div>
          </div>
          <span style={{fontSize:11,opacity:.8,marginLeft:8}}>View →</span>
        </div>
      )}

      {/* Tweaks */}
      {showTweaks&&(
        <div style={{position:'fixed',bottom:20,right:20,width:260,background:'var(--card)',borderRadius:16,border:'1.5px solid var(--border)',boxShadow:'0 8px 40px rgba(0,0,0,.14)',padding:20,zIndex:999}}>
          <div style={{fontWeight:800,fontSize:14,fontFamily:'Poppins',marginBottom:16,display:'flex',justifyContent:'space-between'}}>
            Tweaks<button onClick={()=>setShowTweaks(false)} style={{border:'none',background:'none',cursor:'pointer',fontSize:18,color:'var(--muted)'}}>×</button>
          </div>
          {[['companyName','Company Name'],['tagline','Tagline']].map(([k,lbl])=>(
            <div key={k} style={{marginBottom:12}}>
              <label style={{display:'block',fontSize:11,fontWeight:700,textTransform:'uppercase',letterSpacing:'.07em',color:'var(--muted)',marginBottom:5}}>{lbl}</label>
              <input value={tweaks[k]} onChange={e=>updateTweak(k,e.target.value)}
                style={{width:'100%',padding:'8px 12px',borderRadius:8,border:'1.5px solid var(--border)',background:'var(--bg)',fontFamily:'inherit',fontSize:14,color:'var(--text)',outline:'none'}}/>
            </div>
          ))}
          <div style={{padding:12,background:'var(--accent-light)',borderRadius:10,fontSize:12,color:'var(--accent-dark)',fontWeight:600,lineHeight:1.6,display:'flex',gap:8,alignItems:'flex-start'}}>
            <Icon name="install" size={14} style={{flexShrink:0,marginTop:2}}/>
            <div>iOS: Share → "Add to Home Screen"<br/>Android: Menu → "Install App"</div>
          </div>
        </div>
      )}
    </div>
  );
}

window.__maxamove_app_ready = true;
window.App = App;
