// Admin back-office — sidebar nav + multiple views

const A = {
  clover: '#1F4435', cloverDeep: '#122A20', cloverMid: '#2E5A47', cloverSoft: '#E6EEE8',
  gold: '#C9A66B', goldDeep: '#A8854A', goldSoft: '#F1E6CE',
  bone: '#F6F3EC', paper: '#FAF8F2',
  ink: '#14140F', ink2: '#2A2A24', mute: '#7A7A72',
  line: '#E6E2D7', line2: '#D5CFBE',
  panel: '#FBFAF5',
};

// ── Brand theming ────────────────────────────────────────────
const DEFAULT_BRAND = {
  clover: '#1F4435', cloverDeep: '#122A20', cloverMid: '#2E5A47', cloverSoft: '#E6EEE8',
  gold: '#C9A66B', goldDeep: '#A8854A', goldSoft: '#F1E6CE',
  bone: '#F6F3EC', paper: '#FAF8F2', ink: '#14140F',
};

function applyBrand(b) {
  const r = document.documentElement;
  r.style.setProperty('--clover',      b.clover);
  r.style.setProperty('--clover-deep', b.cloverDeep);
  r.style.setProperty('--clover-mid',  b.cloverMid);
  r.style.setProperty('--clover-soft', b.cloverSoft);
  r.style.setProperty('--gold',        b.gold);
  r.style.setProperty('--gold-deep',   b.goldDeep);
  r.style.setProperty('--gold-soft',   b.goldSoft);
  r.style.setProperty('--bone',        b.bone);
  r.style.setProperty('--paper',       b.paper);
  r.style.setProperty('--ink',         b.ink);
}

function hexToRgb(hex) {
  if (!hex || hex.length < 7) return '0,0,0';
  return [
    parseInt(hex.slice(1,3),16),
    parseInt(hex.slice(3,5),16),
    parseInt(hex.slice(5,7),16),
  ].join(',');
}

// ───────────────────────────────────────────────────────────
// RBAC — Role-Based Access Control
// Roles (from most to least powerful):
//   super_admin: HQ — all 8 branches, full financials, system settings
//   admin:       like super_admin but cannot manage super_admins
//   manager:     Branch Manager — own branch only, no global settings
//   staff:       Therapist/Consultant — customer profiles + course usage only
// ───────────────────────────────────────────────────────────
const ROLE_LABEL = {
  super_admin: 'Super Admin',
  admin:       'Admin',
  manager:     'Branch Manager',
  staff:       'Staff',
};

// Which roles can access which view. Order: most → least.
const VIEW_ACCESS = {
  overview:    ['super_admin', 'admin', 'manager'],
  branches:    ['super_admin', 'admin'],
  customers:   ['super_admin', 'admin', 'manager', 'staff'],
  'line-users':['super_admin', 'admin', 'manager'],
  accounting:  ['super_admin', 'admin', 'manager'],
  import:      ['super_admin', 'admin'],
  cms:         ['super_admin', 'admin'],
  'reward-new':['super_admin', 'admin'],
  branding:    ['super_admin'],
  settings:    ['super_admin', 'admin'],
  staff:       ['super_admin', 'admin', 'manager'],   // Staff Management
  logs:        ['super_admin', 'admin'],              // Activity Logs — admins only
  claims:      ['super_admin', 'admin', 'manager'],   // Point Claims review
};

function canAccessView(role, view) {
  const allowed = VIEW_ACCESS[view];
  if (!allowed) return true;          // unknown view = allow (avoid lockout)
  return allowed.includes(role);
}

// ───────────────────────────────────────────────────────────
// Activity Logger — fire-and-forget POST to /api/logs/create.
// Components call logActivity({...}) after any tracked action.
// Caller identity is injected via x-user-* headers read from sessionStorage.
// ───────────────────────────────────────────────────────────
function getCurrentUser() {
  try {
    const raw = sessionStorage.getItem('clover-current-user');
    if (raw) return JSON.parse(raw);
  } catch {}
  return {
    id:    'user-super-001',
    name:  'Super Admin',
    role:  'super_admin',
    email: 'kaidaocreative@gmail.com',
    assigned_branch_id: null,
  };
}

function logActivity(entry) {
  const u = getCurrentUser();
  fetch('/api/logs/create', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-user-id':    u.id   || '',
      'x-user-name':  u.name || '',
      'x-user-role':  u.role || '',
    },
    body: JSON.stringify(entry),
  }).catch(() => {});
}

// ───────────────────────────────────────────────────────────
// 403 Forbidden — shown when current role can't access a view
// ───────────────────────────────────────────────────────────
function ForbiddenView({ role, requestedView }) {
  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      alignItems: 'center', justifyContent: 'center', textAlign: 'center', padding: 40,
    }}>
      <div style={{
        width: 96, height: 96, borderRadius: '50%', background: '#FEEFEC',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        border: '2px solid #F1C7C0', marginBottom: 22,
      }}>
        <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#B5453A" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <rect x="3" y="11" width="18" height="11" rx="2" />
          <path d="M7 11V7a5 5 0 0 1 10 0v4" />
        </svg>
      </div>
      <div className="serif" style={{ fontSize: 32, color: A.clover, letterSpacing: -0.4 }}>403 — Forbidden</div>
      <div style={{ fontSize: 14, color: A.mute, marginTop: 8, maxWidth: 420, lineHeight: 1.55 }}>
        Your role <b style={{ color: A.ink }}>{ROLE_LABEL[role] || role}</b> does not have permission
        to view <code style={{ background: A.bone, padding: '2px 6px', borderRadius: 4 }}>{requestedView}</code>.
        Contact a Super Admin if you need access.
      </div>
      <div style={{ marginTop: 24, padding: '10px 16px', borderRadius: 8,
                    background: A.bone, fontSize: 12, color: A.mute, fontFamily: 'JetBrains Mono, monospace' }}>
        REQUESTED: {requestedView} · ROLE: {role}
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// Shared data store — branches & customers across all views
// Persisted to localStorage so changes survive a page reload.
// ───────────────────────────────────────────────────────────
const DataContext = React.createContext(null);
const useData = () => React.useContext(DataContext);

const LS_BRANCHES   = 'clover-branches';
const LS_CUSTOMERS  = 'clover-customers';
const LS_DATA_VER   = 'clover-data-version';
const DATA_VERSION  = '4';   // bump to force-clear any cached seed data

// One-time migration: wipe stale localStorage if version is older than current.
try {
  if (typeof localStorage !== 'undefined' && localStorage.getItem(LS_DATA_VER) !== DATA_VERSION) {
    localStorage.removeItem(LS_BRANCHES);
    localStorage.removeItem(LS_CUSTOMERS);
    localStorage.setItem(LS_DATA_VER, DATA_VERSION);
  }
} catch {}

function _nextBranchId(branches, name) {
  const slug = (name || 'branch').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24);
  let candidate = slug || ('branch-' + Date.now().toString(36).slice(-5));
  let i = 1;
  while (branches.some(b => b.id === candidate)) {
    candidate = (slug || 'branch') + '-' + (++i);
  }
  return candidate;
}

// Customer ID pattern:  CV{Thai-BE year last 2 digits}-NNNNN
//   2026 AD = 2569 BE → CV69-00001
//   2027 AD = 2570 BE → CV70-00001
// Each new customer increments the running counter for that BE-year prefix.
function _customerIdPrefix(date = new Date()) {
  const be = date.getFullYear() + 543;
  return 'CV' + String(be).slice(-2);  // e.g. "CV69"
}

function _nextCustomerId(customers) {
  const prefix = _customerIdPrefix();
  // Only consider IDs that share the current year's prefix
  const re = new RegExp('^' + prefix + '-(\\d+)$');
  const nums = (customers || [])
    .map(c => {
      const m = (c.id || '').match(re);
      return m ? parseInt(m[1], 10) : NaN;
    })
    .filter(n => !isNaN(n));
  const max = nums.length ? Math.max(...nums) : 0;
  return prefix + '-' + String(max + 1).padStart(5, '0');
}

function DataProvider({ children }) {
  // D1 is the source of truth. localStorage is used only as an offline cache:
  // we render from it instantly, then refetch from D1 on mount so the UI is
  // always consistent with the database.
  const [branches, setBranches] = useState(() => {
    try {
      const cached = JSON.parse(localStorage.getItem(LS_BRANCHES));
      if (Array.isArray(cached)) return cached;
    } catch {}
    return [];
  });

  const [customers, setCustomers] = useState(() => {
    try {
      const cached = JSON.parse(localStorage.getItem(LS_CUSTOMERS));
      if (Array.isArray(cached)) return cached;
    } catch {}
    return [];
  });

  const [syncing, setSyncing] = useState(false);
  const [lastSyncAt, setLastSyncAt] = useState(null);

  // Fetch everything from D1. Called on mount and after every mutation.
  const refreshFromD1 = React.useCallback(async () => {
    setSyncing(true);
    try {
      const [bRes, cRes] = await Promise.all([
        fetch('/api/branches/list').then(r => r.json()).catch(() => ({ data: [] })),
        fetch('/api/members/list?limit=500').then(r => r.json()).catch(() => ({ data: [] })),
      ]);
      if (Array.isArray(bRes?.data)) {
        setBranches(bRes.data);
        window.BRANCHES = bRes.data;
      }
      if (Array.isArray(cRes?.data)) setCustomers(cRes.data);
      setLastSyncAt(new Date().toISOString());
    } finally {
      setSyncing(false);
    }
  }, []);

  // Hydrate from D1 once on mount.
  useEffect(() => { refreshFromD1(); }, [refreshFromD1]);

  // Persist a local copy on every change (offline cache only).
  useEffect(() => {
    try { localStorage.setItem(LS_BRANCHES, JSON.stringify(branches)); } catch {}
  }, [branches]);
  useEffect(() => {
    try { localStorage.setItem(LS_CUSTOMERS, JSON.stringify(customers)); } catch {}
  }, [customers]);

  // Branch mutators — with D1 sync + activity log
  const addBranch = (data) => {
    const id = _nextBranchId(branches, data.name);
    const newBranch = { ...data, id };
    setBranches(bs => [...bs, newBranch]);

    // Save to D1
    fetch('/api/branches/create', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(newBranch),
    })
    .then(r => r.json())
    .then(result => {
      if (result.success) console.log('[Admin] Branch saved to D1:', id);
      else console.warn('[Admin] Failed to save branch to D1:', result.error);
    })
    .catch(err => console.warn('[Admin] Error saving branch to D1:', err));

    logActivity({
      action_type: 'branch_create',
      action_label: `Created branch "${data.name}"`,
      branch_id: id, target_type: 'branch', target_id: id, target_name: data.name,
      details: data,
    });

    return id;
  };
  const updateBranch = (updated) => {
    setBranches(bs => bs.map(b => b.id === updated.id ? updated : b));
    // If the branch was renamed, rename it on every customer too
    const prev = branches.find(b => b.id === updated.id);
    if (prev && prev.name !== updated.name) {
      setCustomers(cs => cs.map(c => c.branch === prev.name ? { ...c, branch: updated.name } : c));
    }

    // Save to D1
    fetch(`/api/branches/${updated.id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updated),
    })
    .then(r => r.json())
    .then(result => {
      if (result.success) console.log('[Admin] Branch updated in D1:', updated.id);
      else console.warn('[Admin] Failed to update branch in D1:', result.error);
    })
    .catch(err => console.warn('[Admin] Error updating branch in D1:', err));

    logActivity({
      action_type: 'branch_update',
      action_label: `Updated branch "${updated.name}"`,
      branch_id: updated.id, target_type: 'branch', target_id: updated.id, target_name: updated.name,
      details: { before: prev, after: updated },
    });
  };
  const deleteBranch = (id) => {
    const target = branches.find(b => b.id === id);
    setBranches(bs => bs.filter(b => b.id !== id));
    // Clear branch on customers that referenced it
    if (target) {
      setCustomers(cs => cs.map(c => c.branch === target.name ? { ...c, branch: '' } : c));
    }

    // Delete from D1
    fetch(`/api/branches/${id}`, {
      method: 'DELETE',
      headers: { 'Content-Type': 'application/json' },
    })
    .then(r => r.json())
    .then(result => {
      if (result.success) console.log('[Admin] Branch deleted from D1:', id);
      else console.warn('[Admin] Failed to delete branch from D1:', result.error);
    })
    .catch(err => console.warn('[Admin] Error deleting branch from D1:', err));

    logActivity({
      action_type: 'branch_delete',
      action_label: `Deleted branch "${target?.name || id}"`,
      branch_id: id, target_type: 'branch', target_id: id, target_name: target?.name || id,
    });
  };

  // Customer mutators — with D1 sync
  const addCustomer = (data) => {
    const id = _nextCustomerId(customers);
    const newCustomer = { ...data, id };
    setCustomers(cs => [...cs, newCustomer]);

    // Save to D1
    fetch('/api/members/create', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(newCustomer),
    })
    .then(r => r.json())
    .then(result => {
      if (result.success) console.log('[Admin] Customer saved to D1:', id);
      else console.warn('[Admin] Failed to save customer to D1:', result.error);
    })
    .catch(err => console.warn('[Admin] Error saving customer to D1:', err));

    logActivity({
      action_type: 'customer_create',
      action_label: `Created customer "${data.name || id}"`,
      target_type: 'member', target_id: id, target_name: data.name || id,
      details: data,
    });

    return id;
  };
  const updateCustomer = async (updated) => {
    // If the admin edited the customer ID, do a cascading rename first
    // so all child rows (line_users, claims, transactions, rewards) follow.
    const oldId = updated._originalId || updated.id;
    const newId = updated.id;
    const isRename = oldId && newId && oldId !== newId;

    const before = customers.find(c => c.id === oldId);
    // Optimistic local update (handles both rename + field changes)
    setCustomers(cs => cs.map(c =>
      c.id === oldId ? { ...updated, id: newId } : c
    ));

    if (isRename) {
      try {
        const me = getCurrentUser();
        const res = await fetch(`/api/members/${encodeURIComponent(oldId)}/rename`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'x-user-id':   me.id   || '',
            'x-user-name': me.name || '',
            'x-user-role': me.role || '',
          },
          body: JSON.stringify({ new_id: newId }),
        });
        const result = await res.json();
        if (!res.ok || !result.success) throw new Error(result.error || 'rename failed');
        console.log('[Admin] Customer renamed:', oldId, '→', newId);
      } catch (err) {
        console.warn('[Admin] Rename failed:', err);
        // Roll back the optimistic local update
        if (before) setCustomers(cs => cs.map(c => c.id === newId ? { ...before } : c));
        alert('เปลี่ยนรหัสไม่สำเร็จ: ' + err.message);
        return;
      }
    }

    // Then update the rest of the fields on the (possibly-renamed) id
    fetch(`/api/members/${encodeURIComponent(newId)}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updated),
    })
    .then(r => r.json())
    .then(result => {
      if (result.success) console.log('[Admin] Customer updated in D1:', newId);
      else console.warn('[Admin] Failed to update customer in D1:', result.error);
    })
    .catch(err => console.warn('[Admin] Error updating customer in D1:', err));

    // Special-case points changes — log as a points_adjust event in addition
    // to the generic customer_update, so accounting/audit can find them quickly.
    if (before && Number(before.points) !== Number(updated.points)) {
      const delta = Number(updated.points || 0) - Number(before.points || 0);
      logActivity({
        action_type: 'points_adjust',
        action_label: `Adjusted points for ${updated.name} (${delta > 0 ? '+' : ''}${delta})`,
        target_type: 'member', target_id: updated.id, target_name: updated.name,
        details: { before: before.points, after: updated.points, delta },
      });
    }

    logActivity({
      action_type: 'customer_update',
      action_label: `Updated customer "${updated.name || updated.id}"`,
      target_type: 'member', target_id: updated.id, target_name: updated.name,
      details: { before, after: updated },
    });
  };
  const deleteCustomer = (id) => {
    const target = customers.find(c => c.id === id);
    setCustomers(cs => cs.filter(c => c.id !== id));

    // Delete from D1
    fetch(`/api/members/${id}`, {
      method: 'DELETE',
      headers: { 'Content-Type': 'application/json' },
    })
    .then(r => r.json())
    .then(result => {
      if (result.success) console.log('[Admin] Customer deleted from D1:', id);
      else console.warn('[Admin] Failed to delete customer from D1:', result.error);
    })
    .catch(err => console.warn('[Admin] Error deleting customer from D1:', err));

    logActivity({
      action_type: 'customer_delete',
      action_label: `Deleted customer "${target?.name || id}"`,
      target_type: 'member', target_id: id, target_name: target?.name || id,
    });
  };

  const value = {
    branches, customers,
    addBranch, updateBranch, deleteBranch,
    addCustomer, updateCustomer, deleteCustomer,
    refreshFromD1, syncing, lastSyncAt,
  };

  return <DataContext.Provider value={value}>{children}</DataContext.Provider>;
}

// ───────────────────────────────────────────────────────────
// Sidebar
// ───────────────────────────────────────────────────────────
function AdminSidebar({ view, setView, role, setRole, brand: b = DEFAULT_BRAND, logoUrl, onSignOut }) {
  const { t } = useTranslation();
  const bg    = b.cloverDeep || A.cloverDeep;
  const gold  = b.gold       || A.gold;
  const goldD = b.goldDeep   || A.goldDeep;

  const items = [
    { id: 'branches',  label: t('admin.branches'),    I: IconBranch },
    { id: 'customers', label: t('admin.customers'),   I: IconUsers },
    { id: 'line-users',label: 'LINE Logins',          I: IconUsers },
    { id: 'claims',    label: 'Point Claims',         I: IconGift },
    { id: 'staff',     label: 'Staff Management',     I: IconUsers },
    { id: 'logs',      label: 'Activity Logs',        I: IconFile },
    { id: 'import',    label: t('admin.import_data'), I: IconUpload },
    { id: 'cms',       label: t('admin.cms_rewards'), I: IconImage },
    { id: 'reward-new',label: 'New reward',           I: IconGift },
    { id: 'branding',  label: 'Branding',             I: IconSettings },
    { id: 'settings',  label: t('admin.settings'),    I: IconSettings },
  ];

  return (
    <div style={{
      width: 232, background: bg, color: '#fff', flexShrink: 0,
      display: 'flex', flexDirection: 'column', padding: 14,
    }}>
      {/* Logo / clinic name */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 8px 18px' }}>
        {logoUrl
          ? <img src={logoUrl} style={{ height: 28, maxWidth: 140, objectFit: 'contain' }} />
          : (
            <>
              <CloverMark size={26} color={gold} />
              <div>
                <div className="serif" style={{ fontSize: 17, color: '#fff', letterSpacing: -0.2 }}>Clover Clinic</div>
                <div style={{ fontSize: 10, color: 'rgba(255,255,255,0.5)', letterSpacing: 1.5, textTransform: 'uppercase' }}>{t('admin.console')}</div>
              </div>
            </>
          )
        }
      </div>

      {/* Nav items */}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
        {items.filter(i => canAccessView(role, i.id)).map(item => {
          const active = view === item.id;
          return (
            <div key={item.id}
              onClick={() => setView(item.id)}
              style={{
                padding: '10px 12px', borderRadius: 8,
                background: active ? `rgba(${hexToRgb(gold)},0.14)` : 'transparent',
                color: active ? gold : 'rgba(255,255,255,0.75)',
                display: 'flex', alignItems: 'center', gap: 10,
                fontSize: 13, fontWeight: active ? 600 : 500,
                cursor: 'pointer', transition: 'background .12s, color .12s',
                position: 'relative',
              }}
              onMouseEnter={e => !active && (e.currentTarget.style.background = 'rgba(255,255,255,0.04)')}
              onMouseLeave={e => !active && (e.currentTarget.style.background = 'transparent')}
            >
              {active && <div style={{ position: 'absolute', left: -2, top: 8, bottom: 8, width: 2, background: gold, borderRadius: 2 }} />}
              <item.I size={16} color="currentColor" strokeWidth={1.7} />
              <span style={{ flex: 1 }}>{item.label}</span>
            </div>
          );
        })}
      </div>

      <div style={{ flex: 1 }} />

      {/* User card */}
      <div style={{
        padding: 12, borderRadius: 10, background: 'rgba(255,255,255,0.04)',
        fontSize: 11, color: 'rgba(255,255,255,0.65)', lineHeight: 1.5, marginBottom: 10,
      }}>
        <div style={{ fontWeight: 600, color: '#fff', marginBottom: 2 }}>{getCurrentUser().name || 'Admin'}</div>
        {getCurrentUser().email || '—'}<br/>
        <span style={{ color: gold }}>{ROLE_LABEL[role] || role}</span>
      </div>

      {/* RBAC switcher — ONLY visible to super_admin. Admin / Manager / Staff
          cannot impersonate other roles. We check the ORIGINAL login role
          (stored at sign-in) so flipping the preview doesn't hide the
          switcher itself. */}
      {sessionStorage.getItem('clover-login-role') === 'super_admin' && (
        <div style={{ padding: '8px 10px', borderRadius: 8, background: 'rgba(255,255,255,0.04)', marginBottom: 8, fontSize: 11 }}>
          <div style={{ fontSize: 9, letterSpacing: 1.2, textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)', fontWeight: 600, marginBottom: 6 }}>Preview as role</div>
          <div style={{ display: 'flex', gap: 4, background: 'rgba(0,0,0,0.2)', borderRadius: 6, padding: 3 }}>
            {[
              { id: 'super_admin', label: 'Super' },
              { id: 'admin',       label: 'Admin' },
              { id: 'manager',     label: 'Manager' },
            ].map(r => (
              <div key={r.id} onClick={() => setRole && setRole(r.id)} style={{
                flex: 1, padding: '5px 6px', borderRadius: 4, cursor: 'pointer',
                background: role === r.id ? gold : 'transparent',
                color: role === r.id ? bg : 'rgba(255,255,255,0.65)',
                fontSize: 10, fontWeight: 700, letterSpacing: 0.3, textAlign: 'center',
              }}>{r.label}</div>
            ))}
          </div>
        </div>
      )}

      <LanguageSwitcher variant="pill" dark={true} />

      {/* Sign out */}
      {onSignOut && (
        <button type="button" onClick={onSignOut} style={{
          marginTop: 8, width: '100%', padding: '8px 12px', borderRadius: 8,
          background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)',
          color: 'rgba(255,255,255,0.55)', fontSize: 12, fontWeight: 600, cursor: 'pointer',
          fontFamily: 'inherit', textAlign: 'left', display: 'flex', alignItems: 'center', gap: 8,
          transition: 'background .12s, color .12s',
        }}
          onMouseEnter={(e) => { e.currentTarget.style.background='rgba(255,80,60,0.15)'; e.currentTarget.style.color='#FF8070'; }}
          onMouseLeave={(e) => { e.currentTarget.style.background='rgba(255,255,255,0.05)'; e.currentTarget.style.color='rgba(255,255,255,0.55)'; }}
        >
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>
          </svg>
          Sign out
        </button>
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// Topbar
// ───────────────────────────────────────────────────────────
function AdminTopbar({ title, subtitle, right }) {
  return (
    <div style={{
      padding: '20px 28px', display:'flex', alignItems:'flex-end', justifyContent:'space-between',
      borderBottom: '1px solid ' + A.line, background: '#fff',
    }}>
      <div>
        <div className="serif" style={{ fontSize: 26, color: A.clover, letterSpacing: -0.3 }}>{title}</div>
        {subtitle && <div style={{ fontSize: 12.5, color: A.mute, marginTop: 4 }}>{subtitle}</div>}
      </div>
      <div style={{ display:'flex', gap: 10, alignItems:'center' }}>{right}</div>
    </div>
  );
}

function Btn({ children, primary, onClick, style = {} }) {
  return (
    <button onClick={onClick} style={{
      padding: '8px 14px', borderRadius: 8, fontSize: 13, fontWeight: 600,
      border: primary ? 'none' : '1px solid ' + A.line2,
      background: primary ? A.clover : '#fff',
      color: primary ? '#fff' : A.ink,
      cursor: 'pointer',
      ...style,
    }}>{children}</button>
  );
}

// ───────────────────────────────────────────────────────────
// OVERVIEW view
// ───────────────────────────────────────────────────────────
function OverviewView() {
  const { t } = useTranslation();
  const { branches, customers } = useData();

  // Per-branch numbers: customer count + visit count derived from live data
  // (revenue/delta still rotated through a small mock array for visual variety)
  const REV_DEFAULTS   = [4.2, 3.8, 3.1, 2.9, 2.2, 1.6, 1.4, 0.8];
  const DELTA_DEFAULTS = [+8, +12, -3, +4, +18, +22, +9, +110];
  const branchData = branches.map((b, i) => {
    const memberCount = customers.filter(c => c.branch === b.name).length;
    return {
      ...b,
      revenue: REV_DEFAULTS[i % REV_DEFAULTS.length] || 1,
      visits:  Math.max(30, memberCount * 18 + 40),
      delta:   DELTA_DEFAULTS[i % DELTA_DEFAULTS.length] || 0,
      members: memberCount,
    };
  });
  const maxRev = branchData.length ? Math.max(...branchData.map(b => b.revenue)) : 1;

  // Live KPIs from the shared store
  const totalMembers = customers.length;
  const diamondCount = customers.filter(c => c.tier === 'Diamond').length;
  const platinumCount = customers.filter(c => c.tier === 'Platinum').length;
  const totalPoints  = customers.reduce((s, c) => s + (c.pts || 0), 0);
  const avgPoints    = totalMembers ? Math.round(totalPoints / totalMembers) : 0;

  return (
    <div>
      <AdminTopbar
        title={t('admin.group_overview')}
        subtitle={`${branches.length} branches · ${totalMembers} members · ${diamondCount + platinumCount} VIP`}
        right={<>
          <Btn>{t('admin.last_7_days')} ▾</Btn>
          <Btn primary><IconDownload size={14} style={{ verticalAlign: -2, marginRight: 6 }} />{t('common.export')}</Btn>
        </>}
      />

      {/* KPI grid — live from shared store */}
      <div style={{ padding: 28, display:'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14 }}>
        {[
          { label: 'Total members',          value: totalMembers.toLocaleString(),         delta: '+18%',  tone: 'pos' },
          { label: 'VIP (Diamond+Platinum)', value: (diamondCount + platinumCount).toString(), delta: '+5.1%', tone: 'pos' },
          { label: 'Avg points balance',     value: avgPoints.toLocaleString(),            delta: '+2.2%', tone: 'pos' },
          { label: 'Active branches',        value: branches.length.toString(),            delta: '—',     tone: 'pos' },
        ].map((k, i) => (
          <div key={i} style={{
            background: '#fff', border: '1px solid ' + A.line, borderRadius: 12,
            padding: '16px 18px',
          }}>
            <div style={{ fontSize: 11, letterSpacing: 1.4, textTransform:'uppercase', color: A.mute, fontWeight: 600 }}>{k.label}</div>
            <div className="serif" style={{ fontSize: 30, color: A.ink, marginTop: 8, letterSpacing: -0.5 }}>{k.value}</div>
            <div style={{ fontSize: 11.5, color: k.tone === 'pos' ? '#4F7A4A' : '#B5453A', marginTop: 4, fontWeight: 600 }}>
              {k.delta} <span style={{ color: A.mute, fontWeight: 400 }}>{t('admin.vs_last_wk')}</span>
            </div>
          </div>
        ))}
      </div>

      {/* Branches table */}
      <div style={{ padding: '0 28px 28px' }}>
        <div style={{ background: '#fff', border: '1px solid ' + A.line, borderRadius: 12, overflow: 'hidden' }}>
          <div style={{ padding: '14px 18px', borderBottom: '1px solid ' + A.line, display:'flex', justifyContent:'space-between', alignItems:'center' }}>
            <div className="serif" style={{ fontSize: 17, color: A.ink }}>{t('admin.performance_by_branch')}</div>
            <div style={{ fontSize: 11.5, color: A.mute }}>{t('admin.sorted_by_revenue')}</div>
          </div>
          <div>
            <div style={{
              display:'grid', gridTemplateColumns: '32px 1.4fr 1fr 1fr 1.2fr 80px',
              padding: '8px 18px', background: A.bone, fontSize: 10.5, letterSpacing: 1.2, textTransform:'uppercase',
              color: A.mute, fontWeight: 600,
            }}>
              <div>#</div><div>Branch</div><div>Revenue</div><div>Members</div><div>Share</div><div style={{ textAlign:'right' }}>Δ wk</div>
            </div>
            {branchData.length === 0 && (
              <div style={{ padding: '28px 18px', textAlign:'center', color: A.mute, fontSize: 13 }}>
                No branches yet. Create one in <strong style={{ color: A.clover }}>Branches</strong>.
              </div>
            )}
            {branchData.map((b, i) => (
              <div key={b.id} style={{
                display:'grid', gridTemplateColumns: '32px 1.4fr 1fr 1fr 1.2fr 80px',
                padding: '13px 18px', alignItems:'center',
                borderTop: '1px solid ' + A.line, fontSize: 13,
              }}>
                <div style={{ color: A.mute, fontFamily: 'JetBrains Mono, monospace', fontSize: 12 }}>{String(i + 1).padStart(2, '0')}</div>
                <div>
                  <div style={{ color: A.ink, fontWeight: 600 }}>{b.name}</div>
                  <div style={{ fontSize: 11, color: A.mute, marginTop: 1 }}>{b.city}</div>
                </div>
                <div className="mono" style={{ color: A.ink, fontWeight: 500 }}>฿{b.revenue.toFixed(1)}M</div>
                <div className="mono" style={{ color: A.ink2 }}>{b.members}</div>
                <div style={{ display:'flex', alignItems:'center', gap: 8 }}>
                  <div style={{ flex: 1, height: 5, background: A.bone, borderRadius: 3, overflow:'hidden' }}>
                    <div style={{ width: ((b.revenue / maxRev) * 100) + '%', height: '100%', background: A.clover }} />
                  </div>
                  <div style={{ fontSize: 11, color: A.mute, width: 32, fontFamily: 'JetBrains Mono, monospace' }}>
                    {Math.round((b.revenue / branchData.reduce((s,x) => s + x.revenue, 0)) * 100)}%
                  </div>
                </div>
                <div style={{
                  textAlign:'right', fontSize: 12, fontWeight: 600,
                  color: b.delta >= 0 ? '#4F7A4A' : '#B5453A',
                }}>{b.delta >= 0 ? '+' : ''}{b.delta}%</div>
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* Lower row */}
      <div style={{ padding: '0 28px 28px', display:'grid', gridTemplateColumns: '2fr 1fr', gap: 14 }}>
        <div style={{ background:'#fff', border:'1px solid '+A.line, borderRadius: 12, padding: 18 }}>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 18 }}>
            <div className="serif" style={{ fontSize: 17, color: A.ink }}>Revenue trend</div>
            <div style={{ fontSize: 11, color: A.mute }}>Daily · last 30 days</div>
          </div>
          <Sparkline />
        </div>
        <div style={{ background:'#fff', border:'1px solid '+A.line, borderRadius: 12, padding: 18 }}>
          <div className="serif" style={{ fontSize: 17, color: A.ink, marginBottom: 14 }}>Tier mix</div>
          <TierMix />
        </div>
      </div>
    </div>
  );
}

function Sparkline() {
  const pts = [3.2, 3.4, 3.1, 3.6, 3.9, 4.1, 3.8, 3.5, 3.7, 4.0, 4.2, 4.5, 4.1, 3.9, 4.3, 4.6, 4.4, 4.7, 4.5, 4.2, 4.6, 4.9, 5.1, 4.7, 4.9, 5.2, 5.0, 4.8, 5.1, 5.4];
  const w = 540, h = 160, pad = 8;
  const max = Math.max(...pts), min = Math.min(...pts) - 0.3;
  const x = (i) => pad + (i * (w - pad*2)) / (pts.length - 1);
  const y = (v) => pad + ((max - v) / (max - min)) * (h - pad*2);
  const path = pts.map((v, i) => `${i === 0 ? 'M' : 'L'} ${x(i)} ${y(v)}`).join(' ');
  const area = path + ` L ${x(pts.length-1)} ${h-pad} L ${pad} ${h-pad} Z`;
  return (
    <svg viewBox={`0 0 ${w} ${h}`} width="100%" height={h}>
      <defs>
        <linearGradient id="spkgrad" x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%" stopColor={A.clover} stopOpacity="0.18"/>
          <stop offset="100%" stopColor={A.clover} stopOpacity="0"/>
        </linearGradient>
      </defs>
      {[0,1,2,3].map(i => (
        <line key={i} x1={pad} x2={w-pad} y1={pad + (h-pad*2)*i/3} y2={pad + (h-pad*2)*i/3} stroke={A.line} strokeDasharray="2 3"/>
      ))}
      <path d={area} fill="url(#spkgrad)"/>
      <path d={path} fill="none" stroke={A.clover} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
      {pts.map((v, i) => i === pts.length - 1 && (
        <circle key={i} cx={x(i)} cy={y(v)} r="4" fill={A.gold} stroke="#fff" strokeWidth="2"/>
      ))}
    </svg>
  );
}

function TierMix() {
  const rows = [
    { label: 'Diamond', pct: 6,  count: '412',   color: A.clover },
    { label: 'Platinum', pct: 14, count: '961',   color: A.cloverMid },
    { label: 'Gold',     pct: 32, count: '2,198', color: A.gold },
    { label: 'Silver',   pct: 48, count: '3,294', color: A.line2 },
  ];
  return (
    <div>
      <div style={{ display:'flex', height: 8, borderRadius: 4, overflow:'hidden', marginBottom: 14 }}>
        {rows.map(r => <div key={r.label} style={{ width: r.pct + '%', background: r.color }} />)}
      </div>
      {rows.map(r => (
        <div key={r.label} style={{ display:'flex', alignItems:'center', gap: 8, padding: '5px 0', fontSize: 12.5 }}>
          <div style={{ width: 8, height: 8, borderRadius: 2, background: r.color }}/>
          <span style={{ color: A.ink, flex: 1 }}>{r.label}</span>
          <span className="mono" style={{ color: A.mute }}>{r.count}</span>
          <span className="mono" style={{ color: A.ink, width: 36, textAlign:'right', fontWeight: 600 }}>{r.pct}%</span>
        </div>
      ))}
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// IMPORT view — Excel/CSV upload engine (per brief)
// ───────────────────────────────────────────────────────────
// Auto-guess which target field a source column name maps to
function _guessTarget(col) {
  const c = col.toLowerCase().replace(/[_\s-]/g, '');
  if (/custid|customerid|cust_id|member_?id|id$/.test(c)) return 'customer_id';
  if (/name|ชื่อ/.test(c)) return 'full_name';
  if (/mobile|phone|tel|เบอร์/.test(c)) return 'phone';
  if (/email|อีเมล/.test(c)) return 'email';
  if (/tier|level|ระดับ/.test(c)) return 'tier';
  if (/point|pt|คะแนน/.test(c)) return 'points_balance';
  if (/branch|สาขา/.test(c)) return 'home_branch';
  if (/course|คอร์ส/.test(c)) return 'course_template';
  if (/session|visit|ครั้ง/.test(c)) return 'sessions_left';
  if (/unit|tox|โบท/.test(c)) return 'units_left';
  if (/dob|birth|วันเกิด/.test(c)) return 'birthday';
  return '— skip —';
}

const TARGET_FIELDS = [
  { value: 'customer_id',    label: 'customer_id',    required: true },
  { value: 'full_name',      label: 'full_name',      required: true },
  { value: 'phone',          label: 'phone',          required: true },
  { value: 'email',          label: 'email',          required: false },
  { value: 'tier',           label: 'tier',           required: false },
  { value: 'points_balance', label: 'points_balance', required: false },
  { value: 'home_branch',    label: 'home_branch',    required: false },
  { value: 'course_template',label: 'course_template',required: false },
  { value: 'sessions_left',  label: 'sessions_left',  required: false },
  { value: 'units_left',     label: 'units_left',     required: false },
  { value: 'birthday',       label: 'birthday',       required: false },
  { value: '— skip —',       label: '— skip —',       required: false },
];

function ImportView() {
  const { t } = useTranslation();
  const { addCustomer, branches } = useData();
  const [stage, setStage] = useState('select');
  const [progress, setProgress] = useState(0);
  const [fileData, setFileData] = useState(null); // { name, size, rows, headers, preview }
  const [importedCount, setImportedCount] = useState(0);

  // When the import completes, actually push parsed rows into the shared store
  // so they appear in Customers / Overview live.
  const commitImport = () => {
    if (!fileData || !fileData.preview || !fileData.preview.length) return 0;
    const headers = fileData.headers || [];
    const defaultBranch = (branches[0] && branches[0].name) || '';
    let count = 0;
    fileData.preview.forEach(row => {
      const get = (re) => {
        const idx = headers.findIndex(h => re.test(h));
        return idx >= 0 ? String(row[idx] ?? '').trim() : '';
      };
      const name  = get(/name|ชื่อ/i);
      const phone = get(/phone|mobile|tel|เบอร์|โทร/i);
      if (!name && !phone) return; // skip empty rows
      const email = get(/email|อีเมล/i);
      const tier  = (get(/tier|ระดับ/i) || 'Silver').replace(/^\w/, ch => ch.toUpperCase());
      const pts   = parseInt(get(/point|pts|คะแนน/i), 10) || 0;
      const branch = get(/branch|สาขา/i) || defaultBranch;
      addCustomer({
        name: name || 'Unknown',
        phone, email, tier, pts, branch, notes: '',
      });
      count++;
    });
    return count;
  };

  useEffect(() => {
    if (stage === 'running') {
      let p = 0;
      const id = setInterval(() => {
        p += 3 + Math.random() * 4;
        if (p >= 100) {
          setProgress(100);
          const n = commitImport();
          setImportedCount(n);
          setTimeout(() => setStage('done'), 600);
          clearInterval(id);
        } else {
          setProgress(p);
        }
      }, 80);
      return () => clearInterval(id);
    }
  }, [stage]);

  return (
    <div>
      <AdminTopbar
        title={t('admin.import_title')}
        subtitle={t('admin.import_sub')}
        right={<>
          <Btn><IconDownload size={14} style={{ verticalAlign: -2, marginRight: 6 }}/>Template</Btn>
          <Btn>Import history</Btn>
        </>}
      />

      <div style={{ padding: '20px 28px 0' }}>
        <div style={{ display:'flex', alignItems:'center', gap: 0 }}>
          {[
            { id: 'select',  label: 'Upload file' },
            { id: 'mapping', label: 'Map columns' },
            { id: 'preview', label: 'Preview' },
            { id: 'running', label: 'Import' },
            { id: 'done',    label: 'Complete' },
          ].map((s, i, arr) => {
            const stageOrder = ['select','mapping','preview','running','done'];
            const curIdx = stageOrder.indexOf(stage);
            const past = stageOrder.indexOf(s.id) < curIdx;
            const active = s.id === stage;
            return (
              <React.Fragment key={s.id}>
                <div style={{ display:'flex', alignItems:'center', gap: 8 }}>
                  <div style={{
                    width: 24, height: 24, borderRadius: 12,
                    background: past || active ? A.clover : '#fff',
                    border: '1.5px solid ' + (past || active ? A.clover : A.line2),
                    color: past || active ? '#fff' : A.mute,
                    display:'flex', alignItems:'center', justifyContent:'center',
                    fontSize: 11, fontWeight: 700,
                  }}>
                    {past ? <IconCheck size={12} color="#fff"/> : i + 1}
                  </div>
                  <div style={{
                    fontSize: 12.5, fontWeight: active ? 700 : 500,
                    color: active ? A.ink : (past ? A.ink2 : A.mute),
                  }}>{s.label}</div>
                </div>
                {i < arr.length - 1 && (
                  <div style={{ flex: 1, height: 1, background: past ? A.clover : A.line, margin: '0 14px' }} />
                )}
              </React.Fragment>
            );
          })}
        </div>
      </div>

      <div style={{ padding: '24px 28px 28px' }}>
        {stage === 'select'  && <ImportSelect  onFileLoad={setFileData} onNext={() => setStage('mapping')} />}
        {stage === 'mapping' && <ImportMapping fileData={fileData} onBack={() => setStage('select')} onNext={() => setStage('preview')} />}
        {stage === 'preview' && <ImportPreview fileData={fileData} onBack={() => setStage('mapping')} onRun={() => { setProgress(0); setStage('running'); }} />}
        {stage === 'running' && <ImportRunning progress={progress} total={fileData ? fileData.rows : 0} />}
        {stage === 'done'    && <ImportDone    total={importedCount || (fileData ? fileData.rows : 0)} onAnother={() => { setFileData(null); setImportedCount(0); setStage('select'); }} />}
      </div>
    </div>
  );
}

function ImportSelect({ onNext, onFileLoad }) {
  const inputRef = useRef(null);
  const [drag, setDrag] = useState(false);
  const [file, setFile] = useState(null);
  const [parseError, setParseError] = useState(null);
  const [parsing, setParsing] = useState(false);

  const handleFile = (f) => {
    if (!f) return;
    const ext = f.name.split('.').pop().toLowerCase();
    if (!['xlsx','xls','csv','tsv'].includes(ext)) {
      setParseError('Unsupported file type. Please upload .xlsx, .xls, .csv, or .tsv.');
      return;
    }
    setParseError(null);
    setParsing(true);

    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        let rows = 0, headers = [], preview = [];
        if (ext === 'csv' || ext === 'tsv') {
          const sep = ext === 'tsv' ? '\t' : ',';
          const text = e.target.result;
          const lines = text.split(/\r?\n/).filter(l => l.trim());
          headers = lines[0].split(sep).map(h => h.replace(/^"|"$/g, '').trim());
          rows = Math.max(0, lines.length - 1);
          preview = lines.slice(1, 7).map(l =>
            l.split(sep).map(v => v.replace(/^"|"$/g, '').trim())
          );
        } else if (window.XLSX) {
          const wb = window.XLSX.read(e.target.result, { type: 'array' });
          const ws = wb.Sheets[wb.SheetNames[0]];
          const data = window.XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' });
          headers = (data[0] || []).map(h => String(h).trim());
          rows = Math.max(0, data.length - 1);
          preview = data.slice(1, 7).map(row => row.map(v => String(v)));
        } else {
          setParseError('Excel parser not loaded yet. Please refresh and try again.');
          setParsing(false);
          return;
        }
        const fd = {
          name: f.name,
          size: f.size < 1024 * 1024
            ? (f.size / 1024).toFixed(0) + ' KB'
            : (f.size / 1024 / 1024).toFixed(1) + ' MB',
          rows, headers, preview,
        };
        setFile(fd);
        onFileLoad(fd);
      } catch (err) {
        setParseError('Could not parse file: ' + err.message);
      }
      setParsing(false);
    };
    reader.onerror = () => { setParseError('File read error.'); setParsing(false); };

    if (ext === 'xlsx' || ext === 'xls') reader.readAsArrayBuffer(f);
    else reader.readAsText(f, 'UTF-8');
  };

  return (
    <div style={{ display:'grid', gridTemplateColumns: '1.4fr 1fr', gap: 18 }}>
      <div>
        <input
          ref={inputRef} type="file"
          accept=".xlsx,.xls,.csv,.tsv"
          style={{ display: 'none' }}
          onChange={(e) => handleFile(e.target.files && e.target.files[0])}
        />
        <div
          onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
          onDragLeave={() => setDrag(false)}
          onDrop={(e) => {
            e.preventDefault(); setDrag(false);
            handleFile(e.dataTransfer.files && e.dataTransfer.files[0]);
          }}
          onClick={() => inputRef.current && inputRef.current.click()}
          style={{
            background: drag ? A.cloverSoft : '#fff',
            border: '2px dashed ' + (drag ? A.clover : A.line2),
            borderRadius: 14, padding: '48px 24px', textAlign:'center',
            cursor:'pointer', transition: 'background .15s, border-color .15s',
          }}
        >
          <div style={{
            width: 56, height: 56, borderRadius: 14, background: A.cloverSoft, margin: '0 auto',
            display:'flex', alignItems:'center', justifyContent:'center',
          }}>
            <IconUpload size={24} color={A.clover} />
          </div>
          <div className="serif" style={{ fontSize: 22, marginTop: 16, color: A.ink }}>
            {parsing ? 'Reading file…' : 'Drop Excel or CSV file here'}
          </div>
          <div style={{ fontSize: 13, color: A.mute, marginTop: 6 }}>
            Or click to browse · max 100 MB · .xlsx, .xls, .csv
          </div>
          <div style={{ marginTop: 18, display:'flex', justifyContent:'center', gap: 8 }}>
            {['.xlsx','.csv','.tsv'].map(ext => (
              <span key={ext} style={{ fontSize: 11, padding: '5px 10px', background: A.bone, borderRadius: 4, color: A.ink2, fontFamily:'JetBrains Mono, monospace' }}>{ext}</span>
            ))}
          </div>
        </div>

        {parseError && (
          <div style={{ marginTop: 10, padding: '10px 14px', background: '#FEF0EF', border: '1px solid #F5C5C2', borderRadius: 8, fontSize: 12.5, color: '#B5453A' }}>
            {parseError}
          </div>
        )}

        {file && !parseError && (
          <div className="slide-up" style={{
            marginTop: 14, background:'#fff', border:'1px solid '+A.line, borderRadius: 12,
            padding: '14px 16px', display:'flex', alignItems:'center', gap: 14,
          }}>
            <div style={{ width: 40, height: 40, borderRadius: 8, background: A.goldSoft, display:'flex', alignItems:'center', justifyContent:'center' }}>
              <IconFile size={18} color={A.goldDeep}/>
            </div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 13.5, fontWeight: 600, color: A.ink }}>{file.name}</div>
              <div style={{ fontSize: 11.5, color: A.mute, marginTop: 2 }}>
                {file.size} · <strong style={{ color: A.ink }}>{file.rows.toLocaleString()}</strong> data rows · {file.headers.length} columns
              </div>
            </div>
            <Btn primary onClick={onNext}>Next: Map columns →</Btn>
          </div>
        )}
      </div>

      <div style={{ background: A.bone, borderRadius: 12, padding: 18 }}>
        <div style={{ fontSize: 11, letterSpacing: 1.4, textTransform:'uppercase', color: A.mute, fontWeight: 600 }}>Tips</div>
        <ul style={{ fontSize: 12.5, color: A.ink2, lineHeight: 1.65, paddingLeft: 16, marginTop: 8 }}>
          <li>The first row must contain headers — see the template.</li>
          <li>Each <span className="mono" style={{ background:'#fff', padding:'1px 5px', borderRadius:3 }}>customer_id</span> across rows is treated as one member.</li>
          <li>Course rows ("Meso 3/5") become <span className="mono" style={{ background:'#fff', padding:'1px 5px', borderRadius:3 }}>course_balances</span>.</li>
          <li>Point balances are imported as-is. Earn rule (฿25 = 1 pt) applies to future orders only.</li>
          <li>Branch codes must match the slugs in Settings · Branches.</li>
        </ul>
        <div style={{ marginTop: 18, paddingTop: 14, borderTop: '1px solid ' + A.line }}>
          <div style={{ fontSize: 11, letterSpacing: 1.4, textTransform:'uppercase', color: A.mute, fontWeight: 600 }}>Recent imports</div>
          {[
            { name: 'thonglor-may-w1.csv', when: '2 days ago', rows: 482, status: 'ok' },
            { name: 'phromphong-apr.xlsx', when: '12 days ago', rows: 1206, status: 'ok' },
            { name: 'siam-legacy.xlsx',    when: 'Apr 14',      rows: 3144, status: 'warn' },
          ].map((r, i) => (
            <div key={i} style={{ padding: '10px 0', display:'flex', alignItems:'center', gap: 8,
              borderTop: i === 0 ? 'none' : '1px solid ' + A.line, fontSize: 12 }}>
              <IconFile size={14} color={A.mute}/>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ color: A.ink, fontWeight: 500, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{r.name}</div>
                <div style={{ fontSize: 10.5, color: A.mute }}>{r.when} · {r.rows} rows</div>
              </div>
              <Tag color={r.status === 'ok' ? '#4F7A4A' : A.goldDeep} bg={r.status === 'ok' ? '#E6EFE2' : A.goldSoft}>
                {r.status === 'ok' ? 'OK' : '12 warns'}
              </Tag>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function ImportMapping({ fileData, onBack, onNext }) {
  const headers = fileData ? fileData.headers : [
    'CUST_ID','NAME_TH','MOBILE','EMAIL_ADDR','TIER_CODE','PT_BAL',
    'BRANCH','COURSE_CODE','SESSIONS_LEFT','TOX_UNITS_LEFT','DOB_THAI','NOTE',
  ];

  const [mapping, setMapping] = useState(() => {
    const m = {};
    headers.forEach(h => { m[h] = _guessTarget(h); });
    return m;
  });

  const warnings = headers.filter(h => mapping[h] === 'home_branch' || mapping[h] === 'birthday');

  return (
    <div>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 14 }}>
        <div>
          <div className="serif" style={{ fontSize: 18, color: A.ink }}>Map your columns</div>
          <div style={{ fontSize: 12.5, color: A.mute, marginTop: 2 }}>
            Auto-detected {headers.filter(h => mapping[h] !== '— skip —').length} of {headers.length} columns. Review and adjust as needed.
          </div>
        </div>
        <div style={{ display:'flex', gap: 8 }}>
          <Btn onClick={onBack}>← Back</Btn>
          <Btn primary onClick={onNext}>Next: Preview →</Btn>
        </div>
      </div>

      <div style={{ background:'#fff', border:'1px solid '+A.line, borderRadius: 12, overflow:'hidden' }}>
        <div style={{
          display:'grid', gridTemplateColumns: '1fr 24px 1fr 100px 80px',
          padding: '10px 18px', background: A.bone,
          fontSize: 10.5, letterSpacing: 1.2, textTransform:'uppercase', color: A.mute, fontWeight: 600,
        }}>
          <div>Source column</div><div/><div>Maps to</div><div>Status</div><div style={{ textAlign:'right' }}>Required</div>
        </div>
        {headers.map((h, i) => {
          const target = mapping[h] || '— skip —';
          const tf = TARGET_FIELDS.find(f => f.value === target);
          const isSkip = target === '— skip —';
          const isWarn = target === 'home_branch' || target === 'birthday';
          const status = isSkip ? 'skip' : isWarn ? 'warn' : 'ok';
          return (
            <div key={i} style={{
              display:'grid', gridTemplateColumns: '1fr 24px 1fr 100px 80px',
              padding: '12px 18px', alignItems:'center',
              borderTop: '1px solid ' + A.line, fontSize: 13,
            }}>
              <div className="mono" style={{ color: A.ink, fontWeight: 500 }}>{h}</div>
              <div style={{ color: A.mute, textAlign:'center' }}>→</div>
              <div>
                <select
                  value={target}
                  onChange={(e) => setMapping(m => ({ ...m, [h]: e.target.value }))}
                  style={{
                    padding: '4px 8px', borderRadius: 6, border: '1px solid ' + A.line2,
                    fontFamily: 'JetBrains Mono, monospace', fontSize: 11.5,
                    background: isSkip ? A.bone : A.cloverSoft,
                    color: isSkip ? A.mute : A.clover, cursor: 'pointer',
                  }}
                >
                  {TARGET_FIELDS.map(f => <option key={f.value} value={f.value}>{f.value}</option>)}
                </select>
                {!isSkip && <div style={{ fontSize: 10.5, color: A.mute, marginTop: 3 }}>Auto-detected</div>}
              </div>
              <div>
                <Tag
                  bg={status === 'ok' ? '#E6EFE2' : status === 'warn' ? A.goldSoft : A.bone}
                  color={status === 'ok' ? '#4F7A4A' : status === 'warn' ? A.goldDeep : A.mute}
                >
                  {status === 'ok' ? 'Matched' : status === 'warn' ? 'Check' : 'Skipped'}
                </Tag>
              </div>
              <div style={{ textAlign:'right', fontSize: 11, color: tf && tf.required ? A.clover : A.mute }}>
                {tf && tf.required ? '● required' : 'optional'}
              </div>
            </div>
          );
        })}
      </div>

      {warnings.length > 0 && (
        <div style={{ marginTop: 14, padding: 14, background: A.goldSoft, borderRadius: 10, display:'flex', gap: 12, alignItems:'flex-start' }}>
          <div style={{ width: 28, height: 28, borderRadius: 14, background: A.goldDeep, color:'#fff', display:'flex', alignItems:'center', justifyContent:'center', flexShrink: 0, fontWeight: 700, fontSize: 12 }}>!</div>
          <div style={{ fontSize: 12.5, color: A.ink2, lineHeight: 1.55 }}>
            <strong>{warnings.length} column{warnings.length > 1 ? 's' : ''} need review.</strong>
            {warnings.includes('home_branch') && ' Branch codes must match the slugs in Settings · Branches.'}
            {warnings.includes('birthday') && ' Birthday column: if using Buddhist year (พ.ศ.), values will be converted with −543 offset.'}
          </div>
        </div>
      )}
    </div>
  );
}

function ImportPreview({ fileData, onBack, onRun }) {
  const rows = fileData && fileData.preview && fileData.preview.length > 0
    ? fileData.preview
    : null;
  const headers = fileData ? fileData.headers : [];
  const total = fileData ? fileData.rows : 0;

  return (
    <div>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 14 }}>
        <div>
          <div className="serif" style={{ fontSize: 18, color: A.ink }}>
            Preview · first {rows ? rows.length : 0} of {total.toLocaleString()} rows
          </div>
          <div style={{ fontSize: 12.5, color: A.mute, marginTop: 2 }}>
            Review sample rows before importing.
          </div>
        </div>
        <div style={{ display:'flex', gap: 8 }}>
          <Btn onClick={onBack}>← Back</Btn>
          <Btn primary onClick={onRun}>Import {total.toLocaleString()} rows</Btn>
        </div>
      </div>

      <div style={{ background:'#fff', border:'1px solid '+A.line, borderRadius: 12, overflow:'auto' }}>
        <table style={{ width:'100%', borderCollapse:'collapse', fontSize: 12.5 }}>
          <thead>
            <tr style={{ background: A.bone }}>
              {headers.map((h, i) => (
                <th key={i} style={{ padding: '10px 14px', textAlign:'left', fontSize: 10.5, letterSpacing: 1.1, textTransform:'uppercase', color: A.mute, fontWeight: 600, whiteSpace:'nowrap', borderBottom:'1px solid '+A.line }}>
                  {h}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {rows ? rows.map((row, i) => (
              <tr key={i} style={{ borderTop: '1px solid ' + A.line }}>
                {row.map((cell, j) => (
                  <td key={j} style={{ padding: '11px 14px', color: A.ink2, whiteSpace:'nowrap', maxWidth: 180, overflow:'hidden', textOverflow:'ellipsis' }}>
                    {cell}
                  </td>
                ))}
              </tr>
            )) : (
              <tr><td colSpan={Math.max(headers.length, 1)} style={{ padding: 24, textAlign:'center', color: A.mute }}>No preview data available.</td></tr>
            )}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function ImportRunning({ progress, total }) {
  const processed = Math.floor((progress / 100) * total);
  return (
    <div style={{ maxWidth: 540, margin: '40px auto 0', textAlign:'center' }}>
      <div style={{
        width: 84, height: 84, borderRadius: '50%', background: A.cloverSoft,
        margin: '0 auto', display:'flex', alignItems:'center', justifyContent:'center',
        position:'relative',
      }}>
        <IconUpload size={32} color={A.clover}/>
        <svg width="84" height="84" style={{ position:'absolute', top:0, left:0, transform: 'rotate(-90deg)' }}>
          <circle cx="42" cy="42" r="38" fill="none" stroke={A.line} strokeWidth="3"/>
          <circle cx="42" cy="42" r="38" fill="none" stroke={A.clover} strokeWidth="3"
            strokeDasharray={`${(progress/100) * 238} 238`}
            strokeLinecap="round"
            style={{ transition: 'stroke-dasharray .15s' }}/>
        </svg>
      </div>
      <div className="serif" style={{ fontSize: 22, color: A.ink, marginTop: 22 }}>Importing customers…</div>
      <div style={{ fontSize: 13, color: A.mute, marginTop: 6 }}>
        Processing row <span className="mono" style={{ color: A.ink }}>{processed.toLocaleString()}</span> of <span className="mono">{total.toLocaleString()}</span>
      </div>
      <div style={{ marginTop: 24, padding: 16, background:'#fff', border:'1px solid '+A.line, borderRadius: 12, textAlign:'left' }}>
        {[
          'Validating schema',
          'Creating customers',
          progress > 30 && 'Importing point balances',
          progress > 55 && 'Linking course balances',
          progress > 80 && 'Reindexing search',
        ].filter(Boolean).map((s, i) => (
          <div key={i} className="fade-in" style={{ display:'flex', alignItems:'center', gap: 8, padding: '6px 0', fontSize: 12.5, color: A.ink2 }}>
            <IconCheck size={14} color={A.clover}/>{s}
          </div>
        ))}
      </div>
    </div>
  );
}

function ImportDone({ total, onAnother }) {
  const imported = Math.max(0, total - Math.floor(total * 0.002));
  const deferred = total - imported;
  return (
    <div style={{ maxWidth: 540, margin: '40px auto 0', textAlign:'center' }}>
      <div style={{
        width: 84, height: 84, borderRadius: '50%', background: A.clover,
        margin: '0 auto', display:'flex', alignItems:'center', justifyContent:'center',
      }}>
        <IconCheck size={36} color="#fff"/>
      </div>
      <div className="serif" style={{ fontSize: 26, color: A.clover, marginTop: 18 }}>Import complete</div>
      <div style={{ fontSize: 13.5, color: A.mute, marginTop: 8, lineHeight: 1.55 }}>
        {imported.toLocaleString()} customers imported · {deferred} deferred for review
      </div>
      <div style={{ marginTop: 22, display:'flex', gap: 10, justifyContent:'center' }}>
        <Btn onClick={onAnother}>Import another file</Btn>
        <Btn primary>View deferred rows</Btn>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// CMS view
// ───────────────────────────────────────────────────────────
function CMSView({ setView }) {
  const { t } = useTranslation();
  const [tab, setTab] = useState('banners');
  const [pointRate, setPointRate] = useState(25);

  // Rewards loaded from D1 (always fresh, never stale)
  const [rewards, setRewards] = useState([]);
  const [editingReward, setEditingReward] = useState(null);

  // Reload rewards from D1
  const loadRewards = React.useCallback(() => {
    fetch('/api/rewards/list')
      .then(r => r.json())
      .then(d => {
        if (d?.success && Array.isArray(d.data)) {
          // Normalize for the admin UI which expects `cost` and `category`
          setRewards(d.data.map(r => ({
            ...r,
            cost: r.points_required ?? r.cost ?? 0,
            category: r.category || r.description || 'Reward',
          })));
        }
      })
      .catch(() => {});
  }, []);
  useEffect(() => { loadRewards(); }, [loadRewards]);

  const [banners, setBanners] = useState([
    { id: 'b1', tag: 'Live',      title: 'Complimentary Glow IV with any Profhilo', audience: 'Diamond, Platinum' },
    { id: 'b2', tag: 'Scheduled', title: "May Mother's Day · 10× points on facials", audience: 'All members' },
    { id: 'b3', tag: 'Draft',     title: 'Summer Pico Toning · save ฿2,000',         audience: 'Silver, Gold' },
  ]);
  const [editingBanner, setEditingBanner] = useState(null);

  // Dirty flag + toast feedback for Discard / Publish
  const [dirty, setDirty] = useState(false);
  const [toast, setToast] = useState(null);
  const showToast = (msg, kind = 'ok') => {
    setToast({ msg, kind });
    setTimeout(() => setToast(null), 2400);
  };

  // Save reward straight to D1. No "publish" step needed — customer sees
  // the new reward on their next refresh.
  const handleSaveReward = async (next) => {
    setEditingReward(null);
    // Optimistic local update
    setRewards(rs => {
      const exists = rs.some(r => r.id === next.id);
      return exists ? rs.map(r => r.id === next.id ? next : r) : [...rs, next];
    });

    const me = getCurrentUser();
    try {
      const res = await fetch('/api/rewards/create', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-user-id':   me.id   || '',
          'x-user-name': me.name || '',
          'x-user-role': me.role || '',
        },
        body: JSON.stringify({
          id:               next.id,
          name:             next.name,
          description:      next.category || next.description || null,
          image_url:        next.image_url || null,
          points_required:  Number(next.cost ?? next.points_required ?? 0),
        }),
      });
      const data = await res.json();
      if (!res.ok || !data.success) throw new Error(data.error || 'Failed');
      loadRewards();
      showToast('✓ บันทึก reward เรียบร้อย');
    } catch (e) {
      showToast('บันทึก reward ไม่สำเร็จ: ' + e.message, 'err');
      loadRewards();   // revert optimistic update
    }
  };

  // Hard-delete a reward (and remove it from the live customer catalog).
  const handleDeleteReward = async (reward) => {
    if (!confirm(`ลบ "${reward.name}" ออกถาวร?`)) return;
    setRewards(rs => rs.filter(r => r.id !== reward.id));
    const me = getCurrentUser();
    try {
      const res = await fetch(`/api/rewards/${reward.id}?hard=1`, {
        method: 'DELETE',
        headers: {
          'x-user-id':   me.id   || '',
          'x-user-name': me.name || '',
          'x-user-role': me.role || '',
        },
      });
      const data = await res.json();
      if (!res.ok || !data.success) throw new Error(data.error || 'Failed');
      showToast('🗑️ ลบ reward เรียบร้อย');
    } catch (e) {
      showToast('ลบไม่สำเร็จ: ' + e.message, 'err');
      loadRewards();
    }
  };
  const handleSaveBanner = (next) => {
    setBanners(bs => bs.map(b => b.id === next.id ? next : b));
    setEditingBanner(null);
    setDirty(true);
    showToast('Banner updated · changes pending publish');
  };
  const handleDiscard = () => {
    if (!dirty) { showToast('Nothing to discard', 'mute'); return; }
    setDirty(false);
    showToast('Local changes discarded', 'mute');
  };
  const handlePublish = () => {
    if (!dirty) { showToast('No pending changes', 'mute'); return; }
    setDirty(false);

    // Save all rewards to D1
    Promise.all(rewards.map(r =>
      fetch('/api/rewards/create', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(r),
      })
      .then(res => res.json())
      .catch(err => console.warn('[Admin] Error saving reward to D1:', err))
    ))
    .then(() => console.log('[Admin] All rewards published to D1'))
    .catch(err => console.warn('[Admin] Error publishing rewards:', err));

    showToast('Changes published to all branches');
  };

  return (
    <div style={{ position: 'relative' }}>
      <AdminTopbar
        title={t('admin.content_rules')}
        subtitle={t('admin.content_sub')}
        right={<>
          {dirty && <span style={{
            display:'inline-flex', alignItems:'center', gap: 6,
            padding: '4px 10px', borderRadius: 999, marginRight: 4,
            background: A.goldSoft, color: A.goldDeep, fontSize: 11, fontWeight: 600,
          }}>● Unpublished changes</span>}
          <Btn onClick={handleDiscard}>{t('common.discard')}</Btn>
          <Btn primary onClick={handlePublish}>{t('common.publish')}</Btn>
        </>}
      />

      {toast && (
        <div style={{
          position: 'absolute', top: 70, right: 28, zIndex: 60,
          padding: '10px 16px', borderRadius: 10,
          background: toast.kind === 'mute' ? A.ink2 : A.clover,
          color: '#fff', fontSize: 13, fontWeight: 500,
          boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
        }} className="slide-up">{toast.msg}</div>
      )}

      <div style={{ padding: '16px 28px 0', display:'flex', gap: 4, borderBottom: '1px solid ' + A.line }}>
        {[
          { id: 'banners', label: t('admin.home_banners') },
          { id: 'rewards', label: t('admin.rewards_catalog') },
          { id: 'rules',   label: t('admin.point_rules') },
        ].map(tt => (
          <div key={tt.id} onClick={() => setTab(tt.id)} style={{
            padding: '10px 16px', cursor:'pointer', fontSize: 13, fontWeight: 600,
            color: tab === tt.id ? A.clover : A.mute,
            borderBottom: '2px solid ' + (tab === tt.id ? A.clover : 'transparent'),
            marginBottom: -1,
          }}>{tt.label}</div>
        ))}
      </div>

      <div style={{ padding: 28 }}>
        {tab === 'banners' && (
          <div style={{ display:'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 14 }}>
            {banners.map((b) => (
              <div key={b.id} style={{ background:'#fff', border:'1px solid '+A.line, borderRadius: 12, overflow:'hidden' }}>
                <div style={{ height: 120, position:'relative', overflow:'hidden', background: A.bone }}>
                  {b.imageUrl
                    ? <img src={b.imageUrl} alt={b.title} style={{ width:'100%', height:'100%', objectFit:'cover', display:'block' }} />
                    : <div className="stripes" style={{ height:'100%' }} />
                  }
                  <div style={{ position:'absolute', top: 10, left: 10 }}>
                    <Tag bg="rgba(255,255,255,0.85)" color={b.tag === 'Live' ? A.clover : (b.tag === 'Scheduled' ? A.goldDeep : A.mute)}>
                      ● {b.tag}
                    </Tag>
                  </div>
                </div>
                <div style={{ padding: 14 }}>
                  <div className="serif" style={{ fontSize: 16, color: A.ink, lineHeight: 1.25 }}>{b.title}</div>
                  <div style={{ fontSize: 11.5, color: A.mute, marginTop: 6 }}>For: {b.audience}</div>
                  <div style={{ display:'flex', gap: 6, marginTop: 12 }}>
                    <Btn onClick={() => setEditingBanner(b)} style={{ flex: 1, fontSize: 12, padding: '7px 0' }}>Edit</Btn>
                    <Btn onClick={() => showToast('Preview opened in new tab (mock)', 'mute')} style={{ flex: 1, fontSize: 12, padding: '7px 0' }}>Preview</Btn>
                  </div>
                </div>
              </div>
            ))}
            <div
              onClick={() => {
                const id = 'b' + (banners.length + 1);
                const b = { id, tag: 'Draft', title: 'Untitled banner', audience: 'All members' };
                setBanners(bs => [...bs, b]);
                setEditingBanner(b);
                setDirty(true);
              }}
              style={{
                background: A.bone, border: '2px dashed ' + A.line2, borderRadius: 12,
                display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center',
                minHeight: 220, cursor:'pointer', color: A.mute,
              }}
            >
              <IconPlus size={22} color={A.mute}/>
              <div style={{ fontSize: 13, marginTop: 6, fontWeight: 500 }}>New banner</div>
            </div>
          </div>
        )}

        {tab === 'rewards' && (
          <div>
            <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 14 }}>
              <div style={{ fontSize: 12.5, color: A.mute }}>{rewards.length} rewards in catalog · click any row to edit</div>
              <Btn primary onClick={() => setView && setView('reward-new')}>+ {t('common.new')} reward</Btn>
            </div>

            <div style={{ background:'#fff', border:'1px solid '+A.line, borderRadius: 12, overflow:'hidden' }}>
              <div style={{
                display:'grid', gridTemplateColumns: '60px 2fr 1fr 1fr 1fr 110px 80px',
                padding: '10px 18px', background: A.bone,
                fontSize: 10.5, letterSpacing: 1.2, textTransform:'uppercase', color: A.mute, fontWeight: 600,
              }}>
                <div/>
                <div>Reward</div><div>Category</div><div>Point cost</div><div>Redemptions/mo</div><div>Status</div><div/>
              </div>
              {rewards.length === 0 && (
                <div style={{ padding: '40px 18px', textAlign:'center', color: A.mute, fontSize: 13 }}>
                  ยังไม่มี reward — กดปุ่ม "+ New reward" ด้านบนเพื่อเพิ่ม
                </div>
              )}
              {rewards.map((r, i) => (
                <div
                  key={r.id}
                  onClick={() => setEditingReward(r)}
                  style={{
                    display:'grid', gridTemplateColumns: '60px 2fr 1fr 1fr 1fr 110px 140px',
                    padding: '12px 18px', alignItems:'center',
                    borderTop: '1px solid ' + A.line, fontSize: 13,
                    cursor: 'pointer', transition: 'background 0.12s',
                  }}
                  onMouseEnter={(e) => e.currentTarget.style.background = A.bone}
                  onMouseLeave={(e) => e.currentTarget.style.background = '#fff'}
                >
                  {(r.image_url || r.imageUrl) ? (
                    <img src={r.image_url || r.imageUrl} alt={r.name} style={{ width: 40, height: 40, borderRadius: 8, objectFit: 'cover', display: 'block' }} />
                  ) : (
                    <div className="stripes" style={{ width: 40, height: 40, borderRadius: 8, fontSize: 7 }}>IMG</div>
                  )}
                  <div style={{ color: A.ink, fontWeight: 500 }}>{r.name}</div>
                  <div style={{ color: A.ink2 }}>{r.category || r.description || '—'}</div>
                  <div className="mono" style={{ color: A.ink, fontWeight: 500 }}>{Number(r.cost ?? r.points_required ?? 0).toLocaleString()} pts</div>
                  <div className="mono" style={{ color: A.ink2 }}>{r.redeemed_count ?? 0}</div>
                  <div><Tag bg="#E6EFE2" color="#4F7A4A">● Live</Tag></div>
                  <div style={{ textAlign:'right', display: 'flex', gap: 6, justifyContent: 'flex-end' }}>
                    <button
                      type="button"
                      onClick={(e) => { e.stopPropagation(); setEditingReward(r); }}
                      style={{
                        padding: '6px 10px', borderRadius: 6, fontSize: 11, fontWeight: 600,
                        border: '1px solid ' + A.line2, background: '#fff', color: A.clover, cursor: 'pointer',
                      }}
                    >Edit</button>
                    <button
                      type="button"
                      onClick={(e) => { e.stopPropagation(); handleDeleteReward(r); }}
                      style={{
                        padding: '6px 10px', borderRadius: 6, fontSize: 11, fontWeight: 600,
                        border: '1px solid #F1C7C0', background: '#fff', color: '#B5453A', cursor: 'pointer',
                      }}
                    >🗑️ Delete</button>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}

        {editingReward && (
          <RewardEditModal
            reward={editingReward}
            onCancel={() => setEditingReward(null)}
            onSave={handleSaveReward}
          />
        )}

        {editingBanner && (
          <BannerEditModal
            banner={editingBanner}
            onCancel={() => setEditingBanner(null)}
            onSave={handleSaveBanner}
          />
        )}

        {tab === 'rules' && (
          <div style={{ display:'grid', gridTemplateColumns: '1.4fr 1fr', gap: 18 }}>
            <div style={{ background:'#fff', border:'1px solid '+A.line, borderRadius: 12, padding: 22 }}>
              <div className="serif" style={{ fontSize: 18, color: A.ink }}>Point earn rate</div>
              <div style={{ fontSize: 12.5, color: A.mute, marginTop: 4 }}>Spend per 1 point. Applies to all branches.</div>

              <div style={{ display:'flex', alignItems:'center', gap: 14, marginTop: 24 }}>
                <div style={{
                  padding: '12px 16px', background: A.bone, borderRadius: 10,
                  display:'flex', alignItems:'center', gap: 6, fontWeight: 600, color: A.ink,
                  border: '1px solid ' + A.line,
                }}>
                  <span style={{ fontSize: 13, color: A.mute }}>฿</span>
                  <input
                    type="number" value={pointRate} onChange={(e) => setPointRate(e.target.value)}
                    style={{ width: 60, border: 'none', background:'transparent', fontSize: 22, fontWeight: 600, fontFamily: 'inherit', outline: 'none', color: A.clover }}
                  />
                </div>
                <div style={{ fontSize: 18, color: A.mute }}>=</div>
                <div style={{
                  padding: '12px 16px', background: A.cloverSoft, borderRadius: 10,
                  fontWeight: 600, color: A.clover, fontSize: 22, fontFamily:'Cormorant Garamond, serif',
                }}>1 point</div>
              </div>

              <div style={{ marginTop: 18, padding: 14, background: A.bone, borderRadius: 8, fontSize: 12, color: A.ink2, lineHeight: 1.6 }}>
                <strong>Example:</strong> a ฿38,500 order at the current rate would award <span className="mono" style={{ color: A.clover, fontWeight: 600 }}>{Math.floor(38500/pointRate).toLocaleString()} points</span>.
              </div>

              <div style={{ marginTop: 22, paddingTop: 18, borderTop: '1px solid ' + A.line }}>
                <div className="serif" style={{ fontSize: 16, color: A.ink, marginBottom: 10 }}>Multipliers</div>
                {[
                  { label: 'Diamond tier', mult: '2×' },
                  { label: 'Platinum tier', mult: '1.5×' },
                  { label: 'Birthday month', mult: '3×' },
                ].map((m, i) => (
                  <div key={i} style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'10px 0', borderTop: i === 0 ? 'none' : '1px solid ' + A.line }}>
                    <div style={{ fontSize: 13, color: A.ink }}>{m.label}</div>
                    <div style={{ display:'flex', alignItems:'center', gap: 12 }}>
                      <span className="mono" style={{ color: A.gold, fontWeight: 600 }}>{m.mult}</span>
                      <ToggleSmall on={true}/>
                    </div>
                  </div>
                ))}
              </div>
            </div>

            <div style={{ background: A.bone, borderRadius: 12, padding: 22 }}>
              <div className="serif" style={{ fontSize: 17, color: A.ink, marginBottom: 8 }}>Tier thresholds</div>
              {TIERS.map((t, i) => (
                <div key={t.id} style={{ padding: '12px 0', borderTop: i === 0 ? 'none' : '1px solid ' + A.line, display:'flex', justifyContent:'space-between' }}>
                  <span style={{ fontSize: 13, color: A.ink, fontWeight: 500 }}>{t.label}</span>
                  <span className="mono" style={{ fontSize: 12.5, color: A.mute }}>
                    {t.next ? `฿${(t.min/1000).toFixed(0)}k – ฿${(t.next/1000).toFixed(0)}k` : `≥ ฿${(t.min/1000).toFixed(0)}k`}
                  </span>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// CMS edit modals
// ───────────────────────────────────────────────────────────
function CMSModalShell({ title, onCancel, onSave, children }) {
  return (
    <div
      onClick={onCancel}
      className="fade-in"
      style={{
        position: 'fixed', inset: 0, zIndex: 80,
        background: 'rgba(20,20,15,0.5)',
        backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)',
        display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
      }}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        className="slide-up"
        style={{
          width: '100%', maxWidth: 520, background: '#fff',
          borderRadius: 16, boxShadow: '0 30px 80px rgba(0,0,0,0.3)',
          overflow: 'hidden',
        }}
      >
        <div style={{
          padding: '18px 22px', borderBottom: '1px solid ' + A.line,
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        }}>
          <div className="serif" style={{ fontSize: 20, color: A.clover }}>{title}</div>
          <button
            type="button"
            onClick={onCancel}
            style={{
              border: 'none', background: A.bone, width: 30, height: 30, borderRadius: 15,
              fontSize: 18, lineHeight: 1, cursor: 'pointer', color: A.ink, padding: 0,
            }}
          >×</button>
        </div>
        <div style={{ padding: 22, maxHeight: '70vh', overflow: 'auto' }}>{children}</div>
        <div style={{
          padding: '14px 22px', borderTop: '1px solid ' + A.line, background: A.bone,
          display: 'flex', justifyContent: 'flex-end', gap: 8,
        }}>
          <Btn onClick={onCancel}>Cancel</Btn>
          <Btn primary onClick={onSave}>Save changes</Btn>
        </div>
      </div>
    </div>
  );
}

function CMSField({ label, hint, children }) {
  return (
    <div style={{ marginBottom: 16 }}>
      <div style={{ fontSize: 11, letterSpacing: 1.2, textTransform:'uppercase', color: A.mute, fontWeight: 600, marginBottom: 6 }}>{label}</div>
      {children}
      {hint && <div style={{ fontSize: 11, color: A.mute, marginTop: 4 }}>{hint}</div>}
    </div>
  );
}

function cmsInputStyle() {
  return {
    width: '100%', padding: '10px 12px',
    border: '1px solid ' + A.line2, borderRadius: 8,
    fontSize: 13.5, fontFamily: 'inherit', color: A.ink,
    background: '#fff', outline: 'none',
  };
}

function ImageUploadField({ value, onChange, aspect = '16/9', placeholder = 'image' }) {
  const inputRef = useRef(null);
  const [drag, setDrag] = useState(false);
  const [error, setError] = useState(null);

  const handleFile = (file) => {
    if (!file) return;
    if (!file.type.startsWith('image/')) { setError('File must be an image'); return; }
    if (file.size > 4 * 1024 * 1024) { setError('Max size is 4MB'); return; }
    setError(null);
    const reader = new FileReader();
    reader.onload = () => onChange(reader.result);
    reader.readAsDataURL(file);
  };

  return (
    <div>
      <input
        ref={inputRef}
        type="file"
        accept="image/png,image/jpeg,image/webp"
        style={{ display: 'none' }}
        onChange={(e) => handleFile(e.target.files && e.target.files[0])}
      />

      <div
        onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
        onDragLeave={() => setDrag(false)}
        onDrop={(e) => {
          e.preventDefault(); setDrag(false);
          handleFile(e.dataTransfer.files && e.dataTransfer.files[0]);
        }}
        onClick={() => inputRef.current && inputRef.current.click()}
        style={{
          position: 'relative', aspectRatio: aspect, borderRadius: 10,
          border: '2px dashed ' + (drag ? A.clover : A.line2),
          background: drag ? A.cloverSoft : A.bone,
          cursor: 'pointer', overflow: 'hidden',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          transition: 'border-color 0.15s, background 0.15s',
        }}
      >
        {value ? (
          <img src={value} alt="reward" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
        ) : (
          <div style={{ textAlign: 'center', color: A.mute, padding: 16 }}>
            <IconPlus size={26} color={A.mute} />
            <div style={{ fontSize: 12.5, marginTop: 6, fontWeight: 500 }}>
              Click or drop an image
            </div>
            <div className="mono" style={{ fontSize: 10, marginTop: 4, letterSpacing: 0.5, textTransform: 'uppercase' }}>
              {placeholder}
            </div>
          </div>
        )}

        {value && (
          <div style={{
            position: 'absolute', top: 8, right: 8, display: 'flex', gap: 6,
          }}>
            <button
              type="button"
              onClick={(e) => { e.stopPropagation(); inputRef.current && inputRef.current.click(); }}
              style={cmsImageBtnStyle()}
            >Replace</button>
            <button
              type="button"
              onClick={(e) => { e.stopPropagation(); onChange(null); }}
              style={{ ...cmsImageBtnStyle(), color: A.danger || '#B5453A' }}
            >Remove</button>
          </div>
        )}
      </div>

      {error && <div style={{ fontSize: 11, color: '#B5453A', marginTop: 6 }}>{error}</div>}
    </div>
  );
}

function cmsImageBtnStyle() {
  return {
    padding: '5px 10px', borderRadius: 6, fontSize: 11, fontWeight: 600,
    background: 'rgba(255,255,255,0.92)', border: '1px solid ' + A.line2,
    color: A.ink, cursor: 'pointer', backdropFilter: 'blur(8px)',
  };
}

function RewardEditModal({ reward, onCancel, onSave }) {
  const [name, setName] = useState(reward.name || '');
  const [category, setCategory] = useState(reward.category || 'Treatment');
  const [cost, setCost] = useState(reward.cost || 0);
  const [grantKind, setGrantKind] = useState((reward.grant && reward.grant.kind) || 'voucher');
  const [status, setStatus] = useState(reward.status || 'live');
  const [imageUrl, setImageUrl] = useState(reward.imageUrl || null);

  const handleSave = () => {
    onSave({
      ...reward,
      name: name.trim() || 'Untitled reward',
      category, status,
      cost: Number(cost) || 0,
      grant: { ...(reward.grant || {}), kind: grantKind },
      imageUrl,
    });
  };

  return (
    <CMSModalShell title={`Edit reward · ${reward.name}`} onCancel={onCancel} onSave={handleSave}>
      <CMSField label="Reward image" hint="Square 1:1 jpg/png recommended · max 4MB · uploads to R2 storage in production.">
        <ImageUploadField value={imageUrl} onChange={setImageUrl} aspect="1/1" placeholder="reward photo · 800×800" />
      </CMSField>

      <CMSField label="Reward name">
        <input value={name} onChange={(e) => setName(e.target.value)} style={cmsInputStyle()} />
      </CMSField>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <CMSField label="Category">
          <select value={category} onChange={(e) => setCategory(e.target.value)} style={cmsInputStyle()}>
            {['Treatment', 'Lifestyle', 'Gift', 'Voucher'].map(c => <option key={c} value={c}>{c}</option>)}
          </select>
        </CMSField>
        <CMSField label="Point cost" hint="Cost in Clover Points (25 THB = 1 point)">
          <input type="number" value={cost} onChange={(e) => setCost(e.target.value)} style={cmsInputStyle()} />
        </CMSField>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <CMSField label="Grant type">
          <select value={grantKind} onChange={(e) => setGrantKind(e.target.value)} style={cmsInputStyle()}>
            <option value="session">Treatment session</option>
            <option value="voucher">Voucher</option>
            <option value="physical">Physical item</option>
            <option value="upgrade">Tier upgrade</option>
          </select>
        </CMSField>
        <CMSField label="Status">
          <select value={status} onChange={(e) => setStatus(e.target.value)} style={cmsInputStyle()}>
            <option value="live">Live</option>
            <option value="draft">Draft</option>
            <option value="paused">Paused</option>
          </select>
        </CMSField>
      </div>
    </CMSModalShell>
  );
}

function BannerEditModal({ banner, onCancel, onSave }) {
  const [title, setTitle] = useState(banner.title || '');
  const [audience, setAudience] = useState(banner.audience || 'All members');
  const [tag, setTag] = useState(banner.tag || 'Draft');
  const [imageUrl, setImageUrl] = useState(banner.imageUrl || null);

  const handleSave = () => {
    onSave({
      ...banner,
      title: title.trim() || 'Untitled banner',
      audience: audience.trim() || 'All members',
      tag,
      imageUrl,
    });
  };

  return (
    <CMSModalShell title={`Edit banner · ${banner.tag}`} onCancel={onCancel} onSave={handleSave}>
      <CMSField label="Banner image" hint="Recommended 1600×600 jpg/png · max 4MB · uploads to R2 in production.">
        <ImageUploadField value={imageUrl} onChange={setImageUrl} aspect="8/3" placeholder="banner · 1600×600" />
      </CMSField>

      <CMSField label="Banner title">
        <input value={title} onChange={(e) => setTitle(e.target.value)} style={cmsInputStyle()} />
      </CMSField>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <CMSField label="Audience" hint="Comma-separated tier names">
          <input value={audience} onChange={(e) => setAudience(e.target.value)} style={cmsInputStyle()} />
        </CMSField>
        <CMSField label="Status">
          <select value={tag} onChange={(e) => setTag(e.target.value)} style={cmsInputStyle()}>
            <option value="Live">Live</option>
            <option value="Scheduled">Scheduled</option>
            <option value="Draft">Draft</option>
          </select>
        </CMSField>
      </div>
    </CMSModalShell>
  );
}

function ToggleSmall({ on: defaultOn = true }) {
  const [on, setOn] = useState(defaultOn);
  return (
    <div onClick={() => setOn(!on)} style={{
      width: 34, height: 20, borderRadius: 10, cursor:'pointer',
      background: on ? A.clover : A.line2, position:'relative', transition: 'background .15s',
    }}>
      <div style={{
        position:'absolute', top: 2, left: on ? 16 : 2, width: 16, height: 16, borderRadius: '50%',
        background: '#fff', transition: 'left .15s', boxShadow: '0 1px 2px rgba(0,0,0,0.2)',
      }}/>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// Stub views: Branches, Customers, Settings (lightweight)
// ───────────────────────────────────────────────────────────
// ── Branch Modal: add/edit form ─────────────────────────────
function BranchModal({ branch, onCancel, onSave, onDelete }) {
  const isNew = !branch.id;
  const [name, setName]     = useState(branch.name   || '');
  const [city, setCity]     = useState(branch.city   || 'Bangkok');
  const [opened, setOpened] = useState(branch.opened || String(new Date().getFullYear()));
  const [errors, setErrors] = useState({});

  const handleSave = () => {
    const e = {};
    if (!name.trim()) e.name = 'Branch name is required';
    if (!city.trim()) e.city = 'City is required';
    if (Object.keys(e).length) { setErrors(e); return; }
    onSave({
      ...branch,
      name:   name.trim(),
      city:   city.trim(),
      opened: opened.trim() || String(new Date().getFullYear()),
    });
  };

  const inputStyle = cmsInputStyle();
  const labelStyle = { fontSize: 11, fontWeight: 600, color: A.mute, letterSpacing: 0.6, textTransform:'uppercase', marginBottom: 5 };

  return (
    <div onClick={onCancel} style={{ position:'fixed', inset:0, zIndex:200, background:'rgba(18,42,32,0.45)', backdropFilter:'blur(6px)', display:'flex', alignItems:'center', justifyContent:'center', padding:20 }}>
      <div onClick={(e) => e.stopPropagation()} className="slide-up" style={{ width:'100%', maxWidth:460, background:'#fff', borderRadius:18, boxShadow:'0 24px 64px rgba(0,0,0,0.28)', overflow:'hidden' }}>
        <div style={{ padding:'18px 22px', borderBottom:'1px solid '+A.line, display:'flex', justifyContent:'space-between', alignItems:'center' }}>
          <div className="serif" style={{ fontSize:20, color:A.ink }}>{isNew ? 'New branch' : 'Edit branch'}</div>
          <button type="button" onClick={onCancel} style={{ background:'none', border:'none', cursor:'pointer', color:A.mute, fontSize:20, lineHeight:1, padding:4 }}>×</button>
        </div>
        <div style={{ padding:'20px 22px' }}>
          <div style={{ marginBottom: 14 }}>
            <div style={{ ...labelStyle, color: errors.name ? '#B5453A' : A.mute }}>Branch name</div>
            <input
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="e.g. Thonglor Flagship"
              style={inputStyle}
              autoFocus
            />
            {errors.name && <div style={{ fontSize: 11, color:'#B5453A', marginTop: 4 }}>{errors.name}</div>}
          </div>
          <div style={{ display:'grid', gridTemplateColumns:'1.4fr 1fr', gap:12 }}>
            <div style={{ marginBottom: 14 }}>
              <div style={{ ...labelStyle, color: errors.city ? '#B5453A' : A.mute }}>City</div>
              <input
                value={city}
                onChange={(e) => setCity(e.target.value)}
                placeholder="Bangkok"
                style={inputStyle}
              />
              {errors.city && <div style={{ fontSize: 11, color:'#B5453A', marginTop: 4 }}>{errors.city}</div>}
            </div>
            <div style={{ marginBottom: 14 }}>
              <div style={labelStyle}>Year opened</div>
              <input
                value={opened}
                onChange={(e) => setOpened(e.target.value)}
                placeholder="2025"
                style={inputStyle}
              />
            </div>
          </div>
        </div>
        <div style={{ padding:'14px 22px', borderTop:'1px solid '+A.line, display:'flex', justifyContent:'space-between', alignItems:'center', gap:8 }}>
          <div>
            {!isNew && (
              <button
                type="button"
                onClick={() => { if (window.confirm(`Delete branch "${branch.name}"?`)) onDelete(branch); }}
                style={{
                  padding:'8px 14px', borderRadius:8, fontSize:13, fontWeight:600,
                  border:'1px solid #E5BCB7', background:'#FEF0EF', color:'#B5453A', cursor:'pointer',
                }}
              >Delete</button>
            )}
          </div>
          <div style={{ display:'flex', gap:8 }}>
            <Btn onClick={onCancel}>Cancel</Btn>
            <Btn primary onClick={handleSave}>{isNew ? 'Create branch' : 'Save changes'}</Btn>
          </div>
        </div>
      </div>
    </div>
  );
}

function BranchesView() {
  const { branches, addBranch, updateBranch, deleteBranch, customers } = useData();
  const [editing, setEditing] = useState(null); // null | {} (new) | existing branch
  const [toast, setToast]     = useState(null);

  const showToast = (msg) => {
    setToast(msg);
    setTimeout(() => setToast(null), 2200);
  };

  const handleSave = (saved) => {
    if (!saved.id) {
      addBranch(saved);
      showToast('Branch created');
    } else {
      updateBranch(saved);
      showToast('Branch updated');
    }
    setEditing(null);
  };

  const handleDelete = (target) => {
    deleteBranch(target.id);
    setEditing(null);
    showToast('Branch deleted');
  };

  // Live customer count per branch
  const customerCountFor = (name) => customers.filter(c => c.branch === name).length;

  return (
    <div style={{ position:'relative' }}>
      {editing !== null && (
        <BranchModal
          branch={editing}
          onCancel={() => setEditing(null)}
          onSave={handleSave}
          onDelete={handleDelete}
        />
      )}

      {toast && (
        <div className="fade-in" style={{
          position:'fixed', top: 20, left:'50%', transform:'translateX(-50%)',
          background: A.clover, color:'#fff', padding:'10px 18px', borderRadius: 10,
          fontSize: 13, fontWeight: 600, zIndex: 300,
          boxShadow:'0 8px 28px rgba(0,0,0,0.18)',
        }}>{toast}</div>
      )}

      <AdminTopbar
        title="Branches"
        subtitle={`${branches.length} location${branches.length === 1 ? '' : 's'} · operators, staff, opening hours, treatment availability`}
        right={
          <Btn primary onClick={() => setEditing({})}>
            <IconPlus size={14} style={{verticalAlign:-2,marginRight:6}}/>New branch
          </Btn>
        }
      />

      <div style={{ padding: 28 }}>
        {branches.length === 0 ? (
          <div style={{
            background:'#fff', border:'1px dashed '+A.line2, borderRadius: 12,
            padding: 64, textAlign:'center', color: A.mute, fontSize: 13.5,
          }}>
            No branches yet. Click <strong style={{ color: A.clover }}>New branch</strong> to create one.
          </div>
        ) : (
          <div style={{ display:'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 14 }}>
            {branches.map((b, i) => (
              <div
                key={b.id}
                onClick={() => setEditing(b)}
                style={{
                  background:'#fff', border:'1px solid '+A.line, borderRadius: 12,
                  padding: 18, display:'flex', gap: 14, cursor:'pointer',
                  transition:'border-color .15s, box-shadow .15s',
                }}
                onMouseEnter={e => { e.currentTarget.style.borderColor = A.line2; e.currentTarget.style.boxShadow = '0 4px 14px rgba(0,0,0,0.04)'; }}
                onMouseLeave={e => { e.currentTarget.style.borderColor = A.line; e.currentTarget.style.boxShadow = 'none'; }}
              >
                <div className="stripes" style={{ width: 64, height: 64, borderRadius: 10, fontSize: 8 }}>BRANCH</div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="serif" style={{ fontSize: 18, color: A.ink }}>{b.name}</div>
                  <div style={{ fontSize: 12, color: A.mute, marginTop: 2, display:'flex', gap: 8 }}>
                    <span>{b.city}</span><span>·</span><span>Opened {b.opened}</span>
                  </div>
                  <div style={{ display:'flex', gap: 14, marginTop: 12, fontSize: 11.5 }}>
                    <div><span className="mono" style={{ color: A.clover, fontWeight:600 }}>{customerCountFor(b.name)}</span> <span style={{ color: A.mute }}>customers</span></div>
                    <div><span className="mono" style={{ color: A.clover, fontWeight:600 }}>{14 + (i % 7)}</span> <span style={{ color: A.mute }}>staff</span></div>
                    <div><span className="mono" style={{ color: A.clover, fontWeight:600 }}>{18 + (i % 6) * 2}</span> <span style={{ color: A.mute }}>treatments</span></div>
                  </div>
                </div>
                <button
                  type="button"
                  onClick={(e) => { e.stopPropagation(); if (window.confirm(`Delete branch "${b.name}"?`)) handleDelete(b); }}
                  title="Delete branch"
                  style={{
                    background:'transparent', border:'none', cursor:'pointer',
                    color: A.mute, padding: 6, alignSelf:'flex-start',
                    borderRadius: 6, fontSize: 16, lineHeight: 1,
                    transition:'background .1s, color .1s',
                  }}
                  onMouseEnter={e => { e.currentTarget.style.background = '#FEF0EF'; e.currentTarget.style.color = '#B5453A'; }}
                  onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = A.mute; }}
                >×</button>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// Label + field wrapper — declared at module scope so it has a stable
// component identity. (Previously defined inside CustomerModal, which made
// React unmount/remount every input on each keystroke → typing was lost.)
function CustomerField({ label, err, hint, children }) {
  return (
    <div style={{ marginBottom: 14 }}>
      <div style={{ fontSize: 11, fontWeight: 600, color: err ? '#B5453A' : A.mute, letterSpacing: 0.6, textTransform:'uppercase', marginBottom: 5 }}>{label}</div>
      {children}
      {hint && !err && <div style={{ fontSize: 11, color: A.mute, marginTop: 4, fontStyle: 'italic' }}>{hint}</div>}
      {err && <div style={{ fontSize: 11, color:'#B5453A', marginTop: 4 }}>{err}</div>}
    </div>
  );
}

function CustomerModal({ customer, onCancel, onSave, onDelete, branches }) {
  const isNew = !customer.id;
  const branchOptions = (branches && branches.length) ? branches : BRANCHES;
  // ID is editable. For new customers, suggest the next CV<BE>-NNNNN code.
  const [id,     setId]     = useState(customer.id || '');
  const [name,   setName]   = useState(customer.name   || '');
  const [phone,  setPhone]  = useState(customer.phone  || '');
  const [email,  setEmail]  = useState(customer.email  || '');
  const [tier,   setTier]   = useState(customer.tier   || 'Silver');
  const [branch, setBranch] = useState(customer.branch || (branchOptions[0] && branchOptions[0].name) || '');
  const [pts,    setPts]    = useState(String(customer.pts ?? ''));
  const [notes,  setNotes]  = useState(customer.notes  || '');
  const [errors, setErrors] = useState({});

  // Validate the CV<BE>-NNNNN pattern. We allow free-form too, but warn.
  const ID_PATTERN = /^CV\d{2}-\d{3,6}$/;
  const idLooksValid = !id || ID_PATTERN.test(id.trim());

  const handleSave = () => {
    const e = {};
    if (!name.trim())  e.name  = 'Name is required';
    if (!phone.trim()) e.phone = 'Phone is required';
    if (id && !ID_PATTERN.test(id.trim())) {
      e.id = 'รหัสควรเป็นรูปแบบ CV69-00001';
    }
    if (Object.keys(e).length) { setErrors(e); return; }
    onSave({
      ...customer,
      id:           (id.trim() || customer.id),   // keep edited id (auto-fills if blank for new)
      _originalId:  customer.id || null,          // so updateCustomer can detect a rename
      name:   name.trim(),
      phone:  phone.trim(),
      email:  email.trim(),
      tier,
      branch,
      pts:    parseInt(pts, 10) || 0,
      notes:  notes.trim(),
    });
  };

  const inputStyle = cmsInputStyle();

  return (
    <div onClick={onCancel} style={{ position:'fixed', inset:0, zIndex:200, background:'rgba(18,42,32,0.45)', backdropFilter:'blur(6px)', display:'flex', alignItems:'center', justifyContent:'center', padding:20 }}>
      <div onClick={(e) => e.stopPropagation()} className="slide-up" style={{ width:'100%', maxWidth:480, background:'#fff', borderRadius:18, boxShadow:'0 24px 64px rgba(0,0,0,0.28)', overflow:'hidden' }}>
        <div style={{ padding:'18px 22px', borderBottom:'1px solid '+A.line, display:'flex', justifyContent:'space-between', alignItems:'center' }}>
          <div className="serif" style={{ fontSize:20, color:A.ink }}>{isNew ? 'New customer' : 'Edit customer'}</div>
          <button type="button" onClick={onCancel} style={{ background:'none', border:'none', cursor:'pointer', color:A.mute, fontSize:20, lineHeight:1, padding:4 }}>×</button>
        </div>
        <div style={{ padding:'20px 22px', maxHeight:'70vh', overflowY:'auto' }}>
          <CustomerField label={`Customer ID${isNew ? ' (จะถูกสร้างอัตโนมัติถ้าเว้นว่าง)' : ''}`} err={errors.id}
            hint={!idLooksValid ? '⚠️ รูปแบบที่แนะนำ: CV69-00001 (CV + เลขปี พ.ศ. 2 ตัวสุดท้าย + เลขที่)' : null}
          >
            <input
              value={id}
              onChange={(e) => setId(e.target.value.toUpperCase())}
              placeholder={'CV' + String(new Date().getFullYear() + 543).slice(-2) + '-00001'}
              style={{
                ...inputStyle,
                fontFamily: 'JetBrains Mono, monospace',
                letterSpacing: 0.5,
              }}
            />
          </CustomerField>
          <CustomerField label="Full name" err={errors.name}>
            <input
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="e.g. Apinya Sukosol"
              style={inputStyle}
              autoFocus
            />
          </CustomerField>
          <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:12 }}>
            <CustomerField label="Phone" err={errors.phone}>
              <input
                value={phone}
                onChange={(e) => setPhone(e.target.value)}
                placeholder="+66 8x xxx xxxx"
                style={inputStyle}
              />
            </CustomerField>
            <CustomerField label="Email">
              <input
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                placeholder="optional"
                style={inputStyle}
              />
            </CustomerField>
          </div>
          <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:12 }}>
            <CustomerField label="Tier">
              <select value={tier} onChange={(e) => setTier(e.target.value)} style={inputStyle}>
                {['Silver','Gold','Platinum','Diamond'].map(t => <option key={t}>{t}</option>)}
              </select>
            </CustomerField>
            <CustomerField label="Points balance">
              <input
                value={pts}
                onChange={(e) => setPts(e.target.value)}
                placeholder="0"
                type="number"
                min="0"
                style={inputStyle}
              />
            </CustomerField>
          </div>
          <CustomerField label="Home branch">
            <select value={branch} onChange={(e) => setBranch(e.target.value)} style={inputStyle}>
              {branchOptions.map(b => <option key={b.id} value={b.name}>{b.name}</option>)}
            </select>
          </CustomerField>
          <CustomerField label="Notes">
            <textarea
              value={notes}
              onChange={(e) => setNotes(e.target.value)}
              rows={2}
              placeholder="Internal notes…"
              style={{ ...inputStyle, resize:'vertical' }}
            />
          </CustomerField>
        </div>
        <div style={{ padding:'14px 22px', borderTop:'1px solid '+A.line, display:'flex', justifyContent:'space-between', alignItems:'center', gap:8 }}>
          <div>
            {!isNew && (
              <button
                type="button"
                onClick={() => { if (window.confirm(`Delete customer "${customer.name}"?`)) onDelete(customer); }}
                style={{
                  padding:'8px 14px', borderRadius:8, fontSize:13, fontWeight:600,
                  border:'1px solid #E5BCB7', background:'#FEF0EF', color:'#B5453A', cursor:'pointer',
                }}
              >Delete</button>
            )}
          </div>
          <div style={{ display:'flex', gap:8 }}>
            <Btn onClick={onCancel}>Cancel</Btn>
            <Btn primary onClick={handleSave}>{isNew ? 'Create customer' : 'Save changes'}</Btn>
          </div>
        </div>
      </div>
    </div>
  );
}

function CustomersView() {
  const { customers, branches, addCustomer, updateCustomer, deleteCustomer } = useData();
  const [search, setSearch]   = useState('');
  const [editing, setEditing] = useState(null); // null | {} (new) | existing customer
  const [toast, setToast]     = useState(null);

  const showToast = (msg) => {
    setToast(msg);
    setTimeout(() => setToast(null), 2200);
  };

  // Normalize records coming from D1 (which uses snake_case + different field names)
  // into the shape the admin UI expects. Always-defined strings/numbers so the
  // render code can't crash on a missing field.
  const normalize = (c) => {
    if (!c) return null;
    const tierLabel = (raw) => {
      const t = String(raw || '').toLowerCase();
      if (t === 'diamond')  return 'Diamond';
      if (t === 'platinum') return 'Platinum';
      if (t === 'gold')     return 'Gold';
      if (t === 'silver')   return 'Silver';
      return 'Silver';
    };
    return {
      ...c,
      id:     c.id    || '',
      name:   c.name  || '',
      phone:  c.phone || '',
      email:  c.email || '',
      tier:   tierLabel(c.tier ?? c.tier_id),
      pts:    Number(c.pts ?? c.points_balance ?? c.points ?? 0),
      branch: c.branch || c.branch_id || '',
      notes:  c.notes || '',
    };
  };

  const normalized = (customers || []).map(normalize).filter(Boolean);

  const q = search.trim().toLowerCase();
  const filtered = q
    ? normalized.filter(c =>
        c.name.toLowerCase().includes(q) ||
        c.id.toLowerCase().includes(q) ||
        c.phone.includes(q) ||
        c.email.toLowerCase().includes(q)
      )
    : normalized;

  const handleSave = (saved) => {
    if (!saved.id) {
      addCustomer(saved);
      showToast('Customer created');
    } else {
      updateCustomer(saved);
      showToast('Customer updated');
    }
    setEditing(null);
  };

  const handleDelete = (target) => {
    deleteCustomer(target.id);
    setEditing(null);
    showToast('Customer deleted');
  };

  const tierColor = { Diamond: A.clover, Platinum: A.cloverMid, Gold: A.gold, Silver: A.mute };

  return (
    <div style={{ position:'relative' }}>
      {editing !== null && (
        <CustomerModal
          customer={editing}
          onCancel={() => setEditing(null)}
          onSave={handleSave}
          onDelete={handleDelete}
          branches={branches}
        />
      )}

      {toast && (
        <div className="fade-in" style={{
          position:'fixed', top: 20, left:'50%', transform:'translateX(-50%)',
          background: A.clover, color:'#fff', padding:'10px 18px', borderRadius: 10,
          fontSize: 13, fontWeight: 600, zIndex: 300,
          boxShadow:'0 8px 28px rgba(0,0,0,0.18)',
        }}>{toast}</div>
      )}

      <AdminTopbar
        title="Customers"
        subtitle={`${normalized.length.toLocaleString()} members · ${normalized.filter(c=>c.tier==='Diamond').length} Diamond · ${normalized.filter(c=>c.tier==='Platinum').length} Platinum`}
        right={<>
          <div style={{ background: A.bone, padding: '7px 12px', borderRadius: 8, display:'flex', alignItems:'center', gap: 8, fontSize: 12.5, color: q ? A.ink : A.mute, width: 240 }}>
            <IconSearch size={14} color={A.mute}/>
            <input
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              placeholder="Search by name, ID, or phone"
              style={{ border:'none', background:'transparent', outline:'none', width:'100%', fontSize:12.5, color:A.ink, fontFamily:'inherit' }}
            />
            {q && (
              <button type="button" onClick={() => setSearch('')} style={{ background:'none', border:'none', cursor:'pointer', color:A.mute, padding:0, fontSize:14, lineHeight:1 }}>×</button>
            )}
          </div>
          <Btn primary onClick={() => setEditing({})}>+ New</Btn>
        </>}
      />

      <div style={{ padding: 28 }}>
        <div style={{ background:'#fff', border:'1px solid '+A.line, borderRadius: 12, overflow:'hidden' }}>
          <div style={{
            display:'grid', gridTemplateColumns: '110px 1.6fr 1.4fr 1fr 90px 1.2fr 64px',
            padding: '10px 18px', background: A.bone,
            fontSize: 10.5, letterSpacing: 1.2, textTransform:'uppercase', color: A.mute, fontWeight: 600,
          }}>
            <div>ID</div><div>Name</div><div>Phone</div><div>Tier</div><div style={{textAlign:'right'}}>Points</div><div>Home branch</div><div/>
          </div>

          {filtered.length === 0 && (
            <div style={{ padding: '32px 18px', textAlign:'center', color: A.mute, fontSize: 13 }}>
              {search ? `No customers match "${search}"` : 'ยังไม่มีลูกค้า · กดปุ่ม + New เพื่อเพิ่ม'}
            </div>
          )}

          {filtered.map((r) => (
            <div
              key={r.id}
              onClick={() => setEditing(r)}
              style={{
                display:'grid', gridTemplateColumns: '110px 1.6fr 1.4fr 1fr 90px 1.2fr 64px',
                padding: '13px 18px', alignItems:'center', fontSize: 13,
                borderTop: '1px solid ' + A.line, cursor:'pointer',
                transition: 'background .1s',
              }}
              onMouseEnter={e => e.currentTarget.style.background = A.bone}
              onMouseLeave={e => e.currentTarget.style.background = ''}
            >
              <div className="mono" style={{ color: A.mute, fontSize:11.5 }}>{r.id || '—'}</div>
              <div style={{ color: A.ink, fontWeight: 600 }}>{r.name || '—'}</div>
              <div className="mono" style={{ color: A.ink2, fontSize:12 }}>{r.phone || '—'}</div>
              <div style={{ display:'flex', alignItems:'center', gap: 6 }}>
                <div style={{ width: 6, height: 6, borderRadius: 3, background: tierColor[r.tier] || A.mute }}/>
                {r.tier || 'Silver'}
              </div>
              <div className="mono" style={{ textAlign:'right', color: A.ink }}>{Number(r.pts || 0).toLocaleString()}</div>
              <div style={{ color: A.ink2 }}>{r.branch || '—'}</div>
              <div style={{ textAlign:'right' }}><IconChevronRight size={14} color={A.mute}/></div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// LINE Logins — shows everyone who has signed in via LIFF.
// Data lives in the `line_users` table; refreshed on mount.
// ───────────────────────────────────────────────────────────
function LineUsersView() {
  const { customers } = useData();
  const [users, setUsers]   = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError]   = useState(null);
  const [search, setSearch] = useState('');
  const [linking, setLinking] = useState(null);   // line_user object being linked
  const [toast, setToast]   = useState(null);

  const showToast = (msg, kind = 'ok') => {
    setToast({ msg, kind });
    setTimeout(() => setToast(null), 2800);
  };

  const load = () => {
    setLoading(true);
    fetch('/api/line/users')
      .then(r => r.json())
      .then(json => {
        if (json.error) setError(json.error);
        setUsers(json.data || []);
      })
      .catch(err => setError(err.message))
      .finally(() => setLoading(false));
  };

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

  // Build a quick lookup for member_id → customer (for link badges)
  const customersById = React.useMemo(() => {
    const map = {};
    (customers || []).forEach(c => { map[c.id] = c; });
    return map;
  }, [customers]);

  const me = getCurrentUser();
  const linkHeaders = () => ({
    'Content-Type':  'application/json',
    'x-user-id':     me.id   || '',
    'x-user-name':   me.name || '',
    'x-user-role':   me.role || '',
    'x-user-branch': me.assigned_branch_id || '',
  });

  const doLink = async (line_user_id, member_id) => {
    try {
      const res = await fetch('/api/line/link', {
        method: 'POST', headers: linkHeaders(),
        body: JSON.stringify({ line_user_id, member_id }),
      });
      const data = await res.json();
      if (!res.ok || !data.success) throw new Error(data.error || 'Failed');
      setLinking(null);
      load();
      showToast(member_id ? '🔗 เชื่อมกับลูกค้าเรียบร้อย' : 'ยกเลิกการเชื่อมแล้ว');
    } catch (e) {
      showToast(e.message, 'err');
    }
  };

  const q = search.trim().toLowerCase();
  const filtered = q
    ? users.filter(u =>
        (u.display_name || '').toLowerCase().includes(q) ||
        (u.line_user_id || '').toLowerCase().includes(q)
      )
    : users;

  const fmtTime = (iso) => {
    if (!iso) return '—';
    const d = new Date(iso.replace(' ', 'T') + 'Z');
    if (isNaN(d)) return iso;
    const diff = (Date.now() - d.getTime()) / 1000;
    if (diff < 60)     return Math.floor(diff)        + 's ago';
    if (diff < 3600)   return Math.floor(diff / 60)   + 'm ago';
    if (diff < 86400)  return Math.floor(diff / 3600) + 'h ago';
    return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
  };

  const totalLogins = users.reduce((sum, u) => sum + (u.login_count || 0), 0);

  return (
    <div>
      <AdminTopbar
        title="LINE Logins"
        subtitle={`${users.length.toLocaleString()} users · ${totalLogins.toLocaleString()} total logins`}
        right={<>
          <div style={{ background: A.bone, padding: '7px 12px', borderRadius: 8, display:'flex', alignItems:'center', gap: 8, fontSize: 12.5, color: q ? A.ink : A.mute, width: 240 }}>
            <IconSearch size={14} color={A.mute}/>
            <input
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              placeholder="Search by name or LINE ID"
              style={{ border:'none', background:'transparent', outline:'none', width:'100%', fontSize:12.5, color:A.ink, fontFamily:'inherit' }}
            />
            {q && (
              <button type="button" onClick={() => setSearch('')} style={{ background:'none', border:'none', cursor:'pointer', color:A.mute, padding:0, fontSize:14, lineHeight:1 }}>×</button>
            )}
          </div>
          <Btn onClick={load}>↻ Refresh</Btn>
        </>}
      />

      <div style={{ padding: 28, position: 'relative' }}>
        {toast && (
          <div style={{
            position:'absolute', top: 8, right: 28, zIndex: 60,
            padding: '10px 16px', borderRadius: 10,
            background: toast.kind === 'err' ? '#B5453A' : A.clover,
            color: '#fff', fontSize: 13, fontWeight: 500,
            boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
          }} className="slide-up">{toast.msg}</div>
        )}

        {error && (
          <div style={{
            background: '#FDEDEC', color: '#B5453A', border: '1px solid #F5C9C4',
            padding: '12px 16px', borderRadius: 10, marginBottom: 16, fontSize: 13,
          }}>
            Error: {error}
          </div>
        )}

        <div style={{ background:'#fff', border:'1px solid '+A.line, borderRadius: 12, overflow:'hidden' }}>
          <div style={{
            display:'grid', gridTemplateColumns: '50px 1.4fr 1fr 1.4fr 80px 1fr 110px',
            padding: '10px 18px', background: A.bone,
            fontSize: 10.5, letterSpacing: 1.2, textTransform:'uppercase', color: A.mute, fontWeight: 600,
          }}>
            <div></div>
            <div>Display name</div>
            <div>LINE ID</div>
            <div>Linked Customer</div>
            <div style={{ textAlign:'right' }}>Logins</div>
            <div>Last login</div>
            <div style={{ textAlign:'right' }}>Action</div>
          </div>

          {loading && (
            <div style={{ padding: '32px 18px', textAlign:'center', color: A.mute, fontSize: 13 }}>
              Loading…
            </div>
          )}

          {!loading && filtered.length === 0 && (
            <div style={{ padding: '40px 18px', textAlign:'center', color: A.mute, fontSize: 13 }}>
              {users.length === 0
                ? 'No LINE logins yet. When customers sign in via LINE, they will appear here.'
                : `No users match "${search}"`}
            </div>
          )}

          {!loading && filtered.map((u) => {
            const linkedCustomer = u.member_id ? customersById[u.member_id] : null;
            return (
              <div
                key={u.line_user_id}
                style={{
                  display:'grid', gridTemplateColumns: '50px 1.4fr 1fr 1.4fr 80px 1fr 110px',
                  padding: '13px 18px', alignItems:'center', fontSize: 13,
                  borderTop: '1px solid ' + A.line,
                }}
              >
                <div>
                  {u.picture_url
                    ? <img src={u.picture_url} alt="" style={{ width: 34, height: 34, borderRadius: '50%', objectFit:'cover', border:'1px solid '+A.line }}/>
                    : <div style={{ width: 34, height: 34, borderRadius:'50%', background: A.bone, border:'1px solid '+A.line, display:'flex', alignItems:'center', justifyContent:'center', fontSize: 12, color: A.mute, fontWeight: 600 }}>
                        {(u.display_name || '?').slice(0, 1).toUpperCase()}
                      </div>
                  }
                </div>
                <div>
                  <div style={{ color: A.ink, fontWeight: 600 }}>{u.display_name || '—'}</div>
                  {u.status_message && (
                    <div style={{ color: A.mute, fontSize: 11.5, marginTop: 2, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{u.status_message}</div>
                  )}
                </div>
                <div className="mono" style={{ color: A.mute, fontSize: 10.5, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', paddingRight: 8 }}>
                  {u.line_user_id}
                </div>
                <div>
                  {u.member_id ? (
                    <div>
                      <Tag bg={A.cloverSoft} color={A.clover} style={{ fontSize: 10 }}>🔗 LINKED</Tag>
                      <div style={{ color: A.ink, fontWeight: 600, marginTop: 4, fontSize: 12.5 }}>
                        {linkedCustomer?.name || u.member_id}
                      </div>
                      {linkedCustomer?.phone && (
                        <div style={{ color: A.mute, fontSize: 11 }}>{linkedCustomer.phone}</div>
                      )}
                    </div>
                  ) : (
                    <Tag bg={A.bone} color={A.mute} style={{ fontSize: 10 }}>NOT LINKED</Tag>
                  )}
                </div>
                <div className="mono" style={{ textAlign:'right', color: A.ink, fontWeight: 600 }}>
                  {(u.login_count || 0).toLocaleString()}
                </div>
                <div style={{ color: A.ink2, fontSize: 12 }}>{fmtTime(u.last_login_at)}</div>
                <div style={{ textAlign:'right' }}>
                  {u.member_id ? (
                    <Btn onClick={() => {
                      if (confirm(`ยกเลิกการเชื่อม "${u.display_name}" กับลูกค้านี้?`)) {
                        doLink(u.line_user_id, null);
                      }
                    }} style={{ fontSize: 11, padding: '5px 10px', color: '#B5453A', borderColor: '#F1C7C0' }}>
                      Unlink
                    </Btn>
                  ) : (
                    <Btn primary onClick={() => setLinking(u)} style={{ fontSize: 11, padding: '5px 10px' }}>
                      🔗 Link
                    </Btn>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {linking && (
        <LinkMemberModal
          lineUser={linking}
          customers={customers}
          onCancel={() => setLinking(null)}
          onLink={(member_id) => doLink(linking.line_user_id, member_id)}
        />
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// LinkMemberModal — search for a customer and link the LINE user to them.
// ───────────────────────────────────────────────────────────
function LinkMemberModal({ lineUser, customers, onCancel, onLink }) {
  const [query, setQuery] = useState('');
  const q = query.trim().toLowerCase();

  const results = React.useMemo(() => {
    const list = customers || [];
    if (!q) return list.slice(0, 25);
    return list.filter(c =>
      (c.name  || '').toLowerCase().includes(q) ||
      (c.phone || '').toLowerCase().includes(q) ||
      (c.email || '').toLowerCase().includes(q) ||
      (c.id    || '').toLowerCase().includes(q)
    ).slice(0, 50);
  }, [q, customers]);

  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 200,
      background: 'rgba(20,20,15,0.55)', backdropFilter: 'blur(4px)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
    }} onClick={onCancel}>
      <div onClick={e => e.stopPropagation()} className="slide-up" style={{
        width: '100%', maxWidth: 560, maxHeight: '88%', overflow: 'hidden',
        background: '#fff', borderRadius: 16, display: 'flex', flexDirection: 'column',
        boxShadow: '0 40px 100px rgba(0,0,0,0.5)',
      }}>
        {/* Header */}
        <div style={{ padding: '18px 22px', borderBottom: '1px solid ' + A.line }}>
          <div style={{ display:'flex', alignItems:'center', gap: 12 }}>
            {lineUser.picture_url
              ? <img src={lineUser.picture_url} alt="" style={{ width: 40, height: 40, borderRadius: '50%' }} />
              : <div style={{ width: 40, height: 40, borderRadius: '50%', background: A.bone, display:'flex', alignItems:'center', justifyContent:'center', color: A.clover, fontWeight: 700 }}>
                  {(lineUser.display_name || '?').slice(0, 1).toUpperCase()}
                </div>
            }
            <div style={{ flex: 1, minWidth: 0 }}>
              <div className="serif" style={{ fontSize: 18, color: A.clover, letterSpacing: -0.2 }}>
                เชื่อม LINE → ลูกค้า
              </div>
              <div style={{ fontSize: 12.5, color: A.mute, marginTop: 2 }}>
                <b style={{ color: A.ink }}>{lineUser.display_name}</b> · เลือกลูกค้าที่จะเชื่อมด้านล่าง
              </div>
            </div>
          </div>
        </div>

        {/* Search */}
        <div style={{ padding: '14px 22px', borderBottom: '1px solid ' + A.line, background: A.bone }}>
          <input
            autoFocus
            value={query} onChange={(e) => setQuery(e.target.value)}
            placeholder="ค้นหาด้วยชื่อ / เบอร์ / อีเมล / ID"
            style={{
              width: '100%', padding: '10px 14px', borderRadius: 8,
              border: '1px solid ' + A.line2, background: '#fff',
              fontSize: 13.5, color: A.ink, fontFamily: 'inherit', outline: 'none',
            }}
          />
        </div>

        {/* Results */}
        <div style={{ overflowY: 'auto', maxHeight: 440 }}>
          {results.length === 0 && (
            <div style={{ padding: '40px 20px', textAlign:'center', color: A.mute, fontSize: 13 }}>
              {q ? `ไม่พบลูกค้าที่ตรงกับ "${query}"` : 'ไม่มีลูกค้าในระบบ'}
            </div>
          )}
          {results.map(c => (
            <div key={c.id}
              onClick={() => {
                if (confirm(`เชื่อม LINE "${lineUser.display_name}" กับลูกค้า "${c.name}"?\n\nถ้าเคยมีโปรไฟล์อัตโนมัติของ LINE user นี้ ระบบจะลบทิ้ง (ถ้าไม่มีการใช้งาน)`)) {
                  onLink(c.id);
                }
              }}
              style={{
                padding: '14px 22px', borderTop: '1px solid ' + A.line, cursor: 'pointer',
                display: 'grid', gridTemplateColumns: '1fr auto', gap: 16, alignItems: 'center',
                transition: 'background .12s',
              }}
              onMouseEnter={e => e.currentTarget.style.background = A.bone}
              onMouseLeave={e => e.currentTarget.style.background = '#fff'}
            >
              <div style={{ minWidth: 0 }}>
                <div style={{ fontSize: 14, color: A.ink, fontWeight: 600 }}>{c.name || '—'}</div>
                <div style={{ fontSize: 11.5, color: A.mute, marginTop: 2 }}>
                  {c.id}
                  {c.phone ? ' · ' + c.phone : ''}
                  {c.email ? ' · ' + c.email : ''}
                </div>
              </div>
              <div style={{ fontSize: 11, color: A.clover, fontWeight: 600 }}>🔗 เชื่อม</div>
            </div>
          ))}
        </div>

        {/* Footer */}
        <div style={{ padding: '12px 22px', borderTop: '1px solid ' + A.line, display: 'flex', justifyContent: 'flex-end' }}>
          <Btn onClick={onCancel}>ยกเลิก</Btn>
        </div>
      </div>
    </div>
  );
}

function SettingsView() {
  return (
    <div>
      <AdminTopbar title="Settings" subtitle="Roles, auth, integrations" />
      <div style={{ padding: 28, color: A.mute, fontSize: 13 }}>
        <div style={{ background:'#fff', border:'1px solid '+A.line, borderRadius: 12, padding: 22 }}>
          <div className="serif" style={{ fontSize: 18, color: A.ink, marginBottom: 6 }}>Auth methods</div>
          <div style={{ fontSize: 12.5, color: A.mute }}>Customer-facing sign-in providers</div>
          <div style={{ marginTop: 14 }}>
            {[
              { label: 'LINE Login (OA: @cloverclinic)', on: true },
              { label: 'Google OAuth (clover-clinic.app)', on: true },
              { label: 'Phone OTP (SMS · Twilio TH)', on: true },
              { label: 'Magic link (email)', on: true },
            ].map((r, i) => (
              <div key={i} style={{ padding: '12px 0', borderTop: i === 0 ? 'none' : '1px solid '+A.line, display:'flex', alignItems:'center', justifyContent:'space-between' }}>
                <span style={{ color: A.ink, fontSize: 13 }}>{r.label}</span>
                <ToggleSmall on={r.on}/>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// Shell
// ───────────────────────────────────────────────────────────
// ── Branding view ────────────────────────────────────────────
function BrandingView({ brand, setBrand, logoUrl, setLogoUrl }) {
  const [toast, setToast] = useState(null);

  // Apply brand live as sliders move
  useEffect(() => { applyBrand(brand); }, [brand]);

  const set = (key, val) => setBrand(b => ({ ...b, [key]: val }));

  const handleSave = () => {
    localStorage.setItem('clover-brand', JSON.stringify(brand));
    if (logoUrl) localStorage.setItem('clover-logo', logoUrl);
    else localStorage.removeItem('clover-logo');
    setToast('Brand saved — reload customer.html to apply colors there too');
    setTimeout(() => setToast(null), 3500);
  };

  const handleReset = () => {
    setBrand(DEFAULT_BRAND);
    setLogoUrl(null);
    applyBrand(DEFAULT_BRAND);
    localStorage.removeItem('clover-brand');
    localStorage.removeItem('clover-logo');
  };

  const GROUPS = [
    { label: 'Brand green', items: [
      { key:'clover',     label:'Primary',   hint:'--clover' },
      { key:'cloverDeep', label:'Deep',      hint:'--clover-deep' },
      { key:'cloverMid',  label:'Mid',       hint:'--clover-mid' },
      { key:'cloverSoft', label:'Soft tint', hint:'--clover-soft' },
    ]},
    { label: 'Gold accent', items: [
      { key:'gold',     label:'Gold',      hint:'--gold' },
      { key:'goldDeep', label:'Deep gold', hint:'--gold-deep' },
      { key:'goldSoft', label:'Gold tint', hint:'--gold-soft' },
    ]},
    { label: 'Surface & text', items: [
      { key:'bone',  label:'Bone (bg)',    hint:'--bone' },
      { key:'paper', label:'Paper (card)', hint:'--paper' },
      { key:'ink',   label:'Ink (text)',   hint:'--ink' },
    ]},
  ];

  return (
    <div style={{ position: 'relative' }}>
      {toast && (
        <div className="slide-up" style={{
          position: 'fixed', bottom: 28, left: '50%', transform: 'translateX(-50%)',
          background: A.clover, color: '#fff', padding: '12px 24px', borderRadius: 99,
          fontSize: 13, fontWeight: 500, zIndex: 9999, whiteSpace: 'nowrap',
          boxShadow: '0 8px 24px rgba(0,0,0,0.25)',
        }}>{toast}</div>
      )}

      <AdminTopbar
        title="Branding"
        subtitle="Customize colors and logo — live preview updates instantly · save to persist on next load"
        right={<>
          <Btn onClick={handleReset}>Reset defaults</Btn>
          <Btn primary onClick={handleSave}>Save brand</Btn>
        </>}
      />

      <div style={{ padding: 28, display: 'grid', gridTemplateColumns: '1fr 300px', gap: 22, alignItems: 'start' }}>
        {/* Controls */}
        <div>
          {/* Logo */}
          <div style={{ background: '#fff', border: '1px solid '+A.line, borderRadius: 14, padding: 22, marginBottom: 16 }}>
            <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1.4, textTransform: 'uppercase', color: A.mute, marginBottom: 14 }}>Clinic logo</div>
            <ImageUploadField value={logoUrl} onChange={setLogoUrl} aspect="3/1" placeholder="transparent PNG · recommended 600×200" />
            <div style={{ fontSize: 11.5, color: A.mute, marginTop: 8, lineHeight: 1.5 }}>
              Replaces the Clover mark in the admin sidebar and customer app header.
            </div>
          </div>

          {/* Color groups */}
          {GROUPS.map(g => (
            <div key={g.label} style={{ background: '#fff', border: '1px solid '+A.line, borderRadius: 14, padding: 22, marginBottom: 14 }}>
              <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1.4, textTransform: 'uppercase', color: A.mute, marginBottom: 18 }}>{g.label}</div>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
                {g.items.map(({ key, label, hint }) => (
                  <div key={key} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
                    <label style={{ position: 'relative', flexShrink: 0, cursor: 'pointer' }}>
                      <div style={{
                        width: 44, height: 44, borderRadius: 12,
                        background: brand[key],
                        border: '2px solid rgba(0,0,0,0.08)',
                        boxShadow: '0 2px 10px rgba(0,0,0,0.14)',
                      }} />
                      <input
                        type="color" value={brand[key]}
                        onChange={(e) => set(key, e.target.value)}
                        style={{ position:'absolute', inset:0, opacity:0.001, width:'100%', height:'100%', cursor:'pointer' }}
                      />
                    </label>
                    <div>
                      <div style={{ fontSize: 13, color: A.ink, fontWeight: 600 }}>{label}</div>
                      <div className="mono" style={{ fontSize: 10, color: A.mute, marginTop: 1 }}>{hint}</div>
                      <div className="mono" style={{ fontSize: 10.5, color: A.cloverMid, marginTop: 1 }}>{brand[key]}</div>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          ))}
        </div>

        {/* Live preview — sticky */}
        <div style={{ position: 'sticky', top: 20 }}>
          <BrandPreview brand={brand} logoUrl={logoUrl} />
        </div>
      </div>
    </div>
  );
}

function BrandPreview({ brand: b, logoUrl }) {
  return (
    <div>
      <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1.4, textTransform: 'uppercase', color: A.mute, marginBottom: 14 }}>Live preview</div>

      {/* ── Mobile app preview ── */}
      <div style={{ marginBottom: 18 }}>
        <div style={{ fontSize: 11.5, fontWeight: 600, color: A.ink2, marginBottom: 8 }}>Customer mobile</div>
        <div style={{
          background: b.bone, borderRadius: 24, padding: 18, overflow: 'hidden',
          border: '1px solid rgba(0,0,0,0.07)',
          boxShadow: '0 8px 32px rgba(0,0,0,0.10)',
        }}>
          {/* Top bar */}
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 14 }}>
            {logoUrl
              ? <img src={logoUrl} style={{ height: 22, objectFit:'contain', maxWidth: 110 }} />
              : <div style={{ display:'flex', alignItems:'center', gap: 6 }}>
                  <div style={{ width:20, height:20, borderRadius:'50%', background: b.clover, display:'flex', alignItems:'center', justifyContent:'center' }}>
                    <div style={{ width:8, height:8, borderRadius:'50%', background:'rgba(255,255,255,0.5)' }} />
                  </div>
                  <span style={{ fontSize:13, fontWeight:700, color: b.clover, fontFamily:'serif' }}>Clover Clinic</span>
                </div>
            }
            <div style={{ fontSize:9, fontWeight:700, letterSpacing:1, color: b.goldDeep, padding:'3px 8px', borderRadius:99, background: b.goldSoft }}>PLATINUM</div>
          </div>

          {/* Points card */}
          <div style={{ background:`linear-gradient(135deg, ${b.cloverDeep} 0%, ${b.clover} 100%)`, borderRadius:16, padding:'16px 18px', color:'#fff' }}>
            <div style={{ fontSize:8.5, opacity:0.65, letterSpacing:1.2, textTransform:'uppercase' }}>Clover Points</div>
            <div style={{ fontSize:28, fontWeight:700, letterSpacing:-1, marginTop:3 }}>8,210</div>
            <div style={{ fontSize:9, opacity:0.65, marginTop:3 }}>≈ ฿8,210 spend value</div>
            <div style={{ marginTop:10, height:3, borderRadius:2, background:'rgba(255,255,255,0.18)' }}>
              <div style={{ height:'100%', width:'68%', background: b.gold, borderRadius:2 }} />
            </div>
          </div>

          {/* Quick actions */}
          <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:8, marginTop:12 }}>
            {[{ label:'My QR', c: b.clover }, { label:'Courses', c: b.cloverMid }, { label:'Rewards', c: b.gold }].map(({ label, c }) => (
              <div key={label} style={{ background: b.paper, borderRadius:12, padding:'10px 4px', textAlign:'center', border:'1px solid rgba(0,0,0,0.05)' }}>
                <div style={{ width:28, height:28, borderRadius:8, background: b.cloverSoft, margin:'0 auto', display:'flex', alignItems:'center', justifyContent:'center' }}>
                  <div style={{ width:12, height:12, borderRadius:3, background: c }} />
                </div>
                <div style={{ fontSize:9.5, fontWeight:600, color: b.ink, marginTop:5 }}>{label}</div>
              </div>
            ))}
          </div>

          {/* Nav bar */}
          <div style={{ display:'flex', justifyContent:'space-around', marginTop:12, paddingTop:10, borderTop:'1px solid rgba(0,0,0,0.06)' }}>
            {['Home','Orders','Points','Profile'].map((n, i) => (
              <div key={n} style={{ fontSize:9, fontWeight: i===0 ? 700 : 500, color: i===0 ? b.clover : '#9A9A92', textAlign:'center' }}>
                <div style={{ width:16, height:16, borderRadius:4, background: i===0 ? b.cloverSoft : 'transparent', margin:'0 auto 3px' }} />
                {n}
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* ── Admin sidebar preview ── */}
      <div>
        <div style={{ fontSize:11.5, fontWeight:600, color: A.ink2, marginBottom:8 }}>Admin sidebar</div>
        <div style={{ background: b.cloverDeep, borderRadius:16, padding:14, overflow:'hidden' }}>
          <div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:12, paddingBottom:12, borderBottom:'1px solid rgba(255,255,255,0.08)' }}>
            {logoUrl
              ? <img src={logoUrl} style={{ height:20, objectFit:'contain', maxWidth:110 }} />
              : <>
                  <div style={{ width:20, height:20, borderRadius:6, background: b.gold }} />
                  <div>
                    <div style={{ fontSize:12, color:'#fff', fontWeight:700 }}>Clover Clinic</div>
                    <div style={{ fontSize:8.5, color:'rgba(255,255,255,0.4)', letterSpacing:1, textTransform:'uppercase' }}>Console</div>
                  </div>
                </>
            }
          </div>
          {[
            { label:'Overview',   active:true },
            { label:'Branches',   active:false },
            { label:'Customers',  active:false },
            { label:'Branding',   active:false },
          ].map(({ label, active }) => (
            <div key={label} style={{
              padding:'8px 10px', borderRadius:7, marginBottom:3,
              background: active ? `rgba(${hexToRgb(b.gold)},0.16)` : 'transparent',
              color: active ? b.gold : 'rgba(255,255,255,0.6)',
              fontSize:12, fontWeight: active ? 700 : 400,
              display:'flex', alignItems:'center', gap:8,
            }}>
              <div style={{ width:13, height:13, borderRadius:3, background: active ? `rgba(${hexToRgb(b.gold)},0.5)` : 'rgba(255,255,255,0.14)' }} />
              {label}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ── Shell ─────────────────────────────────────────────────────
// ───────────────────────────────────────────────────────────
// Staff Management — invite staff, assign to branch & role
// ───────────────────────────────────────────────────────────
function StaffManagementView({ role }) {
  const { branches } = useData();
  const [staff, setStaff] = useState([]);
  const [loading, setLoading] = useState(true);
  const [showInvite, setShowInvite] = useState(false);
  const [editingUser, setEditingUser] = useState(null);
  const [toast, setToast] = useState(null);

  const showToast = (msg, kind = 'ok') => {
    setToast({ msg, kind });
    setTimeout(() => setToast(null), 2600);
  };

  const me = getCurrentUser();
  const reloadStaff = () => {
    setLoading(true);
    const headers = { 'x-user-role': me.role, 'x-user-branch': me.assigned_branch_id || '' };
    fetch('/api/staff/list', { headers })
      .then(r => r.json())
      .then(d => setStaff(d.data || []))
      .catch(() => setStaff([]))
      .finally(() => setLoading(false));
  };
  useEffect(reloadStaff, []);

  const roleOptions = role === 'super_admin'
    ? ['super_admin', 'admin', 'manager', 'staff']
    : role === 'admin' ? ['admin', 'manager', 'staff']
    : ['staff'];

  return (
    <div style={{ position: 'relative' }}>
      <AdminTopbar
        title="Staff Management"
        subtitle="Invite new staff, assign them to a branch, and set their role"
        right={<Btn primary onClick={() => setShowInvite(true)}>+ Invite Staff</Btn>}
      />

      {toast && (
        <div style={{
          position: 'absolute', top: 70, right: 28, zIndex: 60,
          padding: '10px 16px', borderRadius: 10,
          background: toast.kind === 'err' ? '#B5453A' : (toast.kind === 'mute' ? A.ink2 : A.clover),
          color: '#fff', fontSize: 13, fontWeight: 500,
          boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
        }} className="slide-up">{toast.msg}</div>
      )}

      <div style={{ padding: 28 }}>
        <div style={{ background: '#fff', border: '1px solid ' + A.line, borderRadius: 12, overflow: 'hidden' }}>
          <div style={{
            display: 'grid', gridTemplateColumns: '2fr 2fr 1.2fr 1.5fr 1fr 1fr',
            padding: '12px 18px', background: A.bone, fontSize: 11, color: A.mute,
            fontWeight: 600, letterSpacing: 1, textTransform: 'uppercase',
          }}>
            <div>Name</div><div>Email</div><div>Role</div><div>Branch</div><div>Last Login</div><div></div>
          </div>

          {loading && (
            <div style={{ padding: '40px 18px', textAlign: 'center', color: A.mute, fontSize: 13 }}>Loading staff…</div>
          )}

          {!loading && staff.length === 0 && (
            <div style={{ padding: '40px 18px', textAlign: 'center', color: A.mute, fontSize: 13 }}>
              No staff yet. Click "Invite Staff" to add the first one.
            </div>
          )}

          {!loading && staff.map((u, i) => (
            <div key={u.id} style={{
              display: 'grid', gridTemplateColumns: '2fr 2fr 1.2fr 1.5fr 1fr 1fr',
              padding: '14px 18px', alignItems: 'center',
              borderTop: i === 0 ? 'none' : '1px solid ' + A.line, fontSize: 13,
            }}>
              <div style={{ color: A.ink, fontWeight: 600 }}>{u.name}</div>
              <div style={{ color: A.mute, fontSize: 12 }}>{u.email}</div>
              <div>
                <Tag bg={
                  u.role === 'super_admin' ? '#FBE6CE' :
                  u.role === 'admin'       ? A.cloverSoft :
                  u.role === 'manager'     ? A.goldSoft : A.bone
                } color={
                  u.role === 'super_admin' ? A.goldDeep :
                  u.role === 'admin'       ? A.clover :
                  u.role === 'manager'     ? A.goldDeep : A.mute
                }>{ROLE_LABEL[u.role] || u.role}</Tag>
              </div>
              <div style={{ color: A.ink2 }}>{u.branch_name || '— All branches'}</div>
              <div style={{ color: A.mute, fontSize: 12 }}>
                {u.last_login_at ? new Date(u.last_login_at).toLocaleDateString() : 'Never'}
              </div>
              <div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end' }}>
                <Btn onClick={() => setEditingUser(u)} style={{ fontSize: 11, padding: '5px 10px' }}>Edit</Btn>
              </div>
            </div>
          ))}
        </div>
      </div>

      {showInvite && (
        <InviteStaffModal
          branches={branches}
          roleOptions={roleOptions}
          callerRole={role}
          callerBranch={me.assigned_branch_id}
          onCancel={() => setShowInvite(false)}
          onSaved={(result) => {
            setShowInvite(false);
            reloadStaff();
            const pw = result?.data?.temp_password;
            showToast(pw ? `Invited! Temp password: ${pw}` : 'Staff invited');
          }}
          onError={(msg) => showToast(msg, 'err')}
        />
      )}

      {editingUser && (
        <EditStaffModal
          user={editingUser}
          branches={branches}
          roleOptions={roleOptions}
          callerRole={role}
          onCancel={() => setEditingUser(null)}
          onSaved={() => { setEditingUser(null); reloadStaff(); showToast('Staff updated'); }}
          onDeactivated={() => { setEditingUser(null); reloadStaff(); showToast('Staff deactivated', 'mute'); }}
          onError={(msg) => showToast(msg, 'err')}
        />
      )}
    </div>
  );
}

function InviteStaffModal({ branches, roleOptions, callerRole, callerBranch, onCancel, onSaved, onError }) {
  const [form, setForm] = useState({
    name: '', email: '', phone: '',
    role: roleOptions[roleOptions.length - 1],
    assigned_branch_id: callerRole === 'manager' ? (callerBranch || '') : '',
  });
  const [submitting, setSubmitting] = useState(false);

  const handleSubmit = async () => {
    if (!form.name || !form.email) { onError && onError('Name and email required'); return; }
    setSubmitting(true);
    try {
      const me = getCurrentUser();
      const res = await fetch('/api/staff/invite', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-user-id':     me.id,
          'x-user-name':   me.name,
          'x-user-role':   me.role,
          'x-user-branch': me.assigned_branch_id || '',
        },
        body: JSON.stringify(form),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Failed');
      onSaved && onSaved(data);
    } catch (e) {
      onError && onError(e.message);
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <CMSModalShell title="Invite Staff Member" onCancel={onCancel} onSave={handleSubmit}>
      <CMSField label="Full name">
        <input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} style={cmsInputStyle()} placeholder="Praew Ratanasiri" />
      </CMSField>
      <CMSField label="Email">
        <input type="email" value={form.email} onChange={e => setForm({ ...form, email: e.target.value })} style={cmsInputStyle()} placeholder="praew@clover.clinic" />
      </CMSField>
      <CMSField label="Phone (optional)">
        <input value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} style={cmsInputStyle()} placeholder="+66 89 ..." />
      </CMSField>
      <CMSField label="Role">
        <select value={form.role} onChange={e => setForm({ ...form, role: e.target.value })} style={cmsInputStyle()}>
          {roleOptions.map(r => <option key={r} value={r}>{ROLE_LABEL[r]}</option>)}
        </select>
      </CMSField>
      <CMSField label="Assigned branch" hint={form.role === 'super_admin' || form.role === 'admin' ? 'Optional — leave blank for all branches' : 'Required'}>
        <select
          value={form.assigned_branch_id}
          onChange={e => setForm({ ...form, assigned_branch_id: e.target.value })}
          disabled={callerRole === 'manager'}
          style={cmsInputStyle()}
        >
          <option value="">— No specific branch —</option>
          {(branches || []).map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
        </select>
      </CMSField>
      {submitting && <div style={{ padding: '8px 0', fontSize: 12, color: A.mute }}>Inviting…</div>}
    </CMSModalShell>
  );
}

function EditStaffModal({ user, branches, roleOptions, callerRole, onCancel, onSaved, onDeactivated, onError }) {
  const [form, setForm] = useState({
    name: user.name || '',
    role: user.role || 'staff',
    assigned_branch_id: user.assigned_branch_id || '',
    phone: user.phone || '',
  });
  const [submitting, setSubmitting] = useState(false);

  const headers = () => {
    const me = getCurrentUser();
    return {
      'Content-Type':  'application/json',
      'x-user-id':     me.id,
      'x-user-name':   me.name,
      'x-user-role':   me.role,
      'x-user-branch': me.assigned_branch_id || '',
    };
  };

  const handleSave = async () => {
    setSubmitting(true);
    try {
      const res = await fetch(`/api/staff/${user.id}`, {
        method: 'PUT', headers: headers(), body: JSON.stringify(form),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Failed');
      onSaved && onSaved();
    } catch (e) { onError && onError(e.message); }
    finally { setSubmitting(false); }
  };

  const handleDeactivate = async () => {
    if (!confirm(`Deactivate ${user.name}?`)) return;
    setSubmitting(true);
    try {
      const res = await fetch(`/api/staff/${user.id}`, { method: 'DELETE', headers: headers() });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Failed');
      onDeactivated && onDeactivated();
    } catch (e) { onError && onError(e.message); }
    finally { setSubmitting(false); }
  };

  return (
    <CMSModalShell title={`Edit · ${user.name}`} onCancel={onCancel} onSave={handleSave}>
      <CMSField label="Full name">
        <input value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} style={cmsInputStyle()} />
      </CMSField>
      <CMSField label="Role">
        <select value={form.role} onChange={e => setForm({ ...form, role: e.target.value })} style={cmsInputStyle()}>
          {roleOptions.map(r => <option key={r} value={r}>{ROLE_LABEL[r]}</option>)}
        </select>
      </CMSField>
      <CMSField label="Assigned branch">
        <select value={form.assigned_branch_id} onChange={e => setForm({ ...form, assigned_branch_id: e.target.value })} style={cmsInputStyle()}>
          <option value="">— No specific branch —</option>
          {(branches || []).map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
        </select>
      </CMSField>
      <CMSField label="Phone">
        <input value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })} style={cmsInputStyle()} />
      </CMSField>
      <div style={{ paddingTop: 14, borderTop: '1px solid ' + A.line, marginTop: 4 }}>
        <Btn onClick={handleDeactivate} style={{ color: '#B5453A', borderColor: '#F1C7C0' }}>Deactivate this user</Btn>
      </div>
      {submitting && <div style={{ padding: '8px 0', fontSize: 12, color: A.mute }}>Saving…</div>}
    </CMSModalShell>
  );
}

// ───────────────────────────────────────────────────────────
// Activity Logs — searchable audit trail
// ───────────────────────────────────────────────────────────
const ACTION_TYPES = [
  { id: '',                  label: 'All actions' },
  { id: 'login_success',     label: 'Login (success)' },
  { id: 'login_failed',      label: 'Login (failed)' },
  { id: 'points_add',        label: 'Points · Added' },
  { id: 'points_redeem',     label: 'Points · Redeemed' },
  { id: 'points_adjust',     label: 'Points · Manual adjust' },
  { id: 'course_use',        label: 'Course · Session used' },
  { id: 'course_add',        label: 'Course · Added' },
  { id: 'export_accounting', label: 'Export · Accounting' },
  { id: 'export_customers',  label: 'Export · Customers' },
  { id: 'customer_create',   label: 'Customer · Created' },
  { id: 'customer_update',   label: 'Customer · Updated' },
  { id: 'customer_delete',   label: 'Customer · Deleted' },
  { id: 'staff_invite',      label: 'Staff · Invited' },
  { id: 'staff_update',      label: 'Staff · Updated' },
  { id: 'staff_delete',      label: 'Staff · Deactivated' },
  { id: 'branch_create',     label: 'Branch · Created' },
  { id: 'branch_update',     label: 'Branch · Updated' },
  { id: 'branch_delete',     label: 'Branch · Deleted' },
];

function ActivityLogsView({ role }) {
  const { branches } = useData();
  const [logs, setLogs] = useState([]);
  const [loading, setLoading] = useState(true);
  const [filters, setFilters] = useState({
    date_from: '', date_to: '', staff_name: '',
    branch_id: '', action_type: '',
  });

  const me = getCurrentUser();
  const reload = () => {
    setLoading(true);
    const qs = new URLSearchParams();
    Object.entries(filters).forEach(([k, v]) => { if (v) qs.set(k, v); });
    qs.set('callerRole', me.role || '');
    qs.set('callerBranch', me.assigned_branch_id || '');
    fetch('/api/logs/list?' + qs.toString(), {
      headers: {
        'x-user-role':   me.role,
        'x-user-branch': me.assigned_branch_id || '',
      },
    })
      .then(r => r.json())
      .then(d => setLogs(d.data || []))
      .catch(() => setLogs([]))
      .finally(() => setLoading(false));
  };
  useEffect(reload, []);

  const exportCsv = () => {
    const rows = [
      ['When', 'User', 'Role', 'Action', 'Branch', 'Target', 'Details'],
      ...logs.map(l => [
        l.created_at,
        l.user_name || l.current_user_name || '',
        l.user_role || '',
        l.action_label || l.action_type,
        l.branch_name || '',
        l.target_name || '',
        (l.details || '').replace(/\s+/g, ' '),
      ]),
    ];
    const csv = '﻿' + rows.map(r => r.map(c => `"${String(c ?? '').replace(/"/g, '""')}"`).join(',')).join('\n');
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `activity-logs-${new Date().toISOString().slice(0, 10)}.csv`;
    document.body.appendChild(a); a.click(); document.body.removeChild(a);
    URL.revokeObjectURL(url);
    logActivity({ action_type: 'export_accounting', action_label: 'Exported activity logs CSV' });
  };

  return (
    <div style={{ position: 'relative' }}>
      <AdminTopbar
        title="Activity Logs"
        subtitle="Who did what and when — searchable audit trail"
        right={<>
          <Btn onClick={reload}>↻ Refresh</Btn>
          <Btn primary onClick={exportCsv}>Export CSV</Btn>
        </>}
      />

      {/* Filters */}
      <div style={{
        padding: '16px 28px', display: 'grid',
        gridTemplateColumns: 'repeat(5, 1fr) auto', gap: 10,
        borderBottom: '1px solid ' + A.line, background: '#fff', alignItems: 'end',
      }}>
        <FilterField label="From">
          <input type="date" value={filters.date_from}
            onChange={e => setFilters({ ...filters, date_from: e.target.value })} style={filterInput()} />
        </FilterField>
        <FilterField label="To">
          <input type="date" value={filters.date_to}
            onChange={e => setFilters({ ...filters, date_to: e.target.value })} style={filterInput()} />
        </FilterField>
        <FilterField label="Staff name">
          <input value={filters.staff_name} placeholder="search…"
            onChange={e => setFilters({ ...filters, staff_name: e.target.value })} style={filterInput()} />
        </FilterField>
        <FilterField label="Branch">
          <select value={filters.branch_id} onChange={e => setFilters({ ...filters, branch_id: e.target.value })} style={filterInput()}
            disabled={role !== 'super_admin' && role !== 'admin'}
          >
            <option value="">All branches</option>
            {(branches || []).map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
          </select>
        </FilterField>
        <FilterField label="Action type">
          <select value={filters.action_type} onChange={e => setFilters({ ...filters, action_type: e.target.value })} style={filterInput()}>
            {ACTION_TYPES.map(a => <option key={a.id} value={a.id}>{a.label}</option>)}
          </select>
        </FilterField>
        <Btn primary onClick={reload}>Apply</Btn>
      </div>

      <div style={{ padding: 28 }}>
        <div style={{ background: '#fff', border: '1px solid ' + A.line, borderRadius: 12, overflow: 'hidden' }}>
          <div style={{
            display: 'grid', gridTemplateColumns: '180px 1.2fr 1fr 1.6fr 1.5fr 1fr',
            padding: '12px 18px', background: A.bone, fontSize: 11, color: A.mute,
            fontWeight: 600, letterSpacing: 1, textTransform: 'uppercase',
          }}>
            <div>When</div><div>User</div><div>Role</div><div>Action</div><div>Target</div><div>Branch</div>
          </div>

          {loading && <div style={{ padding: '40px 18px', textAlign: 'center', color: A.mute, fontSize: 13 }}>Loading logs…</div>}

          {!loading && logs.length === 0 && (
            <div style={{ padding: '40px 18px', textAlign: 'center', color: A.mute, fontSize: 13 }}>
              No activity logs match your filters.
            </div>
          )}

          {!loading && logs.map((l, i) => (
            <div key={l.id} style={{
              display: 'grid', gridTemplateColumns: '180px 1.2fr 1fr 1.6fr 1.5fr 1fr',
              padding: '12px 18px', alignItems: 'center',
              borderTop: i === 0 ? 'none' : '1px solid ' + A.line, fontSize: 12.5,
            }}>
              <div style={{ color: A.mute, fontFamily: 'JetBrains Mono, monospace', fontSize: 11 }}>
                {new Date(l.created_at).toLocaleString()}
              </div>
              <div style={{ color: A.ink, fontWeight: 600 }}>
                {l.user_name || l.current_user_name || '—'}
              </div>
              <div>
                <Tag bg={A.bone} color={A.ink2} style={{ fontSize: 9 }}>
                  {ROLE_LABEL[l.user_role] || l.user_role || '—'}
                </Tag>
              </div>
              <div style={{ color: A.ink }}>
                <div>{l.action_label || l.action_type}</div>
                <div style={{ fontSize: 10, color: A.mute, fontFamily: 'JetBrains Mono, monospace', marginTop: 2 }}>
                  {l.action_type}
                </div>
              </div>
              <div style={{ color: A.ink2 }}>{l.target_name || '—'}</div>
              <div style={{ color: A.mute, fontSize: 12 }}>{l.branch_name || '—'}</div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function FilterField({ label, children }) {
  return (
    <div>
      <div style={{ fontSize: 10, color: A.mute, fontWeight: 600, letterSpacing: 0.8,
                    textTransform: 'uppercase', marginBottom: 4 }}>{label}</div>
      {children}
    </div>
  );
}
function filterInput() {
  return {
    width: '100%', padding: '8px 10px', borderRadius: 8,
    border: '1px solid ' + A.line2, fontSize: 12.5, fontFamily: 'inherit',
    background: '#fff', color: A.ink, outline: 'none',
  };
}

// ───────────────────────────────────────────────────────────
// Point Claims — review receipts submitted by customers.
// Approve to grant points; reject with a reason.
// ───────────────────────────────────────────────────────────
function PointClaimsView({ role }) {
  const { branches } = useData();
  const [statusFilter, setStatusFilter] = useState('pending');
  const [claims, setClaims] = useState([]);
  const [loading, setLoading] = useState(true);
  const [reviewing, setReviewing] = useState(null);
  const [toast, setToast] = useState(null);

  const showToast = (msg, kind = 'ok') => {
    setToast({ msg, kind });
    setTimeout(() => setToast(null), 2400);
  };

  const me = getCurrentUser();
  const headers = () => ({
    'Content-Type':   'application/json',
    'x-user-id':      me.id   || '',
    'x-user-name':    me.name || '',
    'x-user-role':    me.role || '',
    'x-user-branch':  me.assigned_branch_id || '',
  });

  const reload = React.useCallback(() => {
    setLoading(true);
    fetch(`/api/claims/list?status=${statusFilter}`, { headers: headers() })
      .then(r => r.json())
      .then(d => setClaims(d?.data || []))
      .catch(() => setClaims([]))
      .finally(() => setLoading(false));
  }, [statusFilter]);
  useEffect(reload, [reload]);

  const counts = {
    pending: claims.filter(c => c.status === 'pending').length,
  };

  return (
    <div style={{ position: 'relative' }}>
      <AdminTopbar
        title="Point Claims"
        subtitle="ใบเสร็จที่ลูกค้าส่งมาขอคะแนน — กดเข้าไปดูรูปแล้วอนุมัติหรือไม่อนุมัติ"
        right={<Btn onClick={reload}>↻ Refresh</Btn>}
      />

      {toast && (
        <div style={{
          position:'absolute', top: 70, right: 28, zIndex: 60,
          padding: '10px 16px', borderRadius: 10,
          background: toast.kind === 'err' ? '#B5453A' : (toast.kind === 'mute' ? A.ink2 : A.clover),
          color: '#fff', fontSize: 13, fontWeight: 500,
          boxShadow: '0 10px 30px rgba(0,0,0,0.2)',
        }} className="slide-up">{toast.msg}</div>
      )}

      <div style={{ padding: '14px 28px 0', display:'flex', gap: 8, borderBottom: '1px solid '+A.line }}>
        {[
          { id: 'pending',  label: '⏳ Pending' + (counts.pending ? ` (${counts.pending})` : '') },
          { id: 'approved', label: '✓ Approved' },
          { id: 'rejected', label: '✕ Rejected' },
          { id: 'all',      label: 'All' },
        ].map(s => (
          <div key={s.id} onClick={() => setStatusFilter(s.id)} style={{
            padding: '10px 16px', cursor:'pointer', fontSize: 13, fontWeight: 600,
            color: statusFilter === s.id ? A.clover : A.mute,
            borderBottom: '2px solid ' + (statusFilter === s.id ? A.clover : 'transparent'),
            marginBottom: -1,
          }}>{s.label}</div>
        ))}
      </div>

      <div style={{ padding: 28 }}>
        {loading && <div style={{ textAlign:'center', color: A.mute, fontSize: 13, padding: 40 }}>Loading claims…</div>}

        {!loading && claims.length === 0 && (
          <div style={{
            textAlign:'center', color: A.mute, fontSize: 13, padding: 60,
            background: '#fff', border: '1px solid ' + A.line, borderRadius: 12,
          }}>
            No {statusFilter !== 'all' ? statusFilter : ''} claims.
          </div>
        )}

        {!loading && claims.length > 0 && (
          <div style={{ display:'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 14 }}>
            {claims.map(c => (
              <div key={c.id}
                onClick={() => setReviewing(c)}
                style={{
                  background:'#fff', border: '1px solid ' + A.line, borderRadius: 12,
                  padding: 16, cursor: 'pointer', transition: 'border-color .12s',
                }}
                onMouseEnter={e => e.currentTarget.style.borderColor = A.clover}
                onMouseLeave={e => e.currentTarget.style.borderColor = A.line}
              >
                <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom: 8 }}>
                  <div className="serif" style={{ fontSize: 22, color: A.clover, letterSpacing: -0.3 }}>
                    ฿{Number(c.amount_thb).toLocaleString()}
                  </div>
                  <Tag
                    color={c.status === 'approved' ? A.clover : c.status === 'rejected' ? '#B5453A' : A.goldDeep}
                    bg={c.status === 'approved' ? A.cloverSoft : c.status === 'rejected' ? '#FEF0EF' : A.goldSoft}
                  >
                    {c.status}
                  </Tag>
                </div>
                <div style={{ fontSize: 13, color: A.ink, fontWeight: 600 }}>{c.member_name || c.member_id}</div>
                <div style={{ fontSize: 11.5, color: A.mute, marginTop: 4 }}>
                  {c.branch_name || '—'} · {new Date(c.created_at).toLocaleString()}
                </div>
                <div style={{
                  marginTop: 10, padding: '8px 10px', background: A.bone, borderRadius: 8,
                  fontSize: 12, color: A.ink2, fontWeight: 600,
                }}>
                  ขอ {c.points_requested?.toLocaleString() || 0} คะแนน
                  {c.points_awarded != null && c.points_awarded !== c.points_requested && (
                    <span style={{ color: A.clover }}> → ให้ {c.points_awarded.toLocaleString()}</span>
                  )}
                </div>
                {c.customer_note && (
                  <div style={{ fontSize: 11.5, color: A.mute, marginTop: 8, fontStyle: 'italic' }}>
                    "{c.customer_note}"
                  </div>
                )}
              </div>
            ))}
          </div>
        )}
      </div>

      {reviewing && (
        <ClaimReviewModal
          claim={reviewing}
          onClose={() => setReviewing(null)}
          onAction={async (action, payload) => {
            try {
              const url = `/api/claims/${reviewing.id}`;
              const opts = action === 'delete'
                ? { method: 'DELETE', headers: headers() }
                : { method: 'POST',   headers: headers(),
                    body: JSON.stringify({ action, ...payload }) };
              const res = await fetch(url, opts);
              const data = await res.json();
              if (!res.ok || !data.success) throw new Error(data.error || 'Failed');
              setReviewing(null);
              reload();
              const msg = action === 'approve' ? '✓ Approved & points added'
                       : action === 'reject'  ? 'Rejected'
                       : action === 'delete'  ? (data.reversed
                            ? `🗑️ ลบแล้ว · คืน ${data.reversed} คะแนน`
                            : '🗑️ ลบใบเสร็จเรียบร้อย')
                       : 'Done';
              showToast(msg, action === 'approve' ? 'ok' : 'mute');
            } catch (e) {
              showToast(e.message, 'err');
            }
          }}
        />
      )}
    </div>
  );
}

function ClaimReviewModal({ claim, onClose, onAction }) {
  const [fullClaim, setFullClaim] = useState(null);
  const [pointsAwarded, setPointsAwarded] = useState(claim.points_requested || 0);
  const [adminNote, setAdminNote] = useState('');
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    fetch(`/api/claims/${claim.id}`)
      .then(r => r.json())
      .then(d => setFullClaim(d?.data || claim))
      .catch(() => setFullClaim(claim));
  }, [claim.id]);

  const handle = async (action) => {
    if (action === 'reject' && !adminNote.trim()) {
      alert('กรุณาใส่เหตุผลที่ไม่อนุมัติ');
      return;
    }
    if (action === 'delete') {
      const msg = claim.status === 'approved'
        ? `ลบใบเสร็จนี้ถาวร? ระบบจะคืน ${claim.points_awarded || 0} คะแนนออกจากลูกค้าด้วย`
        : 'ลบใบเสร็จนี้ถาวร?';
      if (!confirm(msg)) return;
    }
    setBusy(true);
    await onAction(action, {
      points_awarded: action === 'approve' ? Number(pointsAwarded) : null,
      admin_note: adminNote || null,
    });
    setBusy(false);
  };

  const isPending = claim.status === 'pending';

  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 200,
      background: 'rgba(20,20,15,0.55)', backdropFilter: 'blur(4px)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
    }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()} className="slide-up" style={{
        width: '100%', maxWidth: 680, maxHeight: '92%', overflowY: 'auto',
        background: '#fff', borderRadius: 16, padding: 0,
        boxShadow: '0 40px 100px rgba(0,0,0,0.5)',
      }}>
        <div style={{ padding: '18px 22px', borderBottom: '1px solid ' + A.line,
                      display:'flex', justifyContent:'space-between', alignItems:'center' }}>
          <div className="serif" style={{ fontSize: 22, color: A.clover, letterSpacing: -0.3 }}>Review claim</div>
          <Btn onClick={onClose}>Close</Btn>
        </div>

        <div style={{ padding: 22, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 22 }}>
          <div>
            {fullClaim?.receipt_image ? (
              <img src={fullClaim.receipt_image} alt="receipt" style={{
                width: '100%', borderRadius: 10, border: '1px solid ' + A.line, display: 'block',
              }} />
            ) : (
              <div style={{ padding: 40, textAlign:'center', color: A.mute, fontSize: 12,
                            background: A.bone, borderRadius: 10 }}>Loading image…</div>
            )}
          </div>

          <div>
            <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1, textTransform:'uppercase', color: A.mute }}>Customer</div>
            <div style={{ fontSize: 16, color: A.ink, fontWeight: 600, marginBottom: 12 }}>
              {claim.member_name || claim.member_id}
            </div>

            <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1, textTransform:'uppercase', color: A.mute }}>Bill amount</div>
            <div className="serif" style={{ fontSize: 26, color: A.clover, marginBottom: 12 }}>
              ฿{Number(claim.amount_thb).toLocaleString()}
            </div>

            <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1, textTransform:'uppercase', color: A.mute }}>Branch · Submitted</div>
            <div style={{ fontSize: 13, color: A.ink2, marginBottom: 12 }}>
              {claim.branch_name || '—'}<br/>
              <span style={{ color: A.mute, fontSize: 11 }}>{new Date(claim.created_at).toLocaleString()}</span>
            </div>

            {claim.customer_note && (
              <>
                <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1, textTransform:'uppercase', color: A.mute }}>Customer note</div>
                <div style={{ fontSize: 13, color: A.ink2, fontStyle: 'italic', marginBottom: 12 }}>"{claim.customer_note}"</div>
              </>
            )}

            {isPending && (
              <>
                <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1, textTransform:'uppercase', color: A.mute, marginTop: 8 }}>Points to grant</div>
                <input
                  type="number" min="0"
                  value={pointsAwarded} onChange={(e) => setPointsAwarded(e.target.value)}
                  style={{
                    width: '100%', padding: '10px 12px', borderRadius: 8,
                    border: '1.5px solid ' + A.line2, marginTop: 4, marginBottom: 4,
                    fontSize: 16, color: A.ink, fontFamily:'inherit', outline:'none',
                  }}
                />
                <div style={{ fontSize: 11, color: A.mute, marginBottom: 12 }}>
                  Customer asked for {claim.points_requested} pts (you can adjust)
                </div>

                <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 1, textTransform:'uppercase', color: A.mute }}>Admin note (required on reject)</div>
                <input
                  type="text"
                  value={adminNote} onChange={(e) => setAdminNote(e.target.value)}
                  placeholder="e.g. รูปไม่ชัด, ยอดไม่ตรง"
                  style={{
                    width: '100%', padding: '10px 12px', borderRadius: 8,
                    border: '1px solid ' + A.line2, marginTop: 4, marginBottom: 16,
                    fontSize: 13, color: A.ink, fontFamily:'inherit', outline:'none',
                  }}
                />

                <div style={{ display: 'flex', gap: 8 }}>
                  <Btn onClick={() => handle('reject')} style={{ flex: 1, color: '#B5453A', borderColor: '#F1C7C0' }}>
                    {busy ? '…' : '✕ Reject'}
                  </Btn>
                  <Btn primary onClick={() => handle('approve')} style={{ flex: 1 }}>
                    {busy ? '…' : '✓ Approve & add points'}
                  </Btn>
                </div>
              </>
            )}

            {!isPending && (
              <div style={{
                marginTop: 8, padding: '12px 14px', borderRadius: 10,
                background: claim.status === 'approved' ? A.cloverSoft : '#FEF0EF',
                color: claim.status === 'approved' ? A.clover : '#B5453A',
                fontSize: 13,
              }}>
                <b>{claim.status === 'approved' ? '✓ Approved' : '✕ Rejected'}</b>
                {claim.points_awarded != null && (<><br/>Points: +{claim.points_awarded}</>)}
                {claim.admin_note && (<><br/><span style={{ fontSize: 12 }}>{claim.admin_note}</span></>)}
              </div>
            )}

            {/* Hard delete (any status). For approved claims this also reverses the points. */}
            <div style={{ marginTop: 16, paddingTop: 14, borderTop: '1px solid ' + A.line }}>
              <Btn onClick={() => handle('delete')}
                style={{ width: '100%', color: '#B5453A', borderColor: '#F1C7C0' }}>
                {busy ? '…' : '🗑️ ลบใบเสร็จนี้ถาวร' + (claim.status === 'approved' ? ' (คืนคะแนน)' : '')}
              </Btn>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function AdminApp({ initialView = 'customers', initialRole = 'super_admin', onSignOut }) {
  // Map legacy / unknown roles → known role system.
  const normalizeRole = (r) => {
    if (['super_admin', 'admin', 'manager', 'staff'].includes(r)) return r;
    return 'admin';
  };

  const [view, setView] = useState(initialView);
  // Initial role comes from the user actually logged in (sessionStorage).
  // Falls back to the prop only if no session exists yet.
  const [role, setRole] = useState(() => {
    const u = getCurrentUser();
    return normalizeRole(u?.role || initialRole);
  });

  // Brand & logo: localStorage is the offline cache, D1 is the source of truth.
  // We hydrate from cache instantly, then refetch from D1 on mount.
  const [brand, setBrandState] = useState(() => {
    try { return JSON.parse(localStorage.getItem('clover-brand')) || DEFAULT_BRAND; } catch { return DEFAULT_BRAND; }
  });
  const [logoUrl, setLogoUrlState] = useState(() => localStorage.getItem('clover-logo') || null);

  // Wrap setters to persist to D1 (admin-only PUT) + local cache.
  const setBrand = React.useCallback((next) => {
    const value = typeof next === 'function' ? next(brand) : next;
    setBrandState(value);
    try { localStorage.setItem('clover-brand', JSON.stringify(value)); } catch {}
    applyBrand(value);
    const me = getCurrentUser();
    fetch('/api/settings/brand', {
      method: 'PUT',
      headers: {
        'Content-Type':'application/json',
        'x-user-id':   me.id   || '',
        'x-user-name': me.name || '',
        'x-user-role': me.role || '',
      },
      body: JSON.stringify({ ...value, logoUrl }),
    }).catch(() => {});
  }, [brand, logoUrl]);

  const setLogoUrl = React.useCallback((url) => {
    setLogoUrlState(url);
    if (url) { try { localStorage.setItem('clover-logo', url); } catch {} }
    else     { try { localStorage.removeItem('clover-logo'); } catch {} }
    const me = getCurrentUser();
    fetch('/api/settings/brand', {
      method: 'PUT',
      headers: {
        'Content-Type':'application/json',
        'x-user-id':   me.id   || '',
        'x-user-name': me.name || '',
        'x-user-role': me.role || '',
      },
      body: JSON.stringify({ ...brand, logoUrl: url }),
    }).catch(() => {});
  }, [brand]);

  // On mount: apply cached brand instantly, then refetch from D1.
  useEffect(() => {
    applyBrand(brand);
    fetch('/api/settings/brand')
      .then(r => r.json())
      .then(d => {
        if (d?.success && d.data) {
          const remote = d.data;
          // Only update if it differs — avoid render churn.
          setBrandState(prev => ({ ...prev, ...remote }));
          if (remote.logoUrl !== undefined) setLogoUrlState(remote.logoUrl || null);
          applyBrand({ ...DEFAULT_BRAND, ...remote });
          try { localStorage.setItem('clover-brand', JSON.stringify({ ...DEFAULT_BRAND, ...remote })); } catch {}
        }
      })
      .catch(() => {});
  }, []);

  // Persist the current (preview) role on the user blob so logActivity() and
  // RBAC view-access checks use it. We also stash the ORIGINAL login role in
  // a separate key (clover-login-role) so the sidebar switcher can decide
  // whether to render itself even after the preview role is changed.
  useEffect(() => {
    const u = getCurrentUser();
    if (!sessionStorage.getItem('clover-login-role')) {
      sessionStorage.setItem('clover-login-role', u.role || 'super_admin');
    }
    sessionStorage.setItem('clover-current-user', JSON.stringify({ ...u, role }));
  }, [role]);

  // Forced branch for non-super-admin roles when viewing accounting
  const forcedBranch = (role === 'manager' || role === 'staff')
    ? (getCurrentUser().assigned_branch_id || 'thonglor')
    : null;

  // Auto-redirect off forbidden views when role changes
  useEffect(() => {
    if (!canAccessView(role, view)) {
      // First fallback: customers (everyone can see customers)
      // If even that is blocked, stay on the Forbidden screen.
      if (canAccessView(role, 'customers')) setView('customers');
    }
  }, [role]);

  // SECURITY GATE: render Forbidden if user lacks view access
  const allowed = canAccessView(role, view);

  return (
    <DataProvider>
      <div style={{ display:'flex', height: '100%', background: '#FAFAF7', fontFamily: 'DM Sans, sans-serif' }}>
        <AdminSidebar
          view={view} setView={setView}
          role={role} setRole={setRole}
          brand={brand} logoUrl={logoUrl}
          onSignOut={onSignOut}
        />
        <div style={{ flex: 1, overflow:'auto' }} className="noscroll">
          {!allowed && <ForbiddenView role={role} requestedView={view} />}
          {allowed && view === 'branches'   && <BranchesView/>}
          {allowed && view === 'customers'  && <CustomersView/>}
          {allowed && view === 'line-users' && <LineUsersView/>}
          {allowed && view === 'staff'      && <StaffManagementView role={role}/>}
          {allowed && view === 'logs'       && <ActivityLogsView role={role}/>}
          {allowed && view === 'claims'     && <PointClaimsView role={role}/>}
          {allowed && view === 'import'     && <ImportView/>}
          {allowed && view === 'cms'        && <CMSView setView={setView}/>}
          {allowed && view === 'reward-new' && <AdminRewardCreator/>}
          {allowed && view === 'branding'   && <BrandingView brand={brand} setBrand={setBrand} logoUrl={logoUrl} setLogoUrl={setLogoUrl} />}
          {allowed && view === 'settings'   && <SettingsView/>}
        </div>
      </div>
    </DataProvider>
  );
}

Object.assign(window, { AdminApp });
