// BNI Traffic Light Manager — React SPA (single-file, transpiled in browser)
const { useState, useEffect, useMemo, useRef, useCallback } = React;

// ============ Helpers ============
const TOKEN_KEY = 'bni_admin_token';

function getToken() { return localStorage.getItem(TOKEN_KEY) || ''; }
function setToken(t) { if (t) localStorage.setItem(TOKEN_KEY, t); else localStorage.removeItem(TOKEN_KEY); }

function busted(url) {
  return url + (url.includes('?') ? '&' : '?') + '_=' + Date.now();
}

async function api(path, opts = {}) {
  const headers = Object.assign({}, opts.headers || {});
  const token = getToken();
  if (token) headers['x-admin-token'] = token;
  if (opts.body && !(opts.body instanceof FormData) && !headers['Content-Type']) {
    headers['Content-Type'] = 'application/json';
  }
  const isGet = !opts.method || opts.method === 'GET';
  const fullUrl = isGet ? busted(path) : path;
  const res = await fetch(fullUrl, Object.assign({}, opts, { headers }));
  if (res.status === 401) {
    setToken('');
    window.dispatchEvent(new CustomEvent('bni:unauth'));
    throw new Error('unauthorized');
  }
  if (!res.ok) {
    let msg = 'request failed';
    try { const j = await res.json(); msg = j.error || msg; } catch {}
    throw new Error(msg);
  }
  const ct = res.headers.get('Content-Type') || '';
  if (ct.includes('application/json')) return res.json();
  return res;
}

function formatNum(n, dp = 0) {
  if (n === null || n === undefined || isNaN(n)) return '-';
  return Number(n).toFixed(dp);
}
function formatComma(n) {
  if (n === null || n === undefined || isNaN(n)) return '-';
  return Number(n).toLocaleString('ko-KR');
}
function pad2(n) { return String(n).padStart(2, '0'); }
function shiftMonth(yyyymm, delta) {
  const [y, m] = yyyymm.split('-').map(Number);
  let ny = y, nm = m + delta;
  while (nm <= 0) { nm += 12; ny -= 1; }
  while (nm > 12) { nm -= 12; ny += 1; }
  return ny + '-' + pad2(nm);
}
function defaultMonthClient() {
  // 화면 진입 시 현재 월 (오늘 기준)
  const d = new Date();
  return d.getFullYear() + '-' + pad2(d.getMonth() + 1);
}

// ============ Logo ============
function BniLogo() {
  return (
    <svg viewBox="0 0 130 56" height={24} className="bni-logo-svg">
      <text x="0" y="44" fontFamily="Inter,sans-serif" fontWeight="700" fontSize="48" fill="#cd2026" letterSpacing="-1">BNI</text>
      <text x="113" y="22" fontFamily="Inter,sans-serif" fontWeight="600" fontSize="11" fill="#cd2026">®</text>
    </svg>
  );
}

// ============ Toasts ============
function useToasts() {
  const [toasts, setToasts] = useState([]);
  const push = useCallback((msg, kind = 'info', ms = 2200) => {
    const id = Math.random().toString(36).slice(2);
    setToasts((t) => [...t, { id, msg, kind }]);
    setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), ms);
  }, []);
  const node = (
    <div className="toast-wrap">
      {toasts.map((t) => (<div key={t.id} className={`toast ${t.kind}`}>{t.msg}</div>))}
    </div>
  );
  return { push, node };
}

// ============ Login Modal ============
function LoginModal({ onClose, onLoggedIn, toast }) {
  const [pw, setPw] = useState('');
  const [err, setErr] = useState('');
  const [busy, setBusy] = useState(false);
  const [mode, setMode] = useState('login'); // 'login' | 'forgot'
  const [newPw, setNewPw] = useState('');
  const [forgotInfo, setForgotInfo] = useState(null);

  async function submit(e) {
    e?.preventDefault?.();
    setBusy(true); setErr('');
    try {
      const r = await api('/api/auth/login', { method: 'POST', body: JSON.stringify({ password: pw }) });
      setToken(r.token);
      onLoggedIn();
      toast('로그인되었습니다.', 'ok');
    } catch (e) {
      setErr('비밀번호가 올바르지 않습니다.');
    } finally { setBusy(false); }
  }

  async function submitForgot(e) {
    e?.preventDefault?.();
    if (!newPw || newPw.length < 4) { setErr('새 비밀번호는 4자 이상이어야 합니다.'); return; }
    setBusy(true); setErr('');
    try {
      const r = await api('/api/auth/request-change-public', {
        method: 'POST', body: JSON.stringify({ newPassword: newPw })
      });
      setForgotInfo(r);
    } catch (e) {
      setErr('요청 실패: ' + e.message);
    } finally { setBusy(false); }
  }

  return (
    <div className="modal-backdrop">
      <div className="login-card">
        <h2>{mode === 'login' ? '🔐 관리자 로그인' : '비밀번호 잊음'}</h2>
        {mode === 'login' && (
          <form onSubmit={submit}>
            <div className="field">
              <label>비밀번호</label>
              <input type="password" value={pw} autoFocus
                onChange={(e) => setPw(e.target.value)} placeholder="bni1234!" />
            </div>
            {err && <div className="error-msg">{err}</div>}
            <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
              <button className="btn btn-primary" disabled={busy} type="submit">로그인</button>
              <button className="btn btn-ghost" type="button" onClick={() => { setMode('forgot'); setErr(''); }}>비번 잊음</button>
              {onClose && <button className="btn btn-ghost" type="button" onClick={onClose}>닫기</button>}
            </div>
          </form>
        )}
        {mode === 'forgot' && (
          <form onSubmit={submitForgot}>
            <p style={{ fontSize: 13, color: 'var(--body)', marginTop: 0 }}>
              새 비밀번호를 입력하면 등록 메일주소로 승인 링크가 전송됩니다. (30분 내 1회)
            </p>
            <div className="field">
              <label>새 비밀번호</label>
              <input type="password" value={newPw} onChange={(e) => setNewPw(e.target.value)} placeholder="새 비밀번호" />
            </div>
            {err && <div className="error-msg">{err}</div>}
            {forgotInfo && (
              <div className="info-msg">
                ✓ 승인 메일을 발송했습니다. 메일에서 [비밀번호 변경 승인] 버튼을 눌러주세요.
                {forgotInfo.devLink && (
                  <div className="dev-link">
                    SMTP 미설정 (개발 모드) — 아래 링크를 직접 클릭:
                    <br /><a href={forgotInfo.devLink} target="_blank" rel="noreferrer">{forgotInfo.devLink}</a>
                  </div>
                )}
              </div>
            )}
            <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
              <button className="btn btn-primary" disabled={busy} type="submit">승인 요청 발송</button>
              <button className="btn btn-ghost" type="button" onClick={() => { setMode('login'); setErr(''); setForgotInfo(null); }}>로그인 화면으로</button>
            </div>
          </form>
        )}
      </div>
    </div>
  );
}

// ============ Change Password Modal ============
function ChangePwModal({ onClose, toast }) {
  const [newPw, setNewPw] = useState('');
  const [busy, setBusy] = useState(false);
  const [info, setInfo] = useState(null);
  const [err, setErr] = useState('');

  async function submit(e) {
    e?.preventDefault?.();
    if (!newPw || newPw.length < 4) { setErr('4자 이상'); return; }
    setBusy(true); setErr('');
    try {
      const r = await api('/api/auth/request-change', {
        method: 'POST', body: JSON.stringify({ newPassword: newPw })
      });
      setInfo(r);
      toast('승인 메일을 발송했습니다.', 'ok');
    } catch (e) {
      setErr('요청 실패: ' + e.message);
    } finally { setBusy(false); }
  }

  return (
    <div className="modal-backdrop">
      <div className="login-card">
        <h2>🔐 비밀번호 변경</h2>
        <p style={{ fontSize: 13, color: 'var(--body)', marginTop: 0 }}>
          새 비밀번호를 입력하면 등록 메일주소로 승인 링크가 전송됩니다.
        </p>
        <form onSubmit={submit}>
          <div className="field">
            <label>새 비밀번호</label>
            <input type="password" value={newPw} onChange={(e) => setNewPw(e.target.value)} autoFocus />
          </div>
          {err && <div className="error-msg">{err}</div>}
          {info && (
            <div className="info-msg">
              ✓ 승인 메일 발송됨.
              {info.devLink && (
                <div className="dev-link">
                  SMTP 미설정 (개발 모드) — 직접 클릭:
                  <br /><a href={info.devLink} target="_blank" rel="noreferrer">{info.devLink}</a>
                </div>
              )}
            </div>
          )}
          <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
            <button className="btn btn-primary" disabled={busy} type="submit">승인 요청 발송</button>
            <button className="btn btn-ghost" type="button" onClick={onClose}>닫기</button>
          </div>
        </form>
      </div>
    </div>
  );
}

// ============ Header ============
function Header({ yyyymm, period, onPrev, onNext, isAdmin, onLogin, onLogout, onChangePw }) {
  return (
    <header className="app-header">
      <div className="app-header-inner">
        <BniLogo />
        <div className="app-title">Traffic Light Manager</div>
        <div className="period-text">{period?.text || ''}</div>
        <div className="spacer" />
        <div className="month-nav">
          <button className="btn-icon" onClick={onPrev} title="이전 월">◀</button>
          <span className="month-label">{yyyymm}</span>
          <button className="btn-icon" onClick={onNext} title="다음 월">▶</button>
        </div>
        {isAdmin ? (
          <>
            <span className="admin-badge">관리자</span>
            <button className="btn btn-ghost btn-sm" onClick={onChangePw}>비번 변경</button>
            <button className="btn btn-ghost btn-sm" onClick={onLogout}>로그아웃</button>
          </>
        ) : (
          <button className="btn btn-sm" onClick={onLogin}>관리자 로그인</button>
        )}
      </div>
    </header>
  );
}

// ============ Score helpers (mirror server) ============
function scoreAbsent(a){a=Number(a)||0;if(a===0)return 15;if(a===1)return 10;if(a===2)return 5;return 0;}
function scoreLateEarly(v){v=Number(v)||0;if(v===0)return 10;if(v===1)return 5;return 0;}
function scoreWeeklyReferral(w){w=Number(w)||0;if(w<0.5)return 0;if(w<0.75)return 5;if(w<1)return 10;if(w<1.2)return 15;return 20;}
function scoreThanks(t){t=Number(t)||0;if(t<25e6)return 0;if(t<5e7)return 5;if(t<1e8)return 10;return 15;}
function scoreWeeklyVisitor(w){w=Number(w)||0;if(w<0.1)return 0;if(w<0.2)return 5;if(w<0.4)return 10;if(w<0.6)return 15;return 20;}
function scoreWeeklyOneToOne(w){w=Number(w)||0;if(w<1)return 0;if(w<2)return 5;return 10;}
function scoreCEU(c){c=Number(c)||0;if(c<10)return 0;if(c<20)return 5;return 10;}
function trafficLightOf(t){if(t>=70)return 'green';if(t>=50)return 'yellow';if(t>=30)return 'red';return 'black';}

// ============ Main Table ============
const COLUMNS_INPUT = [
  { key: 'attended', label: '출석' },
  { key: 'absent', label: '결석' },
  { key: 'lateEarly', label: '지각조퇴' },
  { key: 'sickLeave', label: '병가' },
  { key: 'substitute', label: '대리인' },
  { key: 'givenT1', label: '준T1' },
  { key: 'givenT2', label: '준T2' },
  { key: 'receivedT1', label: '받은T1' },
  { key: 'receivedT2', label: '받은T2' },
  { key: 'visitor', label: '비지터' },
  { key: 'oneToOne', label: '1-2-1' },
  { key: 'thanks', label: '감사장' },
  { key: 'ceu', label: 'CEU' }
];

function MainTable({ members, isAdmin, selected, onToggle, onToggleAll, onRowClick, onCellEdit, onJoinedEdit }) {
  const total = members.length;
  const sel = selected.size;
  const allChecked = total > 0 && sel === total;
  const indeterminate = sel > 0 && sel < total;
  const headerCheckRef = useRef(null);
  useEffect(() => {
    if (headerCheckRef.current) headerCheckRef.current.indeterminate = indeterminate;
  }, [indeterminate]);
  return (
    <div className="table-wrap">
      <table className="main-table">
        <thead>
          <tr>
            {isAdmin && (
              <th className="col-checkbox sticky-left" title={allChecked ? '전체 해제' : '전체 선택'}>
                <input
                  ref={headerCheckRef}
                  type="checkbox"
                  checked={allChecked}
                  onChange={() => onToggleAll(allChecked ? 'none' : 'all')}
                  onClick={(e) => e.stopPropagation()}
                />
              </th>
            )}
            <th className={`col-name ${isAdmin ? '' : 'sticky-left'}`}>이름(한글)</th>
            <th>입회년월</th>
            <th className="col-total">Total(100)</th>
            <th className="col-light">Traffic Light</th>
            {COLUMNS_INPUT.map((c) => <th key={c.key}>{c.label}</th>)}
            <th>리퍼럴</th>
            <th>주당리퍼럴</th>
            <th>주당비지터</th>
            <th>주당1-2-1</th>
            <th>결석(15)</th>
            <th>정시도착(10)</th>
            <th>주당리퍼럴(20)</th>
            <th>감사장(15)</th>
            <th>비지터(20)</th>
            <th>1-2-1(10)</th>
            <th>교육참가(10)</th>
          </tr>
        </thead>
        <tbody>
          {members.map((m) => {
            const c = m.computed || {};
            const sc = c.scores || {};
            const isSel = selected.has(m.nameKr);
            return (
              <tr key={m.nameKr}
                  className={isSel ? 'selected' : ''}
                  onClick={() => onRowClick(m)}>
                {isAdmin && (
                  <td className="col-checkbox sticky-left">
                    <input
                      type="checkbox"
                      checked={isSel}
                      onChange={() => onToggle(m.nameKr)}
                      onClick={(e) => e.stopPropagation()}
                    />
                  </td>
                )}
                <td className={`col-name ${isAdmin ? '' : 'sticky-left'}`}>{m.nameKr}</td>
                <td>
                  <span
                    className={m.joinedAt ? 'joined-readonly' : 'joined-readonly placeholder-soft'}
                    title="입회년월은 고정 데이터베이스에서 자동 적용됩니다."
                  >{m.joinedAt || '0000.00'}</span>
                </td>
                <td className="col-total">{formatNum(sc.total, 2)}</td>
                <td className="col-light">
                  <span className="light-pill">
                    <span className={`dot ${c.light || 'black'}`}></span>
                    {c.light || ''}
                  </span>
                </td>
                {COLUMNS_INPUT.map((col) => (
                  <td key={col.key}>
                    {isAdmin ? (
                      <input className="cell-edit"
                        type="number"
                        defaultValue={m[col.key] ?? 0}
                        onClick={(e) => e.stopPropagation()}
                        onBlur={(e) => onCellEdit(m.nameKr, col.key, e.target.value)} />
                    ) : (
                      col.key === 'thanks' ? formatComma(m[col.key]) : (m[col.key] ?? 0)
                    )}
                  </td>
                ))}
                <td>{c.referral ?? 0}</td>
                <td>{formatNum(c.weeklyReferral, 2)}</td>
                <td>{formatNum(c.weeklyVisitor, 2)}</td>
                <td>{formatNum(c.weeklyOneToOne, 2)}</td>
                <td>{formatNum(sc.absent, 2)}</td>
                <td>{formatNum(sc.lateEarly, 2)}</td>
                <td>{formatNum(sc.weeklyReferral, 2)}</td>
                <td>{formatNum(sc.thanks, 2)}</td>
                <td>{formatNum(sc.visitor, 2)}</td>
                <td>{formatNum(sc.oneToOne, 2)}</td>
                <td>{formatNum(sc.ceu, 2)}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

// ============ Personal Modal ============
function PersonalModal({ yyyymm, member, isAdmin, onClose, toast, onAfterDelete }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [currentMonthWeeks, setCurrentMonthWeeks] = useState(2);
  const [plan, setPlan] = useState({ referral: 0, thanks: 0, visitor: 0, oneToOne: 0, ceu: 0 });
  const [dirty, setDirty] = useState(false);
  const [saveStatus, setSaveStatus] = useState(''); // '', 'saved', 'failed', 'unsaved'
  const [busy, setBusy] = useState(false);
  const fileRef = useRef(null);

  async function load() {
    setLoading(true);
    try {
      const r = await api(`/api/personal/${encodeURIComponent(yyyymm)}/${encodeURIComponent(member.nameKr)}`);
      setData(r);
      if (r.savedPlan) {
        setCurrentMonthWeeks(r.savedPlan.currentMonthWeeks || 2);
        setPlan(r.savedPlan.plans);
      } else {
        setCurrentMonthWeeks(2);
        setPlan({ referral: 0, thanks: 0, visitor: 0, oneToOne: 0, ceu: 0 });
      }
      setDirty(false);
      setSaveStatus('');
    } catch (e) {
      toast('개인 데이터 불러오기 실패', 'err');
    } finally {
      setLoading(false);
    }
  }
  useEffect(() => { load(); /* eslint-disable-next-line */ }, [yyyymm, member.nameKr]);

  function markDirty() {
    setDirty(true);
    setSaveStatus('unsaved');
  }

  async function uploadPersonalFile(file) {
    if (!file) return;
    setBusy(true);
    const fd = new FormData();
    fd.append('file', file);
    fd.append('yyyymm', yyyymm);
    try {
      await api('/api/personal/upload', { method: 'POST', body: fd });
      toast('개인 점검표 업로드 완료', 'ok');
      await load();
    } catch (err) {
      toast(err.message || '업로드 실패', 'err');
    } finally {
      if (fileRef.current) fileRef.current.value = '';
      setBusy(false);
    }
  }
  async function uploadPersonal(e) {
    e?.preventDefault?.();
    const f = fileRef.current?.files?.[0];
    if (!f) { toast('파일을 선택하세요', 'err'); return; }
    await uploadPersonalFile(f);
  }

  async function deletePersonal() {
    if (!confirm('이 회원의 개인 점검표/계획을 삭제할까요?')) return;
    try {
      await api(`/api/personal/${encodeURIComponent(yyyymm)}/${encodeURIComponent(member.nameKr)}`, { method: 'DELETE' });
      toast('삭제되었습니다.', 'ok');
      onAfterDelete?.();
    } catch (e) { toast('삭제 실패: ' + e.message, 'err'); }
  }

  async function savePlan() {
    setBusy(true);
    try {
      await api(`/api/personal/${encodeURIComponent(yyyymm)}/${encodeURIComponent(member.nameKr)}/plan`, {
        method: 'POST',
        body: JSON.stringify({ currentMonthWeeks: Number(currentMonthWeeks) || 0, plans: plan })
      });
      setDirty(false);
      setSaveStatus('saved');
    } catch (e) {
      setSaveStatus('failed');
    } finally { setBusy(false); }
  }

  function planMaxScore() {
    if (!data) return;
    const totalWeeks = (data.weeks5m || 0) + (Number(currentMonthWeeks) || 0);
    const sum = data.sum5m || {};
    const next = {
      referral: Math.max(0, Math.ceil(totalWeeks * 1.2 - (sum.referral || 0))),
      thanks: Math.max(0, 100_000_000 - (sum.thanks || 0)),
      visitor: Math.max(0, Math.ceil(totalWeeks * 0.6 - (sum.visitor || 0))),
      oneToOne: Math.max(0, Math.ceil(totalWeeks * 2 - (sum.oneToOne || 0))),
      ceu: Math.max(0, 20 - (sum.ceu || 0))
    };
    setPlan(next);
    markDirty();
  }
  function planZero() {
    setPlan({ referral: 0, thanks: 0, visitor: 0, oneToOne: 0, ceu: 0 });
    markDirty();
  }

  // 5개월 기준 점수 + 6개월 (누적+계획) 기준 점수
  const computed = useMemo(() => {
    if (!data) return null;
    const sum = data.sum5m || {};
    const w5 = data.weeks5m || 0;
    const cw = Number(currentMonthWeeks) || 0;
    const totalWeeks = w5 + cw;

    const ref6 = (sum.referral || 0) + (Number(plan.referral) || 0);
    const thanks6 = (sum.thanks || 0) + (Number(plan.thanks) || 0);
    const vis6 = (sum.visitor || 0) + (Number(plan.visitor) || 0);
    const o2o6 = (sum.oneToOne || 0) + (Number(plan.oneToOne) || 0);
    const ceu6 = (sum.ceu || 0) + (Number(plan.ceu) || 0);

    // 누적점수: 누적수치를 전체주수(누적+잔여)로 나눠 계산 — 실행계획 미반영
    const w5Ref = totalWeeks > 0 ? (sum.referral || 0) / totalWeeks : 0;
    const w5Vis = totalWeeks > 0 ? (sum.visitor || 0) / totalWeeks : 0;
    const w5One = totalWeeks > 0 ? (sum.oneToOne || 0) / totalWeeks : 0;
    const sc5 = {
      absent: scoreAbsent(sum.absent),
      lateEarly: scoreLateEarly(sum.lateEarly),
      referral: scoreWeeklyReferral(w5Ref),
      thanks: scoreThanks(sum.thanks),
      visitor: scoreWeeklyVisitor(w5Vis),
      oneToOne: scoreWeeklyOneToOne(w5One),
      ceu: scoreCEU(sum.ceu)
    };
    const sc5Total = sc5.absent + sc5.lateEarly + sc5.referral + sc5.thanks + sc5.visitor + sc5.oneToOne + sc5.ceu;

    // 6개월 (누적+계획) 기준
    const wkRef6 = totalWeeks > 0 ? ref6 / totalWeeks : 0;
    const wkVis6 = totalWeeks > 0 ? vis6 / totalWeeks : 0;
    const wkOne6 = totalWeeks > 0 ? o2o6 / totalWeeks : 0;
    const sc6 = {
      absent: scoreAbsent(sum.absent),
      lateEarly: scoreLateEarly(sum.lateEarly),
      referral: scoreWeeklyReferral(wkRef6),
      thanks: scoreThanks(thanks6),
      visitor: scoreWeeklyVisitor(wkVis6),
      oneToOne: scoreWeeklyOneToOne(wkOne6),
      ceu: scoreCEU(ceu6)
    };
    const sc6Total = sc6.absent + sc6.lateEarly + sc6.referral + sc6.thanks + sc6.visitor + sc6.oneToOne + sc6.ceu;

    return {
      totalWeeks, w5, cw,
      sum, ref6, thanks6, vis6, o2o6, ceu6,
      wkRef6, wkVis6, wkOne6, w5Ref, w5Vis, w5One,
      sc5, sc5Total, sc6, sc6Total,
      light5: trafficLightOf(sc5Total), light6: trafficLightOf(sc6Total)
    };
  }, [data, plan, currentMonthWeeks]);

  if (loading) {
    return (
      <div className="modal-backdrop" onClick={onClose}>
        <div className="modal" onClick={(e) => e.stopPropagation()}>
          <div className="modal-head">
            <h2>◎ 개인 트래픽라이트 점검표</h2>
            <button className="close-btn" onClick={onClose}>×</button>
          </div>
          <div className="modal-body">불러오는 중…</div>
        </div>
      </div>
    );
  }
  if (!data || !computed) return null;

  const m = data.member;
  const periodFull = data.ranges.full.text;
  const period5m = data.ranges.fiveMonth.text;
  const periodLast = data.ranges.lastMonth.text;

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <h2>◎ 개인 트래픽라이트 점검표</h2>
          <span className="modal-meta">
            · 누적 {computed.w5}주 + 잔여 {computed.cw}주 = 전체 {computed.totalWeeks}주
          </span>
          <div style={{ marginLeft: 'auto', display: 'flex', gap: 8, alignItems: 'center' }}>
            {isAdmin && (
              <>
                <input
                  ref={fileRef}
                  type="file"
                  accept=".xlsx,.xls"
                  style={{ display: 'none' }}
                  onChange={(e) => uploadPersonalFile(e.target.files?.[0])}
                />
                <button
                  className="btn btn-sm btn-primary"
                  onClick={() => fileRef.current?.click()}
                  disabled={busy}
                  title={`${m.nameKr} 개인 점검표 업로드`}
                >📤 업로드</button>
                <a
                  className="btn btn-sm btn-tertiary"
                  href={busted(`/api/template/personal?name=${encodeURIComponent(m.nameKr)}`)}
                  title="개인 양식 다운로드"
                >📄 양식</a>
              </>
            )}
            {isAdmin && data.hasPersonal && (
              <button className="btn btn-danger btn-sm" onClick={deletePersonal}>🗑 삭제</button>
            )}
            <button className="close-btn" onClick={onClose}>×</button>
          </div>
        </div>
        <div className="modal-body">
          <div className="member-info-grid">
            <div className="info"><div className="lab">멤버</div>
              <div className="val">{m.nameKr}{m.nameEn ? ` (${m.nameEn})` : ''}</div></div>
            <div className="info"><div className="lab">전체 평가기간</div>
              <div className="val">{periodFull}</div></div>
            <div className="info"><div className="lab">이번 달</div>
              <div className="val">{periodLast}</div></div>
          </div>

          <div className="weeks-row">
            <span className="weeks-label">누적주수: <b>{computed.w5}주</b> (수정불가)</span>
            <span className="weeks-label">
              잔여주수:{' '}
              <input type="number" min="1" max="20"
                value={currentMonthWeeks}
                disabled={!isAdmin}
                onChange={(e) => { setCurrentMonthWeeks(e.target.value); markDirty(); }} /> 주
            </span>
            {isAdmin && (
              <>
                <button className="btn btn-sm btn-primary" onClick={savePlan} disabled={busy}>💾 저장</button>
                <button className="btn btn-sm btn-tertiary" onClick={planMaxScore}>🎯 최대점수</button>
                <button className="btn btn-sm" onClick={planZero}>↺ 0 초기화</button>
              </>
            )}
            <span className={`save-status ${saveStatus === 'saved' ? 'ok' : saveStatus === 'failed' ? 'err' : saveStatus === 'unsaved' ? 'warn' : ''}`}>
              {saveStatus === 'saved' && '✓ 저장되었습니다.'}
              {saveStatus === 'failed' && '✗ 저장에 실패했습니다. 다시 시도해 주세요.'}
              {saveStatus === 'unsaved' && '⚠ 저장되지 않은 변경사항이 있습니다.'}
            </span>
          </div>

          <ScoreTable computed={computed} plan={plan} setPlan={(p) => { setPlan(p); markDirty(); }} isAdmin={isAdmin} />

        </div>
      </div>
    </div>
  );
}

function ScoreTable({ computed, plan, setPlan, isAdmin }) {
  const sum = computed.sum;
  const sc5 = computed.sc5, sc6 = computed.sc6;
  const update = (key, val) => setPlan({ ...plan, [key]: Math.max(0, Number(val) || 0) });

  return (
    <table className="score-table">
      <thead>
        <tr>
          <th style={{ textAlign: 'left' }}>구분</th>
          <th>누적수치</th>
          <th>실행계획</th>
          <th>6개월 수치</th>
          <th>누적점수</th>
          <th>다음달 점수</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>결석</td><td>-</td><td>-</td><td>-</td>
          <td>{formatNum(sc5.absent, 2)}</td>
          <td>{formatNum(sc6.absent, 2)}</td>
        </tr>
        <tr>
          <td>지각/조퇴</td><td>-</td><td>-</td><td>-</td>
          <td>{formatNum(sc5.lateEarly, 2)}</td>
          <td>{formatNum(sc6.lateEarly, 2)}</td>
        </tr>
        <tr>
          <td>리퍼럴(건)</td>
          <td>{formatNum(sum.referral, 2)}</td>
          <td>{isAdmin ? <input type="number" min="0" value={plan.referral} onChange={(e) => update('referral', e.target.value)} /> : plan.referral}</td>
          <td>{formatNum(computed.wkRef6, 2)}</td>
          <td>{formatNum(sc5.referral, 2)}</td>
          <td>{formatNum(sc6.referral, 2)}</td>
        </tr>
        <tr>
          <td>감사장(원)</td>
          <td>{formatComma(sum.thanks)}</td>
          <td>{isAdmin ? (
            <input
              type="text"
              inputMode="numeric"
              value={Number(plan.thanks || 0).toLocaleString('ko-KR')}
              onChange={(e) => update('thanks', String(e.target.value).replace(/[^\d]/g, ''))}
            />
          ) : formatComma(plan.thanks)}</td>
          <td>{formatComma(computed.thanks6)}</td>
          <td>{formatNum(sc5.thanks, 2)}</td>
          <td>{formatNum(sc6.thanks, 2)}</td>
        </tr>
        <tr>
          <td>비지터(명)</td>
          <td>{formatNum(sum.visitor, 2)}</td>
          <td>{isAdmin ? <input type="number" min="0" value={plan.visitor} onChange={(e) => update('visitor', e.target.value)} /> : plan.visitor}</td>
          <td>{formatNum(computed.wkVis6, 2)}</td>
          <td>{formatNum(sc5.visitor, 2)}</td>
          <td>{formatNum(sc6.visitor, 2)}</td>
        </tr>
        <tr>
          <td>원투원(회)</td>
          <td>{formatNum(sum.oneToOne, 2)}</td>
          <td>{isAdmin ? <input type="number" min="0" value={plan.oneToOne} onChange={(e) => update('oneToOne', e.target.value)} /> : plan.oneToOne}</td>
          <td>{formatNum(computed.wkOne6, 2)}</td>
          <td>{formatNum(sc5.oneToOne, 2)}</td>
          <td>{formatNum(sc6.oneToOne, 2)}</td>
        </tr>
        <tr>
          <td>트레이닝(P)</td>
          <td>{Math.floor(sum.ceu || 0)}</td>
          <td>{isAdmin ? <input type="number" min="0" value={plan.ceu} onChange={(e) => update('ceu', e.target.value)} /> : plan.ceu}</td>
          <td>{Math.floor(computed.ceu6 || 0)}</td>
          <td>{formatNum(sc5.ceu, 2)}</td>
          <td>{formatNum(sc6.ceu, 2)}</td>
        </tr>
      </tbody>
      <tfoot>
        <tr>
          <td>계</td>
          <td></td>
          <td></td>
          <td></td>
          <td>{formatNum(computed.sc5Total, 2)}</td>
          <td>{formatNum(computed.sc6Total, 2)}</td>
        </tr>
      </tfoot>
    </table>
  );
}

// ============ App ============
function App() {
  const { push: toast, node: toastNode } = useToasts();

  const [yyyymm, setYyyymm] = useState(defaultMonthClient());
  const [data, setData] = useState(null); // { title, period, members, empty }
  const [adminToken, setAdminTokenState] = useState(getToken());
  const isAdmin = !!adminToken;

  const [showLogin, setShowLogin] = useState(false);
  const [showChangePw, setShowChangePw] = useState(false);
  const [selected, setSelected] = useState(new Set());
  const [openMember, setOpenMember] = useState(null); // member object

  const mainFileRef = useRef(null);
  const [showPortrait, setShowPortrait] = useState(false);
  const [overrodePortrait, setOverrodePortrait] = useState(false);

  // Listen for unauthorized
  useEffect(() => {
    function onUnauth() {
      setAdminTokenState('');
      setShowLogin(true);
      toast('세션이 만료되었습니다. 다시 로그인해 주세요.', 'err');
    }
    window.addEventListener('bni:unauth', onUnauth);
    return () => window.removeEventListener('bni:unauth', onUnauth);
  }, [toast]);

  // Portrait detection
  useEffect(() => {
    function check() {
      const isPortrait = window.matchMedia('(orientation: portrait) and (max-width: 768px)').matches;
      setShowPortrait(isPortrait && !overrodePortrait);
    }
    check();
    window.addEventListener('resize', check);
    window.addEventListener('orientationchange', check);
    return () => {
      window.removeEventListener('resize', check);
      window.removeEventListener('orientationchange', check);
    };
  }, [overrodePortrait]);

  async function load() {
    try {
      const r = await api(`/api/main/${encodeURIComponent(yyyymm)}`);
      setData(r);
      setSelected(new Set());
    } catch (e) {
      toast('데이터 불러오기 실패: ' + e.message, 'err');
    }
  }
  useEffect(() => { load(); /* eslint-disable-next-line */ }, [yyyymm]);

  // Default month from server (initial)
  useEffect(() => {
    api('/api/months').then((r) => {
      if (r.months && r.months.length && !r.months.includes(yyyymm)) {
        setYyyymm(r.months[0]);
      }
    }).catch(() => {});
    // eslint-disable-next-line
  }, []);

  function loginDone() {
    setAdminTokenState(getToken());
    setShowLogin(false);
  }
  async function logout() {
    try { await api('/api/auth/logout', { method: 'POST' }); } catch (e) {}
    setToken('');
    setAdminTokenState('');
    toast('로그아웃되었습니다.', 'info');
  }

  async function uploadMain(e) {
    e?.preventDefault?.();
    const f = mainFileRef.current?.files?.[0];
    if (!f) { toast('엑셀 파일을 선택하세요', 'err'); return; }
    const fd = new FormData();
    fd.append('file', f);
    fd.append('yyyymm', yyyymm);
    try {
      const r = await api('/api/main/upload', { method: 'POST', body: fd });
      mainFileRef.current.value = '';
      let msg = `업로드 완료 — ${r.count}명`;
      if (r.replaced) msg += ' · 기존 데이터 교체';
      if (r.cleanedPersonals) msg += ` · 개인 ${r.cleanedPersonals}건 정리`;
      toast(msg, 'ok');
      load();
    } catch (e) {
      toast('업로드 실패: ' + e.message, 'err');
    }
  }

  async function deleteSelected() {
    if (!selected.size) return;
    if (!confirm(`선택한 ${selected.size}명을 삭제할까요?`)) return;
    try {
      await api(`/api/main/${encodeURIComponent(yyyymm)}/members/delete`, {
        method: 'POST',
        body: JSON.stringify({ names: Array.from(selected) })
      });
      toast('삭제 완료', 'ok');
      setSelected(new Set());
      load();
    } catch (e) {
      toast('삭제 실패: ' + e.message, 'err');
    }
  }

  async function clearAllData() {
    if (!confirm('모든 데이터(메인/개인/계획)를 초기화합니다. 계속할까요?')) return;
    try {
      await api('/api/data/all', { method: 'DELETE' });
      toast('전체 초기화 완료', 'ok');
      load();
    } catch (e) {
      toast('초기화 실패: ' + e.message, 'err');
    }
  }

  function toggleSelect(name) {
    setSelected((s) => {
      const next = new Set(s);
      if (next.has(name)) next.delete(name); else next.add(name);
      return next;
    });
  }

  function toggleSelectAll(mode) {
    if (mode === 'all') {
      setSelected(new Set((data?.members || []).map((m) => m.nameKr)));
    } else {
      setSelected(new Set());
    }
  }

  async function commitMembers(updater) {
    const next = (data.members || []).map((m) => {
      const x = { ...m };
      delete x.computed;
      return x;
    });
    updater(next);
    try {
      await api(`/api/main/${encodeURIComponent(yyyymm)}/edit`, {
        method: 'POST',
        body: JSON.stringify({ members: next })
      });
      load();
    } catch (e) {
      toast('저장 실패: ' + e.message, 'err');
    }
  }

  function onCellEdit(name, key, val) {
    commitMembers((arr) => {
      const i = arr.findIndex((m) => m.nameKr === name);
      if (i < 0) return;
      const v = key === 'thanks' ? Number(String(val).replace(/[,\s]/g, '')) || 0 : Number(val) || 0;
      arr[i] = { ...arr[i], [key]: v };
    });
  }
  function onJoinedEdit(name, val) {
    commitMembers((arr) => {
      const i = arr.findIndex((m) => m.nameKr === name);
      if (i < 0) return;
      arr[i] = { ...arr[i], joinedAt: String(val || '').trim() };
    });
  }

  // ===== Render =====
  return (
    <>
      <Header
        yyyymm={yyyymm}
        period={data?.period}
        onPrev={() => setYyyymm((m) => shiftMonth(m, -1))}
        onNext={() => setYyyymm((m) => shiftMonth(m, +1))}
        isAdmin={isAdmin}
        onLogin={() => setShowLogin(true)}
        onLogout={logout}
        onChangePw={() => setShowChangePw(true)}
      />

      <main className="app-main">
        {isAdmin && (
          <div className="admin-panel">
            <div className="admin-row">
              <span className="row-label">📤 메인 업로드</span>
              <form onSubmit={uploadMain} style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
                <input ref={mainFileRef} type="file" accept=".xlsx,.xls" className="file-input" />
                <button className="btn btn-sm btn-primary" type="submit">업로드</button>
              </form>
              <a className="btn btn-sm btn-tertiary" href={busted('/api/template/main')}>📄 메인 양식</a>
              <a className="btn btn-sm" href={busted(`/api/main/${encodeURIComponent(yyyymm)}/export`)}>⬇ 결과 엑셀</a>
            </div>
            <div className="admin-row">
              <span className="row-label">선택삭제</span>
              <button className="btn btn-sm btn-danger" disabled={!selected.size} onClick={deleteSelected}>
                선택삭제 (<span className="checkbox-count">{selected.size}</span>)
              </button>
              <button className="btn btn-sm btn-danger" onClick={clearAllData}>전체 데이터 초기화</button>
            </div>
            <div className="admin-row">
              <span className="row-label">비번 변경</span>
              <button className="btn btn-sm" onClick={() => setShowChangePw(true)}>비밀번호 변경 요청</button>
              <span style={{ fontSize: 12, color: 'var(--body)' }}>※ 등록 메일 승인 후 적용</span>
            </div>
            <div className="admin-row">
              <span className="row-label">개인 업로드</span>
              <span style={{ fontSize: 13, color: 'var(--body)' }}>회원 행 클릭 → 개인 모달 안에서 업로드</span>
            </div>
          </div>
        )}

        {!isAdmin && (
          <div className="admin-row" style={{ marginBottom: 16 }}>
            <a className="btn btn-sm btn-tertiary" href={busted('/api/template/main')}>📄 메인 양식</a>
            <a className="btn btn-sm" href={busted(`/api/main/${encodeURIComponent(yyyymm)}/export`)}>⬇ 결과 엑셀</a>
          </div>
        )}

        {!data ? (
          <div className="empty-state">로딩 중…</div>
        ) : data.empty ? (
          <div className="empty-state">
            <h3>{yyyymm} 데이터가 없습니다</h3>
            <p>{isAdmin ? '메인 엑셀을 업로드하거나 다른 월로 이동하세요.' : '관리자가 데이터를 업로드할 때까지 기다려 주세요.'}</p>
          </div>
        ) : (
          <MainTable
            members={data.members}
            isAdmin={isAdmin}
            selected={selected}
            onToggle={toggleSelect}
            onToggleAll={toggleSelectAll}
            onRowClick={(m) => setOpenMember(m)}
            onCellEdit={onCellEdit}
            onJoinedEdit={onJoinedEdit}
          />
        )}
      </main>

      {showLogin && (
        <LoginModal onClose={() => setShowLogin(false)} onLoggedIn={loginDone} toast={toast} />
      )}
      {showChangePw && (
        <ChangePwModal onClose={() => setShowChangePw(false)} toast={toast} />
      )}
      {openMember && (
        <PersonalModal
          yyyymm={yyyymm}
          member={openMember}
          isAdmin={isAdmin}
          onClose={() => setOpenMember(null)}
          toast={toast}
          onAfterDelete={() => { setOpenMember(null); load(); }}
        />
      )}

      {showPortrait && (
        <div className="portrait-warning show">
          <div className="rotate-icon">🔄</div>
          <h3>휴대폰을 가로로 돌려주세요</h3>
          <p>이 앱은 가로 모드에서 가장 잘 보입니다. 한 손에 들고 살짝 기울여 주세요.</p>
          <button className="btn" onClick={() => setOverrodePortrait(true)}>그래도 보기</button>
        </div>
      )}

      {toastNode}
    </>
  );
}

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