/* ============================================================
   바른셈 — 변동분 검수 워크플로 (§8 상태 전이 + 잠금)
   ============================================================ */
function Review({ ctx }) {
  const { clients, statusOf, go, addAudit, toast, t, period, auditOf, hrChangesOf, rejectClient, transition, gateFor } = ctx;
  const FLOW = window.BARUNSEM.FLOW;
  const client = clients.find((c) => c.id === (ctx.params.clientId || clients[0].id)) || clients[0];
  const st = statusOf(client.id);
  const [mark, setMark] = useState({});            // e.id → "ok" | "issue" (상호배타 triage)
  const [showAll, setShowAll] = useState(false);   // 변동 없는 행 펼치기
  const [rejectOpen, setRejectOpen] = useState(false);
  const [rejectText, setRejectText] = useState("");
  const [openTs, setOpenTs] = useState(null);                // §06 근태 근거 drill-down 대상 empId
  const [dismissTarget, setDismissTarget] = useState(null);  // §01 경고 끄기 대상
  const [dismissReason, setDismissReason] = useState("");
  const [dismissMonths, setDismissMonths] = useState(3);   // C9 — 무기한 끄기 금지. 기본 3개월 후 재발화.
  const [resolveTarget, setResolveTarget] = useState(null);  // 근로자 이의제기 처리 대상
  const [resolveMemo, setResolveMemo] = useState("");
  const [rejectCode, setRejectCode] = useState("근거자료 누락");   // §05 반려 사유 구조화
  const [rejectScope, setRejectScope] = useState("client");
  const [rejectTarget, setRejectTarget] = useState("");

  useEffect(() => { setMark({}); setShowAll(false); }, [client.id]);

  const emps = client.employees;
  const reviewable = st === "in_review";
  // §01 검수 자동 점검 findings (순수함수 소비 — 읽기전용 신호). 확정은 노무사.
  const audit = ctx.clientAudit(client);
  const findingsByEmp = {};
  audit.findings.forEach((f) => { if (f.empId) (findingsByEmp[f.empId] = findingsByEmp[f.empId] || []).push(f); });
  const canDismiss = ["submitted", "in_review", "locked", "calculated"].includes(st);
  const dismissedHere = client.employees.reduce((s, e) => s + ((ctx.dismissals[e.id] || []).length), 0);
  // 변동분 판정 — 이번달 variation 이 전월(prevVariation)과 다르면 '변동 있음'
  const VKEYS = ["overtime", "night", "holiday", "holidayOver", "bonus", "absentDays"];
  const hasChange = (e) => { const v = e.variation || {}, p = e.prevVariation || {}; if (e.hourlyMode && (e.workedHours || 0) !== (e.prevWorked || 0)) return true; if ((v.etcDeductions || []).length) return true; return VKEYS.some((k) => (v[k] || 0) !== (p[k] || 0)); };
  const sevRank = (id) => { const fs = findingsByEmp[id] || []; return fs.some((f) => f.severity === "error") ? 0 : fs.some((f) => f.severity === "warn") ? 1 : fs.length ? 2 : 3; };
  // W3-1 — 플래그 있는 행이 위로. 20명 표에서 아래로 스크롤해야 보이는 신호는 없는 신호와 같다(E1).
  const changedEmps = emps.filter(hasChange).sort((a, b) => sevRank(a.id) - sevRank(b.id));
  const unchangedEmps = emps.filter((e) => !hasChange(e));
  const okCount = changedEmps.filter((e) => mark[e.id] === "ok").length;
  const issueEmps = changedEmps.filter((e) => mark[e.id] === "issue");
  const issueCount = issueEmps.length;
  const triagedCount = okCount + issueCount;
  /* C6·C8 잠금 게이트 —
     · 확인 단위는 근로자가 아니라 (근로자 × 플래그 코드). 플래그 3건인데 하나 보고 셋이 처리되면 안 된다.
     · 변동 없는 근로자의 error 플래그도 잠금을 막는다. 전엔 changedEmps만 봐서 그냥 통과했다. */
  const openErrors = ctx.openErrorsOf(client);
  const openErrorKeys = openErrors.filter((f) => f.empId).map((f) => f.empId + "|" + f.code);
  const unresolvedErrors = openErrorKeys.filter((k) => mark[k] !== "ok");
  const allReviewed = changedEmps.every((e) => mark[e.id] === "ok") && unresolvedErrors.length === 0;
  const setOk = (id) => setMark((m) => ({ ...m, [id]: m[id] === "ok" ? undefined : "ok" }));
  const setIssue = (id) => setMark((m) => ({ ...m, [id]: m[id] === "issue" ? undefined : "issue" }));
  // 일괄 확인은 '플래그 없는 행만'. 미해결 플래그를 일괄로 덮으면 검수가 형식이 된다(C6).
  const flaggedIds = new Set(audit.findings.filter((f) => f.severity !== "info" && f.empId).map((f) => f.empId));
  const bulkTargets = changedEmps.filter((e) => !flaggedIds.has(e.id) && mark[e.id] !== "issue");
  const bulkConfirm = () => {
    setMark((m) => { const n = { ...m }; bulkTargets.forEach((e) => { n[e.id] = "ok"; }); return n; });
    const skipped = changedEmps.length - bulkTargets.length;
    toast(`플래그 없는 ${bulkTargets.length}명을 확인 처리했습니다${skipped ? ` · 플래그 ${skipped}명은 개별 확인이 필요합니다` : ""}`, "ok");
  };
  const idx = FLOW.findIndex((f) => f.key === st);

  // 표 행 데이터 셀 (변동 행·자동통과 행 공용)
  const dataCells = (e) => { const v = e.variation, p = e.prevVariation || {}; return (
    <>
      <td className="tc-name"><div style={{ fontWeight: 650, display: "flex", alignItems: "center" }}>{e.name}<FindingDot items={findingsByEmp[e.id]} />{e.timesheet && <button title="근태 raw → 변동분 산출 근거" onClick={() => setOpenTs(openTs === e.id ? null : e.id)} style={{ marginLeft: 6, display: "inline-flex", alignItems: "center", gap: 3, height: 18, padding: "0 6px", borderRadius: 5, border: "1px solid var(--accent-line)", background: openTs === e.id ? "var(--accent)" : "var(--accent-soft)", color: openTs === e.id ? "#fff" : "var(--accent-ink)", fontSize: 10, fontWeight: 700, cursor: "pointer" }}><Ms name="schedule" style={{ fontSize: 12 }} />근태</button>}</div><div className="muted" style={{ fontSize: 11.5 }}>{e.dept} · {e.title}</div></td>
      <td data-label="급여체계"><SchemeTag scheme={e.scheme} />{!e.fiveOrMore && <span className="tag tag-warn" style={{ marginLeft: 4 }}>5인미만</span>}</td>
      <td className="r num" data-label="근로시간">{e.hourlyMode
        ? <div style={{ display: "flex", gap: 6, justifyContent: "flex-end", alignItems: "center" }}>{e.workedHours}h<Delta now={e.workedHours} prev={e.prevWorked} unit="h" /></div>
        : <span className="muted" title="월급·연봉제 — 소정근로시간 고정">고정</span>}</td>
      <td className="r num" data-label="연장(h)"><div style={{ display: "flex", gap: 6, justifyContent: "flex-end", alignItems: "center" }}>{v.overtime ? v.overtime : <span className="muted">0</span>}{v.overtime !== undefined && <Delta now={v.overtime || 0} prev={p.overtime} unit="h" />}</div></td>
      <td className="r num" data-label="야간(h)">{v.night ? <div style={{ display: "flex", gap: 6, justifyContent: "flex-end", alignItems: "center" }}>{v.night}<Delta now={v.night} prev={p.night} unit="h" /></div> : <span className="muted">0</span>}</td>
      <td className="r num" data-label="휴일(h)">{(v.holiday || v.holidayOver) ? ((v.holiday || 0) + (v.holidayOver || 0)) : <span className="muted">0</span>}</td>
      <td className="r num" data-label="상여">{v.bonus ? KRW(v.bonus) : <span className="muted">—</span>}</td>
      <td data-label="기타공제">{(v.etcDeductions || []).length ? v.etcDeductions.map((d) => <span key={d.name} className="tag" style={{ marginRight: 4 }}>{d.name} {KRW(d.amount)}</span>) : <span className="muted">—</span>}</td>
    </>
  ); };
  const log = auditOf(client.id);

  const startReview = () => transition(client.id, "in_review", { toastText: "검수를 시작했습니다" });
  const lock = (reason) => {
    const r = transition(client.id, "locked", { reason,
      label: `검수 완료·입력 잠금 (변동 ${changedEmps.length}명 확인 · 변동없음 ${unchangedEmps.length}명 자동)`,
      toastText: "입력을 잠갔습니다 — 이후 변동분 동결" });
    if (r.ok) setLockOpen(false);
  };
  // C6 — 게이트는 잠금 시점에 선다. 계산 실행 때 처음 묻던 것을 한 단계 앞으로 옮겼다.
  const tryLock = () => {
    if (!ctx.canReview) return toast("검수·입력 잠금은 노무사만 가능합니다", "warn");
    if (unresolvedErrors.length) return setLockOpen(true);          // 사유를 받고 진행
    if (!allReviewed) return toast(`변동 ${changedEmps.length}명 중 ${okCount}명 확인 — 남은 행을 확인하거나 '변동 모두 확인'을 쓰세요`, "warn");
    lock();
  };
  const calculate = () => {
    const r = transition(client.id, "calculated", { toastText: "급여 계산을 완료했습니다" });
    // 지연 이동은 그 사이 사용자가 누른 탭을 덮어써 해시와 화면이 어긋난다 — 바로 옮긴다.
    if (r.ok) go("calc", { clientId: client.id });
  };

  const doDismiss = () => {
    if (!dismissTarget || !dismissReason.trim()) return;
    const until = ctx.dismissFinding(dismissTarget.empId, dismissTarget.code, dismissReason.trim(), { months: dismissMonths });
    addAudit(client.id, `경고 끄기 — ${dismissTarget.empName} · ${window.BARUNSEM.FINDING_LABEL[dismissTarget.code] || dismissTarget.code} (사유: ${dismissReason.trim()} · ${until}까지)`);
    toast(`경고를 껐습니다 — ${until}까지 · 사유 기록`, "ok");
    setDismissTarget(null); setDismissReason("");
  };
  /* W2-4 반려 — 문자열 1줄이 아니라 {code, empIds[], memo} 구조. 고객 포털에서 체크리스트로 렌더된다. */
  const reject = () => {
    const empIds = rejectScope === "emp" && rejectTarget ? [rejectTarget] : issueEmps.map((e) => e.id);
    const r = rejectClient(client.id, [{ code: rejectCode, empIds, memo: rejectText.trim() }]);
    if (r && r.ok === false) return;
    setRejectOpen(false); setRejectText(""); setRejectTarget("");
  };
  const [unlockOpen, setUnlockOpen] = useState(false);
  const [unlockReason, setUnlockReason] = useState("");
  const [lockOpen, setLockOpen] = useState(false);
  const [lockReason, setLockReason] = useState("");
  // C4 — 잠금 해제는 권한 + 사유 + 감사로그. 전엔 1클릭이었다.
  const unlock = () => {
    if (!unlockReason.trim()) return;
    const r = transition(client.id, "in_review", { label: "잠금 해제 (재검수)", reason: unlockReason.trim(), toastText: "잠금을 해제했습니다 — 사유 기록" });
    if (r.ok) { setUnlockOpen(false); setUnlockReason(""); }
  };

  return (
    <div className="page page-wide rise">
      {/* 워크플로 표시 (tweak: reviewStyle) */}
      {t.reviewStyle === "stepper" && (
        <div className="card card-pad" style={{ marginBottom: 18 }}>
          <Stepper flow={FLOW} currentKey={st} />
        </div>
      )}
      {t.reviewStyle === "board" && (
        <div className="card card-pad" style={{ marginBottom: 18 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
            <Ms name="view_kanban" style={{ color: "var(--accent)", fontSize: 19 }} />
            <h3 style={{ fontSize: 14, fontWeight: 650 }}>검수 보드</h3>
            <span className="muted" style={{ fontSize: 12 }}>카드의 ◀ ▶ 는 권한·게이트를 통과할 때만 움직입니다</span>
          </div>
          <Board flow={FLOW} clients={clients} statusOf={statusOf} activeId={client.id}
            onSelect={(cid) => go("review", { clientId: cid })}
            /* C1 — 보드 이동도 게이트웨이를 지난다. 전엔 여기서 CAN 검사·스냅샷 없이 상태가 바뀌었다. */
            canMove={(cid, toKey) => gateFor(cid, toKey)}
            onMove={(cid, toKey, dir) => transition(cid, toKey, { label: (dir > 0 ? "보드에서 단계 진행" : "보드에서 단계 되돌림"), reason: dir < 0 ? "보드에서 되돌림" : undefined })} />
        </div>
      )}

      <div className={t.reviewStyle === "timeline" ? "l-split" : ""} style={t.reviewStyle === "timeline" ? { "--rail": "300px" } : null}>
        <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
          {/* §01 검수 자동 점검 (findings 패널) — §09 근거 inline 펼침 + 플래그별 확인·반려 */}
          <div className="card">
            <CardHead icon="rule" title="검수 자동 점검" sub="가장 먼저 볼 것 — 전월 대비·최저임금·4대보험·주52·근태원본. 플래그마다 근거 펼침·확인·반려 (확정은 노무사)"
              right={<RiskBadge errors={audit.errors} warns={audit.warns} infos={audit.infos} />} />
            <div style={{ padding: "2px 22px 14px" }}>
              {audit.findings.length === 0
                ? <div style={{ display: "flex", gap: 10, alignItems: "center", padding: "16px 0", color: "var(--ink-3)", fontSize: 13 }}><Ms name="verified" style={{ color: "var(--ok)", fontSize: 21 }} />자동 점검 통과 — 이상 신호 없음</div>
                : audit.findings.map((f, i) => <ReviewFinding key={i} f={f} client={client} emp={f.empId ? emps.find((e) => e.id === f.empId) : null} mark={mark[f.empId]} reviewable={reviewable} onOk={setOk} onIssue={setIssue} canDismiss={canDismiss} onDismiss={(ff) => { setDismissTarget(ff); setDismissReason(""); }}
                    onResolveDispute={(ff) => { setResolveTarget(ff); setResolveMemo(""); }} />)}
              {dismissedHere > 0 && (
                <div style={{ display: "flex", gap: 7, alignItems: "center", paddingTop: 11, color: "var(--ink-3)", fontSize: 12 }}>
                  <Ms name="notifications_off" style={{ fontSize: 15 }} />끈 경고 {dismissedHere}건 — 사유·기간 기록됨 · 기간이 끝나면 다시 뜹니다
                </div>
              )}
            </div>
          </div>

          {/* 제출 정보 */}
          <div className="card">
            <CardHead icon="cloud_done" title="고객사 제출 변동분" sub={`제출 ${client.submittedAt}`}
              right={<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                <RiskBadge errors={audit.errors} warns={audit.warns} infos={audit.infos} />
                {reviewable
                  ? <>
                      <span className="tag"><Ms name="filter_alt" style={{ fontSize: 12 }} />변동 {changedEmps.length} · 변동없음 {unchangedEmps.length}</span>
                      <button className="btn btn-default btn-sm" onClick={bulkConfirm} disabled={changedEmps.length === 0 || changedEmps.every((e) => mark[e.id] === "ok")}><Ms name="done_all" />변동 모두 확인</button>
                    </>
                  : <span className="tag tag-accent"><Ms name="language" style={{ fontSize: 13 }} />웹 폼 직접 제출</span>}
              </div>} />
            <div style={{ padding: "12px 22px", display: "flex", gap: 22, fontSize: 12.5, color: "var(--ink-3)", alignItems: "center", flexWrap: "wrap" }}>
              <span><Ms name="person" style={{ fontSize: 15, verticalAlign: "-2px" }} /> 제출자 {client.contact}</span>
              <span><Ms name="auto_fix_high" style={{ fontSize: 15, verticalAlign: "-2px", color: "var(--accent)" }} /> 표준 스키마 자동 정규화 — 엑셀 재입력 없음</span>
              <span style={{ color: st === "in_review" || idx >= 2 ? "var(--warn)" : "inherit", fontWeight: 600 }}>{client.note}</span>
            </div>

            <table className="tbl tbl-cards">
              <thead>
                <tr>
                  <th>근로자</th><th>급여체계</th><th className="r">근로시간</th>
                  <th className="r">연장(h)</th><th className="r">야간(h)</th><th className="r">휴일(h)</th>
                  <th className="r">상여</th><th>기타공제</th><th style={{ width: 96, textAlign: "center" }}>검수</th>
                </tr>
              </thead>
              <tbody>
                {(reviewable ? changedEmps : emps).map((e) => {
                  const mk = mark[e.id];
                  return (
                    <React.Fragment key={e.id}>
                    <tr>
                      {dataCells(e)}
                      <td className="tc-action" data-label="검수" style={{ textAlign: "center" }}>
                        {reviewable ? (
                          <div style={{ display: "flex", gap: 6, justifyContent: "center" }}>
                            <button className="iconbtn" aria-label={`${e.name} 확인 · 이상 없음`} aria-pressed={mk === "ok"} title="확인 · 이상 없음" style={mk === "ok" ? { background: "var(--ok-soft)", borderColor: "var(--ok-line)", color: "var(--ok)" } : null}
                              onClick={() => setOk(e.id)}><Ms name="check" /></button>
                            <button className="iconbtn" aria-label={`${e.name} 이상 표시`} aria-pressed={mk === "issue"} title="이상 표시 · 고객사 재확인 대상" style={mk === "issue" ? { background: "var(--danger-soft)", borderColor: "var(--danger-line)", color: "var(--danger)" } : null}
                              onClick={() => setIssue(e.id)}><Ms name="flag" /></button>
                          </div>
                        ) : idx >= 3 ? <Ms name="lock" style={{ color: "var(--ink-4)", fontSize: 18 }} /> : <span className="muted" style={{ fontSize: 12 }}>대기</span>}
                      </td>
                    </tr>
                    {openTs === e.id && e.timesheet && (
                      <tr className="tc-plain"><td colSpan="9" style={{ padding: 0, background: "var(--bg-sunken)", borderBottom: "1px solid var(--line)" }}>
                        <AttendanceEvidence e={e} client={client} />
                      </td></tr>
                    )}
                    </React.Fragment>
                  );
                })}
                {reviewable && unchangedEmps.length > 0 && !showAll && (
                  <tr className="tc-plain"><td colSpan="9" style={{ padding: 0 }}>
                    <button className="autopass-toggle" onClick={() => setShowAll(true)}>
                      <Ms name="check_circle" style={{ fontSize: 16, color: "var(--ok)" }} />
                      변동 없음 <b style={{ margin: "0 2px" }}>{unchangedEmps.length}명</b> · 자동 통과
                      <span className="muted" style={{ fontWeight: 400 }}>— 전월과 동일, 확인 불필요</span>
                      <Ms name="expand_more" style={{ marginLeft: "auto", fontSize: 18 }} />
                    </button>
                  </td></tr>
                )}
                {reviewable && showAll && unchangedEmps.map((e) => (
                  <tr key={e.id} style={{ opacity: .5 }}>
                    {dataCells(e)}
                    <td className="tc-action" data-label="검수" style={{ textAlign: "center" }}><span className="tag tag-ok" style={{ height: 19, fontSize: 10 }}><Ms name="check" style={{ fontSize: 12 }} />자동 통과</span></td>
                  </tr>
                ))}
                {reviewable && showAll && unchangedEmps.length > 0 && (
                  <tr className="tc-plain"><td colSpan="9" style={{ padding: 0 }}>
                    <button className="autopass-toggle" onClick={() => setShowAll(false)}>
                      <Ms name="expand_less" style={{ fontSize: 18 }} />변동 없는 {unchangedEmps.length}행 접기
                    </button>
                  </td></tr>
                )}
              </tbody>
            </table>
          </div>

          {/* 인사변동 (입·퇴사 → 4대보험 자격취득·상실) */}
          {hrChangesOf(client.id).length > 0 && (() => {
            const hr = hrChangesOf(client.id);
            const joins = hr.filter((x) => x.kind === "join");
            const leaves = hr.filter((x) => x.kind === "leave");
            return (
              <div className="card">
                <CardHead icon="manage_accounts" title="인사변동" sub={`입사 ${joins.length}건 · 퇴사 ${leaves.length}건 — 4대보험 신고 연동`}
                  right={<span className="tag tag-accent"><Ms name="sync_alt" style={{ fontSize: 13 }} />신고용 서식 자동 반영</span>} />
                <table className="tbl tbl-cards">
                  <thead><tr><th style={{ width: 90 }}>구분</th><th>근로자</th><th>부서·직급</th><th>일자</th><th>비고</th></tr></thead>
                  <tbody>
                    {hr.map((x) => (
                      <tr key={x.id}>
                        <td data-label="구분">{x.kind === "join"
                          ? <span className="pill pill-ok" style={{ height: 21 }}><Ms name="login" style={{ fontSize: 13 }} />입사</span>
                          : <span className="pill pill-warn" style={{ height: 21 }}><Ms name="logout" style={{ fontSize: 13 }} />퇴사</span>}</td>
                        <td data-label="근로자" style={{ fontWeight: 650 }}>{x.name}</td>
                        <td className="muted" data-label="부서·직급" style={{ fontSize: 12.5 }}>{x.dept}{x.title ? " · " + x.title : ""}</td>
                        <td className="num" data-label="일자">{x.date}</td>
                        <td className="muted" data-label="비고" style={{ fontSize: 12.5 }}>
                          {x.kind === "join"
                            ? <>자격취득 · {x.scheme} {x.base ? KRW(x.base) + "원" : ""}</>
                            : <>자격상실 · {x.reason || "—"}</>}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            );
          })()}

          {/* 액션 바 */}
          <div className="card card-pad action-bar" style={{ display: "flex", alignItems: "center", gap: 16, position: "sticky", bottom: 16, boxShadow: "var(--sh-2)" }}>
            {st === "submitted" && (<>
              <Ms name="inbox" style={{ color: "var(--accent)", fontSize: 26 }} />
              <div style={{ flex: 1 }}><div style={{ fontWeight: 650 }}>검수를 시작하세요</div><div className="muted" style={{ fontSize: 12.5 }}>고객사가 제출한 변동분 {emps.length}건을 확인합니다.</div></div>
              <button className="btn btn-primary btn-lg" onClick={startReview}><Ms name="play_arrow" />검수 시작</button>
            </>)}
            {st === "in_review" && (<>
              <div style={{ flex: 1, display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
                <div style={{ width: 120 }}><div className="prog"><i style={{ width: (changedEmps.length ? triagedCount / changedEmps.length * 100 : 100) + "%" }} /></div></div>
                <span style={{ fontWeight: 650 }}>변동 {changedEmps.length}명 중 {triagedCount}명 검토</span>
                {unchangedEmps.length > 0 && <span className="tag"><Ms name="check_circle" style={{ fontSize: 13 }} />변동없음 {unchangedEmps.length} 자동</span>}
                {issueCount > 0 && <span className="tag tag-warn"><Ms name="flag" style={{ fontSize: 13 }} />이상 {issueCount}건</span>}
                {unresolvedErrors.length > 0 && <span className="tag tag-danger"><Ms name="error" style={{ fontSize: 13 }} />미해결 오류 {unresolvedErrors.length}건 — 잠금 불가</span>}
              </div>
              <button className={"btn " + (issueCount > 0 ? "btn-primary" : "btn-danger")} onClick={() => setRejectOpen(true)} title="이상 건을 고객사 입력화면에 재확인 요청으로 보냅니다">
                <Ms name="undo" />{issueCount > 0 ? `고객사 반려 · 재확인 ${issueCount}건` : "고객사 반려"}
              </button>
              {/* 게이트가 막을 때는 버튼을 죽이지 않고 '왜'와 '다음 행동'을 준다.
                  disabled로만 두면 사유 입력이라는 탈출구에 도달할 방법이 없다(E2·W2-2). */}
              <button className="btn btn-primary btn-lg"
                disabled={!ctx.canReview}
                onClick={tryLock}
                title={!ctx.canReview ? "검수·잠금 — 노무사(담당·팀장·대표)만 가능" : unresolvedErrors.length ? `미해결 오류 ${unresolvedErrors.length}건 — 해결하거나 사유 입력 필요` : issueCount > 0 ? `이상 ${issueCount}건 — 고객사 반려·재확인 후 잠금` : allReviewed ? "" : "변동 행 모두 확인 후 잠금"}>
                <Ms name="lock" />검수 완료 · 입력 잠금
              </button>
            </>)}
            {st === "locked" && (<>
              <Ms name="lock" style={{ color: "var(--accent)", fontSize: 26 }} />
              <div style={{ flex: 1 }}><div style={{ fontWeight: 650 }}>입력이 잠겼습니다 — 변동분 동결</div><div className="muted" style={{ fontSize: 12.5 }}>이제 급여를 계산할 수 있습니다. 결과는 노무사 확정 전까지 발송되지 않습니다.</div></div>
              <button className="btn btn-default" onClick={() => setUnlockOpen(true)}><Ms name="lock_open" />잠금 해제</button>
              <button className="btn btn-primary btn-lg" disabled={!ctx.canReview} title={ctx.canReview ? "" : "급여 계산은 노무사만 가능"} onClick={() => { if (!ctx.canReview) { toast("급여 계산은 노무사만 가능합니다"); return; } calculate(); }}><Ms name="calculate" />급여 계산 실행</button>
            </>)}
            {idx >= 4 && (<>
              <Ms name="task_alt" style={{ color: "var(--ok)", fontSize: 26 }} />
              <div style={{ flex: 1 }}><div style={{ fontWeight: 650 }}>검수·잠금 완료</div><div className="muted" style={{ fontSize: 12.5 }}>계산 결과를 검토하세요.</div></div>
              <button className="btn btn-primary" onClick={() => go("calc", { clientId: client.id })}><Ms name="calculate" />계산 결과 보기</button>
            </>)}
          </div>
        </div>

        {/* 타임라인 사이드 (tweak) */}
        {t.reviewStyle === "timeline" && (
          <div className="card card-pad">
            <h3 style={{ fontSize: 14, fontWeight: 650, marginBottom: 16 }}>진행 단계</h3>
            <Timeline flow={FLOW} currentKey={st} history={log.byStage} />
          </div>
        )}
      </div>

      {/* 감사 로그 */}
      <div className="card" style={{ marginTop: 18 }}>
        <CardHead icon="history" title="처리 이력" sub="audit log — 누가·언제·무엇을" />
        <div style={{ padding: "8px 22px 16px" }}>
          {log.entries.length === 0 ? <div className="muted" style={{ fontSize: 13, padding: "10px 0" }}>아직 기록된 처리 내역이 없습니다.</div> :
            log.entries.map((a, i) => (
              <div key={i} style={{ display: "flex", gap: 12, padding: "9px 0", borderBottom: i < log.entries.length - 1 ? "1px solid var(--line)" : "none", alignItems: "center" }}>
                <div className="avatar" style={{ width: 26, height: 26, fontSize: 11 }}>{(a.actor || "강")[0]}</div>
                <div style={{ flex: 1, fontSize: 13 }}><b>{(a.actor || "강도현").replace(/\s*노무사$/, "")}</b>{a.actorRole && ctx.ROLE_LABEL[a.actorRole] && <span className="tag" style={{ height: 16, fontSize: 9.5, padding: "0 5px", marginLeft: 5 }}>{ctx.ROLE_LABEL[a.actorRole]}</span>}{!a.actorId && <span className="tag" style={{ height: 16, fontSize: 9.5, padding: "0 5px", marginLeft: 5 }}>고객사</span>} · {a.action}{a.approvalOf && <span className="tag tag-accent" style={{ height: 16, fontSize: 9.5, padding: "0 5px", marginLeft: 5 }}><Ms name="approval" style={{ fontSize: 11 }} />결재</span>}</div>
                <span className="muted num" style={{ fontSize: 11.5 }}>{a.at}</span>
              </div>
            ))}
        </div>
      </div>

      {resolveTarget && (
        <Modal title="이의제기 처리" onClose={() => setResolveTarget(null)}
          footer={<><button className="btn btn-default" onClick={() => setResolveTarget(null)}>취소</button>
            <button className="btn btn-primary" disabled={!resolveMemo.trim()} onClick={() => {
              ctx.resolveDispute(resolveTarget.empId, resolveTarget.disputeId, resolveMemo.trim());
              addAudit(client.id, `근로자 이의제기 처리 — ${resolveTarget.empName}: ${resolveMemo.trim()}`);
              toast("이의제기를 처리했습니다 — 회신 내용이 기록되었습니다", "ok");
              setResolveTarget(null); setResolveMemo("");
            }}><Ms name="task_alt" />처리 완료</button></>}>
          <p style={{ marginBottom: 10 }}><b>{resolveTarget.empName}</b> 근로자의 이의제기를 처리합니다.</p>
          <div className="card card-pad" style={{ background: "var(--bg-sunken)", fontSize: 12.5, marginBottom: 12, lineHeight: 1.6 }}>{resolveTarget.message}</div>
          <div style={{ fontSize: 11.5, fontWeight: 700, color: "var(--ink-2)", marginBottom: 5 }}>회신 내용 (근로자에게 전달·감사로그 기록)</div>
          <textarea className="input" style={{ height: 84, padding: 10, resize: "none", width: "100%" }}
            placeholder="예: 5/20 토요 진료는 휴일근로 8h로 이미 반영되어 있습니다 — 명세서 휴일근로수당 항목 확인 요청" value={resolveMemo} onChange={(e) => setResolveMemo(e.target.value)} />
        </Modal>
      )}

      {/* C6 — 미해결 오류가 있어도 잠글 수는 있다. 단, 사유를 남겨야 한다(자동 차단 아님 · 판단은 노무사). */}
      {lockOpen && (
        <Modal title="미해결 오류가 있는 상태로 잠금" onClose={() => setLockOpen(false)}
          footer={<><button className="btn btn-default" onClick={() => setLockOpen(false)}>취소</button>
            <button className="btn btn-danger" disabled={!lockReason.trim()} onClick={() => lock(lockReason.trim())}><Ms name="lock" />사유 기록하고 잠금</button></>}>
          <p style={{ marginBottom: 10 }}>미해결 <b style={{ color: "var(--danger)" }}>오류 {unresolvedErrors.length}건</b>이 남아 있습니다. 그대로 잠그려면 사유를 남겨야 합니다 — 이 기록이 나중에 방어의 근거가 됩니다.</p>
          <ul style={{ margin: "0 0 14px", paddingLeft: 18, fontSize: 12.5, lineHeight: 1.7 }}>
            {openErrors.filter((f) => mark[f.empId + "|" + f.code] !== "ok").slice(0, 6).map((f, i) => (
              <li key={i}><b>{f.empName || "고객사"}</b> · {window.BARUNSEM.FINDING_LABEL[f.code] || f.code} — {f.message}</li>
            ))}
          </ul>
          <textarea className="input" rows={3} placeholder="예: 근태 원본 재요청 중, 6/5까지 회신 예정 — 잠정 잠금 후 재검수" value={lockReason} onChange={(e) => setLockReason(e.target.value)} />
        </Modal>
      )}
      {unlockOpen && (
        <Modal title="잠금 해제" onClose={() => setUnlockOpen(false)}
          footer={<><button className="btn btn-default" onClick={() => setUnlockOpen(false)}>취소</button>
            <button className="btn btn-primary" disabled={!unlockReason.trim()} onClick={unlock}><Ms name="lock_open" />해제</button></>}>
          <p style={{ marginBottom: 10 }}>잠금을 풀면 변동분이 다시 열립니다. <b>사유가 감사로그에 남습니다.</b></p>
          <textarea className="input" rows={3} placeholder="예: 고객사가 5/28 연장 3h 누락분을 추가 통보" value={unlockReason} onChange={(e) => setUnlockReason(e.target.value)} />
        </Modal>
      )}

      {rejectOpen && (
        <Modal title="고객사로 반려" onClose={() => setRejectOpen(false)}
          footer={<><button className="btn btn-default" onClick={() => setRejectOpen(false)}>취소</button><button className="btn btn-primary" onClick={reject}>반려 전송</button></>}>
          <p style={{ marginBottom: 12 }}>반려 사유를 <b>구조화</b>해 전달하면 고객사 화면에 수정 체크리스트로 표시되고, 상태가 <b>작성 중</b>으로 되돌아갑니다.</p>
          <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
            <div>
              <div style={{ fontSize: 11.5, fontWeight: 700, color: "var(--ink-2)", marginBottom: 5 }}>사유 코드</div>
              <div style={{ display: "flex", gap: 7, flexWrap: "wrap" }}>
                {["근거자료 누락", "금액 불일치", "입퇴사일 확인", "연장시간 확인", "기타"].map((rc) => (
                  <button key={rc} className={"btn btn-sm " + (rejectCode === rc ? "btn-primary" : "btn-default")} onClick={() => setRejectCode(rc)}>{rc}</button>
                ))}
              </div>
            </div>
            <div>
              <div style={{ fontSize: 11.5, fontWeight: 700, color: "var(--ink-2)", marginBottom: 5 }}>대상</div>
              <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
                <div className="seg" style={{ height: 32 }}>
                  <button className={rejectScope === "client" ? "on" : ""} onClick={() => setRejectScope("client")} style={{ fontSize: 12 }}>고객사 전체</button>
                  <button className={rejectScope === "emp" ? "on" : ""} onClick={() => setRejectScope("emp")} style={{ fontSize: 12 }}>특정 근로자</button>
                </div>
                {rejectScope === "emp" && (
                  <select className="input" value={rejectTarget} onChange={(e) => setRejectTarget(e.target.value)} style={{ height: 32, padding: "0 8px", fontSize: 13 }}>
                    <option value="">근로자 선택…</option>
                    {changedEmps.map((emp) => <option key={emp.id} value={emp.name}>{emp.name} · {emp.dept}</option>)}
                  </select>
                )}
              </div>
            </div>
            <div>
              <div style={{ fontSize: 11.5, fontWeight: 700, color: "var(--ink-2)", marginBottom: 5 }}>메모 (선택)</div>
              <textarea className="input" style={{ height: 76, padding: 10, resize: "none", width: "100%" }} placeholder="예: 강남점 연장근로 시간 근거자료 첨부 요청" value={rejectText} onChange={(e) => setRejectText(e.target.value)} />
            </div>
            <div style={{ fontSize: 11.5, color: "var(--ink-3)", background: "var(--bg-sunken)", borderRadius: 8, padding: "8px 11px" }}>
              전송 미리보기: <b style={{ color: "var(--ink-2)" }}>[{rejectCode}]{rejectScope === "emp" && rejectTarget ? " · " + rejectTarget : ""}{rejectText ? " — " + rejectText : ""}</b>
            </div>
          </div>
        </Modal>
      )}


      {dismissTarget && (
        <Modal title="경고 끄기" onClose={() => setDismissTarget(null)}
          footer={<><button className="btn btn-default" onClick={() => setDismissTarget(null)}>취소</button><button className="btn btn-primary" onClick={doDismiss} disabled={!dismissReason.trim()}><Ms name="notifications_off" />끄기 확정</button></>}>
          <p style={{ marginBottom: 8 }}><b>{dismissTarget.empName}</b> · {dismissTarget.message}</p>
          <p className="muted" style={{ fontSize: 12.5, marginBottom: 10 }}>이 경고는 <b>해당 근로자의 이 항목에 한해</b> 아래 기간 동안만 숨겨집니다. 기간이 끝나거나 조건이 바뀌면 다시 뜹니다 — 무기한 침묵은 검수를 형식으로 만듭니다.</p>
          <div style={{ display: "flex", gap: 7, marginBottom: 10, flexWrap: "wrap" }}>
            {[1, 3, 6, 12].map((m) => (
              <button key={m} className={"btn btn-sm " + (dismissMonths === m ? "btn-primary" : "btn-default")} onClick={() => setDismissMonths(m)}>{m}개월</button>
            ))}
          </div>
          <textarea className="input" style={{ height: 76, padding: 10, resize: "none", width: "100%" }} placeholder="예: 등기임원 — 4대보험 적용제외 / 상여달 정상 변동" value={dismissReason} onChange={(e) => setDismissReason(e.target.value)} />
        </Modal>
      )}
    </div>
  );
}
window.Review = Review;

/* ============================================================
   §06 근태 근거 drill-down — raw 출퇴근 → splitDay 일별 분해 + 주별 연장(WEEK52 정밀)
   ============================================================ */
function AttendanceEvidence({ e, client }) {
  const B = window.BARUNSEM;
  const rb = B.attendanceRules(client);
  const dv = B.deriveVariation(e.timesheet, e, rb);
  const rsCap = (client && client._rs) || B.RULESET;
  const WEEK_CAP = rsCap.weekOtCap != null ? rsCap.weekOtCap : 12;   // D4 — 룰셋 단일 진실
  const v = e.variation || {};
  const worked = e.timesheet.days.filter((d) => d.type !== "off");
  const round1 = (n) => Math.round(n * 10) / 10;
  const cmp = [
    { k: "연장", d: dv.overtime, r: v.overtime || 0 },
    { k: "야간", d: dv.night, r: v.night || 0 },
    { k: "휴일", d: round1(dv.holiday + dv.holidayOver), r: (v.holiday || 0) + (v.holidayOver || 0) },
  ];
  const src = e.timesheetSource;
  const [openWk, setOpenWk] = useState({});
  const mondayOf = (ds) => { const dt = new Date(ds + "T00:00:00"); const wd = (dt.getDay() + 6) % 7; const base = rb.weekStart === "sun" ? dt.getDay() : wd; const m = new Date(dt); m.setDate(dt.getDate() - base); return m.getFullYear() + "-" + String(m.getMonth() + 1).padStart(2, "0") + "-" + String(m.getDate()).padStart(2, "0"); };
  const weekGroups = (dv.weeks || []).map((w) => {
    const days = worked.filter((d) => mondayOf(d.date) === w.wk);
    const tot = days.reduce((a, d) => { const s = B.splitDay(d, rb); return { work: a.work + s.work + s.hol + s.holOver, ot: a.ot + s.ot, night: a.night + s.night, hol: a.hol + s.hol + s.holOver, absent: a.absent + (s.absent ? 1 : 0), wd: a.wd + (s.absent ? 0 : 1) }; }, { work: 0, ot: 0, night: 0, hol: 0, absent: 0, wd: 0 });
    return { ...w, days, tot };
  });
  const wkEndMD = (wk) => { const d = new Date(wk + "T00:00:00"); d.setDate(d.getDate() + 6); return (d.getMonth() + 1) + "/" + d.getDate(); };
  return (
    <div style={{ padding: "14px 22px 18px", borderLeft: "3px solid var(--accent)" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10, flexWrap: "wrap" }}>
        <Ms name="schedule" style={{ color: "var(--accent)", fontSize: 18 }} />
        <b style={{ fontSize: 13 }}>근태 → 변동분 산출 근거</b>
        <span className="tag tag-accent" style={{ height: 19 }}>근태에서 자동 산출 — 검수 후 확정</span>
        {src && <span className="muted num" style={{ fontSize: 11 }}>{src.fileName} · {src.rawRowCount}행</span>}
      </div>

      {/* 주별 연장 — 주52 정밀 */}
      <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginBottom: 12 }}>
        <span style={{ fontSize: 11.5, fontWeight: 700, color: "var(--ink-2)" }}>주별 연장 (주{WEEK_CAP}h 한도)</span>
        {(dv.weeks || []).map((w) => (
          <span key={w.wk} className={"tag" + (w.wkOt > WEEK_CAP ? " tag-danger" : "")} style={{ height: 20 }} title={w.wk + " 주 시작 · 근로 " + w.wkHours + "h"}>
            {w.wkOt > WEEK_CAP && <Ms name="warning" style={{ fontSize: 12 }} />}{w.wk.slice(5)} · {w.wkOt}h
          </span>
        ))}
        {(dv.weeks || []).some((w) => w.wkOt > WEEK_CAP)
          ? <span style={{ fontSize: 11, color: "var(--danger)", fontWeight: 600 }}><Ms name="error" style={{ fontSize: 13, verticalAlign: "-2px" }} />주{WEEK_CAP}h 초과 주 발견 — 근사 경고를 정밀 확인으로 승격</span>
          : <span style={{ fontSize: 11, color: "color-mix(in oklab, var(--ok) 60%, black)", fontWeight: 600 }}><Ms name="check" style={{ fontSize: 13, verticalAlign: "-2px" }} />모든 주 한도 이내</span>}
      </div>

      {/* 일별 분해 — 주 단위 접기/펼치기 (모바일 컴팩트) */}
      <div style={{ background: "var(--surface)", border: "1px solid var(--line)", borderRadius: 10, overflow: "hidden" }}>
        {weekGroups.map((g) => {
          const open = openWk[g.wk];
          const over = g.wkOt > WEEK_CAP;
          return (
            <div key={g.wk} style={{ borderBottom: "1px solid var(--line)" }}>
              <button onClick={() => setOpenWk((o) => ({ ...o, [g.wk]: !o[g.wk] }))} style={{ width: "100%", display: "flex", alignItems: "center", gap: 9, padding: "9px 13px", border: "none", background: open ? "var(--bg-sunken)" : "var(--surface)", cursor: "pointer", textAlign: "left", fontFamily: "inherit", flexWrap: "wrap" }}>
                <Ms name={open ? "expand_more" : "chevron_right"} style={{ fontSize: 18, color: "var(--ink-4)", flex: "none" }} />
                <span style={{ fontWeight: 650, fontSize: 12.5 }}>{g.wk.slice(5)}~{wkEndMD(g.wk)}</span>
                <span className="muted" style={{ fontSize: 11.5 }}>근로 {g.tot.wd}일{g.tot.absent ? ` · 결근 ${g.tot.absent}` : ""}</span>
                <div style={{ flex: 1 }} />
                <span className="num" style={{ fontSize: 12, color: "var(--ink-2)", fontWeight: 600 }}>{round1(g.tot.work)}h</span>
                {g.wkOt > 0 && <span className={"tag" + (over ? " tag-danger" : "")} style={{ height: 19 }}>{over && <Ms name="warning" style={{ fontSize: 11 }} />}연장 {round1(g.tot.ot)}h</span>}
                {g.tot.night > 0 && <span className="tag" style={{ height: 19 }}>야간 {round1(g.tot.night)}h</span>}
                {g.tot.hol > 0 && <span className="tag tag-warn" style={{ height: 19 }}>휴일 {round1(g.tot.hol)}h</span>}
              </button>
              {open && (
                <div style={{ overflowX: "auto", borderTop: "1px solid var(--line)" }}>
                  <table className="tbl" style={{ fontSize: 12, minWidth: 520 }}>
                    <thead><tr><th style={{ width: 64 }}>날짜</th><th style={{ width: 50 }}>구분</th><th>출퇴근</th><th className="r">휴게</th><th className="r">실근로</th><th className="r">연장</th><th className="r">야간</th><th className="r">휴일</th></tr></thead>
                    <tbody>
                      {g.days.map((d, i) => {
                        const s = B.splitDay(d, rb); const absent = s.absent;
                        return (
                          <tr key={i} style={absent ? { background: "var(--danger-soft)" } : null}>
                            <td className="num">{d.date.slice(5)}</td>
                            <td>{d.type === "holiday" ? <span className="tag tag-warn" style={{ height: 18, fontSize: 10 }}>휴일</span> : absent ? <span className="tag tag-danger" style={{ height: 18, fontSize: 10 }}>결근</span> : <span className="tag" style={{ height: 18, fontSize: 10 }}>근무</span>}</td>
                            <td className="num">{absent ? <span style={{ color: "var(--danger)" }}>미출근</span> : (d.in + "–" + d.out)}</td>
                            <td className="r num muted">{d.breakMin != null ? d.breakMin + "분" : "—"}</td>
                            <td className="r num" style={{ fontWeight: 600 }}>{round1(s.work + s.hol + s.holOver) || "—"}</td>
                            <td className="r num" style={{ color: s.ot ? "var(--accent-ink)" : "var(--ink-4)" }}>{round1(s.ot) || "—"}</td>
                            <td className="r num" style={{ color: s.night ? "var(--accent-ink)" : "var(--ink-4)" }}>{round1(s.night) || "—"}</td>
                            <td className="r num" style={{ color: (s.hol || s.holOver) ? "var(--accent-ink)" : "var(--ink-4)" }}>{round1(s.hol + s.holOver) || "—"}</td>
                          </tr>
                        );
                      })}
                    </tbody>
                  </table>
                </div>
              )}
            </div>
          );
        })}
        <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 13px", background: "var(--bg-sunken)", fontSize: 12, flexWrap: "wrap" }}>
          <b>월 합계</b><span className="muted" style={{ fontSize: 11.5 }}>근로일 {dv.workDays}일{dv.absentDays ? ` · 결근 ${dv.absentDays}일` : ""}</span>
          <div style={{ flex: 1 }} />
          <span className="num" style={{ fontWeight: 600 }}>근로 {dv.workedHours}h</span>
          <span className="tag tag-accent" style={{ height: 19 }}>연장 {dv.overtime}h</span>
          <span className="tag tag-accent" style={{ height: 19 }}>야간 {dv.night}h</span>
          <span className="tag tag-accent" style={{ height: 19 }}>휴일 {round1(dv.holiday + dv.holidayOver)}h</span>
        </div>
      </div>

      {/* 대사 — 산출 vs 신고 변동분 */}
      <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 10, flexWrap: "wrap" }}>
        <span style={{ fontSize: 11.5, fontWeight: 700, color: "var(--ink-2)" }}>근태 산출 ↔ 적용 변동분 대사</span>
        {e.timesheet && e.timesheet.excluded && e.timesheet.excluded.length > 0 && (
          <span className="tag tag-warn" style={{ height: 20 }}>깨진 행 {e.timesheet.excluded.length}건 제외 후 산출</span>
        )}
        {cmp.map((c) => {
          const match = Math.abs(c.d - c.r) < 0.05;
          return (
            <span key={c.k} className={"tag" + (match ? " tag-ok" : " tag-warn")} style={{ height: 20 }}>
              {c.k} {c.d}h {match ? "= " : "≠ "}<span style={{ opacity: .7 }}>신고 {c.r}h</span>
            </span>
          );
        })}
      </div>
    </div>
  );
}
window.AttendanceEvidence = AttendanceEvidence;

/* ============================================================
   §09 검수 강점 축 UX — findings 한 줄에서 근거→플래그→판단(확인·반려)
   근태 근거(06)와 검수 플래그(01)를 한 시선에 묶는다(눈 이동 최소화).
   ============================================================ */
const ATT_EVIDENCE_CODES = ["WEEK52", "DELTA_SPIKE", "ATT_ANOMALY", "TARDY_CUM", "COMMON_CHANGED", "INS_MISSING"];
function ReviewFinding({ f, client, emp, mark, reviewable, onOk, onIssue, canDismiss, onDismiss, onResolveDispute }) {
  const [open, setOpen] = useState(false);
  const s = window.SEV[f.severity] || window.SEV.info;
  const hasTs = emp && emp.timesheet;
  const isAnom = f.code === "ATT_ANOMALY" && emp && emp.timesheetRaw;
  const canEvidence = (hasTs && ATT_EVIDENCE_CODES.includes(f.code)) || isAnom;
  return (
    <div style={{ borderBottom: "1px solid var(--line)" }}>
      <div style={{ display: "flex", gap: 11, padding: "11px 2px", alignItems: "flex-start" }}>
        <Ms name={s.icon} style={{ fontSize: 19, color: s.dot, flex: "none", marginTop: 1 }} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: "flex", gap: 7, alignItems: "baseline", flexWrap: "wrap" }}>
            {f.empName && <b style={{ fontSize: 13 }}>{f.empName}</b>}
            <span style={{ fontSize: 13, color: "var(--ink)" }}>{f.message}</span>
            <span style={{ fontSize: 10, fontWeight: 700, letterSpacing: ".02em", color: s.ink, background: s.soft, border: "1px solid " + s.line, padding: "0 6px", borderRadius: 5, height: 17, display: "inline-flex", alignItems: "center", fontVariantNumeric: "tabular-nums" }}>{(window.BARUNSEM.FINDING_LABEL && window.BARUNSEM.FINDING_LABEL[f.code]) || f.code}</span>
          </div>
          {f.hint && <div className="muted" style={{ fontSize: 11.5, marginTop: 2 }}><b style={{ fontWeight: 600, color: "var(--ink-3)" }}>왜 떴나 ·</b> {f.hint}</div>}
          <div style={{ display: "flex", gap: 6, marginTop: 8, flexWrap: "wrap", alignItems: "center" }}>
            {canEvidence && (
              <button className="btn btn-default btn-sm" style={{ height: 27 }} onClick={() => setOpen((o) => !o)}>
                <Ms name={open ? "unfold_less" : "unfold_more"} style={{ fontSize: 15 }} />{open ? "근거 접기" : (isAnom ? "원본 이상 보기" : "근태 근거 보기")}
              </button>
            )}
            {reviewable && emp && (<>
              <button className="btn btn-sm" style={{ height: 27, ...(mark === "ok" ? { background: "var(--ok-soft)", borderColor: "var(--ok-line)", color: "var(--ok)" } : {}) }} onClick={() => onOk(emp.id)}>
                <Ms name="check" style={{ fontSize: 15 }} />{mark === "ok" ? "확인됨" : "확인"}
              </button>
              <button className="btn btn-sm" style={{ height: 27, ...(mark === "issue" ? { background: "var(--danger-soft)", borderColor: "var(--danger-line)", color: "var(--danger)" } : {}) }} onClick={() => onIssue(emp.id)}>
                <Ms name="flag" style={{ fontSize: 15 }} />{mark === "issue" ? "반려 대상" : "반려 대상으로"}
              </button>
            </>)}
            {/* 이의제기는 발송 후에 들어온다 — 검수 단계가 아니어도 닫을 수 있어야 한다.
                닫을 방법이 없으면 플래그가 영원히 남아 다음 달 검수까지 오염시킨다. */}
            {f.code === "DISPUTE" && onResolveDispute && (
              <button className="btn btn-primary btn-sm" style={{ height: 27 }} onClick={() => onResolveDispute(f)}>
                <Ms name="task_alt" style={{ fontSize: 15 }} />회신·처리 완료
              </button>
            )}
            {canDismiss && f.empId && f.code !== "DISPUTE" && (
              <button className="btn btn-ghost btn-sm" style={{ height: 27 }} title="이 경고를 끕니다 (사유·기간 기록 — 만료되면 다시 뜹니다)" onClick={() => onDismiss(f)}>
                <Ms name="notifications_off" style={{ fontSize: 15 }} />끄기
              </button>
            )}
          </div>
        </div>
      </div>
      {open && canEvidence && (
        <div style={{ background: "var(--bg-sunken)", borderTop: "1px solid var(--line)", borderRadius: "0 0 8px 8px", overflow: "hidden", marginBottom: 8 }}>
          {isAnom ? <RawAnomalyPanel e={emp} client={client} /> : <AttendanceEvidence e={emp} client={client} />}
        </div>
      )}
    </div>
  );
}
window.ReviewFinding = ReviewFinding;

/* §09 깨진 근태 원본(raw) 이상 — 검수 화면 inline 패널 (auditTimesheet 소비) */
function RawAnomalyPanel({ e, client }) {
  const B = window.BARUNSEM;
  const at = B.auditTimesheet(e.timesheetRaw, B.attendanceRules(client));
  const LBL = window.BARUNSEM.ANOM_LABEL;
  const src = e.timesheetRawSource;
  return (
    <div style={{ padding: "13px 22px 16px", borderLeft: "3px solid var(--danger)" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 9, flexWrap: "wrap" }}>
        <Ms name="report" style={{ color: "var(--danger)", fontSize: 18 }} />
        <b style={{ fontSize: 13 }}>근태 원본 이상 — 정제 전 raw</b>
        <span className="tag tag-danger" style={{ height: 19 }}>{at.count}건</span>
        {src && <span className="muted num" style={{ fontSize: 11 }}>{src.fileName} · {src.rawRowCount}행</span>}
      </div>
      <div style={{ background: "var(--surface)", border: "1px solid var(--line)", borderRadius: 9, overflow: "hidden" }}>
        <table className="tbl" style={{ fontSize: 12 }}>
          <thead><tr><th style={{ width: 76 }}>날짜</th><th style={{ width: 96 }}>유형</th><th>내용</th></tr></thead>
          <tbody>
            {at.anomalies.map((a, i) => (
              <tr key={i}>
                <td className="num" style={{ whiteSpace: "nowrap" }}>{window.BARUNSEM.mdLabel(a.date)}</td>
                <td><span className={"tag " + (a.severity === "error" ? "tag-danger" : "tag-warn")} style={{ height: 18, fontSize: 10 }}>{LBL[a.code] || a.code}</span></td>
                <td className="muted" style={{ fontSize: 11.5 }}>{a.message}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <div style={{ display: "flex", gap: 7, alignItems: "center", marginTop: 9, fontSize: 11.5, color: "var(--ink-3)" }}>
        <Ms name="shield" style={{ fontSize: 15, color: "var(--ok)" }} />깨진 행은 산출에서 제외됨 — 고객사에 정정 요청(반려)하거나 노무사가 보정 후 재산출하세요.
      </div>
    </div>
  );
}
window.RawAnomalyPanel = RawAnomalyPanel;
