// AppShell — Sidebar, TopBar, shared UI primitives

const IC_PATHS = {
  dashboard:   'M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z M9 22V12h6v10',
  ticket:      'M15 5v2m0 4v2m0 4v2M5 5a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2 2 2 0 0 1 0 4v3a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3a2 2 0 0 1-2 0V9a2 2 0 0 0 2 0V5a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2',
  support:     'M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0z',
  megaphone:   'M11 5.882V19.24a1.76 1.76 0 0 1-3.417.592l-2.147-6.15M18 13a3 3 0 1 0 0-6M5.436 13.683A4.001 4.001 0 0 1 7 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 0 1-1.564-.317z',
  userplus:    'M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 1 1-8 0 4 4 0 0 1 8 0zM3 20a6 6 0 0 1 12 0v1H3v-1z',
  book:        'M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253',
  chart:       'M9 19v-6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2zm0 0V9a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v10m-6 0a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2m0 0V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2z',
  userminus:   'M13 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0zM9 14a6 6 0 0 0-6 6v1h12v-1a6 6 0 0 0-6-6zM21 12h-6',
  users:       'M5 7a4 4 0 1 0 8 0a4 4 0 1 0-8 0 M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2 M22 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75',
  plus:        'M12 5v14M5 12h14',
  search:      'M21 21l-6-6m2-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0',
  filter:      'M22 3H2l8 9.46V19l4 2v-8.54L22 3z',
  bell:        'M15 17h5l-1.405-1.405A2.032 2.032 0 0 1 18 14.158V11a6.002 6.002 0 0 0-4-5.659V5a2 2 0 1 0-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 1 1-6 0v-1m6 0H9',
  chevron_r:   'M9 18l6-6-6-6',
  chevron_d:   'M6 9l6 6 6-6',
  check:       'M20 6 9 17l-5-5',
  clock:       'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z M12 6v6l4 2',
  alert:       'M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z M12 9v4 M12 17h.01',
  settings:    'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z',
  logout:      'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4 M16 17l5-5-5-5 M21 12H9',
  external:    'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6 M15 3h6v6 M10 14L21 3',
  list:        'M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2 M9 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2 M9 5a2 2 0 0 0-2 0 M9 12h6 M9 16h6',
  x:           'M18 6 6 18M6 6l12 12',
  edit:        'M11 5H6a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-5m-1.414-9.414a2 2 0 1 1 2.828 2.828L11.828 15H9v-2.828l8.586-8.586z',
  attachment:  'M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48',
  inbox:       'M22 12h-6l-2 3h-4l-2-3H2 M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z',
  progress:    'M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z M12 6v6l4 2',
  send:        'M22 2L11 13 M22 2l-7 20-4-9-9-4 20-7z',
  trash:       'M3 6h18 M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2',
  download:    'M4 16v1a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4',
  dollar:      'M12 1v22 M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6',
  more:        'M5 12h.01 M12 12h.01 M19 12h.01',
  lock:        'M19 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2z M7 11V7a5 5 0 0 1 10 0v4',
  eye:         'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z M12 12m-3 0a3 3 0 1 0 6 0 3 3 0 0 0-6 0',
  eyeoff:      'M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24 M1 1l22 22',
  menu:        'M3 6h18 M3 12h18 M3 18h18',
  search:      'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z M21 21l-4.35-4.35',
  plus:        'M12 5v14 M5 12h14',
  camera:      'M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z M12 17a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
  copy:        'M20 9h-9a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2z M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1',
  arrow_left:  'M19 12H5 M12 19l-7-7 7-7',
  refresh:     'M23 4v6h-6 M1 20v-6h6 M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15',
};

const BRAND_ORANGE = '#D96228';
const BRAND_NAVY   = '#1A5090';

function useIsMobile() {
  const [mob, setMob] = React.useState(window.innerWidth < 768);
  React.useEffect(() => {
    const fn = () => setMob(window.innerWidth < 768);
    window.addEventListener('resize', fn);
    return () => window.removeEventListener('resize', fn);
  }, []);
  return mob;
}

function IC({ name, size = 18, color = 'currentColor', strokeWidth = 1.8, style }) {
  const d = IC_PATHS[name] || IC_PATHS.ticket;
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
      style={style}>
      {d.split(' M').map((seg, i) => (
        <path key={i} d={i === 0 ? seg : 'M' + seg} />
      ))}
    </svg>
  );
}

function BrandLogo({ height = 28 }) {
  return (
    <img src="/assets/logo.webp" alt="K-IT Services" style={{ height, maxWidth: '100%', display: 'block', objectFit: 'contain' }} />
  );
}

// ── Shared primitives ────────────────────────────────────────────────────────
// Everyone's avatar: coloured initials worked out from their name, so it is the same on every screen
// and follows a rename straight away. (initials and color props are accepted but no longer used.)
function Avatar({ name, size = 32 }) {
  const [bg, fg] = window.monogramFor(name);
  return (
    <div aria-hidden="true" style={{
      width: size, height: size, borderRadius: '50%', background: bg, color: fg,
      fontWeight: 800, fontSize: Math.round(size * 0.4),
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      flexShrink: 0, userSelect: 'none',
    }}>
      {window.monogramLetters(name)}
    </div>
  );
}

function StatusBadge({ status }) {
  const map = {
    'Open':        { bg: '#EFF6FF', color: '#2563EB', dot: '#3B82F6' },
    'In Progress': { bg: '#FFFBEB', color: '#B45309', dot: '#F59E0B' },
    'Resolved':    { bg: '#ECFDF5', color: '#065F46', dot: '#10B981' },
    'Closed':      { bg: '#F1F5F9', color: '#475569', dot: '#94A3B8' },
  };
  const c = map[status] || map['Open'];
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 5,
      background: c.bg, color: c.color,
      padding: '3px 10px', borderRadius: 20, fontSize: 12, fontWeight: 600,
      whiteSpace: 'nowrap',
    }}>
      <span style={{ width: 6, height: 6, borderRadius: '50%', background: c.dot, display: 'inline-block', flexShrink: 0 }} />
      {status}
    </span>
  );
}

function PriorityBadge({ priority }) {
  const map = {
    'Low':    { bg: '#F1F5F9', color: '#64748B' },
    'Medium': { bg: '#FFFBEB', color: '#92400E' },
    'High':   { bg: '#FEF2F2', color: '#B91C1C' },
    'Urgent': { bg: '#DC2626', color: '#ffffff' },
  };
  const c = map[priority] || map['Low'];
  return (
    <span style={{
      background: c.bg, color: c.color,
      padding: '2px 8px', borderRadius: 4, fontSize: 11, fontWeight: 700,
      textTransform: 'uppercase', letterSpacing: '0.5px', whiteSpace: 'nowrap',
    }}>
      {priority}
    </span>
  );
}

function Btn({ children, variant = 'primary', onClick, size = 'md', icon, type, style: extraStyle, disabled }) {
  const base = {
    display: 'inline-flex', alignItems: 'center', gap: 6,
    border: 'none', cursor: disabled ? 'not-allowed' : 'pointer', fontFamily: 'inherit',
    fontWeight: 600, borderRadius: 8, transition: 'opacity 0.15s',
    whiteSpace: 'nowrap', opacity: disabled ? 0.5 : 1,
  };
  const p = size === 'sm' ? '6px 14px' : '10px 20px';
  const fs = size === 'sm' ? 13 : 14;
  const variants = {
    primary: { background: BRAND_ORANGE, color: '#fff', padding: p, fontSize: fs },
    navy:    { background: BRAND_NAVY,   color: '#fff', padding: p, fontSize: fs },
    ghost:   { background: 'transparent', color: '#475569', padding: p, fontSize: fs, border: '1.5px solid #E2E8F0' },
    danger:  { background: '#FEF2F2', color: '#B91C1C', padding: p, fontSize: fs },
  };
  return (
    <button type={type || 'button'} style={{ ...base, ...variants[variant], ...extraStyle }} onClick={onClick} disabled={disabled}>
      {icon && <IC name={icon} size={14} color="currentColor" />}
      {children}
    </button>
  );
}

// ── Sidebar ──────────────────────────────────────────────────────────────────
const NAV_ITEMS = [
  { id: 'dashboard',          label: 'Dashboard',             icon: 'dashboard', section: null },
  { id: 'my_tickets',         label: 'Concerns',              icon: 'inbox',     section: null },
  { id: 'admin_clients',      label: 'Clients',               icon: 'users',     section: 'Admin', adminOnly: true },
  { id: 'admin_users',        label: 'Users',                 icon: 'settings',  section: 'Admin', adminOnly: true },
  { id: 'admin_types',        label: 'Ticket Types',          icon: 'lock',      section: 'Admin', adminOnly: true },
  { id: 'admin_audit',        label: 'Audit Log',             icon: 'list',      section: 'Admin', adminOnly: true },
  { id: 'admin_mail',         label: 'Email',                 icon: 'send',      section: 'Admin', adminOnly: true },
];

function Sidebar({ page, onNav, isAdmin, tickets, user, onChangePassword, isMobile, isOpen, onClose }) {
  const [hovered, setHovered] = React.useState(null);

  function handleLogout() {
    localStorage.removeItem('hub_token');
    localStorage.removeItem('hub_user');
    window.location.replace('/login.html');
  }

  function getBadge(id) {
    if (id === 'my_tickets')    return tickets.filter(t => t.status === 'Open').length;
    const count = tickets.filter(t => t.categoryId === id && (t.status === 'Open' || t.status === 'In Progress')).length;
    return count || null;
  }

  const visibleItems = NAV_ITEMS
    .filter(i => !i.adminOnly || isAdmin)
    ;
  const sectionLabel = 'Admin';
  const sections = isAdmin ? [{ key: 'Admin', label: sectionLabel }] : [];

  return (
    <div style={{
      width: 244, flexShrink: 0, height: '100vh',
      background: '#0F2540', display: 'flex', flexDirection: 'column',
      overflow: 'hidden',
      ...(isMobile ? {
        position: 'fixed', top: 0, left: 0, zIndex: 200,
        transform: isOpen ? 'translateX(0)' : 'translateX(-244px)',
        transition: 'transform 0.25s ease',
        boxShadow: isOpen ? '4px 0 32px rgba(0,0,0,0.35)' : 'none',
      } : { zIndex: 10 }),
    }}>
      {/* Logo area */}
      <div style={{
        padding: '14px 18px', minHeight: 66,
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        borderBottom: '1px solid rgba(255,255,255,0.07)',
      }}>
        <div style={{
          background: '#fff', borderRadius: 9, padding: '7px 14px',
          display: 'inline-flex', alignItems: 'center', minWidth: 0, maxWidth: isMobile ? 'calc(100% - 52px)' : '100%',
          boxShadow: '0 1px 3px rgba(0,0,0,0.15)',
        }}>
          <BrandLogo height={34} />
        </div>
        {isMobile && (
          <button onClick={onClose} aria-label="Close menu" style={{
            background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: 8,
            padding: 8, cursor: 'pointer', display: 'flex',
          }}>
            <IC name="x" size={18} color="#fff" />
          </button>
        )}
      </div>

      {/* Navigation */}
      <nav aria-label="Main" style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '10px 0' }}>
        {/* Top-level (no section) */}
        {visibleItems.filter(i => !i.section).map(item => (
          <NavItem key={item.id} item={item} active={page === item.id}
            badge={getBadge(item.id)} onClick={() => onNav(item.id)}
            hovered={hovered === item.id} onHover={setHovered} />
        ))}

        {/* New Ticket CTA */}
        {<div style={{ margin: '2px 8px' }}>
          <button type="button" onClick={() => onNav('submit')} style={{
            background: BRAND_ORANGE, color: '#fff', borderRadius: 8, border: 'none', width: '100%', textAlign: 'left',
            padding: '9px 14px', cursor: 'pointer', fontSize: 13.5,
            fontWeight: 600, fontFamily: 'inherit',
            display: 'flex', alignItems: 'center', gap: 10,
          }}>
            <IC name="plus" size={17} color="#fff" strokeWidth={2.2} />
            New Concern
          </button>
        </div>}

        {/* Sectioned nav */}
        {sections.map(sec => {
          const items = visibleItems.filter(i => i.section === sec.key);
          if (!items.length) return null;
          return (
            <div key={sec.key}>
              <div style={{
                padding: '16px 18px 4px', fontSize: 10, fontWeight: 700,
                color: 'rgba(255,255,255,0.28)', textTransform: 'uppercase', letterSpacing: '0.1em',
              }}>
                {sec.label}
              </div>
              {items.map(item => (
                <NavItem key={item.id} item={item} active={page === item.id}
                  badge={getBadge(item.id)} onClick={() => onNav(item.id)}
                  hovered={hovered === item.id} onHover={setHovered} />
              ))}
            </div>
          );
        })}
      </nav>

      {/* User footer */}
      <div style={{
        borderTop: '1px solid rgba(255,255,255,0.08)',
        padding: '13px 16px',
        display: 'flex', alignItems: 'center', gap: 10,
      }}>
        {user?.has_photo
          ? <UserAvatar userId={user.id} name={user.name} hasPhoto version={user.photo_version || 0} size={34} />
          : user?.role === 'client' && user?.client_name
          ? <ClientLogo clientId={user.client_id} name={user.client_name} hasLogo={!!user.client_has_logo} version={user.client_logo_version || 0} size={34} radius={9} />
          : <Avatar name={user?.name || 'User'} size={32} color={BRAND_ORANGE} />}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ color: '#fff', fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
            {user?.name || 'User'}
          </div>
          <div style={{ color: 'rgba(255,255,255,0.38)', fontSize: 11, marginTop: 1 }}>
            {{ admin: 'Admin', agent: 'Developer', client: 'Client' }[user?.role] || 'User'}{user?.client_name ? ' · ' + user.client_name : ''}
          </div>
        </div>
        <button onClick={onChangePassword} title="Change password" style={{
          background: 'rgba(255,255,255,0.08)', border: 'none', borderRadius: 8,
          padding: 7, cursor: 'pointer', display: 'flex', flexShrink: 0,
        }}>
          <IC name="lock" size={15} color="rgba(255,255,255,0.5)" />
        </button>
        <button onClick={handleLogout} title="Sign out" aria-label="Sign out" style={{
          background: 'rgba(255,255,255,0.08)', border: 'none', borderRadius: 8,
          padding: 7, cursor: 'pointer', display: 'flex', flexShrink: 0,
        }}>
          <IC name="logout" size={15} color="rgba(255,255,255,0.5)" />
        </button>
      </div>
    </div>
  );
}

function NavItem({ item, active, badge, onClick, hovered, onHover }) {
  return (
    <button type="button" aria-current={active ? 'page' : undefined}
      onMouseEnter={() => onHover(item.id)}
      onMouseLeave={() => onHover(null)}
      onClick={onClick}
      style={{
        display: 'flex', alignItems: 'center', gap: 10, width: 'calc(100% - 16px)', textAlign: 'left',
        padding: '9px 14px', margin: '1px 8px', borderRadius: 8, border: 'none', font: 'inherit', color: 'inherit',
        cursor: 'pointer', position: 'relative',
        background: active ? 'rgba(255,255,255,0.1)' : hovered ? 'rgba(255,255,255,0.05)' : 'transparent',
        transition: 'background 0.12s',
      }}>
      {active && (
        <div style={{
          position: 'absolute', left: 0, top: '18%', bottom: '18%',
          width: 3, background: BRAND_ORANGE, borderRadius: '0 3px 3px 0',
        }} />
      )}
      <IC name={item.icon} size={17}
        color={active ? '#fff' : 'rgba(255,255,255,0.5)'}
        strokeWidth={active ? 2.2 : 1.8} />
      <span style={{
        flex: 1, fontSize: 13.5, fontWeight: active ? 600 : 400,
        color: active ? '#fff' : 'rgba(255,255,255,0.6)',
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
      }}>
        {item.label}
      </span>
      {badge > 0 && (
        <span style={{
          background: active ? BRAND_ORANGE : 'rgba(255,255,255,0.12)',
          color: active ? '#fff' : 'rgba(255,255,255,0.7)',
          fontSize: 11, fontWeight: 700, padding: '1px 7px',
          borderRadius: 10, minWidth: 20, textAlign: 'center',
        }}>
          {badge}
        </span>
      )}
    </button>
  );
}

// ── TopBar ───────────────────────────────────────────────────────────────────
function TopBar({ title, subtitle, actions, user, isMobile, onToggleSidebar }) {
  return (
    <div style={{
      height: 60, background: '#fff', borderBottom: '1px solid #E2E8F0',
      display: 'flex', alignItems: 'center', padding: isMobile ? '0 16px' : '0 28px',
      flexShrink: 0, gap: 12, justifyContent: 'space-between',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, minWidth: 0 }}>
        {isMobile && (
          <button onClick={onToggleSidebar} aria-label="Open menu" style={{
            background: 'none', border: 'none', cursor: 'pointer', padding: 4,
            display: 'flex', alignItems: 'center', flexShrink: 0,
          }}>
            <IC name="menu" size={22} color="#0F172A" />
          </button>
        )}
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: isMobile ? 15 : 17, fontWeight: 700, color: '#0F172A', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{title}</div>
          {subtitle && !isMobile && <div style={{ fontSize: 12, color: '#94A3B8', marginTop: 1 }}>{subtitle}</div>}
        </div>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
        {!isMobile && actions}
        <div style={{ width: 1, height: 24, background: '#E2E8F0' }} />
        <Avatar name={user?.name || 'User'} size={34} color={BRAND_ORANGE} />
      </div>
    </div>
  );
}

// ── AppShell ─────────────────────────────────────────────────────────────────
function AppShell({ page, onNav, isAdmin, tickets, topBar, children, user, onChangePassword }) {
  const isMobile = useIsMobile();
  const [sidebarOpen, setSidebarOpen] = React.useState(false);

  function handleNav(id, extra) {
    if (isMobile) setSidebarOpen(false);
    onNav(id, extra);
  }

  return (
    <div style={{ display: 'flex', height: '100vh', fontFamily: "'Plus Jakarta Sans', sans-serif", background: '#F5F7FA' }}>
      {isMobile && sidebarOpen && (
        <div onClick={() => setSidebarOpen(false)} style={{
          position: 'fixed', inset: 0, background: 'rgba(15,37,64,0.5)', zIndex: 150,
        }} />
      )}
      <Sidebar
        page={page} onNav={handleNav} isAdmin={isAdmin} tickets={tickets}
        user={user} onChangePassword={onChangePassword}
        isMobile={isMobile} isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)}
      />
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minWidth: 0 }}>
        {React.cloneElement(topBar, { isMobile, onToggleSidebar: () => setSidebarOpen(o => !o) })}
        <div style={{ flex: 1, overflowY: 'auto', padding: isMobile ? 16 : 28 }}>
          {children}
        </div>
      </div>
    </div>
  );
}

// ── Change Password Modal ─────────────────────────────────────────────────────
const CPW_FIELD = {
  width: '100%', padding: '10px 40px 10px 14px', borderRadius: 8,
  border: '1.5px solid #E2E8F0', fontSize: 14, fontFamily: 'inherit',
  color: '#1E293B', background: '#FAFBFF', outline: 'none', boxSizing: 'border-box',
};
const CPW_LBL = {
  display: 'block', fontSize: 11, fontWeight: 700, color: '#374151',
  textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 6,
};

function ChangePasswordModal({ forced = false, onClose }) {
  const [form, setForm]     = React.useState({ current: '', next: '', confirm: '' });
  const [show, setShow]     = React.useState({ current: false, next: false, confirm: false });
  const [error, setError]   = React.useState(null);
  const [saving, setSaving] = React.useState(false);
  const [done, setDone]     = React.useState(false);

  function toggleShow(field) { setShow(s => ({ ...s, [field]: !s[field] })); }

  async function handleSubmit(e) {
    e.preventDefault();
    setError(null);
    if (form.next.length < 8)       { setError('New password must be at least 8 characters.'); return; }
    if (form.next !== form.confirm) { setError('Passwords do not match.'); return; }
    if (form.next === form.current) { setError('New password must be different from the current one.'); return; }
    setSaving(true);
    try {
      const res  = await fetch('/api/auth/change-password', {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + localStorage.getItem('hub_token') },
        body: JSON.stringify({ currentPassword: form.current, newPassword: form.next }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Failed');
      const stored = JSON.parse(localStorage.getItem('hub_user') || '{}');
      stored.force_password_change = false;
      localStorage.setItem('hub_user', JSON.stringify(stored));
      window._synUser = stored;
      setDone(true);
    } catch (e) { setError(e.message); }
    finally { setSaving(false); }
  }

  const eyeBtn = (field) => (
    <button type="button" onClick={() => toggleShow(field)} aria-label={show[field] ? 'Hide password' : 'Show password'} style={{
      position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)',
      background: 'none', border: 'none', cursor: 'pointer', padding: 2, display: 'flex',
    }}>
      <IC name={show[field] ? 'eyeoff' : 'eye'} size={16} color="#94A3B8" />
    </button>
  );

  return (
    <div role="dialog" aria-modal="true" aria-label={forced ? 'Set your password' : 'Change password'} style={{
      position: 'fixed', inset: 0, background: 'rgba(15,37,64,0.6)', zIndex: 3000,
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
    }}>
      <div style={{ background: '#fff', borderRadius: 16, width: '100%', maxWidth: 440, boxShadow: '0 32px 80px rgba(0,0,0,0.2)', overflow: 'hidden' }}>
        {/* Header */}
        <div style={{ background: 'linear-gradient(135deg, #0F2540 0%, #1A5090 100%)', padding: '22px 28px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <div>
            <div style={{ color: '#fff', fontWeight: 800, fontSize: 16 }}>
              {forced ? 'Set Your Password' : 'Change Password'}
            </div>
            <div style={{ color: 'rgba(255,255,255,0.5)', fontSize: 12, marginTop: 3 }}>
              {forced ? 'You must set a new password before continuing' : 'Update your account password'}
            </div>
          </div>
          {!forced && (
            <button onClick={onClose} style={{ background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: 8, padding: 8, cursor: 'pointer', display: 'flex' }}>
              <IC name="x" size={16} color="#fff" />
            </button>
          )}
        </div>

        {done ? (
          <div style={{ padding: '40px 28px', textAlign: 'center' }}>
            <div style={{ width: 56, height: 56, borderRadius: '50%', background: '#ECFDF5', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px' }}>
              <IC name="check" size={28} color="#10B981" strokeWidth={2.5} />
            </div>
            <div style={{ fontSize: 18, fontWeight: 800, color: '#0F172A', marginBottom: 8 }}>Password Updated!</div>
            <div style={{ color: '#64748B', fontSize: 14, marginBottom: 24 }}>Your new password is active. Use it next time you log in.</div>
            <Btn variant="navy" onClick={onClose}>Continue</Btn>
          </div>
        ) : (
          <form onSubmit={handleSubmit} style={{ padding: '28px' }}>
            {forced && (
              <div style={{ background: '#FFF7ED', border: '1.5px solid #FED7AA', borderRadius: 8, padding: '10px 14px', marginBottom: 20, fontSize: 13, color: '#92400E', fontWeight: 600 }}>
                Your account was set up with a temporary password. Please choose a new one to continue.
              </div>
            )}
            {error && (
              <div style={{ background: '#FEF2F2', border: '1.5px solid #FECACA', borderRadius: 8, padding: '10px 14px', marginBottom: 16, fontSize: 13, color: '#B91C1C', fontWeight: 600, display: 'flex', gap: 8 }}>
                <IC name="x" size={14} color="#EF4444" strokeWidth={2.5} style={{ flexShrink: 0, marginTop: 1 }} />
                {error}
              </div>
            )}
            {/* Current Password */}
            <div style={{ marginBottom: 16 }}>
              <label htmlFor="cpw-current" style={CPW_LBL}>Current Password</label>
              <div style={{ position: 'relative' }}>
                <input id="cpw-current" type={show.current ? 'text' : 'password'} value={form.current} required
                  onChange={e => { setForm(f => ({ ...f, current: e.target.value })); setError(null); }}
                  style={CPW_FIELD} />
                {eyeBtn('current')}
              </div>
            </div>
            {/* New Password */}
            <div style={{ marginBottom: 16 }}>
              <label htmlFor="cpw-next" style={CPW_LBL}>New Password</label>
              <div style={{ position: 'relative' }}>
                <input id="cpw-next" type={show.next ? 'text' : 'password'} value={form.next} required
                  onChange={e => { setForm(f => ({ ...f, next: e.target.value })); setError(null); }}
                  style={CPW_FIELD} />
                {eyeBtn('next')}
              </div>
            </div>
            {/* Confirm Password */}
            <div style={{ marginBottom: 6 }}>
              <label htmlFor="cpw-confirm" style={CPW_LBL}>Confirm New Password</label>
              <div style={{ position: 'relative' }}>
                <input id="cpw-confirm" type={show.confirm ? 'text' : 'password'} value={form.confirm} required
                  onChange={e => { setForm(f => ({ ...f, confirm: e.target.value })); setError(null); }}
                  style={CPW_FIELD} />
                {eyeBtn('confirm')}
              </div>
            </div>
            <div style={{ fontSize: 11, color: '#94A3B8', marginBottom: 20, marginTop: 6 }}>Minimum 8 characters</div>
            <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
              {!forced && <Btn variant="ghost" type="button" onClick={onClose}>Cancel</Btn>}
              <Btn variant="navy" type="submit" style={{ opacity: saving ? 0.6 : 1 }}>
                {saving ? 'Saving…' : 'Update Password'}
              </Btn>
            </div>
          </form>
        )}
      </div>
    </div>
  );
}

// ── Excel Export Utility ─────────────────────────────────────────────────────
async function exportTicketsToExcel(tickets, filenameBase) {
  if (!window.ExcelJS) {
    alert('Export library is still loading — please try again in a moment.');
    return;
  }
  if (!tickets || tickets.length === 0) {
    alert('No concerns to export.');
    return;
  }

  const wb = new window.ExcelJS.Workbook();
  wb.creator = 'K-IT Services Support Hub';
  wb.created = new Date();

  // ── Pre-sort: most actionable tickets first ─────────────────────────────────
  const _SR = { 'Open': 0, 'In Progress': 1, 'Resolved': 2, 'Closed': 3 };
  const _PR = { 'Urgent': 0, 'High': 1, 'Medium': 2, 'Low': 3 };
  const sortedTickets = [...tickets].sort((a, b) => {
    const s = (_SR[a.status]   ?? 99) - (_SR[b.status]   ?? 99);  if (s) return s;
    const p = (_PR[a.priority] ?? 99) - (_PR[b.priority] ?? 99);  if (p) return p;
    return String(b.createdAt || '').localeCompare(String(a.createdAt || ''));
  });

  const ws = wb.addWorksheet('Concerns', {
    // Freeze header row (ySplit:1) AND Ticket ID column (xSplit:1)
    // so both stay visible while scrolling in any direction
    views: [{ state: 'frozen', xSplit: 1, ySplit: 1 }],
    pageSetup: { orientation: 'landscape', fitToPage: true, fitToWidth: 1 },
  });

  // Column order: ID & Title first (no filter), then the filterable group (C–G),
  // then people/description at the end (no filter needed)
  ws.columns = [
    { header: 'Ticket ID',    key: 'ticketId',     width: 14 },  // col 1
    { header: 'Title',        key: 'title',        width: 42 },  // col 2
    { header: 'Type',         key: 'category',     width: 22 },  // col 3 ← filter start
    { header: 'Client',       key: 'team',         width: 18 },  // col 4
    { header: 'Status',       key: 'status',       width: 15 },  // col 5
    { header: 'Priority',     key: 'priority',     width: 12 },  // col 6
    { header: 'Date',         key: 'date',         width: 18 },  // col 7 ← filter end
    { header: 'Submitted By', key: 'submittedBy',  width: 24 },  // col 8
    { header: 'Assigned To',  key: 'assignedTo',   width: 24 },  // col 9
    { header: 'Description',  key: 'description',  width: 70 },  // col 10
  ];

  // AutoFilter only on the meaningful columns: Type, Client, Status, Priority, Date
  ws.autoFilter = 'C1:F1';

  // Style header row
  const headerRow = ws.getRow(1);
  headerRow.height = 30;
  headerRow.eachCell(cell => {
    cell.font      = { bold: true, color: { argb: 'FFFFFFFF' }, size: 11, name: 'Calibri' };
    cell.fill      = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF1A5090' } };
    cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: false };
    cell.border    = {
      top:    { style: 'thin',   color: { argb: 'FF0F3460' } },
      bottom: { style: 'medium', color: { argb: 'FF0F3460' } },
      left:   { style: 'thin',   color: { argb: 'FF0F3460' } },
      right:  { style: 'thin',   color: { argb: 'FF0F3460' } },
    };
  });

  const STATUS_FILL = {
    'Open':        'FFDBEAFE',
    'In Progress': 'FFFEF3C7',
    'Resolved':    'FFD1FAE5',
    'Closed':      'FFF1F5F9',
  };
  const STATUS_FONT_COLOR = {
    'Open':        'FF1D4ED8',
    'In Progress': 'FFB45309',
    'Resolved':    'FF065F46',
    'Closed':      'FF475569',
  };
  const PRIORITY_FILL = {
    'Urgent': 'FFFEE2E2',
    'High':   'FFFFEDD5',
    'Medium': 'FFFEF9C3',
    'Low':    'FFF0FDF4',
  };
  const PRIORITY_FONT_COLOR = {
    'Urgent': 'FFB91C1C',
    'High':   'FFEA580C',
    'Medium': 'FFCA8A04',
    'Low':    'FF15803D',
  };

  const CELL_BORDER = {
    top:    { style: 'thin', color: { argb: 'FFE2E8F0' } },
    bottom: { style: 'thin', color: { argb: 'FFE2E8F0' } },
    left:   { style: 'thin', color: { argb: 'FFE2E8F0' } },
    right:  { style: 'thin', color: { argb: 'FFE2E8F0' } },
  };

  sortedTickets.forEach((t, i) => {
    const cat   = (window.MOCK?.categories || []).find(c => c.id === t.categoryId);
    const rowBg = i % 2 === 0 ? 'FFFFFFFF' : 'FFF8FAFC';

    const desc = t.description || '';
    const isLongDesc = desc.length > 80;

    const row = ws.addRow({
      ticketId:    t.id || '',
      title:       t.title || '',
      category:    cat?.label || t.categoryId || '',
      team:        t.clientName || '',
      status:      t.status || '',
      priority:    t.priority || '',
      submittedBy: t.submittedBy?.name || '',
      assignedTo:  t.assignedTo?.name || '—',
      description: desc,
      date:        t.createdAt || '',
    });
    // Only set a fixed height for short-description rows; long ones auto-expand via wrapText
    if (!isLongDesc) row.height = 22;

    row.eachCell((cell, col) => {
      cell.border    = CELL_BORDER;
      cell.alignment = { vertical: 'middle', wrapText: false };

      if (col === 1) {
        // Ticket ID — monospace, branded blue
        cell.font  = { bold: true, color: { argb: 'FF1A5090' }, size: 10, name: 'Courier New' };
        cell.fill  = { type: 'pattern', pattern: 'solid', fgColor: { argb: rowBg } };
        cell.alignment = { vertical: 'middle', horizontal: 'center' };
      } else if (col === 5 && STATUS_FILL[t.status]) {
        // Status — colored badge
        cell.fill  = { type: 'pattern', pattern: 'solid', fgColor: { argb: STATUS_FILL[t.status] } };
        cell.font  = { bold: true, color: { argb: STATUS_FONT_COLOR[t.status] }, size: 10, name: 'Calibri' };
        cell.alignment = { vertical: 'middle', horizontal: 'center' };
      } else if (col === 6 && PRIORITY_FILL[t.priority]) {
        // Priority — colored badge
        cell.fill  = { type: 'pattern', pattern: 'solid', fgColor: { argb: PRIORITY_FILL[t.priority] } };
        cell.font  = { bold: true, color: { argb: PRIORITY_FONT_COLOR[t.priority] }, size: 10, name: 'Calibri' };
        cell.alignment = { vertical: 'middle', horizontal: 'center' };
      } else if (col === 10) {
        // Description — wrap long text, top-align so it reads naturally
        cell.fill  = { type: 'pattern', pattern: 'solid', fgColor: { argb: rowBg } };
        cell.font  = { color: { argb: 'FF64748B' }, size: 10, name: 'Calibri' };
        cell.alignment = { vertical: 'top', wrapText: true };
      } else {
        cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: rowBg } };
        cell.font = { size: 10, name: 'Calibri' };
      }
    });
  });

  // Summary footer row
  ws.addRow([]);
  const footerRowNum = tickets.length + 3;
  const summaryRow = ws.addRow([
    `K-IT Services Support Hub  ·  ${tickets.length} concern${tickets.length !== 1 ? 's' : ''} exported  ·  ${new Date().toLocaleDateString('en-US', { weekday: 'short', month: 'long', day: 'numeric', year: 'numeric' })}`,
  ]);
  summaryRow.height = 18;
  const summaryCell = summaryRow.getCell(1);
  summaryCell.font      = { italic: true, color: { argb: 'FF94A3B8' }, size: 9, name: 'Calibri' };
  summaryCell.alignment = { vertical: 'middle' };
  ws.mergeCells(`A${summaryRow.number}:J${summaryRow.number}`);

  // ── "How to Use" tips sheet ─────────────────────────────────────────────────
  const tips = wb.addWorksheet('How to Use', { tabColor: { argb: 'FF1A5090' } });
  tips.columns = [{ width: 6 }, { width: 62 }];

  function addTipRow(label, text, isSectionHeader) {
    const row = tips.addRow(['', isSectionHeader ? label : `${label}   ${text}`]);
    row.height = isSectionHeader ? 22 : 18;
    const cell = row.getCell(2);
    if (isSectionHeader) {
      cell.font      = { bold: true, color: { argb: 'FFFFFFFF' }, size: 11, name: 'Calibri' };
      cell.fill      = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF1A5090' } };
      cell.alignment = { vertical: 'middle', indent: 1 };
    } else {
      cell.font      = { size: 10, name: 'Calibri', color: { argb: 'FF1E293B' } };
      cell.alignment = { vertical: 'middle', indent: 2 };
    }
  }

  tips.addRow([]);
  addTipRow('K-IT Services Support Hub — Concerns Export Guide', '', true);
  tips.addRow([]);
  addTipRow('SORTING & FILTERING', '', true);
  addTipRow('▼ Filter arrow', 'Click the ▼ arrow in Type, Client, Status, or Priority headers to filter or sort.');
  addTipRow('Sort A→Z / Z→A', 'Click the ▼ arrow → "Sort A to Z" or "Sort Z to A" to reorder any column.');
  addTipRow('Filter by value', 'Click the ▼ arrow → uncheck "Select All" → tick only the values you want to see.');
  addTipRow('Clear a filter', 'Click the ▼ arrow → "Clear Filter From…" to show all rows again.');
  addTipRow('Multi-column sort', 'Data → Sort → Add Level to sort by Status first, then Priority, then Date.');
  tips.addRow([]);
  addTipRow('COLOUR CODES', '', true);
  addTipRow('Status — Blue',   'Open (needs action)');
  addTipRow('Status — Amber',  'In Progress (being worked on)');
  addTipRow('Status — Green',  'Resolved (completed)');
  addTipRow('Status — Grey',   'Closed (archived)');
  addTipRow('Priority — Red',  'Urgent');
  addTipRow('Priority — Orange', 'High');
  addTipRow('Priority — Yellow', 'Medium');
  addTipRow('Priority — Green',  'Low');
  tips.addRow([]);
  addTipRow('NAVIGATION', '', true);
  addTipRow('Frozen panes', 'The header row and Ticket ID column stay visible while scrolling.');
  addTipRow('Switch sheet', 'Click the "Concerns" tab at the bottom to go back to the data.');

  // Generate & download
  const buf  = await wb.xlsx.writeBuffer();
  const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
  const url  = URL.createObjectURL(blob);
  const a    = document.createElement('a');
  a.href     = url;
  a.download = `${filenameBase || 'concerns'}-${new Date().toISOString().slice(0, 10)}.xlsx`;
  document.body.appendChild(a);
  a.click();
  setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 300);
}

Object.assign(window, { IC, Avatar, StatusBadge, PriorityBadge, Btn, AppShell, TopBar, ChangePasswordModal, BRAND_ORANGE, BRAND_NAVY, exportTicketsToExcel, useIsMobile });
