/* Mise — Settings */

const Ic = ({ name, size = 16, color }) => {
  const s = { width: size, height: size, display: 'block', color };
  const p = { fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' };
  switch (name) {
    case 'dashboard': return <svg viewBox="0 0 16 16" style={s}><g {...p}><rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><rect x="9" y="9" width="5" height="5" rx="1"/></g></svg>;
    case 'stores':    return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M2 6h12l-1-3H3z"/><path d="M3 6v8h10V6"/><path d="M6 14V9h4v5"/></g></svg>;
    case 'alerts':    return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M3 12V7a5 5 0 0 1 10 0v5l1 2H2z"/><path d="M6 14a2 2 0 0 0 4 0"/></g></svg>;
    case 'ask':       return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H8l-3 2v-2H4a2 2 0 0 1-2-2z"/></g></svg>;
    case 'docs':      return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M3 2h7l3 3v9H3z"/><path d="M10 2v3h3"/><path d="M5 8h6M5 11h4"/></g></svg>;
    case 'reports':   return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M2 14h12"/><rect x="4" y="6" width="2" height="6"/><rect x="7" y="3" width="2" height="9"/><rect x="10" y="8" width="2" height="4"/></g></svg>;
    case 'settings':  return <svg viewBox="0 0 16 16" style={s}><g {...p}><circle cx="8" cy="8" r="2"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.5 1.5M11.5 11.5L13 13M3 13l1.5-1.5M11.5 4.5L13 3"/></g></svg>;
    case 'chev':      return <svg viewBox="0 0 16 16" style={s}><path d="M5 6l3 3 3-3" {...p}/></svg>;
    case 'plus':      return <svg viewBox="0 0 16 16" style={s}><path d="M8 3v10M3 8h10" {...p}/></svg>;
    case 'sync':      return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M3 8a5 5 0 0 1 8.5-3.5L13 6"/><path d="M13 3v3h-3"/><path d="M13 8a5 5 0 0 1-8.5 3.5L3 10"/><path d="M3 13v-3h3"/></g></svg>;
    case 'search':    return <svg viewBox="0 0 16 16" style={s}><g {...p}><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/></g></svg>;
    case 'bell':      return <svg viewBox="0 0 16 16" style={s}><g {...p}><path d="M3 12V7a5 5 0 0 1 10 0v5l1 2H2z"/><path d="M6 14a2 2 0 0 0 4 0"/></g></svg>;
    case 'dots':      return <svg viewBox="0 0 16 16" style={s}><g fill="currentColor"><circle cx="3" cy="8" r="1.3"/><circle cx="8" cy="8" r="1.3"/><circle cx="13" cy="8" r="1.3"/></g></svg>;
    default: return null;
  }
};

const ORGS = window.MISE.orgs;

const TZ_ABBREV = {
  'America/Los_Angeles': 'PT',
  'America/Phoenix':     'MST',
  'America/Denver':      'MT',
  'America/Chicago':     'CT',
  'America/New_York':    'ET',
};
const tzAbbrev = (tz) => TZ_ABBREV[tz] || (tz || '').split('/').pop().replace(/_/g, ' ');

function Sidebar() {
  const UserTile = window.MISE.SidebarUserTile;
  const [openAlertsCount, setOpenAlertsCount] = React.useState(null);
  React.useEffect(() => {
    let alive = true;
    const refresh = () => window.MISE.fetchAlertsCached().then(d => {
      if (alive) setOpenAlertsCount(d.filter(a => a.status === "open").length);
    });
    refresh();
    const unsub = window.MISE.subscribeAlerts(refresh);
    return () => { alive = false; unsub && unsub(); };
  }, []);
  const [open, setOpen] = React.useState(false);
  const [orgId, setOrgId] = React.useState('acme-wingstop');
  const org = ORGS.find(o => o.id === orgId) || ORGS[0];
  const items = [
    { id: 'dash', icon: 'dashboard', label: 'Dashboard', href: '/dashboard' },
    { id: 'stores', icon: 'stores', label: 'Stores', href: '/stores' },
    { id: 'alerts', icon: 'alerts', label: 'Alerts', href: '/alerts', badge: openAlertsCount },
    { id: 'ask', icon: 'ask', label: 'Ask Mise', href: '/ask' },
    { id: 'docs', icon: 'docs', label: 'Documents', href: '/documents' },
    { id: 'reports', icon: 'reports', label: 'Reports', href: '/reports' },
    { id: 'settings', icon: 'settings', label: 'Settings', active: true, href: '/settings' },
  ];
  return (
    <aside className="sidebar" data-screen-label="Sidebar">
      <a href="/" className="sb-brand">
        <span className="glyph">
          <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
            <path d="M3 11 V3 L6 8 L8 3 V11" fill="none" stroke="#0F2A3F" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
            <circle cx="11" cy="9" r="1.2" fill="#C8553D"/>
          </svg>
        </span>
        <span className="word">Mise</span>
      </a>
      <div className="org-switch" style={{ cursor: 'default' }}>
        <div style={{ minWidth: 0 }}>
          <div className="label">Organization</div>
          <div className="name">{org.name}</div>
        </div>
      </div>
      <nav className="nav-list">
        {items.slice(0, 4).map(it => (
          <a key={it.id} href={it.href || '#'} className={`nav-item ${it.active ? 'active' : ''}`}>
            <Ic name={it.icon}/>
            <span>{it.label}</span>
            {it.badge && <span className="badge">{it.badge}</span>}
          </a>
        ))}
        <div className="nav-section">Workspace</div>
        {items.slice(4).map(it => (
          <a key={it.id} href={it.href || '#'} className={`nav-item ${it.active ? 'active' : ''}`}>
            <Ic name={it.icon}/>
            <span>{it.label}</span>
          </a>
        ))}
      </nav>
      <UserTile/>
    </aside>
  );
}

function Topbar() {
  return (
    <header className="topbar" data-screen-label="Top bar">
      <div>
        <span className="crumb serif">Settings</span>
      </div>
      <div className="search">
        <span className="sicon"><Ic name="search" size={14}/></span>
        <input type="text" placeholder="Search settings…" />
        <span className="kbd">⌘K</span>
      </div>
      <div className="top-right">
        <button className="icon-btn" aria-label="Notifications">
          <Ic name="bell" size={16}/>
          <span className="dot"/>
        </button>
      </div>
    </header>
  );
}

const SETTINGS_NAV = [
  { group: 'Organization', items: [
    { id: 'gen', label: 'General' },
    { id: 'stores', label: 'Stores', active: true },
    { id: 'brands', label: 'Brands' },
  ]},
  { group: 'Integrations', items: [
    { id: 'pos', label: 'POS' },
    { id: 'labor', label: 'Labor & Scheduling' },
    { id: 'safety', label: 'Food Safety' },
    { id: 'accounting', label: 'Accounting' },
    { id: 'slack', label: 'Slack & Notifications' },
    { id: 'docsrc', label: 'Document Sources' },
  ]},
  { group: 'People', items: [
    { id: 'users', label: 'Users & Roles' },
    { id: 'sso', label: 'Single Sign-On' },
    { id: 'tokens', label: 'API Tokens' },
  ]},
  { group: 'Alerts & Rules', items: [
    { id: 'rules', label: 'Rule Library' },
    { id: 'routing', label: 'Alert Routing' },
    { id: 'brief', label: 'Daily Brief Schedule' },
  ]},
  { group: 'Security & Compliance', items: [
    { id: 'audit', label: 'Audit Log' },
    { id: 'retention', label: 'Data Retention' },
    { id: 'tenant', label: 'Tenant Isolation Report' },
  ]},
  { group: 'Billing', items: [
    { id: 'plan', label: 'Plan & Invoices' },
    { id: 'usage', label: 'Usage' },
  ]},
];

function SettingsNav({ active, setActive }) {
  return (
    <nav className="set-nav" data-screen-label="Settings nav">
      {SETTINGS_NAV.map(g => (
        <div key={g.group} className="set-group">
          <div className="set-grouph">{g.group}</div>
          {g.items.map(it => (
            <button key={it.id} className={`set-it ${it.id === active ? 'active' : ''}`} onClick={() => setActive(it.id)}>
              {it.label}
            </button>
          ))}
        </div>
      ))}
    </nav>
  );
}

/* —— Brand & integration logos —— */
function ToastLogo() {
  return <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><circle cx="7" cy="7" r="6.5" fill="#FF4C00"/><text x="7" y="9" fontFamily="Inter,sans-serif" fontSize="6" fontWeight="700" fill="#fff" textAnchor="middle">T</text></svg>;
}
function SevenShifts() {
  return <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><rect x="0" y="0" width="14" height="14" rx="3" fill="#1E2944"/><text x="7" y="9.4" fontFamily="Inter,sans-serif" fontSize="5.5" fontWeight="700" fill="#fff" textAnchor="middle">7s</text></svg>;
}
function JoltLogo() {
  return <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><rect x="0" y="0" width="14" height="14" rx="3" fill="#00B4A0"/><path d="M7 2.5 L4.5 8 H7 L5.5 11.5 L9.5 6 H7 L8.5 2.5 Z" fill="#fff"/></svg>;
}
function SquareLogo() {
  return <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><rect x="0" y="0" width="14" height="14" rx="3" fill="#000"/><rect x="3" y="3" width="8" height="8" rx="1.4" fill="none" stroke="#fff" strokeWidth="1.4"/></svg>;
}
function CloverLogo() {
  return <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
    <rect x="0" y="0" width="14" height="14" rx="3" fill="#00B259"/>
    <g fill="#fff">
      <circle cx="7" cy="5" r="1.3"/>
      <circle cx="9" cy="7" r="1.3"/>
      <circle cx="7" cy="9" r="1.3"/>
      <circle cx="5" cy="7" r="1.3"/>
      <rect x="5.7" y="5.7" width="2.6" height="2.6"/>
    </g>
  </svg>;
}
function QuickBooksLogo() {
  return <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
    <rect x="0" y="0" width="14" height="14" rx="3" fill="#2CA01C"/>
    <text x="7" y="7" fontFamily="Inter,sans-serif" fontSize="5.5" fontWeight="800"
      fill="#fff" textAnchor="middle" dominantBaseline="central">qb</text>
  </svg>;
}
function DeputyLogo() {
  return <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><rect x="0" y="0" width="14" height="14" rx="3" fill="#FF5447"/><text x="7" y="9.6" fontFamily="Inter,sans-serif" fontSize="7" fontWeight="700" fill="#fff" textAnchor="middle">D</text></svg>;
}
function ShopifyLogo() {
  return <svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
    <rect x="0" y="0" width="14" height="14" rx="3" fill="#95BF47"/>
    <path d="M5.5 6 V5 c0-.85.65-1.4 1.5-1.4 s1.5.55 1.5 1.4 V6"
      fill="none" stroke="#fff" strokeWidth=".8" strokeLinecap="round"/>
    <path d="M3.75 6 L10.25 6 L9.65 10.9 a.4 .4 0 0 1 -.4 .35 H4.75 a.4 .4 0 0 1 -.4 -.35 Z"
      fill="#fff"/>
  </svg>;
}
function LightspeedLogo() {
  // Real mark recolored white-on-red to match the rounded-square
  // treatment of the SVG-drawn logos. Source is 56×56 so it stays
  // crisp at both 14px (settings) and 36px (marketing) display sizes.
  return <img
    src="/design/logos/lightspeed.png"
    width="14" height="14"
    alt="Lightspeed"
    style={{ display: 'inline-block', verticalAlign: 'middle', borderRadius: 3 }}
  />;
}
function NoServiceDot() {
  return <span style={{ display: 'inline-block', width: 14, height: 14, borderRadius: 3, border: '1px dashed var(--hair)' }}/>;
}

/** Pick the SVG logo + display label for an integration block returned
 *  by /stores. Returns the placeholder dot when no service is wired
 *  yet (e.g. food safety until Jolt lands). */
function integrationLogo(integration) {
  switch ((integration && integration.service) || '') {
    case 'Square':       return <SquareLogo/>;
    case 'Clover':       return <CloverLogo/>;
    case 'QuickBooks':   return <QuickBooksLogo/>;
    case 'Deputy':       return <DeputyLogo/>;
    case 'Shopify':      return <ShopifyLogo/>;
    case 'Lightspeed':   return <LightspeedLogo/>;
    case 'Toast':        return <ToastLogo/>;
    case '7shifts':      return <SevenShifts/>;
    case 'Jolt':         return <JoltLogo/>;
    default:             return <NoServiceDot/>;
  }
}

const BRAND_CHIP = { Wingstop: { bg: 'rgba(176,122,25,.12)', color: '#8C5F0F' } };

// Stores are now fetched live from /stores. The hardcoded array used
// to live here as a design-time placeholder; it's been moved into the
// V9__stores.sql Flyway seed so that whoever boots a fresh DB still
// sees the same demo locations.

function StoreRowMenu({ store, onAction }) {
  const [open, setOpen] = React.useState(false);
  const close = () => setOpen(false);
  const pick = (action) => (e) => { e.stopPropagation(); close(); onAction(action, store); };
  const paused = store.status === 'paused';
  const needsAuth = store.pos === 'warn' || store.labor === 'warn' || store.safety === 'warn';
  return (
    <div className="dd-wrap" style={{ position: 'relative' }}>
      {open && <div className="dd-backdrop" onClick={(e) => { e.stopPropagation(); close(); }}/>}
      <button
        className="dt-actions"
        onClick={(e) => { e.stopPropagation(); setOpen(o => !o); }}
        aria-label="Store actions"
      ><Ic name="dots" size={14}/></button>
      {open && (
        <div className="dd-menu right" style={{ minWidth: 220 }} onClick={(e) => e.stopPropagation()}>
          <div className="dd-item" onClick={pick('open')}>
            <div className="dd-text"><div className="dd-title">Open store detail</div></div>
          </div>
          <div className="dd-item" onClick={pick(paused ? 'resume' : 'pause')}>
            <div className="dd-text"><div className="dd-title">{paused ? 'Resume data sync' : 'Pause data sync'}</div></div>
          </div>
          <div className="dd-item" onClick={pick('reauth')}>
            <div className="dd-text">
              <div className="dd-title">Reset integration auth</div>
              {needsAuth && <div className="dd-meta" style={{ fontSize: 11, color: 'var(--accent)' }}>One integration needs re-auth</div>}
            </div>
          </div>
          <div className="dd-item" onClick={pick('edit')}>
            <div className="dd-text"><div className="dd-title">Edit details…</div></div>
          </div>
          <div className="dd-item" onClick={pick('remove')} style={{ color: 'var(--red)' }}>
            <div className="dd-text"><div className="dd-title" style={{ color: 'var(--red)' }}>Remove store</div></div>
          </div>
        </div>
      )}
    </div>
  );
}

function ConnDot({ state, msg, logo }) {
  // Anything that isn't a live, working connection ('on') renders as
  // an em-dash. "Installed but this store isn't mapped" (state='warn')
  // is operationally equivalent to "not connected" for this store, so
  // it gets the same empty cell — operators only see green dots when
  // ingestion is actually happening.
  if (state !== 'on') {
    return (
      <Tooltip text={msg || (state === 'warn' ? 'Installed but not mapped to this store' : 'Not connected')}>
        <span style={{ color: 'var(--muted-2)', fontSize: 14 }}>—</span>
      </Tooltip>
    );
  }
  return (
    <Tooltip text={msg || 'Connected'}>
      <div className="conn">
        <span className="conn-logo">{logo}</span>
        <span className={`conn-dot s-${state}`}/>
      </div>
    </Tooltip>
  );
}

/** Hover-triggered styled tooltip. Pops above the wrapped content with
 *  a small arrow. Uses the same dark-on-light treatment as the chart
 *  tooltips on the store-detail page. */
function Tooltip({ text, children }) {
  const [open, setOpen] = React.useState(false);
  if (!text) return children;
  return (
    <span
      onMouseEnter={() => setOpen(true)}
      onMouseLeave={() => setOpen(false)}
      onFocus={() => setOpen(true)}
      onBlur={() => setOpen(false)}
      style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}
    >
      {children}
      {open && (
        <span
          role="tooltip"
          style={{
            position: 'absolute',
            bottom: 'calc(100% + 8px)',
            left: '50%',
            transform: 'translateX(-50%)',
            background: 'var(--ink)',
            color: 'var(--bg)',
            padding: '6px 10px',
            borderRadius: 6,
            fontSize: 11.5,
            lineHeight: 1.35,
            fontWeight: 400,
            fontFamily: 'inherit',
            whiteSpace: 'normal',
            width: 'max-content',
            maxWidth: 220,
            textAlign: 'center',
            boxShadow: '0 4px 12px rgba(11,27,38,.18)',
            pointerEvents: 'none',
            zIndex: 50,
          }}
        >
          {text}
          <span
            aria-hidden="true"
            style={{
              position: 'absolute',
              top: '100%',
              left: '50%',
              transform: 'translateX(-50%)',
              width: 0, height: 0,
              borderLeft: '5px solid transparent',
              borderRight: '5px solid transparent',
              borderTop: '5px solid var(--ink)',
            }}
          />
        </span>
      )}
    </span>
  );
}

const blankStore = () => ({
  num: '', name: '', city: '', state: '', timezone: 'America/Los_Angeles',
  status: 'active', brandId: null, gmUserId: null,
  auditGrade: '', foodSafetyCompliancePct: '',
});

function StoresSettings() {
  const [selected, setSelected] = React.useState(new Set());
  const [items, setItems] = React.useState([]);
  const [loaded, setLoaded] = React.useState(false);
  const [loadError, setLoadError] = React.useState(null);
  const [notice, setNotice] = React.useState(null);
  const [editing, setEditing] = React.useState(null);
  const [busy, setBusy] = React.useState(false);

  const refresh = React.useCallback(async () => {
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/stores`, { cache: 'no-store' });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      setItems(await res.json());
      setLoadError(null);
    } catch (e) {
      setLoadError(e.message || String(e));
      setItems([]);
    } finally {
      setLoaded(true);
    }
  }, []);

  React.useEffect(() => { refresh(); }, [refresh]);

  React.useEffect(() => {
    if (!notice) return;
    const t = setTimeout(() => setNotice(null), 3500);
    return () => clearTimeout(t);
  }, [notice]);

  // Adapt API DTO → row shape. The integration blocks (pos/labor/
  // safety) come straight through — they're derived from the actual
  // Square workspace + mappings on the backend, so they're trustworthy
  // and the user can't edit them by hand.
  const visibleStores = items.map(s => ({
    id: s.id,
    num: s.num,
    name: s.name,
    city: s.city || '',
    state: s.state || '',
    tz: s.timezone,
    brand: s.brand || null,
    pos: s.pos || { status: 'off' },
    labor: s.labor || { status: 'off' },
    financials: s.financials || { status: 'off' },
    safety: s.safety || { status: 'off' },
    status: s.status,
    raw: s,
  }));

  const allChecked = selected.size > 0 && selected.size === visibleStores.length;
  const toggleOne = (num) => {
    setSelected(prev => {
      const next = new Set(prev);
      if (next.has(num)) next.delete(num); else next.add(num);
      return next;
    });
  };
  const toggleAll = () => setSelected(allChecked ? new Set() : new Set(visibleStores.map(s => s.num)));

  const onRowAction = async (action, store) => {
    if (action === 'open') {
      window.location.href = `/stores/${store.num.replace('#', '')}`;
      return;
    }
    if (action === 'pause' || action === 'resume') {
      const next = action === 'pause' ? 'paused' : 'active';
      try {
        const res = await fetch(`${SETTINGS_API_BASE}/stores/${store.id}/status`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ status: next }),
        });
        if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
        await refresh();
        setNotice(`${store.num} ${store.name} — data sync ${next === 'paused' ? 'paused' : 'resumed'}.`);
      } catch (e) {
        setNotice(`${store.num} ${store.name} — failed to update status: ${e.message || e}`);
      }
      return;
    }
    if (action === 'reauth') {
      setNotice(`${store.num} ${store.name} — re-auth flow not implemented yet (would open Toast/7shifts/Jolt OAuth).`);
      return;
    }
    if (action === 'edit') {
      // Only carry editable columns into the modal — integration state
      // is derived on the server and not user-editable.
      setEditing({
        id: store.raw.id,
        num: store.raw.num,
        name: store.raw.name,
        city: store.raw.city || '',
        state: store.raw.state || '',
        timezone: store.raw.timezone || 'America/Los_Angeles',
        status: store.raw.status || 'active',
        brandId: store.raw.brand?.id || null,
        gmUserId: store.raw.gm?.id || null,
        auditGrade: store.raw.auditGrade || '',
        foodSafetyCompliancePct: store.raw.foodSafetyCompliancePct ?? '',
      });
      return;
    }
    if (action === 'remove') {
      if (!confirm(`Remove ${store.num} ${store.name}?\n\nThis stops ingestion and hides the store from all pages.`)) return;
      try {
        const res = await fetch(`${SETTINGS_API_BASE}/stores/${store.id}`, { method: 'DELETE' });
        if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
        await refresh();
        setSelected(prev => { const next = new Set(prev); next.delete(store.num); return next; });
        setNotice(`${store.num} ${store.name} — removed.`);
      } catch (e) {
        setNotice(`${store.num} ${store.name} — failed to remove: ${e.message || e}`);
      }
      return;
    }
  };

  const onSave = async () => {
    if (!editing) return;
    setBusy(true);
    try {
      const isNew = !editing.id;
      const url = isNew
        ? `${SETTINGS_API_BASE}/stores`
        : `${SETTINGS_API_BASE}/stores/${editing.id}`;
      // Translate UI empties back to nulls + parse the numeric pct so
      // the backend gets typed values rather than blank strings.
      const body = {
        ...editing,
        auditGrade: editing.auditGrade || null,
        foodSafetyCompliancePct: editing.foodSafetyCompliancePct === '' || editing.foodSafetyCompliancePct == null
          ? null
          : Number(editing.foodSafetyCompliancePct),
      };
      const res = await fetch(url, {
        method: isNew ? 'POST' : 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      await refresh();
      setEditing(null);
      setNotice(isNew ? `Added ${editing.num} ${editing.name}.` : `Saved changes to ${editing.num}.`);
    } catch (e) {
      setNotice(`Save failed: ${e.message || e}`);
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="set-pane" data-screen-label="Stores settings">
      <div className="set-page-head" style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16 }}>
        <div>
          <h1 className="serif set-h1">Stores</h1>
          <p className="set-sub">Manage the locations Mise tracks for your organization. Each store inherits brand standards from its assigned brand.</p>
        </div>
        <button onClick={() => setEditing(blankStore())} style={btnPriStyle(false)}>
          <Ic name="plus" size={13}/> Add store
        </button>
      </div>

      <div className={`bulk-bar ${selected.size > 0 ? 'active' : ''}`}>
        <label className="bulk-check">
          <input type="checkbox" checked={allChecked} onChange={toggleAll}/>
          <span className="bk-box"/>
        </label>
        {selected.size === 0 ? (
          <span className="bk-hint">Select stores to bulk edit</span>
        ) : (
          <>
            <span className="bk-count mono">{selected.size} selected</span>
            <div className="bk-actions">
              <button className="bk-btn">Reassign brand</button>
              <button className="bk-btn">Edit timezone</button>
              <button className="bk-btn">Pause</button>
              <button className="bk-btn danger">Remove</button>
            </div>
          </>
        )}
      </div>

      {notice && (
        <div style={{
          margin: '12px 0', padding: '10px 14px', borderRadius: 8,
          background: 'var(--bg-2)', border: '1px solid var(--hair)',
          fontSize: 12.5, color: 'var(--ink)',
        }}>{notice}</div>
      )}

      {loadError && (
        <div style={{
          margin: '12px 0', padding: '10px 14px', borderRadius: 8,
          background: 'rgba(200,85,61,.08)', border: '1px solid rgba(200,85,61,.25)',
          fontSize: 12.5, color: 'var(--accent)',
        }}>Couldn't load stores from the backend: {loadError}</div>
      )}

      {!loaded && (
        <div style={{ padding: '24px 0', color: 'var(--muted)', fontSize: 13 }}>Loading stores…</div>
      )}

      {loaded && visibleStores.length === 0 && !loadError && (
        <div style={{ padding: '24px 0', color: 'var(--muted)', fontSize: 13 }}>
          No stores yet. Click "Add a new store" below to get started.
        </div>
      )}

      <div className="set-table">
        <div className="st-head">
          <div className="stc c-chk"></div>
          <div className="stc c-store">Store</div>
          <div className="stc c-brand">Brand</div>
          <div className="stc c-addr">Address</div>
          <div className="stc c-tz">Timezone</div>
          <div className="stc c-conn">POS</div>
          <div className="stc c-conn">Labor</div>
          <div className="stc c-conn">Financials</div>
          <div className="stc c-conn">Food safety</div>
          <div className="stc c-stat">Status</div>
          <div className="stc c-act"></div>
        </div>
        {visibleStores.map(s => (
          <div key={s.num} className={`st-row ${selected.has(s.num) ? 'sel' : ''}`}>
            <div className="stc c-chk">
              <label className="bulk-check">
                <input type="checkbox" checked={selected.has(s.num)} onChange={() => toggleOne(s.num)}/>
                <span className="bk-box"/>
              </label>
            </div>
            <div className="stc c-store">
              <span className="st-num mono">{s.num}</span>
              <span className="st-name">{s.name}</span>
            </div>
            <div className="stc c-brand">
              <BrandChip brand={s.brand}/>
            </div>
            <div className="stc c-addr">{s.city}, {s.state}</div>
            <div className="stc c-tz mono" title={s.tz}>{tzAbbrev(s.tz)}</div>
            <div className="stc c-conn"><ConnDot state={s.pos.status} msg={s.pos.msg || (s.pos.service ? s.pos.service : 'Not connected')} logo={integrationLogo(s.pos)}/></div>
            <div className="stc c-conn"><ConnDot state={s.labor.status} msg={s.labor.msg || (s.labor.service ? s.labor.service : 'Not connected')} logo={integrationLogo(s.labor)}/></div>
            <div className="stc c-conn"><ConnDot state={s.financials?.status || 'off'} msg={s.financials?.msg || (s.financials?.service ? s.financials.service : 'Not connected')} logo={integrationLogo(s.financials)}/></div>
            <div className="stc c-conn"><ConnDot state={s.safety.status} msg={s.safety.msg || (s.safety.service ? s.safety.service : 'No food-safety integration wired yet')} logo={integrationLogo(s.safety)}/></div>
            <div className="stc c-stat">
              <span className={`st-stat ${s.status}`}>
                <span className="ss-dot"/>
                {s.status === 'active' ? 'Active' : 'Paused'}
              </span>
            </div>
            <div className="stc c-act">
              <StoreRowMenu store={s} onAction={onRowAction}/>
            </div>
          </div>
        ))}
      </div>

      <button className="add-store-cta" onClick={() => setEditing(blankStore())}>
        <span className="ascta-plus"><Ic name="plus" size={14}/></span>
        <div className="ascta-text">
          <div className="ascta-t">Add a new store</div>
          <div className="ascta-s">Mise will pull historical data from your connected sources for the past 90 days.</div>
        </div>
      </button>

      {editing && (
        <StoreEditModal
          editing={editing}
          setEditing={setEditing}
          onSave={onSave}
          onCancel={() => setEditing(null)}
          busy={busy}
        />
      )}
    </div>
  );
}

function StoreEditModal({ editing, setEditing, onSave, onCancel, busy }) {
  const update = (k) => (e) => setEditing({ ...editing, [k]: e.target.value });
  const isNew = !editing.id;
  const [brandOptions, setBrandOptions] = React.useState([]);
  const [userOptions, setUserOptions] = React.useState([]);
  React.useEffect(() => {
    fetchBrandsCached().then(setBrandOptions);
    return subscribeBrands(() => fetchBrandsCached().then(setBrandOptions));
  }, []);
  React.useEffect(() => {
    if (window.MISE && window.MISE.fetchUsersCached) {
      window.MISE.fetchUsersCached().then(setUserOptions);
      return window.MISE.subscribeUsers
        ? window.MISE.subscribeUsers(() => window.MISE.fetchUsersCached().then(setUserOptions))
        : undefined;
    }
  }, []);
  return (
    <div onClick={busy ? null : onCancel} style={{
      position: 'fixed', inset: 0, background: 'rgba(11,27,38,.45)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: 'var(--bg)', borderRadius: 12, padding: 24, width: 520, maxWidth: '92vw',
        boxShadow: '0 20px 60px rgba(11,27,38,.25)', border: '1px solid var(--hair)',
        maxHeight: '90vh', overflowY: 'auto',
      }}>
        <div style={{ fontSize: 11, color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace", letterSpacing: '.05em', textTransform: 'uppercase', marginBottom: 4 }}>
          {isNew ? 'Add store' : 'Edit store'}
        </div>
        <div style={{ fontSize: 16, fontWeight: 600, color: 'var(--ink)', marginBottom: 14 }}>
          {editing.num ? `${editing.num} ${editing.name || ''}`.trim() : 'New store'}
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 12 }}>
          <Field label="Store #"><input value={editing.num} onChange={update('num')} disabled={busy} placeholder="14 or #14" style={inputStyle}/></Field>
          <Field label="Name"><input value={editing.name} onChange={update('name')} disabled={busy} style={inputStyle}/></Field>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 12 }}>
          <Field label="City"><input value={editing.city || ''} onChange={update('city')} disabled={busy} style={inputStyle}/></Field>
          <Field label="State"><input value={editing.state || ''} onChange={update('state')} disabled={busy} placeholder="CA" maxLength={4} style={inputStyle}/></Field>
        </div>
        <Field label="Timezone">
          <select value={editing.timezone || ''} onChange={update('timezone')} disabled={busy} style={inputStyle}>
            <option value="America/Los_Angeles">America/Los_Angeles (PT)</option>
            <option value="America/Denver">America/Denver (MT)</option>
            <option value="America/Phoenix">America/Phoenix (Arizona)</option>
            <option value="America/Chicago">America/Chicago (CT)</option>
            <option value="America/New_York">America/New_York (ET)</option>
          </select>
        </Field>

        <Field label="Brand">
          <select
            value={editing.brandId || ''}
            onChange={(e) => setEditing({ ...editing, brandId: e.target.value || null })}
            disabled={busy}
            style={inputStyle}
          >
            <option value="">— No brand —</option>
            {brandOptions.map(b => (
              <option key={b.id} value={b.id}>{b.name}</option>
            ))}
          </select>
          {brandOptions.length === 0 && (
            <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 4 }}>
              No brands defined yet. Add one from <a href="?section=brands" style={{ color: 'var(--primary)' }}>Settings → Brands</a>.
            </div>
          )}
        </Field>

        <Field label="General Manager">
          <select
            value={editing.gmUserId || ''}
            onChange={(e) => setEditing({ ...editing, gmUserId: e.target.value || null })}
            disabled={busy}
            style={inputStyle}
          >
            <option value="">— Unassigned —</option>
            {userOptions.map(u => (
              <option key={u.id} value={u.id}>
                {u.name} {u.role ? `· ${u.role}` : ''}{u.email ? ` (${u.email})` : ''}
              </option>
            ))}
          </select>
          {userOptions.length === 0 && (
            <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 4 }}>
              No users yet. Add one from <a href="?section=users" style={{ color: 'var(--primary)' }}>Settings → Users &amp; Roles</a>.
            </div>
          )}
        </Field>

        <div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 12 }}>
          <Field label="Audit grade">
            <select value={editing.auditGrade || ''} onChange={update('auditGrade')} disabled={busy} style={inputStyle}>
              <option value="">— Not set —</option>
              <option value="A">A</option>
              <option value="B">B</option>
              <option value="C">C</option>
              <option value="D">D</option>
              <option value="F">F</option>
            </select>
          </Field>
          <Field label="Food safety compliance %">
            <input
              type="number" min="0" max="100" step="0.1"
              value={editing.foodSafetyCompliancePct ?? ''}
              onChange={update('foodSafetyCompliancePct')}
              disabled={busy}
              placeholder="0–100"
              style={inputStyle}
            />
          </Field>
        </div>
        <div style={{
          marginBottom: 12, padding: '10px 12px', borderRadius: 8,
          background: 'var(--bg-2)', border: '1px solid var(--hair)',
          fontSize: 11.5, color: 'var(--muted)', lineHeight: 1.5,
        }}>
          Audit grade and food-safety compliance feed the composite
          operational score. Set them based on your most recent audit /
          log review — once a food-safety integration (Jolt, etc.) is
          wired the compliance % will be sourced live. POS, labor, and
          food-safety connection state is derived from installed
          integrations (Square, etc.) and isn't edited here.
        </div>

        <Field label="Status">
          <select value={editing.status} onChange={update('status')} disabled={busy} style={inputStyle}>
            <option value="active">Active</option>
            <option value="paused">Paused</option>
          </select>
        </Field>

        <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 18 }}>
          <button onClick={onCancel} disabled={busy} style={btnOutStyle(busy)}>Cancel</button>
          <button onClick={onSave} disabled={busy || !editing.num || !editing.name} style={btnPriStyle(busy || !editing.num || !editing.name)}>
            {busy ? 'Saving…' : (isNew ? 'Add store' : 'Save changes')}
          </button>
        </div>
      </div>
    </div>
  );
}


const SETTINGS_API_BASE = (typeof window !== 'undefined' && window.MISE_API_BASE) || 'http://localhost:8080';

/** Cached brands fetcher so every component that reads /brands shares
 *  one in-flight request and one notification stream. Stores form,
 *  filter chip, brand chips, etc. all subscribe through this. */
const BRANDS_CACHE = { value: null, inflight: null, listeners: new Set() };
function fetchBrandsCached() {
  if (BRANDS_CACHE.value) return Promise.resolve(BRANDS_CACHE.value);
  if (BRANDS_CACHE.inflight) return BRANDS_CACHE.inflight;
  BRANDS_CACHE.inflight = fetch(`${SETTINGS_API_BASE}/brands`, { cache: 'no-store' })
    .then(r => r.ok ? r.json() : [])
    .then(d => { BRANDS_CACHE.value = d; BRANDS_CACHE.inflight = null; return d; })
    .catch(() => { BRANDS_CACHE.inflight = null; return []; });
  return BRANDS_CACHE.inflight;
}
function invalidateBrandsCache() {
  BRANDS_CACHE.value = null;
  BRANDS_CACHE.inflight = null;
  BRANDS_CACHE.listeners.forEach(fn => { try { fn(); } catch {} });
}
function subscribeBrands(fn) {
  BRANDS_CACHE.listeners.add(fn);
  return () => BRANDS_CACHE.listeners.delete(fn);
}
// Make the cache + helpers reachable from the other Babel-rendered
// pages (stores.jsx, store-detail.jsx) — same shape as fetchAlertsCached.
if (typeof window !== 'undefined' && window.MISE) {
  window.MISE.fetchBrandsCached = fetchBrandsCached;
  window.MISE.invalidateBrandsCache = invalidateBrandsCache;
  window.MISE.subscribeBrands = subscribeBrands;
}

/** Tiny color swatch used on the brand chip. Uses #RRGGBB; falls back
 *  to a neutral hair color when the brand doesn't have one set. */
function BrandSwatch({ color, size = 10 }) {
  return <span style={{
    display: 'inline-block', width: size, height: size, borderRadius: '50%',
    background: color || 'var(--hair)', flexShrink: 0,
  }}/>;
}

/** Render the brand as a chip — colored dot + name. Used everywhere
 *  the old "Wingstop" hardcoded chip lived. */
function BrandChip({ brand }) {
  if (!brand) {
    return <span style={{ fontSize: 11.5, color: 'var(--muted-2)' }}>—</span>;
  }
  const bg = brand.color ? brand.color + '1A' : 'rgba(15,42,63,.06)';  // 1A = ~10% alpha
  const fg = brand.color || 'var(--ink)';
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      padding: '2px 8px', borderRadius: 4,
      background: bg, color: fg,
      fontSize: 11, fontWeight: 500,
    }}>
      <BrandSwatch color={brand.color} size={7}/>
      {brand.name}
    </span>
  );
}

function BrandsSettings() {
  const [brands, setBrands] = React.useState([]);
  const [loaded, setLoaded] = React.useState(false);
  const [editing, setEditing] = React.useState(null); // { id?, name, color }
  const [busy, setBusy] = React.useState(false);
  const [notice, setNotice] = React.useState(null);

  const refresh = React.useCallback(async () => {
    invalidateBrandsCache();
    try {
      const data = await fetchBrandsCached();
      setBrands(data || []);
    } catch (e) {
      setNotice('Couldn\'t load brands: ' + (e.message || e));
    } finally {
      setLoaded(true);
    }
  }, []);

  React.useEffect(() => { refresh(); }, [refresh]);

  React.useEffect(() => {
    if (!notice) return;
    const t = setTimeout(() => setNotice(null), 4500);
    return () => clearTimeout(t);
  }, [notice]);

  const onSave = async () => {
    if (!editing) return;
    setBusy(true);
    try {
      const isNew = !editing.id;
      const url = isNew
        ? `${SETTINGS_API_BASE}/brands`
        : `${SETTINGS_API_BASE}/brands/${editing.id}`;
      const res = await fetch(url, {
        method: isNew ? 'POST' : 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: editing.name, color: editing.color || null }),
      });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      await refresh();
      setEditing(null);
      setNotice(isNew ? `Added "${editing.name}".` : `Saved changes to "${editing.name}".`);
    } catch (e) {
      setNotice('Save failed: ' + (e.message || e));
    } finally {
      setBusy(false);
    }
  };

  const onRemove = async (b) => {
    if (!confirm(`Remove "${b.name}"?\n\nStores assigned to this brand will keep existing but lose their brand assignment until you set a new one.`)) return;
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/brands/${b.id}`, { method: 'DELETE' });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      await refresh();
      setNotice(`Removed "${b.name}".`);
    } catch (e) {
      setNotice('Remove failed: ' + (e.message || e));
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="set-pane">
      <div className="set-page-head" style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16 }}>
        <div>
          <h1 className="serif set-h1">Brands</h1>
          <p className="set-sub">
            Each store belongs to a brand. Brands drive the chip on the Stores list,
            and (later) the scoping for brand-specific playbook docs and rules.
          </p>
        </div>
        <button onClick={() => setEditing({ name: '', color: '#C8553D' })} style={btnPriStyle(false)}>
          <Ic name="plus" size={13}/> Add brand
        </button>
      </div>

      {notice && (
        <div style={{
          margin: '12px 0', padding: '10px 14px', borderRadius: 8,
          background: 'var(--bg-2)', border: '1px solid var(--hair)',
          fontSize: 12.5, color: 'var(--ink)',
        }}>{notice}</div>
      )}

      {!loaded && (
        <div style={{ padding: '24px 0', color: 'var(--muted)', fontSize: 13 }}>Loading brands…</div>
      )}

      {loaded && brands.length === 0 && (
        <div style={{
          marginTop: 16, padding: '36px 24px', border: '1px dashed var(--hair)',
          borderRadius: 12, textAlign: 'center', background: 'var(--bg)',
          fontSize: 13, color: 'var(--muted)',
        }}>
          No brands yet. Click <strong>Add brand</strong> above to create one — your
          existing stores will then be assignable from Settings → Stores.
        </div>
      )}

      {loaded && brands.length > 0 && (
        <div style={{
          marginTop: 16, background: 'var(--bg)', border: '1px solid var(--hair)',
          borderRadius: 12, overflow: 'visible',
        }}>
          {brands.map((b, i) => (
            <div key={b.id} style={{
              display: 'grid', gridTemplateColumns: '20px 1fr 130px 100px',
              gap: 14, alignItems: 'center', padding: '14px 18px',
              borderBottom: i === brands.length - 1 ? 'none' : '1px solid var(--hair-2)',
              borderTopLeftRadius: i === 0 ? 12 : 0, borderTopRightRadius: i === 0 ? 12 : 0,
              borderBottomLeftRadius: i === brands.length - 1 ? 12 : 0, borderBottomRightRadius: i === brands.length - 1 ? 12 : 0,
            }}>
              <BrandSwatch color={b.color} size={14}/>
              <div>
                <div style={{ fontSize: 13.5, fontWeight: 500, color: 'var(--ink)' }}>{b.name}</div>
                {b.color && <div style={{ fontSize: 11, color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace" }}>{b.color}</div>}
              </div>
              <button onClick={() => setEditing({ id: b.id, name: b.name, color: b.color || '' })} disabled={busy} style={btnOutStyle(busy)}>
                Edit
              </button>
              <button onClick={() => onRemove(b)} disabled={busy} style={{
                ...btnOutStyle(busy),
                color: 'var(--red)',
                borderColor: 'rgba(178,58,42,.30)',
              }}>
                Remove
              </button>
            </div>
          ))}
        </div>
      )}

      {editing && (
        <BrandEditModal
          editing={editing}
          setEditing={setEditing}
          onSave={onSave}
          onCancel={() => setEditing(null)}
          busy={busy}
        />
      )}
    </div>
  );
}

function BrandEditModal({ editing, setEditing, onSave, onCancel, busy }) {
  const isNew = !editing.id;
  const update = (k) => (e) => setEditing({ ...editing, [k]: e.target.value });
  const presetColors = ['#C8553D', '#0F2A3F', '#2F7D5B', '#B07A19', '#5A6B78', '#8C5F0F', '#7A3E5C', '#3D6E8C'];
  return (
    <div onClick={busy ? null : onCancel} style={{
      position: 'fixed', inset: 0, background: 'rgba(11,27,38,.45)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: 'var(--bg)', borderRadius: 12, padding: 24, width: 460, maxWidth: '92vw',
        boxShadow: '0 20px 60px rgba(11,27,38,.25)', border: '1px solid var(--hair)',
      }}>
        <div style={{ fontSize: 11, color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace", letterSpacing: '.05em', textTransform: 'uppercase', marginBottom: 4 }}>
          {isNew ? 'Add brand' : 'Edit brand'}
        </div>
        <div style={{ fontSize: 16, fontWeight: 600, color: 'var(--ink)', marginBottom: 14 }}>
          {editing.name || 'New brand'}
        </div>

        <Field label="Name">
          <input value={editing.name} onChange={update('name')} disabled={busy} placeholder="Wingstop" style={inputStyle}/>
        </Field>

        <Field label="Brand color">
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <input type="color" value={editing.color || '#C8553D'} onChange={update('color')} disabled={busy} style={{
              width: 44, height: 32, border: '1px solid var(--hair)', borderRadius: 6, padding: 0, cursor: 'pointer',
            }}/>
            <input value={editing.color || ''} onChange={update('color')} disabled={busy} placeholder="#C8553D" style={{ ...inputStyle, fontFamily: "'JetBrains Mono', monospace" }}/>
          </div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 8 }}>
            {presetColors.map(c => (
              <button
                key={c}
                onClick={() => setEditing({ ...editing, color: c })}
                title={c}
                disabled={busy}
                style={{
                  width: 22, height: 22, padding: 0, borderRadius: '50%', cursor: 'pointer',
                  background: c, border: editing.color === c ? '2px solid var(--ink)' : '1px solid var(--hair)',
                }}
              />
            ))}
          </div>
        </Field>

        <div style={{
          margin: '6px 0 14px', padding: '10px 12px', borderRadius: 8,
          background: 'var(--bg-2)', border: '1px solid var(--hair)',
          fontSize: 12, color: 'var(--muted)', display: 'flex', alignItems: 'center', gap: 10,
        }}>
          <span style={{ fontSize: 11.5, color: 'var(--muted)' }}>Preview:</span>
          <BrandChip brand={{ name: editing.name || 'Brand', color: editing.color }}/>
        </div>

        <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 18 }}>
          <button onClick={onCancel} disabled={busy} style={btnOutStyle(busy)}>Cancel</button>
          <button onClick={onSave} disabled={busy || !editing.name.trim()} style={btnPriStyle(busy || !editing.name.trim())}>
            {busy ? 'Saving…' : (isNew ? 'Add brand' : 'Save changes')}
          </button>
        </div>
      </div>
    </div>
  );
}

// The settings page doesn't define .d-btn — these recreate the
// alerts/documents-page button look inline so the Slack panel reads
// the same as the rest of the app.
const btnPriStyle = (disabled) => ({
  display: 'inline-flex', alignItems: 'center', gap: 6,
  padding: '8px 14px', borderRadius: 8, border: 0,
  background: disabled ? 'var(--muted-2)' : 'var(--primary)',
  color: 'var(--bg)', fontFamily: 'inherit', fontSize: 13, fontWeight: 500,
  cursor: disabled ? 'not-allowed' : 'pointer', whiteSpace: 'nowrap',
});
const btnOutStyle = (disabled) => ({
  display: 'inline-flex', alignItems: 'center', gap: 6,
  padding: '8px 14px', borderRadius: 8,
  border: '1px solid var(--hair)', background: 'var(--bg)',
  color: 'var(--ink)', fontFamily: 'inherit', fontSize: 13,
  cursor: disabled ? 'not-allowed' : 'pointer', whiteSpace: 'nowrap',
  opacity: disabled ? .5 : 1,
});

function SlackSettings() {
  const [status, setStatus] = React.useState(null);
  const [statusError, setStatusError] = React.useState(null);
  const [statusLoaded, setStatusLoaded] = React.useState(false);
  const [channels, setChannels] = React.useState([]);
  const [channelLoading, setChannelLoading] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [notice, setNotice] = React.useState(null);

  const refresh = React.useCallback(async () => {
    setStatusError(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/slack/workspace`, { cache: 'no-store' });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      setStatus(await res.json());
    } catch (e) {
      setStatusError(e.message || String(e));
      // Render the panel anyway so the user can read the error.
      setStatus({ connected: false, configured: false });
    } finally {
      setStatusLoaded(true);
    }
  }, []);

  React.useEffect(() => {
    refresh();
    // The OAuth callback redirects back to /settings?slack=connected (or error).
    const params = new URLSearchParams(window.location.search);
    const flag = params.get('slack');
    if (flag === 'connected') setNotice({ kind: 'ok', msg: 'Slack workspace connected.' });
    else if (flag === 'error') setNotice({ kind: 'err', msg: 'Slack install failed: ' + (params.get('reason') || 'unknown') });
    if (flag) {
      window.history.replaceState({}, '', window.location.pathname);
    }
  }, [refresh]);

  React.useEffect(() => {
    if (!status?.connected) return;
    setChannelLoading(true);
    fetch(`${SETTINGS_API_BASE}/slack/channels`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : [])
      .then(setChannels)
      .catch(() => setChannels([]))
      .finally(() => setChannelLoading(false));
  }, [status?.connected]);

  const onConnect = () => { window.location.href = `${SETTINGS_API_BASE}/slack/install`; };

  const onDisconnect = async () => {
    if (!confirm('Disconnect Slack? Future "Send to Slack" buttons will be inert until you reconnect.')) return;
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/slack/workspace`, { method: 'DELETE' });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      setStatus({ ...status, connected: false, teamName: null, teamId: null });
      setChannels([]);
      setNotice({ kind: 'ok', msg: 'Disconnected.' });
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Disconnect failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  const onPickDefault = async (e) => {
    const channelId = e.target.value;
    const channel = channels.find(c => c.id === channelId);
    if (!channel) return;
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/slack/default-channel`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ channelId: channel.id, channelName: channel.name }),
      });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      setStatus({ ...status, defaultChannelId: channel.id, defaultChannelName: channel.name });
    } catch (err) {
      setNotice({ kind: 'err', msg: 'Could not save default: ' + (err.message || err) });
    } finally { setBusy(false); }
  };

  const onTest = async () => {
    if (!status?.defaultChannelId) {
      setNotice({ kind: 'err', msg: 'Pick a default channel first.' });
      return;
    }
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/slack/post`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ channel: status.defaultChannelId, text: ':wave: Test message from Mise.' }),
      });
      if (!res.ok) throw new Error(String(res.status));
      setNotice({ kind: 'ok', msg: `Test message sent to #${status.defaultChannelName}.` });
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Test post failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  if (!statusLoaded) {
    return <div className="set-pane"><div className="set-page-head"><h1 className="serif set-h1">Slack &amp; Notifications</h1></div><div style={{ padding: 32, color: 'var(--muted)' }}>Loading…</div></div>;
  }

  return (
    <div className="set-pane">
      <div className="set-page-head">
        <h1 className="serif set-h1">Slack &amp; Notifications</h1>
        <p className="set-sub">Connect a Slack workspace so Mise can post alerts, daily briefs, and answers without leaving Slack. One workspace per organization for now.</p>
      </div>

      {notice && (
        <div style={{
          margin: '16px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5,
          background: notice.kind === 'ok' ? 'rgba(47,125,91,.08)' : 'rgba(178,58,42,.08)',
          color: notice.kind === 'ok' ? 'var(--green)' : 'var(--red)',
          border: `1px solid ${notice.kind === 'ok' ? 'rgba(47,125,91,.30)' : 'rgba(178,58,42,.30)'}`,
        }}>{notice.msg}</div>
      )}

      {statusError && (
        <div style={{
          margin: '16px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5,
          background: 'rgba(178,58,42,.08)', color: 'var(--red)',
          border: '1px solid rgba(178,58,42,.30)',
        }}>
          Couldn't reach the Slack status endpoint: {statusError}.
          Make sure the backend is running on {SETTINGS_API_BASE}.
        </div>
      )}

      {!status.configured && (
        <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)' }}>
          <div style={{ fontWeight: 600, marginBottom: 6 }}>Slack credentials not set on the server</div>
          <div style={{ fontSize: 13, color: 'var(--muted)' }}>
            An admin needs to add <code className="mono">copilot.slack.client-id</code>,
            <code className="mono"> client-secret</code>, and <code className="mono">signing-secret</code> to
            the server's environment. See <code className="mono">deploy/slack-manifest.json</code>.
          </div>
        </div>
      )}

      {status.configured && !status.connected && (
        <div style={{ marginTop: 18, padding: '24px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>
          <div>
            <div style={{ fontWeight: 600, fontSize: 15, marginBottom: 4 }}>No Slack workspace connected</div>
            <div style={{ fontSize: 13, color: 'var(--muted)', maxWidth: 520 }}>
              Click below to install the Mise Slack app to your workspace. You'll be redirected to Slack to choose
              the workspace and authorize the bot scopes (post, list channels, DM users).
            </div>
          </div>
          <button onClick={onConnect} disabled={busy} style={btnPriStyle(busy)}>
            <Ic name="plus" size={13}/> Add to Slack
          </button>
        </div>
      )}

      {status.connected && (
        <>
          <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)' }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
              <div>
                <div style={{ fontSize: 11, color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace", letterSpacing: '.05em', textTransform: 'uppercase' }}>Connected workspace</div>
                <div style={{ fontSize: 18, fontWeight: 600, color: 'var(--ink)', marginTop: 2 }}>{status.teamName}</div>
                <div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 2, fontFamily: "'JetBrains Mono', monospace" }}>team: {status.teamId} · bot: {status.botUserId}</div>
              </div>
              <button onClick={onDisconnect} disabled={busy} style={btnOutStyle(busy)}>Disconnect</button>
            </div>
          </div>

          <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)' }}>
            <div style={{ fontWeight: 600, marginBottom: 8 }}>Default channel</div>
            <div style={{ fontSize: 13, color: 'var(--muted)', marginBottom: 12 }}>
              Used when "Send to Slack" buttons don't pick a channel explicitly (e.g., daily briefs, alert sends without a per-store GM mapping).
            </div>
            <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
              <select
                value={status.defaultChannelId || ''}
                onChange={onPickDefault}
                disabled={busy || channelLoading || channels.length === 0}
                style={{ padding: '9px 12px', borderRadius: 8, border: '1px solid var(--hair)', background: 'var(--bg)', fontFamily: 'inherit', fontSize: 13, minWidth: 280 }}
              >
                <option value="" disabled>{channelLoading ? 'Loading channels…' : 'Pick a channel'}</option>
                {channels.map(c => (
                  <option key={c.id} value={c.id}>
                    {c.isPrivate ? '🔒' : '#'} {c.name}{!c.isMember && c.isPrivate ? ' (not a member)' : ''}
                  </option>
                ))}
              </select>
              <button onClick={onTest} disabled={busy || !status.defaultChannelId} style={btnOutStyle(busy)}>
                Send test message
              </button>
            </div>
            {status.defaultChannelName && (
              <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 8 }}>
                Currently saved: <span className="mono">#{status.defaultChannelName}</span>
              </div>
            )}
          </div>
        </>
      )}
    </div>
  );
}

const ROLE_OPTIONS = [
  'Director of Operations',
  'Area Coach',
  'GM',
  'Shift Lead',
  'Admin',
];

function UsersSettings() {
  const [users, setUsers] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [slackStatus, setSlackStatus] = React.useState(null);
  const [slackUsers, setSlackUsers] = React.useState([]);
  const [editing, setEditing] = React.useState(null); // null | {id?: string, ...form}
  const [busy, setBusy] = React.useState(false);
  const [notice, setNotice] = React.useState(null);

  const refresh = React.useCallback(() => {
    setError(null);
    fetch(`${SETTINGS_API_BASE}/users`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
      .then(setUsers)
      .catch(e => setError(e.message || String(e)))
      .finally(() => setLoading(false));
  }, []);

  React.useEffect(() => {
    refresh();
    fetch(`${SETTINGS_API_BASE}/slack/workspace`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : null)
      .then(s => {
        setSlackStatus(s);
        if (s?.connected) {
          fetch(`${SETTINGS_API_BASE}/slack/users`, { cache: 'no-store' })
            .then(r => r.ok ? r.json() : [])
            .then(setSlackUsers)
            .catch(() => setSlackUsers([]));
        }
      })
      .catch(() => setSlackStatus(null));
  }, [refresh]);

  React.useEffect(() => {
    if (!notice) return;
    const t = setTimeout(() => setNotice(null), 3500);
    return () => clearTimeout(t);
  }, [notice]);

  const startCreate = () => setEditing({
    name: '', email: '', role: 'GM', slackUserId: '', slackTeamId: '', assignedStoreNum: '',
  });
  const startEdit = (u) => setEditing({ ...u });

  const save = async () => {
    if (!editing) return;
    setBusy(true);
    try {
      const isNew = !editing.id;
      const url = isNew ? `${SETTINGS_API_BASE}/users` : `${SETTINGS_API_BASE}/users/${editing.id}`;
      const method = isNew ? 'POST' : 'PUT';
      const slackUser = slackUsers.find(s => s.id === editing.slackUserId);
      const body = {
        name: editing.name,
        email: editing.email,
        role: editing.role,
        slackUserId: editing.slackUserId || null,
        slackTeamId: slackUser?.teamId || (editing.slackUserId ? slackStatus?.teamId : null),
        assignedStoreNum: editing.assignedStoreNum || null,
      };
      const res = await fetch(url, {
        method,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      setEditing(null);
      setNotice({ kind: 'ok', msg: isNew ? `Added ${editing.name}.` : `Updated ${editing.name}.` });
      await refresh();
    } catch (e) {
      setNotice({ kind: 'err', msg: e.message || String(e) });
    } finally {
      setBusy(false);
    }
  };

  const remove = async (u) => {
    if (!confirm(`Remove ${u.name}? They won't be able to be reassigned to a store until re-added.`)) return;
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/users/${u.id}`, { method: 'DELETE' });
      if (!res.ok && res.status !== 204) throw new Error(`${res.status}`);
      setNotice({ kind: 'ok', msg: `Removed ${u.name}.` });
      await refresh();
    } catch (e) {
      setNotice({ kind: 'err', msg: e.message || String(e) });
    } finally {
      setBusy(false);
    }
  };

  const slackById = new Map(slackUsers.map(s => [s.id, s]));

  return (
    <div className="set-pane">
      <div className="set-page-head" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16 }}>
        <div>
          <h1 className="serif set-h1">Users &amp; Roles</h1>
          <p className="set-sub">People in your organization. Linking a Slack profile lets Mise DM the right person when an alert fires for their store.</p>
        </div>
        <button onClick={startCreate} style={btnPriStyle(busy)} disabled={busy}>
          <Ic name="plus" size={13}/> Invite user
        </button>
      </div>

      {notice && (
        <div style={{
          margin: '14px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5,
          background: notice.kind === 'ok' ? 'rgba(47,125,91,.08)' : 'rgba(178,58,42,.08)',
          color: notice.kind === 'ok' ? 'var(--green)' : 'var(--red)',
          border: `1px solid ${notice.kind === 'ok' ? 'rgba(47,125,91,.30)' : 'rgba(178,58,42,.30)'}`,
        }}>{notice.msg}</div>
      )}

      {error && (
        <div style={{ marginTop: 12, padding: 10, background: 'rgba(178,58,42,.08)', border: '1px solid rgba(178,58,42,.30)', borderRadius: 8, fontSize: 12.5, color: 'var(--red)' }}>
          Error loading users: {error}
        </div>
      )}

      {!slackStatus?.connected && (
        <div style={{ marginTop: 14, padding: '12px 14px', background: 'var(--bg-2)', border: '1px solid var(--hair)', borderRadius: 8, fontSize: 12.5, color: 'var(--muted)' }}>
          Slack workspace isn't connected — you can still add users, but the Slack profile picker will be empty until you <a href="/settings?section=slack" style={{ color: 'var(--accent)' }}>connect Slack</a>.
        </div>
      )}

      <div style={{ marginTop: 18, border: '1px solid var(--hair)', borderRadius: 12, overflow: 'hidden', background: 'var(--bg)' }}>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1.2fr 130px 90px 1.4fr 70px', gap: 12, padding: '11px 16px', background: 'var(--bg-2)', borderBottom: '1px solid var(--hair)', fontFamily: "'JetBrains Mono', monospace", fontSize: 9.5, letterSpacing: '.06em', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 500 }}>
          <div>Name</div><div>Email</div><div>Role</div><div>Store</div><div>Slack</div><div></div>
        </div>
        {loading && users.length === 0 && (
          <div style={{ padding: 30, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>Loading…</div>
        )}
        {!loading && users.length === 0 && (
          <div style={{ padding: 40, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>
            No users yet. Click <strong style={{ color: 'var(--ink)' }}>Invite user</strong> to add one.
          </div>
        )}
        {users.map(u => {
          const slack = u.slackUserId ? slackById.get(u.slackUserId) : null;
          return (
            <div key={u.id} style={{ display: 'grid', gridTemplateColumns: '1fr 1.2fr 130px 90px 1.4fr 70px', gap: 12, padding: '14px 16px', alignItems: 'center', borderBottom: '1px solid var(--hair-2)', fontSize: 13 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                {slack?.avatarUrl
                  ? <img src={slack.avatarUrl} alt="" style={{ width: 28, height: 28, borderRadius: '50%' }}/>
                  : <span style={{ width: 28, height: 28, borderRadius: '50%', background: 'var(--primary)', color: 'var(--bg)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, fontWeight: 600 }}>{u.name.split(/\s+/).slice(0,2).map(p => p[0]).join('').toUpperCase()}</span>}
                <span style={{ fontWeight: 500 }}>{u.name}</span>
              </div>
              <div style={{ color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace", fontSize: 11.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{u.email}</div>
              <div>
                <span style={{ display: 'inline-block', padding: '2px 10px', borderRadius: 999, background: 'var(--bg-2)', border: '1px solid var(--hair)', fontSize: 11.5 }}>{u.role}</span>
              </div>
              <div className="mono" style={{ color: 'var(--muted)' }}>{u.assignedStoreNum || '—'}</div>
              <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 11.5, color: 'var(--muted)' }}>
                {slack ? `@${slack.name}` : (u.slackUserId ? u.slackUserId : 'not linked')}
              </div>
              <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 6 }}>
                <button onClick={() => startEdit(u)} className="ack-btn" disabled={busy}>Edit</button>
                <button onClick={() => remove(u)} className="ack-btn" disabled={busy} style={{ color: 'var(--red)' }}>×</button>
              </div>
            </div>
          );
        })}
      </div>

      {editing && (
        <UserEditModal
          editing={editing}
          setEditing={setEditing}
          onSave={save}
          onCancel={() => setEditing(null)}
          slackUsers={slackUsers}
          stores={window.MISE.stores}
          busy={busy}
        />
      )}
    </div>
  );
}

function UserEditModal({ editing, setEditing, onSave, onCancel, slackUsers, stores, busy }) {
  const update = (k) => (e) => setEditing({ ...editing, [k]: e.target.value });
  return (
    <div onClick={busy ? null : onCancel} style={{
      position: 'fixed', inset: 0, background: 'rgba(11,27,38,.45)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: 'var(--bg)', borderRadius: 12, padding: 24, width: 480, maxWidth: '92vw',
        boxShadow: '0 20px 60px rgba(11,27,38,.25)', border: '1px solid var(--hair)',
      }}>
        <div style={{ fontSize: 11, color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace", letterSpacing: '.05em', textTransform: 'uppercase', marginBottom: 4 }}>
          {editing.id ? 'Edit user' : 'Invite user'}
        </div>
        <div style={{ fontSize: 16, fontWeight: 600, color: 'var(--ink)', marginBottom: 14 }}>{editing.name || 'New user'}</div>

        <Field label="Name"><input value={editing.name} onChange={update('name')} disabled={busy} style={inputStyle}/></Field>
        <Field label="Email"><input type="email" value={editing.email} onChange={update('email')} disabled={busy} style={inputStyle}/></Field>
        <Field label="Role">
          <select value={editing.role} onChange={update('role')} disabled={busy} style={inputStyle}>
            {ROLE_OPTIONS.map(r => <option key={r} value={r}>{r}</option>)}
          </select>
        </Field>
        <Field label="Assigned store (GMs only)">
          <select value={editing.assignedStoreNum || ''} onChange={update('assignedStoreNum')} disabled={busy} style={inputStyle}>
            <option value="">— None —</option>
            {(stores || []).map(s => (
              <option key={s.num} value={s.num}>{s.num} {s.name}</option>
            ))}
          </select>
        </Field>
        <Field label="Slack profile">
          <select value={editing.slackUserId || ''} onChange={update('slackUserId')} disabled={busy || slackUsers.length === 0} style={inputStyle}>
            <option value="">{slackUsers.length === 0 ? '— Slack not connected —' : '— None —'}</option>
            {slackUsers.map(s => (
              <option key={s.id} value={s.id}>{s.realName || s.name} {s.email ? `· ${s.email}` : ''}</option>
            ))}
          </select>
        </Field>

        <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 18 }}>
          <button onClick={onCancel} disabled={busy} style={btnOutStyle(busy)}>Cancel</button>
          <button onClick={onSave} disabled={busy || !editing.name || !editing.email} style={btnPriStyle(busy || !editing.name || !editing.email)}>
            {busy ? 'Saving…' : (editing.id ? 'Save changes' : 'Add user')}
          </button>
        </div>
      </div>
    </div>
  );
}

const inputStyle = {
  width: '100%', padding: '9px 10px', borderRadius: 8,
  border: '1px solid var(--hair)', background: 'var(--bg)',
  fontFamily: 'inherit', fontSize: 13, color: 'var(--ink)',
};

function Field({ label, children }) {
  return (
    <div style={{ marginBottom: 12 }}>
      <label style={{ display: 'block', fontSize: 11, color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace", letterSpacing: '.05em', textTransform: 'uppercase', marginBottom: 6 }}>{label}</label>
      {children}
    </div>
  );
}

function SquareSettings({ kind = 'pos' }) {
  // POS and Labor both flow from the same Square workspace, so this
  // panel renders identically under either section — only the heading
  // and intro copy change. Adding 7shifts/Homebase later means
  // splitting the labor route into its own component.
  const heading = kind === 'labor' ? 'Labor & Scheduling · Square' : 'POS · Square';
  const headingShort = kind === 'labor' ? 'Labor & Scheduling' : 'POS';
  const [status, setStatus] = React.useState(null);
  const [statusError, setStatusError] = React.useState(null);
  const [statusLoaded, setStatusLoaded] = React.useState(false);
  const [locations, setLocations] = React.useState([]);
  const [locsLoading, setLocsLoading] = React.useState(false);
  const [locsError, setLocsError] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [notice, setNotice] = React.useState(null);

  const refresh = React.useCallback(async () => {
    setStatusError(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/square/workspace`, { cache: 'no-store' });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      setStatus(await res.json());
    } catch (e) {
      setStatusError(e.message || String(e));
      setStatus({ connected: false, configured: false, environment: 'sandbox' });
    } finally {
      setStatusLoaded(true);
    }
  }, []);

  React.useEffect(() => {
    refresh();
    const params = new URLSearchParams(window.location.search);
    const flag = params.get('square');
    if (flag === 'connected') setNotice({ kind: 'ok', msg: 'Square workspace connected.' });
    else if (flag === 'error') setNotice({ kind: 'err', msg: 'Square install failed: ' + (params.get('reason') || 'unknown') });
    if (flag) {
      const next = new URLSearchParams(window.location.search);
      next.delete('square');
      next.delete('reason');
      window.history.replaceState({}, '', window.location.pathname + (next.toString() ? '?' + next : ''));
    }
  }, [refresh]);

  React.useEffect(() => {
    if (!notice) return;
    const t = setTimeout(() => setNotice(null), 4500);
    return () => clearTimeout(t);
  }, [notice]);

  const [syncStatus, setSyncStatus] = React.useState(null);
  const [syncing, setSyncing] = React.useState(false);

  const refreshSync = React.useCallback(() => {
    fetch(`${SETTINGS_API_BASE}/square/sync/status`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : null)
      .then(setSyncStatus)
      .catch(() => setSyncStatus(null));
  }, []);

  React.useEffect(() => {
    if (!status?.connected) return;
    setLocsLoading(true);
    setLocsError(null);
    fetch(`${SETTINGS_API_BASE}/square/locations`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
      .then(setLocations)
      .catch(e => setLocsError(e.message || String(e)))
      .finally(() => setLocsLoading(false));
    refreshSync();
  }, [status?.connected, refreshSync]);

  const onSyncNow = async () => {
    setSyncing(true);
    setNotice(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/square/sync?days=30`, { method: 'POST' });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      const r = await res.json();
      if (r.error) {
        setNotice({ kind: 'err', msg: 'Sync failed: ' + r.error });
      } else {
        setNotice({ kind: 'ok', msg: `Synced ${r.locationsSynced} location${r.locationsSynced === 1 ? '' : 's'} · ${r.ordersFetched} orders · ${r.shiftsFetched ?? 0} shifts · ${r.inventoryRowsWritten ?? 0} inventory rows · ${(r.elapsedMs/1000).toFixed(1)}s` });
      }
      refreshSync();
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Sync request failed: ' + (e.message || e) });
    } finally {
      setSyncing(false);
    }
  };

  const onConnect = () => { window.location.href = `${SETTINGS_API_BASE}/square/install`; };

  const onDisconnect = async () => {
    if (!status?.merchantId) return;
    if (!confirm('Disconnect Square? Mapped locations will also be cleared. Reinstalling restores them later.')) return;
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/square/workspace?merchantId=${encodeURIComponent(status.merchantId)}`, { method: 'DELETE' });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      setStatus({ ...status, connected: false, merchantId: null, businessName: null });
      setLocations([]);
      setNotice({ kind: 'ok', msg: 'Disconnected.' });
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Disconnect failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  const onMap = async (locationId, locationName, miseStoreNum) => {
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/square/locations/map`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ squareLocationId: locationId, locationName, miseStoreNum: miseStoreNum || null }),
      });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      setLocations(curr => curr.map(l => l.id === locationId ? { ...l, mappedToStoreNum: miseStoreNum || null } : l));
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Could not save mapping: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  // Disconnected providers no longer render a panel — the section
  // wrapper handles the "Add integration" CTA. Loading swallowed
  // for the same reason.
  if (!statusLoaded || !status?.connected) return null;

  return (
    <div className="set-pane">
      <div className="set-page-head">
        <h1 className="serif set-h1">{heading}</h1>
        <p className="set-sub">
          Connect Square to pull sales, payments, and labor into Mise. After install, map each Square location to a Mise store.
          {' '}<span className="mono" style={{ color: 'var(--muted)' }}>environment: {status.environment}</span>
        </p>
      </div>

      {notice && (
        <div style={{
          margin: '14px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5,
          background: notice.kind === 'ok' ? 'rgba(47,125,91,.08)' : 'rgba(178,58,42,.08)',
          color: notice.kind === 'ok' ? 'var(--green)' : 'var(--red)',
          border: `1px solid ${notice.kind === 'ok' ? 'rgba(47,125,91,.30)' : 'rgba(178,58,42,.30)'}`,
        }}>{notice.msg}</div>
      )}

      {statusError && (
        <div style={{ margin: '12px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5, background: 'rgba(178,58,42,.08)', color: 'var(--red)', border: '1px solid rgba(178,58,42,.30)' }}>
          Couldn't reach the Square status endpoint: {statusError}
        </div>
      )}

      {/* Disconnected / unconfigured states are no longer rendered
          inline — the section wrapper (PosSettings / LaborSettings)
          surfaces an "Add integration" picker for those cases. */}
      {!status.connected && null}

      {status.connected && (
        <>
          <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
            <div>
              <div style={{ fontSize: 11, color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace", letterSpacing: '.05em', textTransform: 'uppercase' }}>Connected merchant</div>
              <div style={{ fontSize: 18, fontWeight: 600, color: 'var(--ink)', marginTop: 2 }}>{status.businessName || status.merchantId}</div>
              <div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 2, fontFamily: "'JetBrains Mono', monospace" }}>merchant: {status.merchantId} · environment: {status.environment}</div>
            </div>
            <button onClick={onDisconnect} disabled={busy} style={btnOutStyle(busy)}>Disconnect</button>
          </div>

          <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
            <div>
              <div style={{ fontWeight: 600, marginBottom: 4 }}>Sales sync</div>
              <div style={{ fontSize: 12.5, color: 'var(--muted)' }}>
                {syncStatus?.lastSyncedAt
                  ? <>Last synced <span className="mono">{new Date(syncStatus.lastSyncedAt).toLocaleString()}</span> through <span className="mono">{syncStatus.lastSyncThrough}</span> · status: <span className="mono">{syncStatus.lastStatus}</span>{syncStatus.lastError ? ` (${syncStatus.lastError})` : ''}</>
                  : 'Never synced. Map at least one location below, then click Sync now.'}
              </div>
            </div>
            <button onClick={onSyncNow} disabled={syncing || busy} style={btnPriStyle(syncing || busy)}>
              <Ic name="sync" size={13}/> {syncing ? 'Syncing…' : 'Sync now (30d)'}
            </button>
          </div>

          <div style={{ marginTop: 18, border: '1px solid var(--hair)', borderRadius: 12, overflow: 'hidden', background: 'var(--bg)' }}>
            <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--hair-2)' }}>
              <div style={{ fontWeight: 600 }}>Map Square locations to Mise stores</div>
              <div style={{ fontSize: 12.5, color: 'var(--muted)', marginTop: 2 }}>
                Each Square location can map to one Mise store. Locations left unmapped are still pulled but won't surface anywhere yet.
              </div>
            </div>
            {locsLoading && (
              <div style={{ padding: 24, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>Loading locations…</div>
            )}
            {locsError && (
              <div style={{ margin: 16, padding: 10, background: 'rgba(178,58,42,.08)', border: '1px solid rgba(178,58,42,.30)', borderRadius: 8, fontSize: 12.5, color: 'var(--red)' }}>
                Couldn't fetch locations: {locsError}
              </div>
            )}
            {!locsLoading && !locsError && locations.length === 0 && (
              <div style={{ padding: 30, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>No Square locations on this merchant.</div>
            )}
            {locations.map(l => (
              <div key={l.id} style={{ display: 'grid', gridTemplateColumns: '1fr 240px', gap: 16, padding: '14px 18px', alignItems: 'center', borderBottom: '1px solid var(--hair-2)' }}>
                <div>
                  <div style={{ fontSize: 13.5, fontWeight: 500 }}>{l.name}</div>
                  <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 2 }}>
                    {l.address || '—'} · <span className="mono">{l.id}</span> · {l.status}
                  </div>
                </div>
                <select
                  value={l.mappedToStoreNum || ''}
                  onChange={(e) => onMap(l.id, l.name, e.target.value)}
                  disabled={busy}
                  style={{ padding: '8px 10px', borderRadius: 8, border: '1px solid var(--hair)', background: 'var(--bg)', fontFamily: 'inherit', fontSize: 13 }}
                >
                  <option value="">— Unmapped —</option>
                  {window.MISE.stores.map(s => (
                    <option key={s.num} value={s.num}>{s.num} {s.name}</option>
                  ))}
                </select>
              </div>
            ))}
          </div>
        </>
      )}
    </div>
  );
}

/** Provider registry. Each entry knows how to fetch its connection
 *  status and where to send the user to start an OAuth install. The
 *  picker walks this list to build the menu of connectable providers,
 *  so adding a new integration (e.g. Toast, 7shifts, Jolt) is just
 *  one row here plus its panel component. */
const INTEGRATION_PROVIDERS = {
  square: {
    id: 'square', label: 'Square', categories: ['pos', 'labor'],
    statusUrl: '/square/workspace',
    installUrl: '/square/install',
    Logo: () => <SquareLogo/>,
  },
  clover: {
    id: 'clover', label: 'Clover', categories: ['pos', 'labor'],
    statusUrl: '/clover/workspace',
    installUrl: '/clover/install',
    Logo: () => <CloverLogo/>,
  },
  intuit: {
    id: 'intuit', label: 'QuickBooks Online', categories: ['accounting'],
    statusUrl: '/intuit/workspace',
    installUrl: '/intuit/install',
    Logo: () => <QuickBooksLogo/>,
  },
  deputy: {
    id: 'deputy', label: 'Deputy', categories: ['labor'],
    statusUrl: '/deputy/workspace',
    // Deputy needs a setup form before OAuth, so the picker reveals
    // the panel inline instead of jumping straight to install.
    installUrl: null,
    // BYOA: "configured" means per-org BYOA creds are saved; auto-
    // render the panel from then on so the user can finish OAuth.
    autoRender: true,
    Logo: () => <DeputyLogo/>,
  },
  shopify: {
    id: 'shopify', label: 'Shopify', categories: ['pos'],
    statusUrl: '/shopify/workspace',
    // Marketplace OAuth, but the authorize URL lives on each shop's
    // own subdomain so the panel collects the shop domain before
    // redirecting. "configured" only means server env is set — the
    // user must click into the picker to start a connection.
    installUrl: null,
    autoRender: false,
    Logo: () => <ShopifyLogo/>,
  },
  lightspeed: {
    id: 'lightspeed', label: 'Lightspeed Retail', categories: ['pos'],
    statusUrl: '/lightspeed/workspace',
    // Like Shopify, X-series authorize takes a domain_prefix query
    // param so we collect it via the inline panel form.
    installUrl: null,
    autoRender: false,
    Logo: () => <LightspeedLogo/>,
  },
};

/** Hook: fetch all relevant providers' status for a category and
 *  surface which are connected vs. configured-but-unconnected. */
function useIntegrationStatuses(category) {
  const [statuses, setStatuses] = React.useState({});
  const [loaded, setLoaded] = React.useState(false);

  React.useEffect(() => {
    const ids = Object.values(INTEGRATION_PROVIDERS)
      .filter(p => p.categories.includes(category))
      .map(p => p.id);
    Promise.all(ids.map(id =>
      fetch(`${SETTINGS_API_BASE}${INTEGRATION_PROVIDERS[id].statusUrl}`, { cache: 'no-store' })
        .then(r => r.ok ? r.json() : null)
        .then(s => [id, s])
        .catch(() => [id, null])
    )).then(pairs => {
      setStatuses(Object.fromEntries(pairs));
      setLoaded(true);
    });
  }, [category]);

  return { statuses, loaded };
}

/** Renders a category section: connected provider panels stacked, plus
 *  an "Add integration" button that drops down a list of unconnected
 *  configured providers. Empty state for nothing-connected. */
function IntegrationSection({ category, panelMap }) {
  const { statuses, loaded } = useIntegrationStatuses(category);
  // BYOA providers (no installUrl) reveal their setup panel inline
  // when the user clicks them in the picker.
  const [revealed, setRevealed] = React.useState(new Set());

  const visible = [];
  const candidates = [];
  for (const p of Object.values(INTEGRATION_PROVIDERS)) {
    if (!p.categories.includes(category)) continue;
    const s = statuses[p.id];
    const Panel = panelMap[p.id];
    if (!Panel) continue;

    // Auto-render rules:
    //   - already connected → render
    //   - user clicked picker → render (revealed)
    //   - BYOA-style provider (autoRender) and configured → render
    //     (Deputy: per-org credentials saved means there's state worth
    //     showing immediately, including the Authorize button)
    // Otherwise show in the picker if the server is configured. BYOA
    // providers (e.g. Deputy) ARE pickable as soon as the server's
    // encryption is ready — that's the chicken-and-egg break-out so
    // operators can start the BYOA setup from the picker.
    const auto = !!p.autoRender;
    const pickable = s?.configured || (auto && s?.encryptionReady);
    if (s?.connected || revealed.has(p.id) || (auto && s?.configured)) {
      visible.push(<Panel key={p.id}/>);
    } else if (pickable) {
      candidates.push(p);
    }
  }

  const onPick = (provider) => {
    if (provider.installUrl) {
      window.location.href = `${SETTINGS_API_BASE}${provider.installUrl}`;
    } else {
      setRevealed(prev => { const next = new Set(prev); next.add(provider.id); return next; });
    }
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 24, paddingBottom: 240 }}>
      {!loaded && (
        <div className="set-pane">
          <div style={{ padding: 32, color: 'var(--muted)' }}>Loading integrations…</div>
        </div>
      )}

      {visible}

      {loaded && visible.length === 0 && (
        <EmptyIntegrationState category={category} candidates={candidates} onPick={onPick}/>
      )}

      {loaded && visible.length > 0 && candidates.length > 0 && (
        <div className="set-pane">
          <div style={{
            padding: '20px 24px', border: '1px dashed var(--hair)',
            borderRadius: 12, background: 'transparent',
            display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8,
          }}>
            <div style={{ fontSize: 12, color: 'var(--muted)' }}>
              Need another provider?
            </div>
            <AddIntegrationButton candidates={candidates} onPick={onPick}/>
          </div>
        </div>
      )}
    </div>
  );
}

function EmptyIntegrationState({ category, candidates, onPick }) {
  const label = category === 'pos' ? 'POS'
    : category === 'labor' ? 'Labor & Scheduling'
    : category === 'accounting' ? 'Accounting'
    : 'Food Safety';
  return (
    <div className="set-pane">
      <div className="set-page-head">
        <h1 className="serif set-h1">{label}</h1>
        <p className="set-sub">No {label.toLowerCase()} integrations connected yet.</p>
      </div>
      <div style={{
        marginTop: 18, padding: '36px 24px', border: '1px dashed var(--hair)',
        borderRadius: 12, textAlign: 'center', background: 'var(--bg)',
      }}>
        {candidates.length > 0 ? (
          <>
            <div style={{ fontSize: 13.5, color: 'var(--muted)', marginBottom: 14 }}>
              Connect one of the supported providers to start syncing data.
            </div>
            <AddIntegrationButton candidates={candidates} onPick={onPick} primary/>
          </>
        ) : (
          <div style={{ fontSize: 13, color: 'var(--muted)' }}>
            No providers configured on the server. Ask an admin to add credentials for one of the supported integrations.
          </div>
        )}
      </div>
    </div>
  );
}

function AddIntegrationButton({ candidates, onPick, primary = false }) {
  const [open, setOpen] = React.useState(false);
  if (candidates.length === 0) return null;
  const handleClick = (provider) => (e) => {
    e.preventDefault();
    setOpen(false);
    onPick(provider);
  };
  return (
    <div style={{ position: 'relative', display: 'inline-block', alignSelf: 'center' }}>
      <button
        onClick={() => setOpen(o => !o)}
        style={primary ? btnPriStyle(false) : btnOutStyle(false)}
      >
        <Ic name="plus" size={13}/> Add integration
      </button>
      {open && (
        <>
          <div
            onClick={() => setOpen(false)}
            style={{ position: 'fixed', inset: 0, zIndex: 40 }}
          />
          <div style={{
            position: 'absolute',
            top: 'calc(100% + 6px)',
            left: '50%',
            transform: 'translateX(-50%)',
            zIndex: 41,
            minWidth: 260,
            background: 'var(--bg)',
            border: '1px solid var(--hair)',
            borderRadius: 10,
            boxShadow: '0 8px 24px rgba(11,27,38,.16)',
            overflow: 'hidden',
          }}>
            <div style={{ padding: '10px 14px', fontSize: 10.5, letterSpacing: '.08em', textTransform: 'uppercase', color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace", borderBottom: '1px solid var(--hair-2)' }}>
              Available providers
            </div>
            {candidates.map(p => (
              <a
                key={p.id}
                href={p.installUrl ? `${SETTINGS_API_BASE}${p.installUrl}` : '#'}
                onClick={handleClick(p)}
                style={{
                  display: 'flex', alignItems: 'center', gap: 10,
                  padding: '12px 14px', textDecoration: 'none',
                  color: 'var(--ink)', fontSize: 13.5,
                  borderBottom: '1px solid var(--hair-2)',
                }}
              >
                <p.Logo/>
                <span style={{ flex: 1 }}>{p.label}</span>
                <span style={{ fontSize: 11.5, color: 'var(--muted)' }}>
                  {p.installUrl ? 'Connect →' : 'Set up →'}
                </span>
              </a>
            ))}
          </div>
        </>
      )}
    </div>
  );
}

/** Shows every wired POS provider stacked. Square + Clover both cover
 *  POS and Labor with one OAuth. Unconnected providers are hidden;
 *  the "Add integration" picker exposes them on demand. */
function PosSettings() {
  return <IntegrationSection category="pos" panelMap={{
    square: () => <SquareSettings kind="pos"/>,
    clover: () => <CloverSettings kind="pos"/>,
    shopify: () => <ShopifySettings/>,
    lightspeed: () => <LightspeedSettings/>,
  }}/>;
}

function LaborSettings() {
  return <IntegrationSection category="labor" panelMap={{
    square: () => <SquareSettings kind="labor"/>,
    clover: () => <CloverSettings kind="labor"/>,
    deputy: () => <DeputySettings/>,
  }}/>;
}

/** Food Safety section. No providers wired yet (Jolt would land
 *  here); the picker just shows "no providers configured" until then. */
function SafetySettings() {
  return <IntegrationSection category="safety" panelMap={{}}/>;
}

/** Accounting section — QuickBooks Online today; Sage / R365 later. */
function AccountingSettings() {
  return <IntegrationSection category="accounting" panelMap={{
    intuit: () => <IntuitSettings/>,
  }}/>;
}

function CloverSettings({ kind = 'pos' }) {
  // Same logic under POS and Labor — Clover's one OAuth covers both.
  const heading = kind === 'labor' ? 'Labor & Scheduling · Clover' : 'POS · Clover';
  const headingShort = kind === 'labor' ? 'Labor & Scheduling' : 'POS';
  const subCopy = kind === 'labor'
    ? 'Pull employee shifts from Clover into Mise. Each Clover location is its own merchant — install once per location, then map each merchant to a Mise store.'
    : 'Connect Clover to pull sales (and labor) into Mise. Each Clover location is its own merchant — install the app once per location, then map each merchant to a Mise store.';
  const [status, setStatus] = React.useState(null);
  const [statusError, setStatusError] = React.useState(null);
  const [statusLoaded, setStatusLoaded] = React.useState(false);
  const [locations, setLocations] = React.useState([]);
  const [locsLoading, setLocsLoading] = React.useState(false);
  const [locsError, setLocsError] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [notice, setNotice] = React.useState(null);
  const [syncStatus, setSyncStatus] = React.useState(null);
  const [syncing, setSyncing] = React.useState(false);

  const refreshSync = React.useCallback(() => {
    fetch(`${SETTINGS_API_BASE}/clover/sync/status`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : null)
      .then(setSyncStatus)
      .catch(() => setSyncStatus(null));
  }, []);

  const onSyncNow = async () => {
    setSyncing(true);
    setNotice(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/clover/sync?days=30`, { method: 'POST' });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      const r = await res.json();
      if (r.error) {
        setNotice({ kind: 'err', msg: 'Sync failed: ' + r.error });
      } else {
        setNotice({ kind: 'ok', msg: `Synced ${r.merchantsSynced} merchant${r.merchantsSynced === 1 ? '' : 's'} · ${r.ordersFetched} orders · ${r.shiftsFetched ?? 0} shifts · ${r.daysWritten} days · ${(r.elapsedMs/1000).toFixed(1)}s` });
      }
      refreshSync();
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Sync request failed: ' + (e.message || e) });
    } finally {
      setSyncing(false);
    }
  };

  const refresh = React.useCallback(async () => {
    setStatusError(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/clover/workspace`, { cache: 'no-store' });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      setStatus(await res.json());
    } catch (e) {
      setStatusError(e.message || String(e));
      setStatus({ connected: false, configured: false, environment: 'sandbox', merchantCount: 0 });
    } finally {
      setStatusLoaded(true);
    }
  }, []);

  React.useEffect(() => {
    refresh();
    const params = new URLSearchParams(window.location.search);
    const flag = params.get('clover');
    if (flag === 'connected') setNotice({ kind: 'ok', msg: 'Clover merchant connected.' });
    else if (flag === 'error') setNotice({ kind: 'err', msg: 'Clover install failed: ' + (params.get('reason') || 'unknown') });
    if (flag) {
      const next = new URLSearchParams(window.location.search);
      next.delete('clover');
      next.delete('reason');
      window.history.replaceState({}, '', window.location.pathname + (next.toString() ? '?' + next : ''));
    }
  }, [refresh]);

  React.useEffect(() => {
    if (!notice) return;
    const t = setTimeout(() => setNotice(null), 4500);
    return () => clearTimeout(t);
  }, [notice]);

  React.useEffect(() => {
    if (!status?.connected) return;
    setLocsLoading(true);
    setLocsError(null);
    fetch(`${SETTINGS_API_BASE}/clover/locations`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
      .then(setLocations)
      .catch(e => setLocsError(e.message || String(e)))
      .finally(() => setLocsLoading(false));
    refreshSync();
  }, [status?.connected, refreshSync]);

  const onConnect = () => { window.location.href = `${SETTINGS_API_BASE}/clover/install`; };

  const onDisconnect = async (merchantId) => {
    if (!merchantId) return;
    if (!confirm('Disconnect this Clover merchant? Mapped store will also be cleared.')) return;
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/clover/workspace?merchantId=${encodeURIComponent(merchantId)}`, { method: 'DELETE' });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      await refresh();
      setLocations(curr => curr.filter(l => l.id !== merchantId));
      setNotice({ kind: 'ok', msg: 'Disconnected.' });
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Disconnect failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  const onMap = async (merchantId, locationName, miseStoreNum) => {
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/clover/locations/map`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ cloverMerchantId: merchantId, locationName, miseStoreNum: miseStoreNum || null }),
      });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      setLocations(curr => curr.map(l => l.id === merchantId ? { ...l, mappedToStoreNum: miseStoreNum || null } : l));
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Could not save mapping: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  if (!statusLoaded || !status?.connected) return null;

  return (
    <div className="set-pane">
      <div className="set-page-head">
        <h1 className="serif set-h1">{heading}</h1>
        <p className="set-sub">
          {subCopy}
          {' '}<span className="mono" style={{ color: 'var(--muted)' }}>environment: {status.environment}</span>
        </p>
      </div>

      {notice && (
        <div style={{
          margin: '14px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5,
          background: notice.kind === 'ok' ? 'rgba(47,125,91,.08)' : 'rgba(178,58,42,.08)',
          color: notice.kind === 'ok' ? 'var(--green)' : 'var(--red)',
          border: `1px solid ${notice.kind === 'ok' ? 'rgba(47,125,91,.30)' : 'rgba(178,58,42,.30)'}`,
        }}>{notice.msg}</div>
      )}

      {statusError && (
        <div style={{ margin: '12px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5, background: 'rgba(178,58,42,.08)', color: 'var(--red)', border: '1px solid rgba(178,58,42,.30)' }}>
          Couldn't reach the Clover status endpoint: {statusError}
        </div>
      )}

      {/* "Connected merchant(s)" header card — Clover supports multiple
          merchants per org, so this stays even when connected. */}
      <div style={{ marginTop: 18, padding: '24px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>
        <div>
          <div style={{ fontWeight: 600, fontSize: 15, marginBottom: 4 }}>
            {status.merchantCount} Clover merchant{status.merchantCount === 1 ? '' : 's'} connected
          </div>
          <div style={{ fontSize: 13, color: 'var(--muted)', maxWidth: 560 }}>
            Each Clover merchant ≈ one location. Add another below or in the section's "Add integration" picker.
          </div>
        </div>
        <button onClick={onConnect} disabled={busy} style={btnOutStyle(busy)}>
          <Ic name="plus" size={13}/> Add another merchant
        </button>
      </div>

      {status.connected && (
        <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
          <div>
            <div style={{ fontWeight: 600, marginBottom: 4 }}>Sales sync</div>
            <div style={{ fontSize: 12.5, color: 'var(--muted)' }}>
              {syncStatus?.lastSyncedAt
                ? <>Last synced <span className="mono">{new Date(syncStatus.lastSyncedAt).toLocaleString()}</span> through <span className="mono">{syncStatus.lastSyncThrough}</span> · status: <span className="mono">{syncStatus.lastStatus}</span>{syncStatus.lastError ? ` (${syncStatus.lastError})` : ''}</>
                : 'Never synced. Map at least one merchant below, then click Sync now.'}
            </div>
          </div>
          <button onClick={onSyncNow} disabled={syncing || busy} style={btnPriStyle(syncing || busy)}>
            <Ic name="sync" size={13}/> {syncing ? 'Syncing…' : 'Sync now (30d)'}
          </button>
        </div>
      )}

      {status.connected && (
        <div style={{ marginTop: 18, border: '1px solid var(--hair)', borderRadius: 12, overflow: 'hidden', background: 'var(--bg)' }}>
          <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--hair-2)' }}>
            <div style={{ fontWeight: 600 }}>Map Clover merchants to Mise stores</div>
            <div style={{ fontSize: 12.5, color: 'var(--muted)', marginTop: 2 }}>
              Each Clover merchant maps to one Mise store. Unmapped merchants are still authorized but won't surface anywhere yet.
            </div>
          </div>
          {locsLoading && (
            <div style={{ padding: 24, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>Loading merchants…</div>
          )}
          {locsError && (
            <div style={{ margin: 16, padding: 10, background: 'rgba(178,58,42,.08)', border: '1px solid rgba(178,58,42,.30)', borderRadius: 8, fontSize: 12.5, color: 'var(--red)' }}>
              Couldn't fetch merchants: {locsError}
            </div>
          )}
          {!locsLoading && !locsError && locations.length === 0 && (
            <div style={{ padding: 30, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>No Clover merchants on this org.</div>
          )}
          {locations.map(l => (
            <div key={l.id} style={{ display: 'grid', gridTemplateColumns: '1fr 240px 100px', gap: 16, padding: '14px 18px', alignItems: 'center', borderBottom: '1px solid var(--hair-2)' }}>
              <div>
                <div style={{ fontSize: 13.5, fontWeight: 500 }}>{l.name || l.id}</div>
                <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 2 }}>
                  {l.address || '—'} · <span className="mono">{l.id}</span>
                </div>
              </div>
              <select
                value={l.mappedToStoreNum || ''}
                onChange={(e) => onMap(l.id, l.name, e.target.value)}
                disabled={busy}
                style={{ padding: '8px 10px', borderRadius: 8, border: '1px solid var(--hair)', background: 'var(--bg)', fontFamily: 'inherit', fontSize: 13 }}
              >
                <option value="">— Unmapped —</option>
                {window.MISE.stores.map(s => (
                  <option key={s.num} value={s.num}>{s.num} {s.name}</option>
                ))}
              </select>
              <button onClick={() => onDisconnect(l.id)} disabled={busy} style={btnOutStyle(busy)}>
                Disconnect
              </button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function IntuitSettings() {
  const [status, setStatus] = React.useState(null);
  const [statusError, setStatusError] = React.useState(null);
  const [statusLoaded, setStatusLoaded] = React.useState(false);
  const [locations, setLocations] = React.useState([]);
  const [locsLoading, setLocsLoading] = React.useState(false);
  const [locsError, setLocsError] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [notice, setNotice] = React.useState(null);
  const [syncStatus, setSyncStatus] = React.useState(null);
  const [syncing, setSyncing] = React.useState(false);

  const refreshSync = React.useCallback(() => {
    fetch(`${SETTINGS_API_BASE}/intuit/sync/status`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : null)
      .then(setSyncStatus)
      .catch(() => setSyncStatus(null));
  }, []);

  const onSyncNow = async () => {
    setSyncing(true);
    setNotice(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/intuit/sync`, { method: 'POST' });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      const r = await res.json();
      if (r.error) {
        setNotice({ kind: 'err', msg: 'Sync failed: ' + r.error });
      } else {
        setNotice({ kind: 'ok', msg: `Synced ${r.companiesSynced} compan${r.companiesSynced === 1 ? 'y' : 'ies'} · ${r.monthsWritten} months · ${(r.elapsedMs/1000).toFixed(1)}s` });
      }
      refreshSync();
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Sync request failed: ' + (e.message || e) });
    } finally {
      setSyncing(false);
    }
  };

  const refresh = React.useCallback(async () => {
    setStatusError(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/intuit/workspace`, { cache: 'no-store' });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      setStatus(await res.json());
    } catch (e) {
      setStatusError(e.message || String(e));
      setStatus({ connected: false, configured: false, environment: 'sandbox', realmCount: 0 });
    } finally {
      setStatusLoaded(true);
    }
  }, []);

  React.useEffect(() => {
    refresh();
    const params = new URLSearchParams(window.location.search);
    const flag = params.get('intuit');
    if (flag === 'connected') setNotice({ kind: 'ok', msg: 'QuickBooks company connected.' });
    else if (flag === 'error') setNotice({ kind: 'err', msg: 'Intuit install failed: ' + (params.get('reason') || 'unknown') });
    if (flag) {
      const next = new URLSearchParams(window.location.search);
      next.delete('intuit');
      next.delete('reason');
      window.history.replaceState({}, '', window.location.pathname + (next.toString() ? '?' + next : ''));
    }
  }, [refresh]);

  React.useEffect(() => {
    if (!notice) return;
    const t = setTimeout(() => setNotice(null), 4500);
    return () => clearTimeout(t);
  }, [notice]);

  React.useEffect(() => {
    if (!status?.connected) return;
    setLocsLoading(true);
    setLocsError(null);
    fetch(`${SETTINGS_API_BASE}/intuit/locations`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
      .then(setLocations)
      .catch(e => setLocsError(e.message || String(e)))
      .finally(() => setLocsLoading(false));
    refreshSync();
  }, [status?.connected, refreshSync]);

  const onConnect = () => { window.location.href = `${SETTINGS_API_BASE}/intuit/install`; };

  const onDisconnect = async (realmId) => {
    if (!realmId) return;
    if (!confirm('Disconnect this QuickBooks company? Mapped store will also be cleared.')) return;
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/intuit/workspace?realmId=${encodeURIComponent(realmId)}`, { method: 'DELETE' });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      await refresh();
      setLocations(curr => curr.filter(l => l.id !== realmId));
      setNotice({ kind: 'ok', msg: 'Disconnected.' });
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Disconnect failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  const onMap = async (realmId, companyName, miseStoreNum) => {
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/intuit/locations/map`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ realmId, companyName, miseStoreNum: miseStoreNum || null }),
      });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      setLocations(curr => curr.map(l => l.id === realmId ? { ...l, mappedToStoreNum: miseStoreNum || null } : l));
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Could not save mapping: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  if (!statusLoaded || !status?.connected) return null;

  return (
    <div className="set-pane">
      <div className="set-page-head">
        <h1 className="serif set-h1">Accounting · QuickBooks</h1>
        <p className="set-sub">
          Pull P&amp;L, COGS, and labor expense from each connected QuickBooks company. Most franchisees keep one company per location — map each company to a Mise store.
          {' '}<span className="mono" style={{ color: 'var(--muted)' }}>environment: {status.environment}</span>
        </p>
      </div>

      {notice && (
        <div style={{
          margin: '14px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5,
          background: notice.kind === 'ok' ? 'rgba(47,125,91,.08)' : 'rgba(178,58,42,.08)',
          color: notice.kind === 'ok' ? 'var(--green)' : 'var(--red)',
          border: `1px solid ${notice.kind === 'ok' ? 'rgba(47,125,91,.30)' : 'rgba(178,58,42,.30)'}`,
        }}>{notice.msg}</div>
      )}

      {statusError && (
        <div style={{ margin: '12px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5, background: 'rgba(178,58,42,.08)', color: 'var(--red)', border: '1px solid rgba(178,58,42,.30)' }}>
          Couldn't reach the Intuit status endpoint: {statusError}
        </div>
      )}

      <div style={{ marginTop: 18, padding: '24px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>
        <div>
          <div style={{ fontWeight: 600, fontSize: 15, marginBottom: 4 }}>
            {status.realmCount} QuickBooks compan{status.realmCount === 1 ? 'y' : 'ies'} connected
          </div>
          <div style={{ fontSize: 13, color: 'var(--muted)', maxWidth: 560 }}>
            Each QuickBooks company is its own install. Add another below or via the section's "Add integration" picker.
          </div>
        </div>
        <button onClick={onConnect} disabled={busy} style={btnOutStyle(busy)}>
          <Ic name="plus" size={13}/> Add another company
        </button>
      </div>

      <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
        <div>
          <div style={{ fontWeight: 600, marginBottom: 4 }}>P&amp;L sync</div>
          <div style={{ fontSize: 12.5, color: 'var(--muted)' }}>
            {syncStatus?.lastSyncedAt
              ? <>Last synced <span className="mono">{new Date(syncStatus.lastSyncedAt).toLocaleString()}</span> through <span className="mono">{syncStatus.lastSyncThrough}</span> · status: <span className="mono">{syncStatus.lastStatus}</span>{syncStatus.lastError ? ` (${syncStatus.lastError})` : ''}</>
              : 'Never synced. Map at least one company below, then click Sync now to pull the trailing 13 months of P&L.'}
          </div>
        </div>
        <button onClick={onSyncNow} disabled={syncing || busy} style={btnPriStyle(syncing || busy)}>
          <Ic name="sync" size={13}/> {syncing ? 'Syncing…' : 'Sync now (13mo)'}
        </button>
      </div>

      <div style={{ marginTop: 18, border: '1px solid var(--hair)', borderRadius: 12, overflow: 'hidden', background: 'var(--bg)' }}>
        <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--hair-2)' }}>
          <div style={{ fontWeight: 600 }}>Map QuickBooks companies to Mise stores</div>
          <div style={{ fontSize: 12.5, color: 'var(--muted)', marginTop: 2 }}>
            Each connected company maps to one Mise store. Unmapped companies still authenticate but won't surface anywhere yet.
          </div>
        </div>
        {locsLoading && (
          <div style={{ padding: 24, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>Loading companies…</div>
        )}
        {locsError && (
          <div style={{ margin: 16, padding: 10, background: 'rgba(178,58,42,.08)', border: '1px solid rgba(178,58,42,.30)', borderRadius: 8, fontSize: 12.5, color: 'var(--red)' }}>
            Couldn't fetch companies: {locsError}
          </div>
        )}
        {!locsLoading && !locsError && locations.length === 0 && (
          <div style={{ padding: 30, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>No QuickBooks companies on this org.</div>
        )}
        {locations.map(l => (
          <div key={l.id} style={{ display: 'grid', gridTemplateColumns: '1fr 240px 100px', gap: 16, padding: '14px 18px', alignItems: 'center', borderBottom: '1px solid var(--hair-2)' }}>
            <div>
              <div style={{ fontSize: 13.5, fontWeight: 500 }}>{l.name || l.id}</div>
              <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 2 }}>
                <span className="mono">realm: {l.id}</span>
              </div>
            </div>
            <select
              value={l.mappedToStoreNum || ''}
              onChange={(e) => onMap(l.id, l.name, e.target.value)}
              disabled={busy}
              style={{ padding: '8px 10px', borderRadius: 8, border: '1px solid var(--hair)', background: 'var(--bg)', fontFamily: 'inherit', fontSize: 13 }}
            >
              <option value="">— Unmapped —</option>
              {window.MISE.stores.map(s => (
                <option key={s.num} value={s.num}>{s.num} {s.name}</option>
              ))}
            </select>
            <button onClick={() => onDisconnect(l.id)} disabled={busy} style={btnOutStyle(busy)}>
              Disconnect
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

function LightspeedSettings() {
  const [status, setStatus] = React.useState(null);
  const [statusLoaded, setStatusLoaded] = React.useState(false);
  const [retailers, setRetailers] = React.useState([]);
  const [outlets, setOutlets] = React.useState([]);
  const [outletsLoading, setOutletsLoading] = React.useState(false);
  const [outletsError, setOutletsError] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [notice, setNotice] = React.useState(null);
  const [prefixInput, setPrefixInput] = React.useState('');
  const [showForm, setShowForm] = React.useState(false);
  const [syncStatus, setSyncStatus] = React.useState(null);
  const [syncing, setSyncing] = React.useState(false);

  const refreshSync = React.useCallback(() => {
    fetch(`${SETTINGS_API_BASE}/lightspeed/sync/status`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : null)
      .then(setSyncStatus)
      .catch(() => setSyncStatus(null));
  }, []);

  const onSyncNow = async () => {
    setSyncing(true);
    setNotice(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/lightspeed/sync?days=30`, { method: 'POST' });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      const r = await res.json();
      if (r.error) {
        setNotice({ kind: 'err', msg: 'Sync failed: ' + r.error });
      } else {
        setNotice({ kind: 'ok', msg: `Synced ${r.retailersSynced} retailer${r.retailersSynced === 1 ? '' : 's'} · ${r.salesFetched} sales · ${r.inventoryRowsWritten} inventory rows · ${r.daysWritten} days · ${(r.elapsedMs/1000).toFixed(1)}s` });
      }
      refreshSync();
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Sync request failed: ' + (e.message || e) });
    } finally {
      setSyncing(false);
    }
  };

  const refresh = React.useCallback(async () => {
    try {
      const [w, r] = await Promise.all([
        fetch(`${SETTINGS_API_BASE}/lightspeed/workspace`, { cache: 'no-store' }).then(x => x.ok ? x.json() : null),
        fetch(`${SETTINGS_API_BASE}/lightspeed/retailers`, { cache: 'no-store' }).then(x => x.ok ? x.json() : []),
      ]);
      setStatus(w);
      setRetailers(r || []);
      setShowForm((r || []).length === 0);
    } catch (e) {
      setStatus({ connected: false, configured: false });
      setRetailers([]);
    } finally {
      setStatusLoaded(true);
    }
  }, []);

  React.useEffect(() => {
    refresh();
    const params = new URLSearchParams(window.location.search);
    const flag = params.get('lightspeed');
    if (flag === 'connected') setNotice({ kind: 'ok', msg: 'Lightspeed retailer connected.' });
    else if (flag === 'error') setNotice({ kind: 'err', msg: 'Lightspeed install failed: ' + (params.get('reason') || 'unknown') });
    if (flag) {
      const next = new URLSearchParams(window.location.search);
      next.delete('lightspeed'); next.delete('reason');
      window.history.replaceState({}, '', window.location.pathname + (next.toString() ? '?' + next : ''));
    }
  }, [refresh]);

  React.useEffect(() => {
    if (!notice) return;
    const t = setTimeout(() => setNotice(null), 4500);
    return () => clearTimeout(t);
  }, [notice]);

  React.useEffect(() => {
    if (!status?.connected) return;
    setOutletsLoading(true);
    setOutletsError(null);
    fetch(`${SETTINGS_API_BASE}/lightspeed/locations`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
      .then(setOutlets)
      .catch(e => setOutletsError(e.message || String(e)))
      .finally(() => setOutletsLoading(false));
    refreshSync();
  }, [status?.connected, refreshSync]);

  const onConnect = () => {
    if (!prefixInput.trim()) return;
    window.location.href = `${SETTINGS_API_BASE}/lightspeed/install?prefix=${encodeURIComponent(prefixInput.trim())}`;
  };

  const onDisconnect = async (prefix) => {
    if (!confirm(`Disconnect ${prefix}? Mapped outlets will be cleared.`)) return;
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/lightspeed/workspace?prefix=${encodeURIComponent(prefix)}`, { method: 'DELETE' });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      await refresh();
      setOutlets(curr => curr.filter(l => l.domainPrefix !== prefix));
      setNotice({ kind: 'ok', msg: `Disconnected ${prefix}.` });
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Disconnect failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  const onMap = async (prefix, outletId, locationName, miseStoreNum) => {
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/lightspeed/locations/map`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          domainPrefix: prefix, outletId, locationName,
          miseStoreNum: miseStoreNum || null,
        }),
      });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      setOutlets(curr => curr.map(l =>
        (l.id === outletId && l.domainPrefix === prefix)
          ? { ...l, mappedToStoreNum: miseStoreNum || null } : l));
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Could not save mapping: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  if (!statusLoaded) {
    return <div className="set-pane"><div style={{ padding: 32, color: 'var(--muted)' }}>Loading Lightspeed…</div></div>;
  }

  return (
    <div className="set-pane">
      <div className="set-page-head">
        <h1 className="serif set-h1">POS · Lightspeed Retail</h1>
        <p className="set-sub">
          Connect each Lightspeed retailer (X-Series) to pull sales, outlets, and inventory into Mise.
          {' '}<span className="mono" style={{ color: 'var(--muted)' }}>environment: {status.environment}</span>
        </p>
      </div>

      {notice && (
        <div style={{
          margin: '14px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5,
          background: notice.kind === 'ok' ? 'rgba(47,125,91,.08)' : 'rgba(178,58,42,.08)',
          color: notice.kind === 'ok' ? 'var(--green)' : 'var(--red)',
          border: `1px solid ${notice.kind === 'ok' ? 'rgba(47,125,91,.30)' : 'rgba(178,58,42,.30)'}`,
        }}>{notice.msg}</div>
      )}

      {!status.configured && (
        <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)' }}>
          <div style={{ fontWeight: 600, marginBottom: 6 }}>Lightspeed credentials not set on the server</div>
          <div style={{ fontSize: 13, color: 'var(--muted)' }}>
            An admin needs to add <code className="mono">copilot.lightspeed.application-id</code> and
            <code className="mono"> application-secret</code> to the server's environment.
          </div>
        </div>
      )}

      {/* Connected retailers */}
      {retailers.length > 0 && (
        <div style={{ marginTop: 18, display: 'flex', flexDirection: 'column', gap: 14 }}>
          {retailers.map(r => (
            <div key={r.domainPrefix} style={{ padding: '16px 20px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>
              <div>
                <div style={{ fontWeight: 600, fontSize: 14 }}>{r.retailerName || r.domainPrefix}</div>
                <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 2, fontFamily: "'JetBrains Mono', monospace" }}>{r.domainPrefix}.retail.lightspeed.app</div>
              </div>
              <button onClick={() => onDisconnect(r.domainPrefix)} disabled={busy} style={btnOutStyle(busy)}>Disconnect</button>
            </div>
          ))}
        </div>
      )}

      {/* Connect form */}
      {status.configured && (!showForm ? (
        <button onClick={() => setShowForm(true)} style={{ ...btnOutStyle(false), marginTop: 18, alignSelf: 'flex-start' }}>
          <Ic name="plus" size={13}/> Connect another retailer
        </button>
      ) : (
        <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)' }}>
          <div style={{ fontWeight: 600, marginBottom: 6 }}>Connect a Lightspeed retailer</div>
          <div style={{ fontSize: 12.5, color: 'var(--muted)', marginBottom: 14, lineHeight: 1.6 }}>
            Enter your retailer's domain prefix — the part before <code className="mono">.retail.lightspeed.app</code>.
            For <code className="mono">acme.retail.lightspeed.app</code>, type <code className="mono">acme</code>.
          </div>
          <Field label="Domain prefix">
            <input
              value={prefixInput}
              onChange={e => setPrefixInput(e.target.value)}
              placeholder="acme"
              disabled={busy}
              style={inputStyle}
              onKeyDown={e => { if (e.key === 'Enter' && prefixInput.trim()) onConnect(); }}
            />
          </Field>
          <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 8 }}>
            {retailers.length > 0 && (
              <button onClick={() => setShowForm(false)} disabled={busy} style={btnOutStyle(busy)}>Cancel</button>
            )}
            <button onClick={onConnect} disabled={busy || !prefixInput.trim()} style={btnPriStyle(busy || !prefixInput.trim())}>
              <Ic name="plus" size={13}/> Authorize on Lightspeed →
            </button>
          </div>
        </div>
      ))}

      {/* Sales + inventory sync */}
      {status.connected && (
        <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
          <div>
            <div style={{ fontWeight: 600, marginBottom: 4 }}>Sales + inventory sync</div>
            <div style={{ fontSize: 12.5, color: 'var(--muted)' }}>
              {syncStatus?.lastSyncedAt
                ? <>Last synced <span className="mono">{new Date(syncStatus.lastSyncedAt).toLocaleString()}</span> through <span className="mono">{syncStatus.lastSyncThrough}</span> · status: <span className="mono">{syncStatus.lastStatus}</span>{syncStatus.lastError ? ` (${syncStatus.lastError})` : ''}</>
                : 'Never synced. Map at least one outlet below, then click Sync now to pull sales and inventory.'}
            </div>
          </div>
          <button onClick={onSyncNow} disabled={syncing || busy} style={btnPriStyle(syncing || busy)}>
            <Ic name="sync" size={13}/> {syncing ? 'Syncing…' : 'Sync now (30d)'}
          </button>
        </div>
      )}

      {/* Outlets mapper */}
      {status.connected && (
        <div style={{ marginTop: 18, border: '1px solid var(--hair)', borderRadius: 12, overflow: 'hidden', background: 'var(--bg)' }}>
          <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--hair-2)' }}>
            <div style={{ fontWeight: 600 }}>Map Lightspeed outlets to Mise stores</div>
            <div style={{ fontSize: 12.5, color: 'var(--muted)', marginTop: 2 }}>
              X-series calls each physical location an "outlet". Map each to a Mise store.
            </div>
          </div>
          {outletsLoading && (
            <div style={{ padding: 24, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>Loading outlets…</div>
          )}
          {outletsError && (
            <div style={{ margin: 16, padding: 10, background: 'rgba(178,58,42,.08)', border: '1px solid rgba(178,58,42,.30)', borderRadius: 8, fontSize: 12.5, color: 'var(--red)' }}>
              Couldn't fetch outlets: {outletsError}
            </div>
          )}
          {!outletsLoading && !outletsError && outlets.length === 0 && (
            <div style={{ padding: 30, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>No outlets on these retailers.</div>
          )}
          {outlets.map(l => (
            <div key={l.domainPrefix + '|' + l.id} style={{ display: 'grid', gridTemplateColumns: '1fr 240px', gap: 16, padding: '14px 18px', alignItems: 'center', borderBottom: '1px solid var(--hair-2)' }}>
              <div>
                <div style={{ fontSize: 13.5, fontWeight: 500 }}>{l.name}</div>
                <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 2 }}>
                  {l.address || '—'} · <span className="mono">{l.domainPrefix}</span> · <span className="mono">{l.id}</span>
                </div>
              </div>
              <select
                value={l.mappedToStoreNum || ''}
                onChange={(e) => onMap(l.domainPrefix, l.id, l.name, e.target.value)}
                disabled={busy}
                style={{ padding: '8px 10px', borderRadius: 8, border: '1px solid var(--hair)', background: 'var(--bg)', fontFamily: 'inherit', fontSize: 13 }}
              >
                <option value="">— Unmapped —</option>
                {window.MISE.stores.map(s => (
                  <option key={s.num} value={s.num}>{s.num} {s.name}</option>
                ))}
              </select>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function ShopifySettings() {
  const [status, setStatus] = React.useState(null);
  const [statusLoaded, setStatusLoaded] = React.useState(false);
  const [shops, setShops] = React.useState([]);
  const [locations, setLocations] = React.useState([]);
  const [locsLoading, setLocsLoading] = React.useState(false);
  const [locsError, setLocsError] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [notice, setNotice] = React.useState(null);
  const [shopInput, setShopInput] = React.useState('');
  const [showForm, setShowForm] = React.useState(false);
  const [syncStatus, setSyncStatus] = React.useState(null);
  const [syncing, setSyncing] = React.useState(false);

  const refreshSync = React.useCallback(() => {
    fetch(`${SETTINGS_API_BASE}/shopify/sync/status`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : null)
      .then(setSyncStatus)
      .catch(() => setSyncStatus(null));
  }, []);

  const onSyncNow = async () => {
    setSyncing(true);
    setNotice(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/shopify/sync?days=30`, { method: 'POST' });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      const r = await res.json();
      if (r.error) {
        setNotice({ kind: 'err', msg: 'Sync failed: ' + r.error });
      } else {
        setNotice({ kind: 'ok', msg: `Synced ${r.shopsSynced} shop${r.shopsSynced === 1 ? '' : 's'} · ${r.ordersFetched} orders · ${r.inventoryRowsWritten} inventory rows · ${r.daysWritten} days · ${(r.elapsedMs/1000).toFixed(1)}s` });
      }
      refreshSync();
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Sync request failed: ' + (e.message || e) });
    } finally {
      setSyncing(false);
    }
  };

  const refresh = React.useCallback(async () => {
    try {
      const [w, s] = await Promise.all([
        fetch(`${SETTINGS_API_BASE}/shopify/workspace`, { cache: 'no-store' }).then(r => r.ok ? r.json() : null),
        fetch(`${SETTINGS_API_BASE}/shopify/shops`, { cache: 'no-store' }).then(r => r.ok ? r.json() : []),
      ]);
      setStatus(w);
      setShops(s || []);
      setShowForm((s || []).length === 0);
    } catch (e) {
      setStatus({ connected: false, configured: false });
      setShops([]);
    } finally {
      setStatusLoaded(true);
    }
  }, []);

  React.useEffect(() => {
    refresh();
    const params = new URLSearchParams(window.location.search);
    const flag = params.get('shopify');
    if (flag === 'connected') setNotice({ kind: 'ok', msg: 'Shopify shop connected.' });
    else if (flag === 'error') setNotice({ kind: 'err', msg: 'Shopify install failed: ' + (params.get('reason') || 'unknown') });
    if (flag) {
      const next = new URLSearchParams(window.location.search);
      next.delete('shopify'); next.delete('reason');
      window.history.replaceState({}, '', window.location.pathname + (next.toString() ? '?' + next : ''));
    }
  }, [refresh]);

  React.useEffect(() => {
    if (!notice) return;
    const t = setTimeout(() => setNotice(null), 4500);
    return () => clearTimeout(t);
  }, [notice]);

  React.useEffect(() => {
    if (!status?.connected) return;
    setLocsLoading(true);
    setLocsError(null);
    fetch(`${SETTINGS_API_BASE}/shopify/locations`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
      .then(setLocations)
      .catch(e => setLocsError(e.message || String(e)))
      .finally(() => setLocsLoading(false));
    refreshSync();
  }, [status?.connected, refreshSync]);

  const onConnect = () => {
    if (!shopInput.trim()) return;
    window.location.href = `${SETTINGS_API_BASE}/shopify/install?shop=${encodeURIComponent(shopInput.trim())}`;
  };

  const onDisconnect = async (shopDomain) => {
    if (!confirm(`Disconnect ${shopDomain}? Mapped locations will be cleared.`)) return;
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/shopify/workspace?shop=${encodeURIComponent(shopDomain)}`, { method: 'DELETE' });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      await refresh();
      setLocations(curr => curr.filter(l => l.shopDomain !== shopDomain));
      setNotice({ kind: 'ok', msg: `Disconnected ${shopDomain}.` });
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Disconnect failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  const onMap = async (shopDomain, locationId, locationName, miseStoreNum) => {
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/shopify/locations/map`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          shopDomain, shopifyLocationId: locationId, locationName,
          miseStoreNum: miseStoreNum || null,
        }),
      });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      setLocations(curr => curr.map(l =>
        (l.id === locationId && l.shopDomain === shopDomain)
          ? { ...l, mappedToStoreNum: miseStoreNum || null } : l));
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Could not save mapping: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  if (!statusLoaded) {
    return <div className="set-pane"><div style={{ padding: 32, color: 'var(--muted)' }}>Loading Shopify…</div></div>;
  }

  return (
    <div className="set-pane">
      <div className="set-page-head">
        <h1 className="serif set-h1">POS · Shopify</h1>
        <p className="set-sub">
          Connect each Shopify shop to pull orders, inventory, and locations into Mise.
          {' '}<span className="mono" style={{ color: 'var(--muted)' }}>environment: {status.environment} · API: {status.connected ? '2024-10' : '—'}</span>
        </p>
      </div>

      {notice && (
        <div style={{
          margin: '14px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5,
          background: notice.kind === 'ok' ? 'rgba(47,125,91,.08)' : 'rgba(178,58,42,.08)',
          color: notice.kind === 'ok' ? 'var(--green)' : 'var(--red)',
          border: `1px solid ${notice.kind === 'ok' ? 'rgba(47,125,91,.30)' : 'rgba(178,58,42,.30)'}`,
        }}>{notice.msg}</div>
      )}

      {!status.configured && (
        <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)' }}>
          <div style={{ fontWeight: 600, marginBottom: 6 }}>Shopify credentials not set on the server</div>
          <div style={{ fontSize: 13, color: 'var(--muted)' }}>
            An admin needs to add <code className="mono">copilot.shopify.client-id</code> and
            <code className="mono"> client-secret</code> to the server's environment.
          </div>
        </div>
      )}

      {/* Connected shops */}
      {shops.length > 0 && (
        <div style={{ marginTop: 18, display: 'flex', flexDirection: 'column', gap: 14 }}>
          {shops.map(s => (
            <div key={s.shopDomain} style={{ padding: '16px 20px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>
              <div>
                <div style={{ fontWeight: 600, fontSize: 14 }}>{s.shopName || s.shopDomain}</div>
                <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 2, fontFamily: "'JetBrains Mono', monospace" }}>{s.shopDomain}</div>
              </div>
              <button onClick={() => onDisconnect(s.shopDomain)} disabled={busy} style={btnOutStyle(busy)}>Disconnect</button>
            </div>
          ))}
        </div>
      )}

      {/* Connect form */}
      {status.configured && (!showForm ? (
        <button onClick={() => setShowForm(true)} style={{ ...btnOutStyle(false), marginTop: 18, alignSelf: 'flex-start' }}>
          <Ic name="plus" size={13}/> Connect another shop
        </button>
      ) : (
        <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)' }}>
          <div style={{ fontWeight: 600, marginBottom: 6 }}>Connect a Shopify shop</div>
          <div style={{ fontSize: 12.5, color: 'var(--muted)', marginBottom: 14, lineHeight: 1.6 }}>
            Enter the shop domain (e.g. <code className="mono">mise-test.myshopify.com</code> or just <code className="mono">mise-test</code>). You'll be redirected to Shopify to authorize Mise on that shop.
          </div>
          <Field label="Shop domain">
            <input
              value={shopInput}
              onChange={e => setShopInput(e.target.value)}
              placeholder="mise-test.myshopify.com"
              disabled={busy}
              style={inputStyle}
              onKeyDown={e => { if (e.key === 'Enter' && shopInput.trim()) onConnect(); }}
            />
          </Field>
          <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 8 }}>
            {shops.length > 0 && (
              <button onClick={() => setShowForm(false)} disabled={busy} style={btnOutStyle(busy)}>Cancel</button>
            )}
            <button onClick={onConnect} disabled={busy || !shopInput.trim()} style={btnPriStyle(busy || !shopInput.trim())}>
              <Ic name="plus" size={13}/> Authorize on Shopify →
            </button>
          </div>
        </div>
      ))}

      {/* Sales + inventory sync */}
      {status.connected && (
        <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
          <div>
            <div style={{ fontWeight: 600, marginBottom: 4 }}>Sales + inventory sync</div>
            <div style={{ fontSize: 12.5, color: 'var(--muted)' }}>
              {syncStatus?.lastSyncedAt
                ? <>Last synced <span className="mono">{new Date(syncStatus.lastSyncedAt).toLocaleString()}</span> through <span className="mono">{syncStatus.lastSyncThrough}</span> · status: <span className="mono">{syncStatus.lastStatus}</span>{syncStatus.lastError ? ` (${syncStatus.lastError})` : ''}</>
                : 'Never synced. Map at least one location below, then click Sync now to pull orders and inventory.'}
            </div>
          </div>
          <button onClick={onSyncNow} disabled={syncing || busy} style={btnPriStyle(syncing || busy)}>
            <Ic name="sync" size={13}/> {syncing ? 'Syncing…' : 'Sync now (30d)'}
          </button>
        </div>
      )}

      {/* Locations mapper */}
      {status.connected && (
        <div style={{ marginTop: 18, border: '1px solid var(--hair)', borderRadius: 12, overflow: 'hidden', background: 'var(--bg)' }}>
          <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--hair-2)' }}>
            <div style={{ fontWeight: 600 }}>Map Shopify locations to Mise stores</div>
            <div style={{ fontSize: 12.5, color: 'var(--muted)', marginTop: 2 }}>
              Each Shopify location maps to one Mise store. Locations across multiple shops are listed together.
            </div>
          </div>
          {locsLoading && (
            <div style={{ padding: 24, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>Loading locations…</div>
          )}
          {locsError && (
            <div style={{ margin: 16, padding: 10, background: 'rgba(178,58,42,.08)', border: '1px solid rgba(178,58,42,.30)', borderRadius: 8, fontSize: 12.5, color: 'var(--red)' }}>
              Couldn't fetch locations: {locsError}
            </div>
          )}
          {!locsLoading && !locsError && locations.length === 0 && (
            <div style={{ padding: 30, textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>No Shopify locations on these shops.</div>
          )}
          {locations.map(l => (
            <div key={l.shopDomain + '|' + l.id} style={{ display: 'grid', gridTemplateColumns: '1fr 240px', gap: 16, padding: '14px 18px', alignItems: 'center', borderBottom: '1px solid var(--hair-2)' }}>
              <div>
                <div style={{ fontSize: 13.5, fontWeight: 500 }}>{l.name}</div>
                <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 2 }}>
                  {l.address || '—'} · <span className="mono">{l.shopDomain}</span> · <span className="mono">{l.id}</span>
                </div>
              </div>
              <select
                value={l.mappedToStoreNum || ''}
                onChange={(e) => onMap(l.shopDomain, l.id, l.name, e.target.value)}
                disabled={busy}
                style={{ padding: '8px 10px', borderRadius: 8, border: '1px solid var(--hair)', background: 'var(--bg)', fontFamily: 'inherit', fontSize: 13 }}
              >
                <option value="">— Unmapped —</option>
                {window.MISE.stores.map(s => (
                  <option key={s.num} value={s.num}>{s.num} {s.name}</option>
                ))}
              </select>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function DeputySettings() {
  const [status, setStatus] = React.useState(null);
  const [statusLoaded, setStatusLoaded] = React.useState(false);
  const [subdomains, setSubdomains] = React.useState([]);
  const [busy, setBusy] = React.useState(false);
  const [notice, setNotice] = React.useState(null);
  // Setup form state. We start collapsed when there are existing
  // subdomains; expanded when there are none yet.
  const [form, setForm] = React.useState({ subdomain: '', clientId: '', clientSecret: '' });
  const [showForm, setShowForm] = React.useState(false);
  // Per-subdomain locations cache.
  const [locsBySub, setLocsBySub] = React.useState({});

  const refreshStatus = React.useCallback(async () => {
    try {
      const [w, s] = await Promise.all([
        fetch(`${SETTINGS_API_BASE}/deputy/workspace`, { cache: 'no-store' }).then(r => r.ok ? r.json() : null),
        fetch(`${SETTINGS_API_BASE}/deputy/subdomains`, { cache: 'no-store' }).then(r => r.ok ? r.json() : []),
      ]);
      setStatus(w);
      setSubdomains(s || []);
      setShowForm((s || []).length === 0);
    } catch (e) {
      setStatus({ connected: false, configured: false, encryptionReady: false });
      setSubdomains([]);
    } finally {
      setStatusLoaded(true);
    }
  }, []);

  React.useEffect(() => {
    refreshStatus();
    const params = new URLSearchParams(window.location.search);
    const flag = params.get('deputy');
    if (flag === 'connected') setNotice({ kind: 'ok', msg: 'Deputy workspace connected.' });
    else if (flag === 'error') setNotice({ kind: 'err', msg: 'Deputy install failed: ' + (params.get('reason') || 'unknown') });
    if (flag) {
      const next = new URLSearchParams(window.location.search);
      next.delete('deputy'); next.delete('reason');
      window.history.replaceState({}, '', window.location.pathname + (next.toString() ? '?' + next : ''));
    }
  }, [refreshStatus]);

  React.useEffect(() => {
    if (!notice) return;
    const t = setTimeout(() => setNotice(null), 5000);
    return () => clearTimeout(t);
  }, [notice]);

  const onConfigure = async () => {
    setBusy(true);
    setNotice(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/deputy/configure`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          subdomain: form.subdomain.trim(),
          clientId: form.clientId.trim(),
          clientSecret: form.clientSecret,
        }),
      });
      if (!res.ok) throw new Error(`${res.status}: ${await res.text() || res.statusText}`);
      setNotice({ kind: 'ok', msg: 'Saved. Client secret encrypted at rest. Click Authorize to complete OAuth.' });
      setForm({ subdomain: '', clientId: '', clientSecret: '' });
      setShowForm(false);
      await refreshStatus();
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Save failed: ' + (e.message || e) });
    } finally {
      setBusy(false);
    }
  };

  const onAuthorize = (sub) => {
    window.location.href = `${SETTINGS_API_BASE}/deputy/install?subdomain=${encodeURIComponent(sub)}`;
  };

  const onDisconnect = async (sub) => {
    if (!confirm(`Disconnect Deputy for ${sub}? OAuth credentials will be removed.`)) return;
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/deputy/workspace?subdomain=${encodeURIComponent(sub)}`, { method: 'DELETE' });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      setNotice({ kind: 'ok', msg: `Disconnected ${sub}.` });
      await refreshStatus();
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Disconnect failed: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  const loadLocations = async (sub) => {
    if (locsBySub[sub]) return;
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/deputy/locations?subdomain=${encodeURIComponent(sub)}`, { cache: 'no-store' });
      if (!res.ok) throw new Error(`${res.status}`);
      const data = await res.json();
      setLocsBySub(prev => ({ ...prev, [sub]: data }));
    } catch (e) {
      setLocsBySub(prev => ({ ...prev, [sub]: { error: e.message || String(e) } }));
    }
  };

  React.useEffect(() => {
    subdomains.filter(s => s.authorized).forEach(s => loadLocations(s.subdomain));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [subdomains]);

  const onMap = async (sub, deputyLocationId, locationName, miseStoreNum) => {
    setBusy(true);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/deputy/locations/map`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          subdomain: sub, deputyLocationId, locationName,
          miseStoreNum: miseStoreNum || null,
        }),
      });
      if (!res.ok && res.status !== 204) throw new Error(String(res.status));
      setLocsBySub(prev => {
        const list = (prev[sub] || []).map(l => l.id === deputyLocationId
          ? { ...l, mappedToStoreNum: miseStoreNum || null } : l);
        return { ...prev, [sub]: list };
      });
    } catch (e) {
      setNotice({ kind: 'err', msg: 'Could not save mapping: ' + (e.message || e) });
    } finally { setBusy(false); }
  };

  if (!statusLoaded) {
    return <div className="set-pane"><div style={{ padding: 32, color: 'var(--muted)' }}>Loading Deputy…</div></div>;
  }

  return (
    <div className="set-pane">
      <div className="set-page-head">
        <h1 className="serif set-h1">Labor & Scheduling · Deputy</h1>
        <p className="set-sub">
          Deputy uses a per-tenant OAuth model — each customer admin creates their own OAuth client inside
          their Deputy workspace and gives Mise the credentials. We never share a marketplace app.
          {' '}<EncryptionBadge ready={status?.encryptionReady}/>
        </p>
      </div>

      {notice && (
        <div style={{
          margin: '14px 0', padding: '10px 14px', borderRadius: 8, fontSize: 12.5,
          background: notice.kind === 'ok' ? 'rgba(47,125,91,.08)' : 'rgba(178,58,42,.08)',
          color: notice.kind === 'ok' ? 'var(--green)' : 'var(--red)',
          border: `1px solid ${notice.kind === 'ok' ? 'rgba(47,125,91,.30)' : 'rgba(178,58,42,.30)'}`,
        }}>{notice.msg}</div>
      )}

      {!status?.encryptionReady && (
        <div style={{ marginTop: 14, padding: '12px 14px', borderRadius: 8, fontSize: 12.5, background: 'rgba(178,58,42,.08)', color: 'var(--red)', border: '1px solid rgba(178,58,42,.30)' }}>
          Master encryption key isn't configured. Set <code className="mono">copilot.crypto.master-key</code> on the server before saving any Deputy credentials.
        </div>
      )}

      {/* Subdomain rows */}
      {subdomains.length > 0 && (
        <div style={{ marginTop: 18, display: 'flex', flexDirection: 'column', gap: 14 }}>
          {subdomains.map(s => (
            <div key={s.subdomain} style={{ border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)', overflow: 'hidden' }}>
              <div style={{ padding: '16px 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, borderBottom: s.authorized ? '1px solid var(--hair-2)' : 'none' }}>
                <div>
                  <div style={{ fontWeight: 600, fontSize: 14 }}>{s.companyName || s.subdomain}</div>
                  <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 2, fontFamily: "'JetBrains Mono', monospace" }}>
                    {s.subdomain} · client_id: {s.clientId} · secret: ••••••••{' '}
                    <span title="Stored AES-256-GCM encrypted. Only the OAuth flow ever decrypts it." style={{ color: status?.encryptionReady ? 'var(--green)' : 'var(--muted)' }}>
                      {status?.encryptionReady ? '🔒 encrypted' : '🔒 enc disabled'}
                    </span>
                  </div>
                </div>
                <div style={{ display: 'flex', gap: 8 }}>
                  {!s.authorized && (
                    <button onClick={() => onAuthorize(s.subdomain)} disabled={busy} style={btnPriStyle(busy)}>
                      Authorize →
                    </button>
                  )}
                  <button onClick={() => onDisconnect(s.subdomain)} disabled={busy} style={btnOutStyle(busy)}>
                    {s.authorized ? 'Disconnect' : 'Remove'}
                  </button>
                </div>
              </div>

              {s.authorized && (
                <div>
                  <div style={{ padding: '12px 20px', fontSize: 12.5, color: 'var(--muted)', borderBottom: '1px solid var(--hair-2)' }}>
                    Map Deputy locations on this subdomain to Mise stores.
                  </div>
                  {(!locsBySub[s.subdomain]) && (
                    <div style={{ padding: 18, textAlign: 'center', fontSize: 13, color: 'var(--muted)' }}>Loading locations…</div>
                  )}
                  {locsBySub[s.subdomain]?.error && (
                    <div style={{ margin: 14, padding: 10, background: 'rgba(178,58,42,.08)', border: '1px solid rgba(178,58,42,.30)', borderRadius: 8, fontSize: 12.5, color: 'var(--red)' }}>
                      Couldn't fetch locations: {locsBySub[s.subdomain].error}
                    </div>
                  )}
                  {Array.isArray(locsBySub[s.subdomain]) && locsBySub[s.subdomain].length === 0 && (
                    <div style={{ padding: 18, textAlign: 'center', fontSize: 13, color: 'var(--muted)' }}>No locations on this Deputy account.</div>
                  )}
                  {Array.isArray(locsBySub[s.subdomain]) && locsBySub[s.subdomain].map(l => (
                    <div key={l.id} style={{ display: 'grid', gridTemplateColumns: '1fr 240px', gap: 16, padding: '12px 20px', alignItems: 'center', borderBottom: '1px solid var(--hair-2)' }}>
                      <div>
                        <div style={{ fontSize: 13, fontWeight: 500 }}>{l.name || l.id}</div>
                        <div style={{ fontSize: 11.5, color: 'var(--muted)', marginTop: 2 }}>{l.address || '—'} · <span className="mono">{l.id}</span></div>
                      </div>
                      <select
                        value={l.mappedToStoreNum || ''}
                        onChange={(e) => onMap(s.subdomain, l.id, l.name, e.target.value)}
                        disabled={busy}
                        style={{ padding: '8px 10px', borderRadius: 8, border: '1px solid var(--hair)', background: 'var(--bg)', fontFamily: 'inherit', fontSize: 13 }}
                      >
                        <option value="">— Unmapped —</option>
                        {window.MISE.stores.map(st => (
                          <option key={st.num} value={st.num}>{st.num} {st.name}</option>
                        ))}
                      </select>
                    </div>
                  ))}
                </div>
              )}
            </div>
          ))}
        </div>
      )}

      {/* Add-subdomain CTA / setup form */}
      {!showForm ? (
        <button onClick={() => setShowForm(true)} style={{ ...btnOutStyle(false), marginTop: 18, alignSelf: 'flex-start' }}>
          <Ic name="plus" size={13}/> Add Deputy subdomain
        </button>
      ) : (
        <div style={{ marginTop: 18, padding: '20px 22px', border: '1px solid var(--hair)', borderRadius: 12, background: 'var(--bg)' }}>
          <div style={{ fontWeight: 600, marginBottom: 6 }}>Configure a Deputy subdomain</div>
          <div style={{ fontSize: 12.5, color: 'var(--muted)', marginBottom: 14, lineHeight: 1.6 }}>
            In your Deputy account go to <code className="mono">/exec/devapp/oauth_clients</code>,
            create a new OAuth client with redirect URI <code className="mono">http://localhost:8080/deputy/callback</code>,
            then paste its <code className="mono">client_id</code> and <code className="mono">client_secret</code> below. The secret is encrypted before it's stored.
          </div>
          <Field label="Subdomain">
            <input value={form.subdomain} onChange={e => setForm({ ...form, subdomain: e.target.value })} disabled={busy}
              placeholder="e.g. acme.na.deputy.com" style={inputStyle}/>
          </Field>
          <Field label="Client ID">
            <input value={form.clientId} onChange={e => setForm({ ...form, clientId: e.target.value })} disabled={busy} style={inputStyle}/>
          </Field>
          <Field label={<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
            Client secret
            <span title="Encrypted at rest with AES-256-GCM. Only the OAuth flow ever decrypts it." style={{
              fontSize: 9.5, fontWeight: 500, padding: '2px 6px', borderRadius: 999,
              color: status?.encryptionReady ? 'var(--green)' : 'var(--muted)',
              background: status?.encryptionReady ? 'rgba(47,125,91,.10)' : 'var(--bg-2)',
              border: `1px solid ${status?.encryptionReady ? 'rgba(47,125,91,.30)' : 'var(--hair)'}`,
              fontFamily: "'JetBrains Mono', monospace", letterSpacing: '.04em', textTransform: 'uppercase',
            }}>
              🔒 {status?.encryptionReady ? 'enc at rest' : 'enc not ready'}
            </span>
          </span>}>
            <input type="password" value={form.clientSecret} onChange={e => setForm({ ...form, clientSecret: e.target.value })} disabled={busy} style={inputStyle}/>
          </Field>
          <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 8 }}>
            <button onClick={() => setShowForm(false)} disabled={busy} style={btnOutStyle(busy)}>Cancel</button>
            <button onClick={onConfigure} disabled={busy || !form.subdomain || !form.clientId || !form.clientSecret || !status?.encryptionReady} style={btnPriStyle(busy || !form.subdomain || !form.clientId || !form.clientSecret || !status?.encryptionReady)}>
              {busy ? 'Saving…' : 'Save & continue'}
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

/** Reusable badge surfaced in the Deputy heading copy so the operator
 *  can see at a glance that customer secrets are encrypted at rest. */
function EncryptionBadge({ ready }) {
  return (
    <span title="Customer-supplied client secrets are encrypted with AES-256-GCM before storage. Master key lives in the server environment, never the database." style={{
      display: 'inline-flex', alignItems: 'center', gap: 4,
      padding: '2px 8px', borderRadius: 999, fontSize: 10.5, fontWeight: 500,
      color: ready ? 'var(--green)' : 'var(--muted)',
      background: ready ? 'rgba(47,125,91,.10)' : 'var(--bg-2)',
      border: `1px solid ${ready ? 'rgba(47,125,91,.30)' : 'var(--hair)'}`,
      fontFamily: "'JetBrains Mono', monospace", letterSpacing: '.04em', textTransform: 'uppercase',
    }}>
      🔒 {ready ? 'AES-256-GCM at rest' : 'encryption disabled'}
    </span>
  );
}

function RulesSettings() {
  const [rules, setRules] = React.useState(null);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    fetch(`${SETTINGS_API_BASE}/alerts/rules`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
      .then(setRules)
      .catch(e => setError(e.message || String(e)));
  }, []);

  if (error) {
    return (
      <div className="set-pane" data-screen-label="Rule library">
        <div className="set-page-head">
          <h1 className="serif set-h1">Rule Library</h1>
          <p className="set-sub">Couldn't load rules: {error}</p>
        </div>
      </div>
    );
  }
  if (rules === null) {
    return (
      <div className="set-pane" data-screen-label="Rule library">
        <div className="set-page-head">
          <h1 className="serif set-h1">Rule Library</h1>
        </div>
        <div style={{ padding: 32, color: 'var(--muted)' }}>Loading rules…</div>
      </div>
    );
  }

  // Group rules by category for the table.
  const groups = {};
  for (const r of rules) {
    if (!groups[r.category]) groups[r.category] = [];
    groups[r.category].push(r);
  }
  const categoryOrder = ['Sales', 'Labor', 'Inventory', 'Financials', 'Operational'];
  const orderedCats = [
    ...categoryOrder.filter(c => groups[c]),
    ...Object.keys(groups).filter(c => !categoryOrder.includes(c)).sort(),
  ];

  return (
    <div className="set-pane" data-screen-label="Rule library">
      <div className="set-page-head">
        <h1 className="serif set-h1">Rule Library</h1>
        <p className="set-sub">
          The conditions Mise watches for across your warehouse data and brand playbook.
          Each rule packages real numbers from a store into a scenario, then the AI
          auditor compares it against your indexed SOPs and writes a finding with
          citations. New rules appear here automatically when shipped.
        </p>
      </div>

      <div style={{
        marginTop: 12, padding: '10px 14px', borderRadius: 8, fontSize: 12.5,
        background: 'var(--bg-2)', border: '1px solid var(--hair)', color: 'var(--muted)',
      }}>
        {rules.length} active rule{rules.length === 1 ? '' : 's'} across {orderedCats.length} categor{orderedCats.length === 1 ? 'y' : 'ies'}.
        Per-org enable/disable toggles land in a follow-up; today every rule runs every scan.
      </div>

      {orderedCats.map(cat => (
        <div key={cat} style={{ marginTop: 24 }}>
          <div style={{ fontSize: 11, color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace", letterSpacing: '.08em', textTransform: 'uppercase', marginBottom: 8 }}>
            {cat} · {groups[cat].length}
          </div>
          <div style={{ border: '1px solid var(--hair)', borderRadius: 12, overflow: 'hidden', background: 'var(--bg)' }}>
            {groups[cat].map((r, idx) => (
              <RuleRow key={r.id} rule={r} isLast={idx === groups[cat].length - 1}/>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

/* —— Daily Brief Schedule —— */

/** Tiny markdown renderer for brief bodies. Handles only the shapes
 *  the composer's prompt locks down (## headings, **bold**, - bullets,
 *  blank-line-separated paragraphs). Keeps us off react-markdown for
 *  this Babel-compiled page. */
function BriefBody({ md }) {
  if (!md) return null;
  const lines = md.split(/\r?\n/);
  const blocks = [];
  let bullets = null;
  const flushBullets = () => {
    if (bullets && bullets.length) blocks.push({ kind: 'ul', items: bullets });
    bullets = null;
  };
  for (const raw of lines) {
    const line = raw.trim();
    if (!line) { flushBullets(); continue; }
    const heading = /^#{1,6}\s+(.*)$/.exec(line);
    if (heading) {
      flushBullets();
      blocks.push({ kind: 'h', text: heading[1] });
      continue;
    }
    const bullet = /^[-*]\s+(.*)$/.exec(line);
    if (bullet) {
      const text = bullet[1].trim();
      if (/^(none|nothing|n\/a)\.?$/i.test(text)) continue;
      if (!bullets) bullets = [];
      bullets.push(text);
      continue;
    }
    flushBullets();
    blocks.push({ kind: 'p', text: line });
  }
  flushBullets();

  const inline = (text) => {
    const parts = [];
    let i = 0;
    let key = 0;
    const re = /\*\*([^*]+)\*\*/g;
    let m;
    while ((m = re.exec(text)) !== null) {
      if (m.index > i) parts.push(text.slice(i, m.index));
      parts.push(<strong key={`b${key++}`}>{m[1]}</strong>);
      i = m.index + m[0].length;
    }
    if (i < text.length) parts.push(text.slice(i));
    return parts;
  };

  return (
    <div style={{ fontSize: 14, lineHeight: 1.6, color: 'var(--ink)' }}>
      {blocks.map((b, i) => {
        if (b.kind === 'h') {
          return (
            <div key={i} style={{
              fontFamily: "'Source Serif 4', Georgia, serif",
              fontSize: 16, fontWeight: 600, color: 'var(--primary)',
              marginTop: i === 0 ? 0 : 14, marginBottom: 6,
            }}>{inline(b.text)}</div>
          );
        }
        if (b.kind === 'ul') {
          return (
            <ul key={i} style={{ margin: '0 0 8px', paddingLeft: 20 }}>
              {b.items.map((it, j) => (
                <li key={j} style={{ marginBottom: 4 }}>{inline(it)}</li>
              ))}
            </ul>
          );
        }
        return (
          <p key={i} style={{ margin: '0 0 8px' }}>{inline(b.text)}</p>
        );
      })}
    </div>
  );
}

function BriefSettings() {
  const [cfg, setCfg] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [saving, setSaving] = React.useState(false);
  const [latest, setLatest] = React.useState(null);
  const [latestState, setLatestState] = React.useState('loading'); // 'loading' | 'empty' | 'ok' | 'error'
  const [running, setRunning] = React.useState(false);
  const [refreshingLatest, setRefreshingLatest] = React.useState(false);
  const [lastCheckedAt, setLastCheckedAt] = React.useState(null);
  const [users, setUsers] = React.useState([]);

  // Initial config load.
  React.useEffect(() => {
    fetch(`${SETTINGS_API_BASE}/settings/daily-brief`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
      .then(d => setCfg({
        enabled: d.enabled !== false,
        timeOfDay: (d.timeOfDay || '06:30').slice(0, 5),
        timezone: d.timezone || 'America/New_York',
        leadBelow: d.leadBelow ?? 70,
        watchBelow: d.watchBelow ?? 80,
        tone: d.tone || 'concise',
        recipients: Array.isArray(d.recipients) ? d.recipients : [],
        slackEnabled: d.slackEnabled === true,
        slackChannel: d.slackChannel || '',
      }))
      .catch(e => setError(e.message || String(e)));
  }, []);

  // Slack channels — only fetched after the toggle is on so we don't
  // hit Slack on every page load. Empty list when no workspace is
  // connected (Slack call returns 4xx, swallowed below).
  const [slackChannels, setSlackChannels] = React.useState(null);
  React.useEffect(() => {
    if (!cfg || !cfg.slackEnabled || slackChannels !== null) return;
    fetch(`${SETTINGS_API_BASE}/slack/channels`, { cache: 'no-store' })
      .then(r => r.ok ? r.json() : [])
      .then(setSlackChannels)
      .catch(() => setSlackChannels([]));
  }, [cfg && cfg.slackEnabled, slackChannels]);

  // Latest brief preview. The button feels like a no-op when nothing
  // has changed (same brief comes back), so surface a "checked at"
  // timestamp + a brief busy state so the click has visible effect.
  const refreshLatest = React.useCallback(() => {
    setRefreshingLatest(true);
    fetch(`${SETTINGS_API_BASE}/daily-brief/latest`, { cache: 'no-store' })
      .then(r => {
        if (r.status === 204) { setLatest(null); setLatestState('empty'); return null; }
        if (!r.ok) return Promise.reject(new Error(`${r.status}`));
        return r.json();
      })
      .then(d => { if (d) { setLatest(d); setLatestState('ok'); } })
      .catch(e => { setError(e.message || String(e)); setLatestState('error'); })
      .finally(() => {
        setLastCheckedAt(new Date());
        setRefreshingLatest(false);
      });
  }, []);

  // Initial load: lighter state — show 'loading' so the empty/error
  // panes don't flash before the first response lands.
  React.useEffect(() => {
    setLatestState('loading');
  }, []);

  React.useEffect(refreshLatest, [refreshLatest]);

  // User options for the recipients picker.
  React.useEffect(() => {
    if (window.MISE && window.MISE.fetchUsersCached) {
      window.MISE.fetchUsersCached().then(setUsers);
    } else {
      fetch(`${SETTINGS_API_BASE}/users`, { cache: 'no-store' })
        .then(r => r.ok ? r.json() : [])
        .then(setUsers)
        .catch(() => setUsers([]));
    }
  }, []);

  const update = (k) => (e) => {
    const v = e && e.target ? (e.target.type === 'checkbox' ? e.target.checked : e.target.value) : e;
    setCfg(prev => ({ ...prev, [k]: v }));
  };
  const updateNum = (k) => (e) => setCfg(prev => ({ ...prev, [k]: Number(e.target.value) }));

  async function save() {
    if (!cfg) return;
    setSaving(true);
    setError(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/settings/daily-brief`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          enabled: cfg.enabled,
          timeOfDay: cfg.timeOfDay,
          timezone: cfg.timezone,
          leadBelow: cfg.leadBelow,
          watchBelow: cfg.watchBelow,
          tone: cfg.tone,
          recipients: cfg.recipients,
          slackEnabled: cfg.slackEnabled,
          slackChannel: cfg.slackChannel || null,
        }),
      });
      if (!res.ok) {
        const txt = await res.text();
        throw new Error(txt || `Save failed (${res.status})`);
      }
      const d = await res.json();
      setCfg({
        enabled: d.enabled !== false,
        timeOfDay: (d.timeOfDay || '06:30').slice(0, 5),
        timezone: d.timezone || 'America/New_York',
        leadBelow: d.leadBelow ?? 70,
        watchBelow: d.watchBelow ?? 80,
        tone: d.tone || 'concise',
        recipients: Array.isArray(d.recipients) ? d.recipients : [],
        slackEnabled: d.slackEnabled === true,
        slackChannel: d.slackChannel || '',
      });
    } catch (e) {
      setError(e.message || String(e));
    } finally {
      setSaving(false);
    }
  }

  async function runNow() {
    setRunning(true);
    setError(null);
    try {
      const res = await fetch(`${SETTINGS_API_BASE}/daily-brief/run-now`, { method: 'POST' });
      if (!res.ok) throw new Error(`${res.status}`);
      const d = await res.json();
      setLatest(d);
      setLatestState('ok');
    } catch (e) {
      setError(e.message || String(e));
    } finally {
      setRunning(false);
    }
  }

  if (error && !cfg) {
    return (
      <div className="set-pane" data-screen-label="Daily brief schedule">
        <div className="set-page-head">
          <h1 className="serif set-h1">Daily Brief Schedule</h1>
          <p className="set-sub">Couldn't load settings: {error}</p>
        </div>
      </div>
    );
  }
  if (!cfg) {
    return (
      <div className="set-pane" data-screen-label="Daily brief schedule">
        <div className="set-page-head">
          <h1 className="serif set-h1">Daily Brief Schedule</h1>
        </div>
        <div style={{ padding: 32, color: 'var(--muted)' }}>Loading…</div>
      </div>
    );
  }

  const inputStyle = {
    width: '100%', padding: '8px 12px', borderRadius: 6,
    border: '1px solid var(--hair)', background: 'var(--bg)',
    fontSize: 14, color: 'var(--ink)', fontFamily: 'inherit',
  };

  const toggleRecipient = (uid) => {
    setCfg(prev => {
      const next = new Set(prev.recipients || []);
      if (next.has(uid)) next.delete(uid); else next.add(uid);
      return { ...prev, recipients: [...next] };
    });
  };

  return (
    <div className="set-pane" data-screen-label="Daily brief schedule">
      <div className="set-page-head">
        <h1 className="serif set-h1">Daily Brief Schedule</h1>
        <p className="set-sub">
          The morning brief composes a prioritized summary of last night's
          alerts, lowest-scoring stores, and a generalized rollup of the
          rest. Priorities are anchored by the thresholds below — the LLM
          renders the prose but doesn't decide what's urgent.
        </p>
      </div>

      {error && (
        <div style={{
          marginTop: 14, padding: '10px 14px', borderRadius: 8, fontSize: 13,
          background: 'rgba(178,58,42,.08)', border: '1px solid rgba(178,58,42,.25)',
          color: 'var(--red)',
        }}>{error}</div>
      )}

      <div style={{ marginTop: 24, display: 'grid', gap: 28 }}>
        {/* Schedule */}
        <section>
          <div style={sectionHeadStyle}>Schedule</div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14 }}>
            <Field label="Status">
              <label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 14 }}>
                <input
                  type="checkbox" checked={cfg.enabled}
                  onChange={update('enabled')} disabled={saving}
                />
                <span>{cfg.enabled ? 'Enabled — runs daily' : 'Paused'}</span>
              </label>
            </Field>
            <Field label="Time of day (local)">
              <input
                type="time" value={cfg.timeOfDay}
                onChange={update('timeOfDay')} disabled={saving}
                style={inputStyle}
              />
            </Field>
            <Field label="Timezone">
              <select value={cfg.timezone} onChange={update('timezone')} disabled={saving} style={inputStyle}>
                <option value="America/Los_Angeles">America/Los_Angeles (PT)</option>
                <option value="America/Denver">America/Denver (MT)</option>
                <option value="America/Phoenix">America/Phoenix (Arizona)</option>
                <option value="America/Chicago">America/Chicago (CT)</option>
                <option value="America/New_York">America/New_York (ET)</option>
              </select>
            </Field>
          </div>
        </section>

        {/* Prioritization */}
        <section>
          <div style={sectionHeadStyle}>Prioritization</div>
          <p style={subParaStyle}>
            Mirrors the alert rule severity model. Stores with critical alerts
            always surface in the lead bucket regardless of composite. Below
            "Lead threshold" forces a store into lead even without an active
            alert. Between thresholds is the watch list. Above "Watch threshold"
            is rolled up into a single line.
          </p>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 14 }}>
            <Field label="Lead threshold (composite below)">
              <input
                type="number" min="0" max="100" value={cfg.leadBelow}
                onChange={updateNum('leadBelow')} disabled={saving} style={inputStyle}
              />
            </Field>
            <Field label="Watch threshold (composite below)">
              <input
                type="number" min="0" max="100" value={cfg.watchBelow}
                onChange={updateNum('watchBelow')} disabled={saving} style={inputStyle}
              />
            </Field>
            <Field label="Tone">
              <select value={cfg.tone} onChange={update('tone')} disabled={saving} style={inputStyle}>
                <option value="concise">Concise (bullets)</option>
                <option value="narrative">Narrative (paragraphs)</option>
              </select>
            </Field>
          </div>
        </section>

        {/* Recipients */}
        <section>
          <div style={sectionHeadStyle}>Recipients</div>
          <p style={subParaStyle}>
            The brief is always viewable in-app. Recipients are the users
            expected to act on it; today this list is informational —
            email/Slack delivery lands in a follow-up.
          </p>
          {users.length === 0 ? (
            <div style={{ fontSize: 13, color: 'var(--muted)' }}>No users yet — add some in <a href="/settings?section=users" style={{ color: 'var(--accent)' }}>Users &amp; Roles</a>.</div>
          ) : (
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 8 }}>
              {users.map(u => {
                const checked = (cfg.recipients || []).includes(u.id);
                return (
                  <label key={u.id} style={{
                    display: 'flex', alignItems: 'center', gap: 10,
                    padding: '8px 12px', border: '1px solid var(--hair)',
                    borderRadius: 8, background: checked ? 'var(--bg-2)' : 'var(--bg)',
                    fontSize: 13, cursor: 'pointer',
                  }}>
                    <input type="checkbox" checked={checked} onChange={() => toggleRecipient(u.id)} disabled={saving}/>
                    <span style={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}>
                      <div style={{ fontWeight: 500 }}>{u.name || u.email}</div>
                      {u.name && u.email && (
                        <div style={{ fontSize: 11.5, color: 'var(--muted)' }}>{u.email}</div>
                      )}
                    </span>
                  </label>
                );
              })}
            </div>
          )}
        </section>

        {/* Slack delivery */}
        <section>
          <div style={sectionHeadStyle}>Slack delivery</div>
          <p style={subParaStyle}>
            Off by default. When enabled, the brief posts to the chosen
            channel each time it generates — manual sends and the daily
            scheduled run both go through. Slack must already be
            connected for the org (Settings → Integrations).
          </p>
          {/* Big, obvious switch + status pill so the on/off state is
              unmistakable and the user knows clicking flips it. */}
          <div
            onClick={() => !saving && setCfg(prev => ({ ...prev, slackEnabled: !prev.slackEnabled }))}
            role="switch"
            aria-checked={cfg.slackEnabled}
            tabIndex={saving ? -1 : 0}
            onKeyDown={(e) => {
              if (saving) return;
              if (e.key === ' ' || e.key === 'Enter') {
                e.preventDefault();
                setCfg(prev => ({ ...prev, slackEnabled: !prev.slackEnabled }));
              }
            }}
            style={{
              display: 'flex', alignItems: 'center', gap: 14,
              padding: '12px 16px', borderRadius: 10,
              border: '1px solid ' + (cfg.slackEnabled ? 'var(--accent)' : 'var(--hair)'),
              background: cfg.slackEnabled ? 'rgba(200,85,61,.06)' : 'var(--bg-2)',
              cursor: saving ? 'wait' : 'pointer',
              userSelect: 'none',
              transition: 'background .15s, border-color .15s',
            }}
          >
            {/* Visual switch */}
            <div style={{
              flex: 'none', width: 40, height: 22, borderRadius: 999,
              background: cfg.slackEnabled ? 'var(--accent)' : 'var(--hair)',
              position: 'relative', transition: 'background .18s ease',
            }}>
              <span style={{
                position: 'absolute', top: 2, left: cfg.slackEnabled ? 20 : 2,
                width: 18, height: 18, borderRadius: '50%',
                background: 'var(--bg)', boxShadow: '0 1px 3px rgba(0,0,0,.2)',
                transition: 'left .18s ease',
              }}/>
            </div>
            <div style={{ minWidth: 0, flex: 1 }}>
              <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ink)' }}>
                {cfg.slackEnabled ? 'On — posting briefs to Slack' : 'Off — click to enable Slack delivery'}
              </div>
              <div style={{ fontSize: 12.5, color: 'var(--muted)', marginTop: 2 }}>
                {cfg.slackEnabled
                  ? 'Each generated brief posts to the channel selected below. Remember to click Save changes.'
                  : 'Briefs are still generated and viewable in-app; only Slack delivery is paused.'}
              </div>
            </div>
            <span style={{
              flex: 'none',
              fontFamily: "'JetBrains Mono', monospace",
              fontSize: 10.5, letterSpacing: '.08em', textTransform: 'uppercase',
              padding: '3px 8px', borderRadius: 4,
              color: cfg.slackEnabled ? 'var(--bg)' : 'var(--muted)',
              background: cfg.slackEnabled ? 'var(--accent)' : 'var(--bg)',
              border: '1px solid ' + (cfg.slackEnabled ? 'var(--accent)' : 'var(--hair)'),
            }}>{cfg.slackEnabled ? 'On' : 'Off'}</span>
          </div>

          {cfg.slackEnabled && (
            <div style={{ marginTop: 14 }}>
              <div style={{
                fontSize: 12, color: 'var(--muted)',
                fontFamily: "'JetBrains Mono', monospace",
                letterSpacing: '.06em', textTransform: 'uppercase',
                marginBottom: 6,
              }}>Channel</div>
              {Array.isArray(slackChannels) && slackChannels.length > 0 ? (
                <select
                  value={cfg.slackChannel}
                  onChange={update('slackChannel')}
                  disabled={saving}
                  style={inputStyle}
                >
                  <option value="">— Pick a channel —</option>
                  {slackChannels.map(c => (
                    <option key={c.id} value={c.id}>
                      #{c.name}{c.isPrivate ? ' (private)' : ''}
                    </option>
                  ))}
                </select>
              ) : (
                <input
                  type="text"
                  placeholder="#channel-name or channel id"
                  value={cfg.slackChannel}
                  onChange={update('slackChannel')}
                  disabled={saving}
                  style={inputStyle}
                />
              )}
              {slackChannels !== null && slackChannels.length === 0 && (
                <div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 6 }}>
                  Slack workspace not detected — paste a channel id manually,
                  or connect Slack in <a href="/settings?section=slack" style={{ color: 'var(--accent)' }}>Integrations → Slack</a>.
                </div>
              )}
            </div>
          )}
        </section>

        <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
          <button
            onClick={save} disabled={saving}
            style={{
              padding: '9px 18px', borderRadius: 6, border: '1px solid var(--primary)',
              background: 'var(--primary)', color: 'var(--bg)', fontSize: 13,
              fontWeight: 500, cursor: saving ? 'wait' : 'pointer',
            }}
          >{saving ? 'Saving…' : 'Save changes'}</button>
          <button
            onClick={runNow} disabled={running}
            style={{
              padding: '9px 18px', borderRadius: 6, border: '1px solid var(--hair)',
              background: 'var(--bg)', color: 'var(--ink)', fontSize: 13,
              cursor: running ? 'wait' : 'pointer',
            }}
          >{running ? 'Composing…' : 'Send test brief'}</button>
        </div>

        {/* Latest brief preview */}
        <section>
          <div style={sectionHeadStyle}>Latest brief</div>
          {latestState === 'loading' && <div style={{ fontSize: 13, color: 'var(--muted)' }}>Loading…</div>}
          {latestState === 'empty' && (
            <div style={{
              padding: '16px 18px', borderRadius: 10, background: 'var(--bg-2)',
              border: '1px solid var(--hair)', color: 'var(--muted)', fontSize: 13.5,
            }}>
              No brief generated yet. The first run will fire at the scheduled time —
              or click "Send test brief" to compose one now.
            </div>
          )}
          {latestState === 'ok' && latest && (
            <div style={{
              padding: '18px 22px', borderRadius: 12, background: 'var(--bg)',
              border: '1px solid var(--hair)',
            }}>
              <div style={{
                display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                gap: 12, marginBottom: 10,
              }}>
                <div style={{ fontSize: 12, color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace", letterSpacing: '.04em' }}>
                  {latest.generatedForDate} · {latest.trigger}
                  {latest.generatedAt && (
                    <span style={{ marginLeft: 8 }}>
                      {new Date(latest.generatedAt).toLocaleString()}
                    </span>
                  )}
                  {lastCheckedAt && (
                    <span style={{ marginLeft: 12, opacity: .65 }}>
                      checked {lastCheckedAt.toLocaleTimeString()}
                    </span>
                  )}
                </div>
                <button
                  onClick={refreshLatest}
                  disabled={refreshingLatest}
                  style={{
                    padding: '4px 10px', borderRadius: 6, border: '1px solid var(--hair)',
                    background: 'var(--bg)',
                    color: refreshingLatest ? 'var(--accent)' : 'var(--muted)',
                    fontSize: 11.5, fontFamily: "'JetBrains Mono', monospace",
                    cursor: refreshingLatest ? 'wait' : 'pointer',
                  }}
                >{refreshingLatest ? 'Refreshing…' : 'Refresh'}</button>
              </div>
              <BriefBody md={latest.bodyMd}/>
            </div>
          )}
        </section>
      </div>
    </div>
  );
}

const sectionHeadStyle = {
  fontSize: 11, color: 'var(--muted)', fontFamily: "'JetBrains Mono', monospace",
  letterSpacing: '.08em', textTransform: 'uppercase', marginBottom: 10,
};
const subParaStyle = {
  fontSize: 13, color: 'var(--muted)', lineHeight: 1.55,
  margin: '0 0 14px', maxWidth: 680,
};

function RuleRow({ rule, isLast }) {
  const [open, setOpen] = React.useState(false);
  const hasDetails = (rule.details && rule.details.trim().length > 0)
    || (Array.isArray(rule.exampleFindings) && rule.exampleFindings.length > 0);
  return (
    <div style={{
      padding: '14px 18px',
      borderBottom: isLast ? 'none' : '1px solid var(--hair-2)',
    }}>
      <div style={{
        display: 'grid', gridTemplateColumns: '1fr 200px',
        gap: 16, alignItems: 'flex-start',
      }}>
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
            <span style={{ fontSize: 14, fontWeight: 600 }}>{rule.name}</span>
            <span className="mono" style={{ fontSize: 10.5, color: 'var(--muted)' }}>{rule.id}</span>
          </div>
          <div style={{ fontSize: 12.5, color: 'var(--muted)', lineHeight: 1.5 }}>
            {rule.description || '(no description provided)'}
          </div>
          {hasDetails && (
            <button
              onClick={() => setOpen(o => !o)}
              style={{
                marginTop: 8, padding: 0, border: 0, background: 'transparent',
                color: 'var(--accent)', fontSize: 12, fontWeight: 500, cursor: 'pointer',
                fontFamily: 'inherit',
              }}
            >
              {open ? '− Hide details' : '+ Show details'}
            </button>
          )}
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6 }}>
          <span style={{
            fontSize: 10, fontWeight: 500, padding: '3px 8px', borderRadius: 999,
            color: 'var(--green)', background: 'rgba(47,125,91,.10)',
            border: '1px solid rgba(47,125,91,.30)',
            fontFamily: "'JetBrains Mono', monospace", letterSpacing: '.04em', textTransform: 'uppercase',
          }}>
            Active
          </span>
          {rule.requiresProvider && (
            <span style={{ fontSize: 11, color: 'var(--muted)', textAlign: 'right' }}>
              needs <span className="mono">{rule.requiresProvider}</span>
            </span>
          )}
        </div>
      </div>

      {open && hasDetails && (
        <div style={{
          marginTop: 12, padding: '14px 16px', borderRadius: 8,
          background: 'var(--bg-2)', border: '1px solid var(--hair)',
          fontSize: 12.5, color: 'var(--ink)', lineHeight: 1.6,
        }}>
          {rule.details && rule.details.split('\n').map((line, i) => {
            const trimmed = line.trim();
            if (!trimmed) return <div key={i} style={{ height: 6 }}/>;
            // Lines that look like section headers (no period, capitalized,
            // < 60 chars) get a bolder treatment.
            const isHeader = trimmed.length < 60 && /^[A-Z]/.test(trimmed) && !trimmed.endsWith('.');
            return (
              <div key={i} style={{
                fontWeight: isHeader ? 600 : 400,
                color: isHeader ? 'var(--ink)' : 'var(--muted)',
                marginTop: isHeader ? 8 : 0,
              }}>{trimmed}</div>
            );
          })}
          {Array.isArray(rule.exampleFindings) && rule.exampleFindings.length > 0 && (
            <div style={{ marginTop: 14 }}>
              <div style={{ fontWeight: 600, color: 'var(--ink)', marginBottom: 6 }}>
                Example findings
              </div>
              <ul style={{ margin: 0, paddingLeft: 18, color: 'var(--muted)' }}>
                {rule.exampleFindings.map((ex, i) => (
                  <li key={i} style={{ marginBottom: 3 }}>{ex}</li>
                ))}
              </ul>
              <div style={{ marginTop: 8, fontSize: 11.5, color: 'var(--muted)', fontStyle: 'italic' }}>
                These are illustrative — actual findings depend on what's in your indexed playbook.
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function PlaceholderPane({ id }) {
  const item = SETTINGS_NAV.flatMap(g => g.items).find(it => it.id === id);
  const label = item ? item.label : id;
  return (
    <div className="set-pane">
      <div className="set-page-head">
        <h1 className="serif set-h1">{label}</h1>
        <p className="set-sub">This settings panel hasn't been built yet. Pick a real section in the sidebar — Stores is fully wired.</p>
      </div>
      <div style={{
        marginTop: 24, padding: '40px 28px', border: '1px dashed var(--hair)',
        borderRadius: 12, color: 'var(--muted)', textAlign: 'center', background: 'var(--bg)'
      }}>
        <div style={{ fontSize: 13, marginBottom: 6 }}>{label}</div>
        <div style={{ fontSize: 12 }}>Coming soon.</div>
      </div>
    </div>
  );
}

function App() {
  // Deep-link support: ?section=slack pins a starting tab; the OAuth
  // callback also redirects with ?slack=connected|error, which should
  // surface the Slack panel since that's the action just taken.
  const [active, setActive] = React.useState(() => {
    if (typeof window === 'undefined') return 'stores';
    const params = new URLSearchParams(window.location.search);
    if (params.has('slack')) return 'slack';
    if (params.has('square') || params.has('clover') || params.has('shopify') || params.has('lightspeed')) return 'pos';
    if (params.has('intuit')) return 'accounting';
    if (params.has('deputy')) return 'labor';
    if (params.get('section')) return params.get('section');
    try {
      const saved = window.localStorage.getItem('mise.settings.section');
      if (saved) return saved;
    } catch (_) { /* private mode etc. */ }
    return 'stores';
  });

  // Persist active tab to both URL and localStorage so refresh / new
  // tab restores it. replaceState (not pushState) keeps the back
  // button useful — settings tabs aren't worth polluting history.
  React.useEffect(() => {
    if (typeof window === 'undefined') return;
    const params = new URLSearchParams(window.location.search);
    if (params.get('section') !== active) {
      params.set('section', active);
      window.history.replaceState({}, '',
        window.location.pathname + '?' + params.toString());
    }
    try { window.localStorage.setItem('mise.settings.section', active); }
    catch (_) { /* private mode etc. */ }
  }, [active]);

  return (
    <div className="shell">
      <Sidebar/>
      <div>
        <Topbar/>
        <main className="set-main">
          <SettingsNav active={active} setActive={setActive}/>
          {active === 'stores' ? <StoresSettings/>
            : active === 'brands' ? <BrandsSettings/>
            : active === 'slack' ? <SlackSettings/>
            : active === 'users' ? <UsersSettings/>
            : active === 'pos' ? <PosSettings/>
            : active === 'labor' ? <LaborSettings/>
            : active === 'safety' ? <SafetySettings/>
            : active === 'accounting' ? <AccountingSettings/>
            : active === 'rules' ? <RulesSettings/>
            : active === 'brief' ? <BriefSettings/>
            : <PlaceholderPane id={active}/>}
        </main>
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);
