/* global React, window */
// FACE/OS alternate shells: PASS (mobile employee card) + TERMINAL (kiosk).
const { useState: useModeState, useEffect: useModeEffect } = React;

function initialsOf(emp) {
  if (!emp) return '—';
  const a = (emp.first_name || '').trim();
  const b = (emp.last_name || '').trim();
  if (a || b) return ((a[0] || '') + (b[0] || '')).toUpperCase() || a.slice(0, 2).toUpperCase();
  return (emp.employee_name || emp.name || '—').slice(0, 2).toUpperCase();
}
function nameOf(emp) {
  if (!emp) return '—';
  return `${emp.first_name || ''} ${emp.last_name || ''}`.trim() || emp.employee_name || emp.name || '—';
}
function fmtClock(d) {
  const p = (n) => String(n).padStart(2, '0');
  return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}

// ── แผ่นเปลี่ยนรหัสผ่านของตัวเอง (พนักงาน + admin บนมือถือ) → PUT /api/auth/password ──
const PwField = ({ label, val, set, auto }) => (
  <label style={{ display: 'block', marginBottom: 10 }}>
    <span style={{ display: 'block', fontSize: 12, color: 'var(--ink-4)', marginBottom: 4 }}>{label}</span>
    <input className="gv-input" type="password" value={val} onChange={(e) => set(e.target.value)} autoComplete={auto} required style={{ fontSize: 16, width: '100%', boxSizing: 'border-box' }}/>
  </label>
);
function PasswordSheet({ onClose }) {
  const [cur, setCur] = useModeState('');
  const [nw, setNw] = useModeState('');
  const [cf, setCf] = useModeState('');
  const [busy, setBusy] = useModeState(false);
  const [err, setErr] = useModeState('');
  const submit = async (e) => {
    e.preventDefault(); setErr('');
    if (nw.length < 6) return setErr('รหัสผ่านใหม่ต้องมีอย่างน้อย 6 ตัวอักษร');
    if (nw !== cf) return setErr('รหัสผ่านใหม่ทั้งสองช่องไม่ตรงกัน');
    if (nw === cur) return setErr('รหัสผ่านใหม่ต้องต่างจากรหัสเดิม');
    setBusy(true);
    try {
      const r = await fetch('/api/auth/password', { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_password: cur, new_password: nw }) });
      const d = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(d.error || 'เปลี่ยนรหัสผ่านไม่สำเร็จ');
      window.appToast && window.appToast('เปลี่ยนรหัสผ่านแล้ว', { tone: 'success' });
      onClose();
    } catch (ex) { setErr(ex.message); }
    finally { setBusy(false); }
  };
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(15,23,42,.45)', zIndex: 70, display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }}>
      <form onSubmit={submit} onClick={(e) => e.stopPropagation()} style={{ width: '100%', maxWidth: 460, background: 'var(--surface)', borderRadius: '22px 22px 0 0', padding: '14px 18px', paddingBottom: 'max(18px, env(safe-area-inset-bottom))', boxSizing: 'border-box' }}>
        <div style={{ width: 40, height: 4, borderRadius: 2, background: 'var(--line)', margin: '0 auto 12px' }}/>
        <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 17, marginBottom: 12 }}>🔑 เปลี่ยนรหัสผ่าน</div>
        <PwField label="รหัสผ่านเดิม" val={cur} set={setCur} auto="current-password"/>
        <PwField label="รหัสผ่านใหม่ (อย่างน้อย 6 ตัว)" val={nw} set={setNw} auto="new-password"/>
        <PwField label="ยืนยันรหัสผ่านใหม่" val={cf} set={setCf} auto="new-password"/>
        {err && <div style={{ fontSize: 12.5, color: 'var(--coral-ink)', marginBottom: 8 }}>{err}</div>}
        <div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
          <button type="button" className="gv-btn no" style={{ flex: 1, justifyContent: 'center' }} onClick={onClose} disabled={busy}>ยกเลิก</button>
          <button type="submit" className="gv-btn ok" style={{ flex: 1.6, justifyContent: 'center' }} disabled={busy}>{busy ? 'กำลังบันทึก…' : 'บันทึกรหัสใหม่'}</button>
        </div>
      </form>
    </div>
  );
}

// ── PASS ────────────────────────────────────────────────────────────────────
function ymd(d) { const p = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; }
function passStatus(s) {
  return ({
    present: { l: 'มาทำงาน', cls: 'c-green' },
    in_only: { l: 'ยังไม่ออก', cls: 'c-amber' },
    late:    { l: 'มาสาย', cls: 'c-coral' },
    absent:  { l: 'ขาดงาน', cls: 'c-gray' },
    leave:   { l: 'ลา', cls: 'c-blue' },
    holiday: { l: 'วันหยุด', cls: 'c-gray' },
    off:     { l: 'วันหยุด', cls: 'c-gray' },
  }[s] || { l: s || '—', cls: 'c-gray' });
}

// Circular person avatar → /faces/<id>.jpg (or explicit photo), falls back to
// the round person-icon placeholder; click opens the profile modal.
function PassAvatar({ id, photo, size = 40 }) {
  const [err, setErr] = useModeState(false);
  const src = err ? '/img/avatar-person.svg' : (photo || (id ? `/faces/${id}.jpg` : '/img/avatar-person.svg'));
  return <img src={src} alt="" onError={() => setErr(true)}
    onClick={() => id && window.openProfile && window.openProfile(id)} title={id ? 'ดูโปรไฟล์' : ''}
    style={{ width: size, height: size, borderRadius: '50%', objectFit: 'cover', flex: 'none',
      cursor: id ? 'pointer' : 'default', border: '2px solid var(--surface)', boxShadow: '0 0 0 1px var(--line)' }} />;
}

function PassView({ clock, onLogout, desktop }) {
  const me = window.CURRENT_USER || {};
  const emps = window.EMPLOYEES || [];
  const emp = emps.find((e) => String(e.id) === String(me.employee_id)) || {};   // ไม่ผูกพนักงาน = ไม่โชว์บัตรของคนอื่น
  const [view, setView] = useModeState('home');   // home | history | leave
  const [pwOpen, setPwOpen] = useModeState(false);
  const [att, setAtt] = useModeState([]);          // this week's attendance rows
  const [recent, setRecent] = useModeState(
    (window.RECENT_SCANS || []).filter((s) => String(s.employee_id || (s.employee && s.employee.id)) === String(emp.id)).slice(0, 6)
  );

  const now = new Date();
  const monday = new Date(now); monday.setDate(now.getDate() - ((now.getDay() + 6) % 7));
  const weekFrom = ymd(monday), weekTo = ymd(now), todayStr = ymd(now);

  const loadAtt = React.useCallback(() => {
    if (!emp.id) return;
    fetch(`/api/attendance/employee/${encodeURIComponent(emp.id)}?from=${weekFrom}&to=${weekTo}`, { credentials: 'include' })
      .then((r) => (r.ok ? r.json() : null)).then((d) => { if (d) setAtt(d.rows || []); }).catch(() => {});
  }, [emp.id, weekFrom, weekTo]);
  useModeEffect(() => { loadAtt(); }, [loadAtt]);
  useModeEffect(() => {
    if (!window.scanStream) return undefined;
    return window.scanStream.subscribe((scan) => {
      if (String(scan.employeeId) !== String(emp.id)) return;
      setRecent((prev) => [scan, ...prev].slice(0, 6));
      loadAtt();
    });
  }, [emp.id, loadAtt]);

  const todayRow = att.find((r) => r.date === todayStr) || {};
  const ci = (todayRow.first_scan || '').slice(11, 16) || '—';
  const co = (todayRow.last_scan || '').slice(11, 16) || '—';
  const checked = ci !== '—';
  const hrs = (m) => ((m || 0) / 60).toFixed(1);
  const weekMin = att.reduce((a, r) => a + (r.work_min || 0), 0);
  const otMin = att.reduce((a, r) => a + (r.ot_min || 0), 0);
  const st = passStatus(todayRow.status);

  const Tab = ({ id, label, icon }) => (
    <button onClick={() => setView(id)} style={{ flex: 1, textAlign: 'center', padding: '11px 6px', border: 'none', background: 'transparent', color: view === id ? 'var(--primary)' : 'var(--ink-4)', fontFamily: 'var(--font-body)', fontSize: 12.5, fontWeight: view === id ? 700 : 500, cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
      <span style={{ fontSize: 17, lineHeight: 1 }}>{icon}</span>{label}
    </button>
  );

  const shell = { background: 'var(--bg)', border: '1px solid var(--line)', borderRadius: 26 };

  return (
    <div className={'fos-passwrap' + (desktop ? ' fos-desk' : '')}>
      <div className="fos-pass" style={shell}>
        {/* top bar (navy) */}
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '13px 18px', background: 'var(--navy)', color: '#fff', flexShrink: 0 }}>
          <span className="tnum" style={{ fontFamily: 'var(--font-display)', fontSize: 15, fontWeight: 700, letterSpacing: .3 }}>{clock}</span>
          <span style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <button onClick={() => setPwOpen(true)} title="เปลี่ยนรหัสผ่าน" style={{ background: 'rgba(255,255,255,.14)', color: '#fff', border: '1px solid rgba(255,255,255,.28)', borderRadius: 'var(--r-pill)', fontFamily: 'inherit', fontSize: 12, fontWeight: 600, padding: '6px 10px', cursor: 'pointer' }}>🔑 รหัสผ่าน</button>
            {onLogout && <button onClick={onLogout} style={{ background: 'rgba(255,255,255,.14)', color: '#fff', border: '1px solid rgba(255,255,255,.28)', borderRadius: 'var(--r-pill)', fontFamily: 'inherit', fontSize: 12, fontWeight: 600, padding: '6px 12px', cursor: 'pointer' }}>ออก</button>}
          </span>
        </div>

        <div style={{ flex: 1, overflow: 'auto', background: 'var(--bg)', padding: 14 }}>
          {/* profile header card */}
          <div className="gv-card" style={{ padding: 18, marginBottom: 14 }}>
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
              <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink-5)' }}>บัตรพนักงาน / Employee</span>
              <span className={`gv-chip ${st.cls}`}>{st.l}</span>
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
              <PassAvatar id={emp.id} photo={emp.photo_url} size={104} />
              <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 22, marginTop: 12, lineHeight: 1.1 }}>{nameOf(emp)}</div>
              <div style={{ fontSize: 13.5, color: 'var(--ink-4)', marginTop: 3 }}>{emp.position || emp.role || me.role || '—'}</div>
              <div style={{ fontSize: 12, color: 'var(--ink-5)', marginTop: 4 }}>{emp.department_name || '—'} · รหัส {emp.id || '----'}</div>
            </div>
          </div>

          {view === 'home' && (
            <>
              {/* today check-in */}
              <div className="gv-card" style={{ padding: 18, marginBottom: 14 }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                  <div style={{ fontSize: 12.5, color: 'var(--ink-4)', fontWeight: 500 }}>เข้างานวันนี้</div>
                  <span className={`gv-chip ${checked ? 'c-green' : 'c-gray'}`}>{checked ? 'ลงเวลาแล้ว' : 'ยังไม่ลงเวลา'}</span>
                </div>
                <div className="tnum" style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 42, lineHeight: 1.05, marginTop: 8 }}>
                  {ci}{co !== '—' && <span style={{ fontSize: 20, color: 'var(--ink-4)' }}> → {co}</span>}
                </div>
                <div style={{ fontSize: 12.5, color: 'var(--ink-4)', marginTop: 6 }}>{todayRow.shift_name || 'ยังไม่ผูกกะ'}</div>
              </div>

              {/* mini stats */}
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10, marginBottom: 14 }}>
                {[['ชม.วันนี้', hrs(todayRow.work_min), 'var(--ink)'], ['ชม./สัปดาห์', hrs(weekMin), 'var(--ink)'], ['OT สัปดาห์', hrs(otMin), 'var(--coral-ink)']].map(([k, v, c]) => (
                  <div key={k} className="gv-card" style={{ padding: '13px 12px', textAlign: 'center' }}>
                    <div className="tnum" style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 22, color: c, lineHeight: 1 }}>{v}</div>
                    <div style={{ fontSize: 11, color: 'var(--ink-4)', marginTop: 5 }}>{k}</div>
                  </div>
                ))}
              </div>

              {/* actions */}
              <div style={{ display: 'flex', gap: 10, marginBottom: 14 }}>
                <button onClick={() => setView('leave')} className="gv-btn ok" style={{ flex: 1 }}>+ ขอลา</button>
                <button onClick={() => setView('correct')} className="gv-btn no" style={{ flex: 1 }}>⏱ แก้เวลา</button>
              </div>

              {/* recent */}
              <div className="gv-card">
                <div className="gv-card-h"><b>สแกนล่าสุด</b><span className="gv-chip c-gray">{recent.length}</span></div>
                <div className="gv-card-b" style={{ paddingTop: 6, paddingBottom: 8 }}>
                  {recent.length === 0 ? (
                    <div className="gv-empty">— ยังไม่มีบันทึก —</div>
                  ) : recent.map((r, i) => {
                    const out = r.type === 'out';
                    const tm = r.time instanceof Date ? fmtClock(r.time) : (r.time || '').slice(0, 8);
                    return (
                      <div key={i} className="gv-row">
                        <span className={`gv-chip ${out ? 'c-blue' : 'c-green'}`}>{out ? 'ออกงาน' : 'เข้างาน'}</span>
                        <span style={{ fontSize: 12.5, color: 'var(--ink-4)', flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.device_name || r.device || '—'}</span>
                        <span className="tnum" style={{ fontSize: 14, fontWeight: 700 }}>{tm}</span>
                      </div>
                    );
                  })}
                </div>
              </div>
            </>
          )}

          {view === 'history' && <PassMonth emp={emp}/>}

          {view === 'leave' && <PassLeaveTab emp={emp} onDone={() => setView('home')}/>}
          {view === 'correct' && <PassCorrection emp={emp} onDone={() => { setView('home'); loadAtt(); }}/>}
        </div>

        {/* bottom tab bar (white, sky active) */}
        <div style={{ display: 'flex', borderTop: '1px solid var(--line)', background: 'var(--surface)', flexShrink: 0 }}>
          <Tab id="home" label="หน้าหลัก" icon="⌂"/>
          <Tab id="history" label="ประวัติ" icon="≣"/>
          <Tab id="leave" label="ขอลา" icon="＋"/>
        </div>
      </div>
      {pwOpen && <PasswordSheet onClose={() => setPwOpen(false)}/>}
    </div>
  );
}

// ── ปฏิทินเดือน: วันมาทำงาน / สาย / ขาด / ลา / วันหยุด + ชั่วโมงรวม ──────────
function PassMonth({ emp }) {
  const [ym, setYm] = useModeState(() => { const d = new Date(); return { y: d.getFullYear(), m: d.getMonth() }; });
  const [rows, setRows] = useModeState([]);
  const [leaves, setLeaves] = useModeState([]);
  const [loading, setLoading] = useModeState(false);
  const p = (n) => String(n).padStart(2, '0');
  const first = `${ym.y}-${p(ym.m + 1)}-01`;
  const lastDay = new Date(ym.y, ym.m + 1, 0).getDate();
  const last = `${ym.y}-${p(ym.m + 1)}-${p(lastDay)}`;
  const today = ymd(new Date());
  const monthName = ['มกราคม','กุมภาพันธ์','มีนาคม','เมษายน','พฤษภาคม','มิถุนายน','กรกฎาคม','สิงหาคม','กันยายน','ตุลาคม','พฤศจิกายน','ธันวาคม'][ym.m];

  useModeEffect(() => {
    if (!emp.id) return;
    setLoading(true);
    Promise.all([
      fetch(`/api/attendance/employee/${encodeURIComponent(emp.id)}?from=${first}&to=${last}`, { credentials: 'include' }).then((r) => r.ok ? r.json() : null).catch(() => null),
      fetch(`/api/leaves?employee_id=${encodeURIComponent(emp.id)}&date_from=${first}&date_to=${last}`, { credentials: 'include' }).then((r) => r.ok ? r.json() : []).catch(() => []),
    ]).then(([a, l]) => { setRows((a && a.rows) || []); setLeaves(Array.isArray(l) ? l : []); }).finally(() => setLoading(false));
  }, [emp.id, first, last]);

  const byDate = {}; rows.forEach((r) => { byDate[r.date] = r; });
  const holidays = new Set((window.HOLIDAYS || []).map((h) => h.date));
  const leaveDays = new Set();
  leaves.filter((l) => l.status === 'approved').forEach((l) => { for (let d = new Date(l.start_date); ymd(d) <= l.end_date; d.setDate(d.getDate() + 1)) leaveDays.add(ymd(d)); });

  // สรุปเดือน
  const stat = { present: 0, late: 0, absent: 0, leave: 0, min: 0 };
  for (let d = 1; d <= lastDay; d++) {
    const ds = `${ym.y}-${p(ym.m + 1)}-${p(d)}`;
    if (ds > today) break;
    const r = byDate[ds];
    if (leaveDays.has(ds)) { stat.leave++; continue; }
    if (!r || r.status === 'off' || r.status === 'holiday') continue;
    if (r.status === 'absent') { stat.absent++; continue; }
    if (r.first_scan) { stat.present++; if ((r.late_min || 0) > 0) stat.late++; stat.min += r.work_min || 0; }
  }
  const dayCell = (d) => {
    const ds = `${ym.y}-${p(ym.m + 1)}-${p(d)}`;
    const r = byDate[ds];
    let bg = 'transparent', fg = 'var(--ink-4)', label = '';
    if (leaveDays.has(ds)) { bg = 'var(--primary-soft, #E0F2FE)'; fg = 'var(--primary-ink, #0369A1)'; label = 'ลา'; }
    else if (holidays.has(ds) || (r && (r.status === 'off' || r.status === 'holiday'))) { fg = 'var(--ink-5)'; }
    else if (r && r.first_scan) { const late = (r.late_min || 0) > 0; bg = late ? 'var(--yellow-soft, #FEF3C7)' : 'var(--mint-soft, #D1FAE5)'; fg = late ? 'var(--yellow-ink, #92400E)' : 'var(--mint-ink, #065F46)'; label = (r.first_scan || '').slice(11, 16); }
    else if (r && r.status === 'absent') { bg = 'var(--coral-soft, #FEE2E2)'; fg = 'var(--coral-ink, #991B1B)'; label = 'ขาด'; }
    const isToday = ds === today;
    return (
      <div key={d} style={{ borderRadius: 10, background: bg, padding: '5px 2px', textAlign: 'center', minHeight: 44, border: isToday ? '2px solid var(--primary)' : '2px solid transparent' }}>
        <div className="tnum" style={{ fontSize: 12.5, fontWeight: 700, color: fg }}>{d}</div>
        <div className="tnum" style={{ fontSize: 9.5, color: fg, marginTop: 1, whiteSpace: 'nowrap' }}>{label}</div>
      </div>
    );
  };
  const firstDow = new Date(ym.y, ym.m, 1).getDay();
  const cells = []; for (let i = 0; i < firstDow; i++) cells.push(<div key={'e' + i}/>); for (let d = 1; d <= lastDay; d++) cells.push(dayCell(d));
  const hrs = (m) => (m / 60).toFixed(1);

  return (
    <>
      <div className="gv-card" style={{ padding: 16, marginBottom: 14 }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <button className="gv-btn no sm" onClick={() => setYm((v) => ({ y: v.m === 0 ? v.y - 1 : v.y, m: (v.m + 11) % 12 }))}>‹</button>
          <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 16 }}>{monthName} {ym.y + 543}</div>
          <button className="gv-btn no sm" onClick={() => setYm((v) => ({ y: v.m === 11 ? v.y + 1 : v.y, m: (v.m + 1) % 12 }))} disabled={ym.y === new Date().getFullYear() && ym.m === new Date().getMonth()}>›</button>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8, marginTop: 14 }}>
          {[['มาทำงาน', stat.present, 'var(--mint-ink)'], ['สาย', stat.late, 'var(--yellow-ink)'], ['ขาด', stat.absent, 'var(--coral-ink)'], ['ลา', stat.leave, 'var(--primary-ink)']].map(([k, v, c]) => (
            <div key={k} style={{ textAlign: 'center', background: 'var(--surface-2)', borderRadius: 12, padding: '10px 4px' }}>
              <div className="tnum" style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 22, color: c, lineHeight: 1 }}>{v}</div>
              <div style={{ fontSize: 11, color: 'var(--ink-4)', marginTop: 4 }}>{k} วัน</div>
            </div>
          ))}
        </div>
        <div style={{ textAlign: 'center', fontSize: 12.5, color: 'var(--ink-4)', marginTop: 10 }}>ชั่วโมงทำงานรวม <b className="tnum" style={{ color: 'var(--ink)' }}>{hrs(stat.min)}</b> ชม.{loading ? ' · กำลังโหลด…' : ''}</div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 4, marginTop: 14 }}>
          {['อา','จ','อ','พ','พฤ','ศ','ส'].map((d) => <div key={d} style={{ textAlign: 'center', fontSize: 11, color: 'var(--ink-5)', fontWeight: 600 }}>{d}</div>)}
          {cells}
        </div>
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginTop: 12, fontSize: 11, color: 'var(--ink-5)' }}>
          <span><span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 3, background: 'var(--mint-soft, #D1FAE5)', verticalAlign: -1 }}/> มา (เวลาเข้า)</span>
          <span><span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 3, background: 'var(--yellow-soft, #FEF3C7)', verticalAlign: -1 }}/> สาย</span>
          <span><span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 3, background: 'var(--coral-soft, #FEE2E2)', verticalAlign: -1 }}/> ขาด</span>
          <span><span style={{ display: 'inline-block', width: 10, height: 10, borderRadius: 3, background: 'var(--primary-soft, #E0F2FE)', verticalAlign: -1 }}/> ลา</span>
        </div>
      </div>

      <div className="gv-card">
        <div className="gv-card-h"><b>รายวัน</b><span className="gv-chip c-gray">{rows.filter((r) => r.first_scan).length} วัน</span></div>
        <div className="gv-card-b" style={{ paddingTop: 6, paddingBottom: 8 }}>
          {rows.filter((r) => r.first_scan || r.status === 'absent').length === 0 ? (
            <div className="gv-empty">— ยังไม่มีข้อมูลเดือนนี้ —</div>
          ) : rows.filter((r) => r.first_scan || r.status === 'absent').slice().reverse().map((r) => {
            const st = passStatus((r.late_min || 0) > 0 && r.status === 'present' ? 'late' : r.status);
            return (
              <div key={r.date} className="gv-row">
                <span className="tnum" style={{ fontSize: 12.5, color: 'var(--ink-4)', width: 46 }}>{r.date.slice(8)}/{r.date.slice(5, 7)}</span>
                <span className="tnum" style={{ fontSize: 13.5, flex: 1 }}>{(r.first_scan || '').slice(11, 16) || '—'} → {(r.last_scan || '').slice(11, 16) || '—'}</span>
                <span className="tnum" style={{ fontSize: 12.5, color: 'var(--ink-4)' }}>{hrs(r.work_min || 0)} ชม</span>
                <span className={`gv-chip ${st.cls}`} style={{ marginLeft: 8 }}>{st.l}{(r.late_min || 0) > 0 ? ` ${r.late_min} น.` : ''}</span>
              </div>
            );
          })}
        </div>
      </div>
    </>
  );
}

// ── แท็บ "ขอลา": ใบลาของฉัน (สถานะ) + ฟอร์มยื่นใหม่ ────────────────────────────
function PassLeaveTab({ emp, onDone }) {
  const [mine, setMine] = useModeState([]);
  const [showForm, setShowForm] = useModeState(false);
  const load = () => {
    if (!emp.id) return;
    fetch(`/api/leaves?employee_id=${encodeURIComponent(emp.id)}`, { credentials: 'include' })
      .then((r) => r.ok ? r.json() : []).then((l) => setMine((Array.isArray(l) ? l : []).slice(0, 30))).catch(() => {});
  };
  useModeEffect(() => { load(); }, [emp.id]);
  const typeName = (code) => ((window.LEAVE_TYPES || []).find((t) => t.code === code) || {}).name || code;
  const stChip = (st) => st === 'approved' ? ['อนุมัติแล้ว', 'c-green'] : st === 'rejected' ? ['ไม่อนุมัติ', 'c-coral'] : ['รออนุมัติ', 'c-amber'];
  const days = (l) => Math.round((new Date(l.end_date) - new Date(l.start_date)) / 86400000) + 1;
  if (showForm) return <PassLeave emp={emp} onDone={() => { setShowForm(false); load(); }}/>;
  return (
    <>
      <button className="gv-btn ok" style={{ width: '100%', justifyContent: 'center', marginBottom: 14, padding: '13px' }} onClick={() => setShowForm(true)}>＋ ยื่นใบลาใหม่</button>
      <div className="gv-card">
        <div className="gv-card-h"><b>ใบลาของฉัน</b><span className="gv-chip c-gray">{mine.length}</span></div>
        <div className="gv-card-b" style={{ paddingTop: 6, paddingBottom: 8 }}>
          {mine.length === 0 ? <div className="gv-empty">— ยังไม่เคยยื่นใบลา —</div> : mine.map((l) => {
            const [lbl, cls] = stChip(l.status);
            return (
              <div key={l.id} style={{ padding: '10px 0', borderBottom: '1px solid var(--line)' }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                  <span style={{ fontSize: 14, fontWeight: 600 }}>{typeName(l.leave_type)} <span className="tnum" style={{ fontWeight: 400, color: 'var(--ink-4)', fontSize: 12.5 }}>· {days(l)} วัน</span></span>
                  <span className={`gv-chip ${cls}`}>{lbl}</span>
                </div>
                <div className="tnum" style={{ fontSize: 12.5, color: 'var(--ink-4)', marginTop: 3 }}>{l.start_date}{l.end_date !== l.start_date ? ` → ${l.end_date}` : ''}{l.reason ? ` · ${l.reason}` : ''}</div>
                {l.status === 'rejected' && l.reject_reason && <div style={{ fontSize: 12, color: 'var(--coral-ink)', marginTop: 2 }}>เหตุผล: {l.reject_reason}</div>}
              </div>
            );
          })}
        </div>
      </div>
    </>
  );
}

// Working leave-request form on mobile → POST /api/leaves (+ shows balance).
function PassLeave({ emp, onDone }) {
  const types = window.LEAVE_TYPES || [];
  const today = ymd(new Date());
  const [type, setType] = useModeState((types[0] && types[0].code) || 'sick');
  const [from, setFrom] = useModeState(today);
  const [to, setTo] = useModeState(today);
  const [reason, setReason] = useModeState('');
  const [busy, setBusy] = useModeState(false);
  const [bal, setBal] = useModeState([]);

  useModeEffect(() => {
    if (!emp.id) return;
    fetch(`/api/leaves/balance?employee_id=${encodeURIComponent(emp.id)}`, { credentials: 'include' })
      .then((r) => (r.ok ? r.json() : null)).then((d) => { if (d) setBal(d.balance || []); }).catch(() => {});
  }, [emp.id]);

  const submit = async () => {
    if (to < from) { window.appToast && window.appToast('วันสิ้นสุดต้องไม่ก่อนวันเริ่ม', { tone: 'error' }); return; }
    setBusy(true);
    try {
      const r = await fetch('/api/leaves', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ employee_id: emp.id, leave_type: type, start_date: from, end_date: to, reason: reason.trim() }) });
      const d = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(d.error || 'ส่งไม่สำเร็จ');
      window.appToast && window.appToast('ยื่นใบลาแล้ว — รออนุมัติ', { tone: 'success' });
      onDone();
    } catch (e) { window.appToast && window.appToast(e.message, { tone: 'error' }); }
    finally { setBusy(false); }
  };

  return (
    <div className="gv-card" style={{ padding: 18 }}>
      <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 19 }}>ยื่นใบลา</div>
      {bal.length > 0 && (
        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: 12 }}>
          {bal.map((b) => (
            <span key={b.code} className="gv-chip c-gray">{b.name} {b.remaining != null ? `เหลือ ${b.remaining}` : `ใช้ ${b.used || 0}`}</span>
          ))}
        </div>
      )}
      <div className="gv-field" style={{ marginTop: 16 }}>
        <label>ประเภทการลา</label>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
          {types.map((t) => (
            <button key={t.code} type="button" onClick={() => setType(t.code)} className={`gv-chip ${type === t.code ? 'c-blue' : 'c-gray'}`} style={{ cursor: 'pointer', border: `1px solid ${type === t.code ? 'var(--primary)' : 'transparent'}`, fontSize: 13, padding: '7px 13px' }}>{t.name}</button>
          ))}
        </div>
      </div>
      <div style={{ display: 'flex', gap: 12 }}>
        <div className="gv-field" style={{ flex: 1 }}><label>วันเริ่ม</label><input type="date" value={from} onChange={(e) => setFrom(e.target.value)} className="gv-input"/></div>
        <div className="gv-field" style={{ flex: 1 }}><label>วันสิ้นสุด</label><input type="date" value={to} onChange={(e) => setTo(e.target.value)} className="gv-input"/></div>
      </div>
      <div className="gv-field"><label>เหตุผล</label><textarea value={reason} onChange={(e) => setReason(e.target.value)} rows={3} placeholder="(ไม่บังคับ)" className="gv-textarea" style={{ resize: 'none' }}/></div>
      <div style={{ display: 'flex', gap: 10, marginTop: 8 }}>
        <button onClick={onDone} disabled={busy} className="gv-btn no" style={{ flex: 1 }}>ยกเลิก</button>
        <button onClick={submit} disabled={busy} className="gv-btn ok" style={{ flex: 1.6 }}>{busy ? 'กำลังส่ง…' : 'ยื่นใบลา'}</button>
      </div>
    </div>
  );
}

// ── TERMINAL (kiosk) ─────────────────────────────────────────────────────────
function KioskView({ clock, dateStr, orgName, deviceLine }) {
  const [state, setState] = useModeState('idle');   // idle | scanning | success
  const [emp, setEmp] = useModeState(null);
  const timers = React.useRef([]);
  const clearTimers = () => { timers.current.forEach(clearTimeout); timers.current = []; };

  const showSuccess = (e) => {
    setEmp(e); setState('success');
    timers.current.push(setTimeout(() => { setState('idle'); setEmp(null); }, 4200));
  };

  // Real Hikvision scans arriving over the WebSocket drive the kiosk. A brief
  // "scanning" flash then the matched person — no simulation.
  useModeEffect(() => {
    if (!window.scanStream) return undefined;
    const unsub = window.scanStream.subscribe((scan) => {
      clearTimers();
      const e = scan.employee || (window.EMPLOYEES || []).find((x) => String(x.id) === String(scan.employeeId)) || { first_name: scan.employee_name || 'พนักงาน', id: scan.employeeId };
      setState('scanning');
      timers.current.push(setTimeout(() => showSuccess(e), 700));
    });
    return () => { unsub(); clearTimers(); };
  }, []);

  return (
    <div className="fos-kiosk">
      <div className="fos-kiosk-head">
        <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
          <div style={{ width: 40, height: 40, background: 'var(--fos-paper)', color: 'var(--fos-ink)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--fos-display)', fontWeight: 700, fontSize: 20 }}>F</div>
          <div>
            <div style={{ fontFamily: 'var(--fos-display)', fontWeight: 700, fontSize: 20, textTransform: 'uppercase', letterSpacing: -.5 }}>{orgName}</div>
            <div style={{ fontFamily: 'var(--fos-mono)', fontSize: 10, letterSpacing: 2, color: '#9A9A9F' }}>FACE TERMINAL · ประตูหลัก</div>
          </div>
        </div>
        <div style={{ textAlign: 'right' }}>
          <div style={{ fontFamily: 'var(--fos-display)', fontWeight: 700, fontSize: 30, letterSpacing: 1, fontVariantNumeric: 'tabular-nums' }}>{clock}</div>
          <div style={{ fontFamily: 'var(--fos-mono)', fontSize: 11, color: '#9A9A9F' }}>{dateStr}</div>
        </div>
      </div>

      <div className="fos-kiosk-main">
        {state === 'idle' && (
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', animation: 'osup .4s ease' }}>
            <div style={{ position: 'relative', width: 288, height: 340, border: '1.5px solid rgba(245,245,244,.4)', background: 'rgba(245,245,244,.04)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <svg width="130" height="130" viewBox="0 0 24 24" fill="none" stroke="rgba(245,245,244,.35)" strokeWidth="1"><circle cx="12" cy="9" r="4"/><path d="M5 20a7 7 0 0 1 14 0" strokeLinecap="round"/></svg>
              <div style={{ position: 'absolute', top: 10, left: 10, width: 32, height: 32, borderTop: '2.5px solid var(--fos-red)', borderLeft: '2.5px solid var(--fos-red)' }}/>
              <div style={{ position: 'absolute', top: 10, right: 10, width: 32, height: 32, borderTop: '2.5px solid var(--fos-red)', borderRight: '2.5px solid var(--fos-red)' }}/>
              <div style={{ position: 'absolute', bottom: 10, left: 10, width: 32, height: 32, borderBottom: '2.5px solid var(--fos-red)', borderLeft: '2.5px solid var(--fos-red)' }}/>
              <div style={{ position: 'absolute', bottom: 10, right: 10, width: 32, height: 32, borderBottom: '2.5px solid var(--fos-red)', borderRight: '2.5px solid var(--fos-red)' }}/>
              <div style={{ position: 'absolute', left: 0, right: 0, height: 2, background: 'var(--fos-red)', animation: 'osscan 2.8s ease-in-out infinite' }}/>
            </div>
            <div style={{ fontFamily: 'var(--fos-display)', fontWeight: 700, fontSize: 30, marginTop: 30 }}>มองที่กล้องเพื่อลงเวลา</div>
            <div style={{ fontFamily: 'var(--fos-mono)', fontSize: 13, letterSpacing: 1, color: '#9A9A9F', marginTop: 8 }}>LOOK AT THE CAMERA TO CLOCK IN / OUT</div>
            <div style={{ marginTop: 28, display: 'flex', alignItems: 'center', gap: 10, fontFamily: 'var(--fos-mono)', fontSize: 12, letterSpacing: 1, color: '#9A9A9F' }}>
              <span style={{ width: 8, height: 8, background: 'var(--fos-red)', borderRadius: '50%', animation: 'osblink 1.4s infinite' }}/>
              รอการสแกนจากเครื่อง · WAITING FOR DEVICE
            </div>
          </div>
        )}
        {state === 'scanning' && (
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', animation: 'osup .3s ease' }}>
            <div style={{ position: 'relative', width: 288, height: 340, border: '1.5px solid var(--fos-red)', background: 'rgba(229,38,28,.05)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
              <svg width="130" height="130" viewBox="0 0 24 24" fill="none" stroke="rgba(245,245,244,.6)" strokeWidth="1"><circle cx="12" cy="9" r="4"/><path d="M5 20a7 7 0 0 1 14 0" strokeLinecap="round"/></svg>
              <div className="fos-ring"/>
              <div style={{ position: 'absolute', left: 0, right: 0, height: 3, background: 'var(--fos-red)', animation: 'osscan 1s ease-in-out infinite' }}/>
            </div>
            <div style={{ fontFamily: 'var(--fos-display)', fontWeight: 700, fontSize: 26, marginTop: 30, display: 'flex', alignItems: 'center', gap: 12 }}><span style={{ width: 12, height: 12, background: 'var(--fos-red)', animation: 'osblink 1s infinite' }}/>กำลังจดจำใบหน้า…</div>
            <div style={{ fontFamily: 'var(--fos-mono)', fontSize: 12, letterSpacing: 1, color: '#9A9A9F', marginTop: 8 }}>HIKVISION MINMOE · PROCESSING</div>
          </div>
        )}
        {state === 'success' && emp && (
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', animation: 'osup .35s ease' }}>
            <div style={{ width: 130, height: 130, border: '1.5px solid var(--fos-paper)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--fos-display)', fontWeight: 700, fontSize: 48 }}>{initialsOf(emp)}</div>
            <div style={{ marginTop: 22, padding: '9px 22px', background: 'var(--fos-green)', color: 'var(--fos-paper)', fontFamily: 'var(--fos-mono)', fontSize: 14, letterSpacing: 2, fontWeight: 600 }}>✓ ลงเวลาสำเร็จ · CONFIRMED</div>
            <div style={{ fontFamily: 'var(--fos-display)', fontWeight: 700, fontSize: 34, marginTop: 22 }}>{nameOf(emp)}</div>
            <div style={{ fontFamily: 'var(--fos-mono)', fontSize: 13, color: '#9A9A9F', marginTop: 6 }}>{(emp.position || emp.role || '')} · {emp.department_name || ''} · EMP//{emp.id}</div>
            <div style={{ fontFamily: 'var(--fos-display)', fontWeight: 700, fontSize: 58, marginTop: 18, letterSpacing: 2, fontVariantNumeric: 'tabular-nums' }}>{clock}</div>
            <div style={{ fontFamily: 'var(--fos-mono)', fontSize: 12, letterSpacing: 1, color: '#9A9A9F', marginTop: 8 }}>ขอให้เป็นวันที่ดี · HAVE A GREAT DAY</div>
          </div>
        )}
      </div>

      <div className="fos-kiosk-foot">
        <span style={{ width: 8, height: 8, background: 'var(--fos-red)', borderRadius: '50%', animation: 'osblink 1.4s infinite' }}/>
        SERVER CONNECTED · ระบบพร้อมใช้งาน · {deviceLine}
      </div>
    </div>
  );
}

// "ลืมสแกนเข้า/ออก" — employee submits a time-correction → HR/admin approves.
function PassCorrection({ emp, onDone }) {
  const today = ymd(new Date());
  const [date, setDate] = useModeState(today);
  const [punch, setPunch] = useModeState('in');
  const [time, setTime] = useModeState('08:00');
  const [reason, setReason] = useModeState('');
  const [busy, setBusy] = useModeState(false);

  const submit = async () => {
    if (!reason.trim()) { window.appToast && window.appToast('กรุณาระบุเหตุผล', { tone: 'error' }); return; }
    setBusy(true);
    try {
      const r = await fetch('/api/corrections', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ employee_id: emp.id, date, punch_type: punch, proposed_time: time, reason: reason.trim() }) });
      const d = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(d.error || 'ส่งไม่สำเร็จ');
      window.appToast && window.appToast('ส่งคำขอแก้เวลาแล้ว — รออนุมัติ', { tone: 'success' });
      onDone();
    } catch (e) { window.appToast && window.appToast(e.message, { tone: 'error' }); }
    finally { setBusy(false); }
  };

  return (
    <div className="gv-card" style={{ padding: 18 }}>
      <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 19 }}>ขอแก้เวลา</div>
      <div style={{ fontSize: 12.5, color: 'var(--ink-4)', marginTop: 4 }}>กรณีลืมสแกนเข้า/ออก — ส่งให้ HR/หัวหน้าอนุมัติ</div>
      <div className="gv-field" style={{ marginTop: 16 }}>
        <label>เข้า หรือ ออก</label>
        <div style={{ display: 'flex', gap: 8 }}>
          {[['in', 'เข้างาน'], ['out', 'ออกงาน']].map(([k, v]) => (
            <button key={k} type="button" onClick={() => setPunch(k)} className={punch === k ? 'gv-btn ok' : 'gv-btn no'} style={{ flex: 1 }}>{v}</button>
          ))}
        </div>
      </div>
      <div style={{ display: 'flex', gap: 12 }}>
        <div className="gv-field" style={{ flex: 1 }}><label>วันที่</label><input type="date" value={date} max={today} onChange={(e) => setDate(e.target.value)} className="gv-input"/></div>
        <div className="gv-field" style={{ flex: 1 }}><label>เวลา</label><input type="time" value={time} onChange={(e) => setTime(e.target.value)} className="gv-input"/></div>
      </div>
      <div className="gv-field"><label>เหตุผล *</label><textarea value={reason} onChange={(e) => setReason(e.target.value)} rows={3} placeholder="เช่น ลืมสแกนตอนเข้า / เครื่องค้าง" className="gv-textarea" style={{ resize: 'none' }}/></div>
      <div style={{ display: 'flex', gap: 10, marginTop: 8 }}>
        <button onClick={onDone} disabled={busy} className="gv-btn no" style={{ flex: 1 }}>ยกเลิก</button>
        <button onClick={submit} disabled={busy} className="gv-btn ok" style={{ flex: 1.6 }}>{busy ? 'กำลังส่ง…' : 'ส่งคำขอ'}</button>
      </div>
    </div>
  );
}

// ── ADMIN/HR mobile: 4 แท็บล่าง (หน้าหลัก · วันนี้ · พนักงาน · อนุมัติ) ──────────
const ADMIN_TABS = [
  { k: 'home', icon: '🏠', label: 'หน้าหลัก' },
  { k: 'today', icon: '📋', label: 'วันนี้' },
  { k: 'emps', icon: '👥', label: 'พนักงาน' },
  { k: 'approve', icon: '✅', label: 'อนุมัติ' },
];

function AdminMobileView({ clock, dateStr, orgName, role, onLogout }) {
  const [tab, setTab] = useModeState(() => { try { return sessionStorage.getItem('fit.mtab') || 'home'; } catch (_) { return 'home'; } });
  const [sum, setSum] = useModeState(null);
  const [recent, setRecent] = useModeState((window.RECENT_SCANS || []).slice(0, 8));
  const [pending, setPending] = useModeState(0);
  const [empTarget, setEmpTarget] = useModeState(null);   // เปิดแท็บพนักงานพร้อมเลือกคน
  const [pwOpen, setPwOpen] = useModeState(false);

  const loadAll = React.useCallback(() => {
    fetch('/api/attendance/summary', { credentials: 'include' }).then((r) => r.ok ? r.json() : null).then((d) => d && setSum(d)).catch(() => {});
    Promise.all([
      fetch('/api/leaves?status=pending', { credentials: 'include' }).then((r) => r.ok ? r.json() : []).catch(() => []),
      fetch('/api/corrections?status=pending', { credentials: 'include' }).then((r) => r.ok ? r.json() : []).catch(() => []),
    ]).then(([lv, co]) => setPending((lv || []).length + (co || []).length));
  }, []);
  useModeEffect(() => { loadAll(); const t = setInterval(loadAll, 60000); return () => clearInterval(t); }, [loadAll]);
  useModeEffect(() => {
    if (!window.scanStream) return undefined;
    return window.scanStream.subscribe((scan) => { setRecent((prev) => [scan, ...prev].slice(0, 8)); loadAll(); });
  }, [loadAll]);
  const go = (k) => { setTab(k); try { sessionStorage.setItem('fit.mtab', k); } catch (_) {} };

  const shell = { background: 'var(--bg)', border: '1px solid var(--line)', borderRadius: 26 };
  const roleName = { admin: 'ผู้ดูแลระบบ', hr: 'ฝ่ายบุคคล', manager: 'หัวหน้ากอง' }[role] || role || 'admin';

  return (
    <div className="fos-passwrap">
      <div className="fos-pass" style={shell}>
        {/* แถบบน */}
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '11px 16px', paddingTop: 'max(11px, env(safe-area-inset-top))', background: 'var(--navy)', color: '#fff', flexShrink: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
            <div style={{ width: 32, height: 32, borderRadius: 9, background: 'var(--primary)', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 16, flex: 'none' }}>{(orgName || 'F').slice(0, 1)}</div>
            <div style={{ minWidth: 0 }}>
              <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 15, lineHeight: 1.1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{orgName}</div>
              <div style={{ fontSize: 11, color: 'rgba(255,255,255,.7)', marginTop: 2 }}>{roleName} · <span className="tnum">{clock}</span></div>
            </div>
          </div>
          <div style={{ display: 'flex', gap: 6, flex: 'none' }}>
            <button onClick={() => setPwOpen(true)} title="เปลี่ยนรหัสผ่าน" style={{ background: 'rgba(255,255,255,.14)', color: '#fff', border: '1px solid rgba(255,255,255,.28)', borderRadius: 'var(--r-pill)', fontFamily: 'inherit', fontSize: 12, fontWeight: 600, padding: '6px 10px', cursor: 'pointer' }}>🔑</button>
            <button onClick={onLogout} style={{ background: 'rgba(255,255,255,.14)', color: '#fff', border: '1px solid rgba(255,255,255,.28)', borderRadius: 'var(--r-pill)', fontFamily: 'inherit', fontSize: 12, fontWeight: 600, padding: '6px 12px', cursor: 'pointer' }}>ออก</button>
          </div>
        </div>

        {/* เนื้อหาแท็บ */}
        <div style={{ flex: 1, overflow: 'auto', background: 'var(--bg)', padding: 14, WebkitOverflowScrolling: 'touch' }}>
          {tab === 'home' && <MobileHome sum={sum} dateStr={dateStr} recent={recent} pending={pending} role={role} go={go}/>}
          {tab === 'today' && <MobileToday/>}
          {tab === 'emps' && <MobileEmployees role={role} initial={empTarget} onOpened={() => setEmpTarget(null)}/>}
          {tab === 'approve' && <MobileApprovals onChange={loadAll}/>}
        </div>

        {pwOpen && <PasswordSheet onClose={() => setPwOpen(false)}/>}
        {/* แถบเมนูล่าง */}
        <div style={{ flexShrink: 0, display: 'flex', borderTop: '1px solid var(--line)', background: 'var(--surface)', paddingBottom: 'env(safe-area-inset-bottom)' }}>
          {ADMIN_TABS.map((t) => {
            const on = tab === t.k;
            return (
              <button key={t.k} onClick={() => go(t.k)} style={{ flex: 1, position: 'relative', border: 'none', background: 'transparent', padding: '9px 4px 8px', fontFamily: 'inherit', cursor: 'pointer', color: on ? 'var(--primary-ink)' : 'var(--ink-5)' }}>
                <div style={{ fontSize: 21, lineHeight: 1, filter: on ? 'none' : 'grayscale(1) opacity(.7)' }}>{t.icon}</div>
                <div style={{ fontSize: 11, fontWeight: on ? 700 : 500, marginTop: 4 }}>{t.label}</div>
                {t.k === 'approve' && pending > 0 && (
                  <span className="tnum" style={{ position: 'absolute', top: 4, left: '50%', marginLeft: 8, minWidth: 18, height: 18, padding: '0 5px', borderRadius: 9, background: 'var(--coral)', color: '#fff', fontSize: 11, fontWeight: 700, lineHeight: '18px' }}>{pending}</span>
                )}
                {on && <div style={{ position: 'absolute', top: 0, left: '30%', right: '30%', height: 3, borderRadius: 2, background: 'var(--primary)' }}/>}
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
}

// ── แท็บหน้าหลัก: ตัวเลขวันนี้ + ปุ่มลัด + สแกนล่าสุด ─────────────────────────
function MobileHome({ sum, dateStr, recent, pending, role, go }) {
  const bs = sum?.by_status || {};
  const present = (bs.present || 0) + (bs.in_only || 0);
  const total = sum?.total ?? (window.EMPLOYEES?.length || 0);
  const noPhoto = (window.EMPLOYEES || []).filter((e) => !e.photo_path && !e.photo_url).length;
  const sName = (s) => s.employee?.first_name ? `${s.employee.first_name} ${s.employee.last_name || ''}`.trim() : (s.first_name ? `${s.first_name} ${s.last_name || ''}`.trim() : (s.employee_name || 'ไม่รู้จัก'));
  const sTime = (s) => { const d = s.time instanceof Date ? s.time : (s.scan_time ? new Date(String(s.scan_time).replace(' ', 'T')) : null); if (!d) return '--:--'; const p = (n) => String(n).padStart(2, '0'); return `${p(d.getHours())}:${p(d.getMinutes())}`; };
  const sId = (s) => (s.employee && s.employee.id) || s.employeeId || s.employee_id || null;
  const sPhoto = (s) => s.photo_url || (s.employee && s.employee.photo_path ? `/faces/${s.employee.photo_path}` : null);

  const Tile = ({ icon, label, sub, onClick, href, tone }) => {
    const st = { display: 'flex', alignItems: 'center', gap: 10, padding: '13px 12px', border: '1px solid var(--line)', borderRadius: 16, background: 'var(--surface)', textAlign: 'left', fontFamily: 'inherit', cursor: 'pointer', textDecoration: 'none', color: 'var(--ink)', minHeight: 64 };
    const inner = (<>
      <span style={{ fontSize: 24, lineHeight: 1 }}>{icon}</span>
      <span style={{ minWidth: 0 }}>
        <span style={{ display: 'block', fontSize: 13.5, fontWeight: 700 }}>{label}</span>
        {sub && <span style={{ display: 'block', fontSize: 11.5, color: tone || 'var(--ink-5)', marginTop: 1 }}>{sub}</span>}
      </span>
    </>);
    return href ? <a href={href} style={st}>{inner}</a> : <button onClick={onClick} style={st}>{inner}</button>;
  };

  return (
    <>
      <div className="gv-card" style={{ padding: 18, marginBottom: 12 }}>
        <div style={{ fontSize: 12, color: 'var(--ink-4)', fontWeight: 500 }}>ภาพรวมวันนี้ · {dateStr}</div>
        <div style={{ display: 'flex', alignItems: 'flex-end', gap: 12, marginTop: 6 }}>
          <div className="tnum" style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 56, lineHeight: .9, letterSpacing: -1, color: 'var(--navy)' }}>{present}</div>
          <div style={{ paddingBottom: 6 }}>
            <div style={{ fontSize: 14, fontWeight: 600 }}>เข้างานแล้ว</div>
            <div className="tnum" style={{ fontSize: 12, color: 'var(--ink-4)' }}>/ {total} คน</div>
          </div>
        </div>
        <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
          {[['สาย', bs.late || 0, 'var(--coral-ink)'], ['ยังไม่มา', bs.absent || 0, 'var(--ink-4)'], ['ลา', bs.leave || 0, 'var(--primary-ink)']].map(([k, v, c]) => (
            <div key={k} style={{ flex: 1, background: 'var(--surface-2)', border: '1px solid var(--line)', borderRadius: 12, padding: '9px 8px', textAlign: 'center' }}>
              <div className="tnum" style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 20, color: c, lineHeight: 1 }}>{v}</div>
              <div style={{ fontSize: 11, color: 'var(--ink-4)', marginTop: 4 }}>{k}</div>
            </div>
          ))}
        </div>
      </div>

      {/* ปุ่มลัด */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 12 }}>
        <Tile icon="✅" label="รออนุมัติ" sub={pending ? `${pending} รายการ` : 'ไม่มีค้าง'} tone={pending ? 'var(--coral-ink)' : undefined} onClick={() => go('approve')}/>
        <Tile icon="📋" label="ใครมาวันนี้" sub="รายชื่อ เข้า/สาย/ขาด" onClick={() => go('today')}/>
        {(role === 'admin' || role === 'hr') && <Tile icon="📷" label="ถ่ายรูปพนักงาน" sub={noPhoto ? `ยังไม่มีรูป ${noPhoto} คน` : 'มีรูปครบแล้ว'} tone={noPhoto ? 'var(--amber-ink, #9a6700)' : undefined} onClick={() => go('emps')}/>}
        <Tile icon="⛶" label="โหมดจัดการเต็ม" sub="เปิด Console" href="?view=desktop"/>
      </div>

      {/* สแกนล่าสุด */}
      <div className="gv-card">
        <div className="gv-card-h"><b>สแกนล่าสุด</b><span className="gv-chip c-gray">{recent.length}</span></div>
        <div className="gv-card-b" style={{ paddingTop: 6, paddingBottom: 8 }}>
          {recent.length === 0 ? (
            <div className="gv-empty">— ยังไม่มีการสแกน —</div>
          ) : recent.map((s, i) => {
            const out = s.type === 'out';
            return (
              <div key={i} className="gv-row">
                <PassAvatar id={sId(s)} photo={sPhoto(s)} size={38} />
                <span style={{ fontSize: 13.5, fontWeight: 600, flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{sName(s)}</span>
                <span className={`gv-chip ${out ? 'c-blue' : 'c-green'}`}>{out ? 'ออก' : 'เข้า'}</span>
                <span className="tnum" style={{ fontSize: 13, color: 'var(--ink-4)', minWidth: 44, textAlign: 'right' }}>{sTime(s)}</span>
              </div>
            );
          })}
        </div>
      </div>
    </>
  );
}

// ── ช่องค้นหา + ชิปกรอง ใช้ร่วมกันหลายแท็บ ────────────────────────────────────
function MobileSearch({ value, onChange, placeholder }) {
  return (
    <div style={{ position: 'relative', marginBottom: 10 }}>
      <input className="gv-input" value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} style={{ fontSize: 16, paddingRight: 34, width: '100%', boxSizing: 'border-box' }}/>
      {value && <button onClick={() => onChange('')} style={{ position: 'absolute', right: 6, top: '50%', transform: 'translateY(-50%)', border: 'none', background: 'var(--surface-2)', borderRadius: '50%', width: 24, height: 24, cursor: 'pointer', color: 'var(--ink-4)', fontSize: 13 }}>✕</button>}
    </div>
  );
}
function MobileChips({ items, value, onChange }) {
  return (
    <div style={{ display: 'flex', gap: 6, overflowX: 'auto', paddingBottom: 6, marginBottom: 6, scrollbarWidth: 'none' }}>
      {items.map(([k, label, n]) => {
        const on = value === k;
        return (
          <button key={k} onClick={() => onChange(k)} style={{ flex: 'none', border: '1px solid ' + (on ? 'var(--primary)' : 'var(--line)'), background: on ? 'var(--primary)' : 'var(--surface)', color: on ? '#fff' : 'var(--ink-3)', borderRadius: 'var(--r-pill)', padding: '6px 12px', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer' }}>
            {label}{n !== undefined && <span className="tnum" style={{ marginLeft: 5, opacity: .8 }}>{n}</span>}
          </button>
        );
      })}
    </div>
  );
}
const groupByDept = (rows) => {
  const g = {};
  for (const r of rows) (g[r.department_name || '— ไม่มีกอง —'] = g[r.department_name || '— ไม่มีกอง —'] || []).push(r);
  return Object.entries(g).sort((a, b) => a[0].localeCompare(b[0], 'th'));
};
const matchQ = (e, q) => !q || [e.id, e.employee_id, e.first_name, e.last_name, e.department_name, e.position].filter(Boolean).some((v) => String(v).toLowerCase().includes(q));

// ── แท็บวันนี้: รายชื่อ เข้า/สาย/ยังไม่มา/ลา แยกกอง ───────────────────────────
function MobileToday() {
  const [rows, setRows] = useModeState(null);
  const [q, setQ] = useModeState('');
  const [f, setF] = useModeState('all');
  const load = () => fetch('/api/attendance/daily', { credentials: 'include' }).then((r) => r.ok ? r.json() : null).then((d) => d && setRows(d.rows || [])).catch(() => {});
  useModeEffect(() => { load(); const t = setInterval(load, 60000); if (window.scanStream) { const un = window.scanStream.subscribe(() => setTimeout(load, 1500)); return () => { clearInterval(t); un && un(); }; } return () => clearInterval(t); }, []);

  if (!rows) return <div className="gv-empty">กำลังโหลด…</div>;
  const kind = (r) => {
    const st = r.status || 'absent';
    if ((st === 'present' || st === 'in_only') && (r.late_min || 0) > 0) return 'late';
    if (st === 'present' || st === 'in_only') return 'in';
    if (st === 'leave') return 'leave';
    if (st === 'holiday' || st === 'off') return 'off';
    return 'absent';
  };
  const count = (k) => rows.filter((r) => kind(r) === k).length;
  const qq = q.trim().toLowerCase();
  const list = rows.filter((r) => (f === 'all' || kind(r) === f) && matchQ(r, qq));
  const hhmm = (s) => (s ? String(s).slice(11, 16) : '');
  const chip = { in: ['เข้าแล้ว', 'c-green'], late: ['สาย', 'c-coral'], absent: ['ยังไม่มา', 'c-gray'], leave: ['ลา', 'c-blue'], off: ['หยุด', 'c-gray'] };

  return (
    <>
      <MobileSearch value={q} onChange={setQ} placeholder="ค้นชื่อ / กอง"/>
      <MobileChips value={f} onChange={setF} items={[['all', 'ทั้งหมด', rows.length], ['in', 'เข้าแล้ว', count('in')], ['late', 'สาย', count('late')], ['absent', 'ยังไม่มา', count('absent')], ['leave', 'ลา', count('leave')]]}/>
      {list.length === 0 && <div className="gv-empty">— ไม่มีรายการ —</div>}
      {groupByDept(list).map(([dept, items]) => (
        <div key={dept} className="gv-card" style={{ marginBottom: 10 }}>
          <div className="gv-card-h"><b style={{ fontSize: 13 }}>{dept}</b><span className="gv-chip c-gray">{items.length}</span></div>
          <div className="gv-card-b" style={{ paddingTop: 2, paddingBottom: 4 }}>
            {items.map((r) => {
              const k = kind(r); const [l, cls] = chip[k];
              return (
                <div key={r.employee_id} className="gv-row" style={{ padding: '8px 0' }}>
                  <PassAvatar id={r.employee_id} photo={r.photo_url} size={36}/>
                  <span style={{ flex: 1, minWidth: 0 }}>
                    <span style={{ display: 'block', fontSize: 13.5, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{r.first_name} {r.last_name}</span>
                    <span className="tnum" style={{ display: 'block', fontSize: 11.5, color: 'var(--ink-5)' }}>
                      {r.first_scan ? `เข้า ${hhmm(r.first_scan)}` : ''}{r.last_scan && r.last_scan !== r.first_scan ? ` · ออก ${hhmm(r.last_scan)}` : ''}{k === 'late' ? ` · สาย ${r.late_min} นาที` : ''}{!r.first_scan && r.shift_name ? r.shift_name : ''}
                    </span>
                  </span>
                  <span className={`gv-chip ${cls}`}>{l}</span>
                </div>
              );
            })}
          </div>
        </div>
      ))}
    </>
  );
}

// ── แท็บพนักงาน: รายชื่อแยกกอง → แตะคน → ถ่ายรูป / ดูข้อมูล ──────────────────
function MobileEmployees({ role, initial, onOpened }) {
  const [emps, setEmps] = useModeState(window.EMPLOYEES || []);
  const [q, setQ] = useModeState('');
  const [f, setF] = useModeState('all');
  const [sel, setSel] = useModeState(initial || null);
  useModeEffect(() => { if (initial) { setSel(initial); onOpened && onOpened(); } }, [initial]);
  const refresh = async () => { try { await window.refreshData(); } catch (_) {} setEmps(window.EMPLOYEES || []); };

  const hasPhoto = (e) => !!(e.photo_path || e.photo_url);
  const qq = q.trim().toLowerCase();
  const active = emps.filter((e) => e.is_active !== 0);
  const list = active.filter((e) => (f === 'all' || (f === 'nophoto' && !hasPhoto(e))) && matchQ(e, qq));
  const canPhoto = role === 'admin' || role === 'hr';

  return (
    <>
      <MobileSearch value={q} onChange={setQ} placeholder="ค้นชื่อ / รหัส / กอง / ตำแหน่ง"/>
      <MobileChips value={f} onChange={setF} items={[['all', 'ทั้งหมด', active.length], ['nophoto', 'ยังไม่มีรูป', active.filter((e) => !hasPhoto(e)).length]]}/>
      {list.length === 0 && <div className="gv-empty">— ไม่พบ —</div>}
      {groupByDept(list).map(([dept, items]) => (
        <div key={dept} className="gv-card" style={{ marginBottom: 10 }}>
          <div className="gv-card-h"><b style={{ fontSize: 13 }}>{dept}</b><span className="gv-chip c-gray">{items.length}</span></div>
          <div className="gv-card-b" style={{ paddingTop: 2, paddingBottom: 4 }}>
            {items.map((e) => (
              <button key={e.id} onClick={() => setSel(e)} className="gv-row" style={{ width: '100%', padding: '8px 0', border: 'none', borderBottom: '1px solid var(--line)', background: 'transparent', textAlign: 'left', fontFamily: 'inherit', cursor: 'pointer' }}>
                <PassAvatar id={e.id} photo={e.photo_url} size={36}/>
                <span style={{ flex: 1, minWidth: 0 }}>
                  <span style={{ display: 'block', fontSize: 13.5, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{e.title || ''}{e.first_name} {e.last_name}</span>
                  <span style={{ display: 'block', fontSize: 11.5, color: 'var(--ink-5)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{e.position && e.position !== 'null' ? e.position : e.id}</span>
                </span>
                {!hasPhoto(e) ? <span className="gv-chip c-amber">ไม่มีรูป</span> : <span style={{ color: 'var(--ink-5)', fontSize: 16 }}>›</span>}
              </button>
            ))}
          </div>
        </div>
      ))}
      {sel && <MobileEmployeeSheet emp={emps.find((x) => x.id === sel.id) || sel} canPhoto={canPhoto} onClose={() => setSel(null)} onSaved={refresh}/>}
    </>
  );
}

// แผ่นล่าง: ข้อมูลพนักงาน + ถ่ายรูป (ย่อรูปในเครื่อง ≤1024px แล้วอัปโหลดเป็นรูปโปรไฟล์ → ใช้ Sync เข้าเครื่องสแกน)
function MobileEmployeeSheet({ emp, canPhoto, onClose, onSaved }) {
  const [preview, setPreview] = useModeState(null);
  const [busy, setBusy] = useModeState(false);
  const [today, setToday] = useModeState(null);
  const inputRef = React.useRef(null);
  const full = `${emp.title || ''}${emp.first_name || ''} ${emp.last_name || ''}`.trim();
  useModeEffect(() => {
    setToday(null);
    fetch(`/api/attendance/employee/${encodeURIComponent(emp.id)}`, { credentials: 'include' }).then((r) => r.ok ? r.json() : null).then((d) => setToday(d && d.rows && d.rows[0] ? d.rows[0] : {})).catch(() => setToday({}));
  }, [emp.id]);

  const onFile = async (ev) => {
    const f = ev.target.files && ev.target.files[0]; ev.target.value = '';
    if (!f) return;
    const blob = await shrinkImage(f);
    setPreview({ blob, url: URL.createObjectURL(blob) });
  };
  const upload = async () => {
    if (!preview) return;
    setBusy(true);
    try {
      const fd = new FormData(); fd.append('photo', preview.blob, `${emp.id}.jpg`);
      const r = await fetch(`/api/employees/${encodeURIComponent(emp.id)}/photo`, { method: 'POST', credentials: 'include', body: fd });
      const d = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(d.error || 'อัปโหลดไม่สำเร็จ');
      window.appToast(`บันทึกรูป ${full} แล้ว`, { tone: 'success' });
      setPreview(null); await onSaved();
    } catch (e) { window.appToast(e.message, { tone: 'error' }); }
    finally { setBusy(false); }
  };
  const hhmm = (s) => (s ? String(s).slice(11, 16) : '—');
  const st = today && today.status ? passStatus((today.status === 'present' || today.status === 'in_only') && today.late_min > 0 ? 'late' : today.status) : null;

  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(15,23,42,.45)', zIndex: 60, display: 'flex', alignItems: 'flex-end' }}>
      <div onClick={(e) => e.stopPropagation()} style={{ width: '100%', background: 'var(--surface)', borderRadius: '22px 22px 0 0', padding: '12px 18px', paddingBottom: 'max(18px, env(safe-area-inset-bottom))', maxHeight: '88vh', overflow: 'auto', boxSizing: 'border-box' }}>
        <div style={{ width: 40, height: 4, borderRadius: 2, background: 'var(--line)', margin: '0 auto 12px' }}/>
        <input ref={inputRef} type="file" accept="image/*" capture="environment" style={{ display: 'none' }} onChange={onFile}/>
        <div style={{ display: 'flex', gap: 14, alignItems: 'center' }}>
          {preview
            ? <img src={preview.url} alt="" style={{ width: 84, height: 84, objectFit: 'cover', borderRadius: 18, border: '3px solid var(--primary)', flex: 'none' }}/>
            : <PassAvatar id={emp.id} photo={emp.photo_url ? emp.photo_url + (emp.photo_url.includes('?') ? '&' : '?') + 'r=' + Date.now() : null} size={84}/>}
          <div style={{ minWidth: 0 }}>
            <div style={{ fontFamily: 'var(--font-display)', fontWeight: 700, fontSize: 17, lineHeight: 1.15 }}>{full}</div>
            <div style={{ fontSize: 12.5, color: 'var(--ink-4)', marginTop: 3 }}>{emp.position && emp.position !== 'null' ? emp.position : '—'}</div>
            <div className="tnum" style={{ fontSize: 12, color: 'var(--ink-5)', marginTop: 2 }}>{emp.id} · {emp.department_name || '—'}</div>
          </div>
        </div>

        {/* วันนี้ */}
        <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
          {[['สถานะวันนี้', st ? <span className={`gv-chip ${st.cls}`}>{st.l}</span> : (today ? '—' : '…')], ['เข้า', hhmm(today && today.first_scan)], ['ออก', hhmm(today && today.last_scan)]].map(([k, v]) => (
            <div key={k} style={{ flex: 1, background: 'var(--surface-2)', border: '1px solid var(--line)', borderRadius: 12, padding: '8px 10px', textAlign: 'center' }}>
              <div style={{ fontSize: 10.5, color: 'var(--ink-5)' }}>{k}</div>
              <div className="tnum" style={{ fontSize: 14, fontWeight: 700, marginTop: 3 }}>{v}</div>
            </div>
          ))}
        </div>

        {preview && <div style={{ fontSize: 11.5, color: 'var(--ink-5)', textAlign: 'center', marginTop: 12 }}>หน้าตรง · แสงสว่าง · ไม่ใส่หมวก/แว่นดำ · ให้หน้าเต็มกรอบ</div>}
        <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
          {emp.phone && !preview && <a className="gv-btn no" href={`tel:${emp.phone}`} style={{ flex: 1, justifyContent: 'center', textDecoration: 'none' }}>📞 โทร</a>}
          {canPhoto && !preview && <button className="gv-btn ok" style={{ flex: 2, justifyContent: 'center' }} onClick={() => inputRef.current && inputRef.current.click()}>📷 {emp.photo_path || emp.photo_url ? 'ถ่ายรูปใหม่' : 'ถ่ายรูป'}</button>}
          {preview && <button className="gv-btn no" style={{ flex: 1, justifyContent: 'center' }} disabled={busy} onClick={() => setPreview(null)}>ยกเลิก</button>}
          {preview && <button className="gv-btn no" style={{ flex: 1, justifyContent: 'center' }} disabled={busy} onClick={() => inputRef.current && inputRef.current.click()}>ถ่ายใหม่</button>}
          {preview && <button className="gv-btn ok" style={{ flex: 1.6, justifyContent: 'center' }} disabled={busy} onClick={upload}>{busy ? 'กำลังบันทึก…' : 'บันทึกรูป'}</button>}
        </div>
        {!preview && <button className="gv-btn no" onClick={onClose} style={{ width: '100%', justifyContent: 'center', marginTop: 8 }}>ปิด</button>}
      </div>
    </div>
  );
}

// ── รายการรออนุมัติบนมือถือ: ใบลา + คำขอแก้เวลา กดอนุมัติ/ไม่อนุมัติได้ทันที ─────
function MobileApprovals({ onChange }) {
  const [leaves, setLeaves] = useModeState([]);
  const [corrs, setCorrs] = useModeState([]);
  const [busy, setBusy] = useModeState(null);
  const load = () => {
    fetch('/api/leaves?status=pending', { credentials: 'include' }).then((r) => r.ok ? r.json() : []).then((l) => setLeaves(Array.isArray(l) ? l : [])).catch(() => {});
    fetch('/api/corrections?status=pending', { credentials: 'include' }).then((r) => r.ok ? r.json() : []).then((c) => setCorrs(Array.isArray(c) ? c : [])).catch(() => {});
  };
  useModeEffect(() => { load(); const t = setInterval(load, 60000); return () => clearInterval(t); }, []);
  const typeName = (code) => ((window.LEAVE_TYPES || []).find((t) => t.code === code) || {}).name || code;
  const empName = (id, fallback) => { const e = (window.EMPLOYEES || []).find((x) => String(x.id) === String(id)); return e ? `${e.first_name} ${e.last_name || ''}`.trim() : (fallback || id); };
  const act = async (kind, id, ok) => {
    let reason = '';
    if (!ok) { reason = window.prompt('เหตุผลที่ไม่อนุมัติ (ไม่บังคับ)') || ''; }
    else { const c = await window.appConfirm({ title: 'อนุมัติรายการนี้?', confirmText: 'อนุมัติ' }); if (!c) return; }
    setBusy(kind + id);
    const r = await fetch(`/api/${kind}/${id}/${ok ? 'approve' : 'reject'}`, { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason }) });
    setBusy(null);
    window.appToast(r.ok ? (ok ? 'อนุมัติแล้ว' : 'ไม่อนุมัติแล้ว') : 'ทำรายการไม่สำเร็จ', { tone: r.ok ? 'success' : 'error' });
    load(); onChange && onChange();
  };
  const total = leaves.length + corrs.length;
  const Row = ({ k, id, title, sub, extra }) => (
    <div style={{ padding: '10px 0', borderBottom: '1px solid var(--line)' }}>
      <div style={{ fontSize: 14, fontWeight: 600 }}>{title}</div>
      <div className="tnum" style={{ fontSize: 12.5, color: 'var(--ink-4)', marginTop: 2 }}>{sub}</div>
      {extra && <div style={{ fontSize: 12.5, color: 'var(--ink-5)', marginTop: 2 }}>{extra}</div>}
      <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
        <button className="gv-btn no sm" style={{ flex: 1, justifyContent: 'center', color: 'var(--coral-ink)' }} disabled={busy === k + id} onClick={() => act(k, id, false)}>ไม่อนุมัติ</button>
        <button className="gv-btn ok sm" style={{ flex: 1.4, justifyContent: 'center' }} disabled={busy === k + id} onClick={() => act(k, id, true)}>✓ อนุมัติ</button>
      </div>
    </div>
  );
  return (
    <div className="gv-card" style={{ marginBottom: 14 }}>
      <div className="gv-card-h"><b>รออนุมัติ</b><span className={`gv-chip ${total ? 'c-amber' : 'c-gray'}`}>{total}</span></div>
      <div className="gv-card-b" style={{ paddingTop: 4, paddingBottom: 6 }}>
        {total === 0 && <div className="gv-empty">— ไม่มีรายการค้าง —</div>}
        {leaves.map((l) => (
          <Row key={'l' + l.id} k="leaves" id={l.id}
            title={`📋 ใบลา · ${empName(l.employee_id, l.employee_name)}`}
            sub={`${typeName(l.leave_type)} · ${l.start_date}${l.end_date !== l.start_date ? ' → ' + l.end_date : ''}`}
            extra={l.reason}/>
        ))}
        {corrs.map((c) => (
          <Row key={'c' + c.id} k="corrections" id={c.id}
            title={`⏱ ขอแก้เวลา · ${empName(c.employee_id, c.employee_name)}`}
            sub={`${c.date || ''} ${c.punch_type === 'out' ? 'ออก' : 'เข้า'} → ${c.proposed_time || ''}`}
            extra={c.reason}/>
        ))}
      </div>
    </div>
  );
}

// ── ย่อรูปในเครื่องก่อนอัปโหลด (≤1024px) ใช้โดยแผ่นพนักงานด้านบน ─────────────────
function shrinkImage(file, max = 1024) {
  return new Promise((resolve) => {
    const done = (blob) => resolve(blob || file);
    const draw = (img, w, h) => {
      const s = Math.min(1, max / Math.max(w, h));
      const c = document.createElement('canvas'); c.width = Math.round(w * s); c.height = Math.round(h * s);
      c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
      c.toBlob(done, 'image/jpeg', 0.86);
    };
    if (window.createImageBitmap) {
      createImageBitmap(file, { imageOrientation: 'from-image' }).then((bm) => draw(bm, bm.width, bm.height)).catch(() => done(null));
    } else {
      const img = new Image(); img.onload = () => draw(img, img.naturalWidth, img.naturalHeight); img.onerror = () => done(null);
      img.src = URL.createObjectURL(file);
    }
  });
}

window.PassView = PassView;
window.KioskView = KioskView;
window.AdminMobileView = AdminMobileView;
