/* ───────────────────────────────────────────────────────────────
   完工検査マスター（段1・設定側）＝会社ごとの「検査項目」と「検査者」を管理する隔離コンポーネント。
   ・invoice.jsx（請求書・横田¥822,282の描画）とは名前衝突させないため IIFE で包み window.GenbaInspection に公開。
   ・invoice.jsx 側は「設定に導線ボタン1個」を置いて <window.GenbaInspection .../> を呼ぶだけ（中身はこのファイル）。
   ・API＝/api/inspection-items・/api/inspection-templates・/api/inspectors（company_id はサーバのsessionで隔離）。
   ・calc.py／単価JSON／genba_items／封印ボタン／yokotaゲート には一切触れない（金額を読まない・書かない）。
   ─────────────────────────────────────────────────────────────── */
window.GenbaInspection = (function () {
  const { useState, useEffect } = React;
  const ITEM_MAX = 15;

  const api = (path, opts) =>
    fetch(path, { credentials: "same-origin", headers: { "Content-Type": "application/json" }, ...opts })
      .then((r) => r.json());

  // ── inspected_at を日本時間(JST)で表示（★保存はUTCのまま／変換は表示層のこの1箇所だけ）──
  //   本番PG(timestamptz)＝RFC/ISO(TZ付き)で来る→そのTZで正しく解釈される。
  //   ローカルSQLite(datetime('now'))＝"YYYY-MM-DD HH:MM:SS" のnaive値＝実体はUTCだがTZ表記なし
  //     →そのまま new Date すると閲覧端末ローカル時刻と誤解される（fmtMDの轍）。'Z'を付けてUTCと明示する。
  //   最後に timeZone:'Asia/Tokyo' で必ずJST表示＝閲覧端末のTZに依存しない。
  const fmtJST = (raw) => {
    if (!raw) return "";
    let s = String(raw).trim();
    const naive = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?$/;   // TZ表記の無いnaive（SQLite）
    if (naive.test(s)) s = s.replace(" ", "T") + "Z";              // naiveはUTC＝'Z'を付けて明示
    const d = new Date(s);
    if (isNaN(d.getTime())) return String(raw);
    return d.toLocaleString("ja-JP", { timeZone: "Asia/Tokyo", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
  };

  const st = {
    backdrop: { position: "fixed", inset: 0, background: "rgba(8,10,14,0.72)", zIndex: 4000,
      display: "flex", justifyContent: "center", alignItems: "flex-start", overflowY: "auto", padding: "24px 12px" },
    panel: { width: "100%", maxWidth: 640, background: "var(--surface)", color: "var(--text)",
      border: "1px solid var(--border)", borderRadius: 14, boxShadow: "0 12px 40px rgba(0,0,0,.5)", overflow: "hidden" },
    bar: { display: "flex", alignItems: "center", justifyContent: "space-between",
      padding: "14px 16px", borderBottom: "1px solid var(--border)", position: "sticky", top: 0,
      background: "var(--surface)" },
    h1: { fontSize: 18, fontWeight: 700, margin: 0 },
    closeBtn: { padding: "8px 14px", borderRadius: 10, border: "1px solid var(--border)",
      background: "var(--bg-sunken)", color: "var(--text)", fontSize: 15, cursor: "pointer" },
    scroll: { padding: 16 },
    card: { border: "1px solid var(--border)", borderRadius: 12, padding: 14, marginBottom: 16,
      background: "var(--surface-raised)" },
    h2: { fontSize: 15, fontWeight: 700, margin: "0 0 4px", display: "flex", alignItems: "center", gap: 8 },
    note: { fontSize: 12, color: "var(--text-dimmer)", margin: "0 0 12px", lineHeight: 1.5 },
    row: { display: "flex", alignItems: "center", gap: 6, padding: "6px 0", borderTop: "1px solid var(--border)" },
    move: { width: 30, height: 30, borderRadius: 8, border: "1px solid var(--border)",
      background: "var(--bg-sunken)", color: "var(--text)", cursor: "pointer", fontSize: 13, lineHeight: 1 },
    inp: { flex: 1, minWidth: 0, padding: "8px 10px", borderRadius: 8, fontSize: 15,
      border: "1px solid var(--border)", background: "var(--bg-sunken)", color: "var(--text)" },
    tag: (on) => ({ padding: "6px 9px", borderRadius: 8, fontSize: 12, cursor: "pointer", whiteSpace: "nowrap",
      border: "1px solid " + (on ? "var(--success-border)" : "var(--border)"),
      background: on ? "var(--success-panel)" : "var(--surface-raised)", color: on ? "var(--success-text)" : "var(--text-dim)" }),
    del: { width: 32, height: 32, borderRadius: 8, border: "1px solid var(--border)",
      background: "var(--surface-raised)", color: "var(--danger)", cursor: "pointer", fontSize: 14 },
    addRow: { display: "flex", gap: 8, marginTop: 10 },
    addBtn: (dis) => ({ padding: "9px 14px", borderRadius: 10, border: "1px solid var(--border)",
      background: dis ? "var(--bg-sunken)" : "var(--blue)", color: dis ? "var(--text-dimmer)" : "var(--on-accent)",
      fontSize: 14, cursor: dis ? "default" : "pointer", whiteSpace: "nowrap" }),
    tmplBtn: { padding: "8px 12px", borderRadius: 10, border: "1px dashed var(--border)",
      background: "transparent", color: "var(--text-dimmer)", fontSize: 13, cursor: "pointer", marginBottom: 10 },
    count: { fontSize: 12, color: "var(--text-dimmer)", fontWeight: 400 },
    empty: { fontSize: 13, color: "var(--text-dimmer)", padding: "10px 0" },
  };

  // 名前をその場で編集（変更があった時だけ保存）
  function EditName({ value, onSave }) {
    const [v, setV] = useState(value);
    useEffect(() => setV(value), [value]);
    return (
      <input style={st.inp} value={v}
        onChange={(e) => setV(e.target.value)}
        onBlur={() => { if (v.trim() && v !== value) onSave(v.trim()); else setV(value); }}
        onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); }} />
    );
  }

  function GenbaInspection({ onClose }) {
    const [items, setItems] = useState(null);
    const [inspectors, setInspectors] = useState(null);
    const [newItem, setNewItem] = useState("");
    const [newInsp, setNewInsp] = useState("");
    const [busy, setBusy] = useState(false);

    const load = () =>
      Promise.all([api("/api/inspection-items"), api("/api/inspectors")]).then(([a, b]) => {
        setItems((a.items || []).slice().sort((x, y) => x.sort - y.sort || x.id - y.id));
        setInspectors((b.inspectors || []).slice().sort((x, y) => x.sort - y.sort || x.id - y.id));
      });
    useEffect(() => { load(); }, []);

    const guard = (fn) => async (...a) => { if (busy) return; setBusy(true); try { await fn(...a); await load(); } finally { setBusy(false); } };

    // 並び替え＝隣とsortを入れ替えて両方PUT
    const move = (list, setUrl, idx, dir) => guard(async () => {
      const j = idx + dir; if (j < 0 || j >= list.length) return;
      const a = list[idx], b = list[j];
      await api(`${setUrl}/${a.id}`, { method: "PUT", body: JSON.stringify({ sort: b.sort }) });
      await api(`${setUrl}/${b.id}`, { method: "PUT", body: JSON.stringify({ sort: a.sort }) });
    })();

    // ── 検査項目 ──
    const addItem = guard(async () => {
      const label = newItem.trim(); if (!label) return;
      const r = await api("/api/inspection-items", { method: "POST", body: JSON.stringify({ label }) });
      if (!r.ok && r.err === "limit_15") alert(`項目は${ITEM_MAX}個までです`);
      setNewItem("");
    });
    const importTmpl = guard(async () => { await api("/api/inspection-items/import-templates", { method: "POST", body: JSON.stringify({}) }); });

    // ── 検査者 ──
    const addInsp = guard(async () => {
      const name = newInsp.trim(); if (!name) return;
      await api("/api/inspectors", { method: "POST", body: JSON.stringify({ name }) });
      setNewInsp("");
    });

    const itemFull = items && items.length >= ITEM_MAX;

    return (
      <div style={st.backdrop} onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
        <div style={st.panel} className="screen-ui">
          <div style={st.bar}>
            <h1 style={st.h1}>✅ 完工検査マスター</h1>
            <button style={st.closeBtn} onClick={onClose}>閉じる</button>
          </div>
          <div style={st.scroll}>

            {/* 検査項目マスター */}
            <section style={st.card}>
              <div style={st.h2}>検査項目 <span style={st.count}>{items ? `${items.length}/${ITEM_MAX}` : ""}</span></div>
              <p style={st.note}>完工のときに確認する項目。自由に打ち込めます（最大{ITEM_MAX}）。上から順に検査するので、並び替えできます。</p>
              {/* ★たたき台は「0件と分かっている時」だけ出す。読み込み中(items===null)は出さない
                  ＝12項目が入っているYC等で、取得が返る前の一瞬にタップされて既存項目の上に
                  たたき台6件が積まれる経路を塞ぐ（無効化ではなく非表示＝押せる物を置かない）。
                  項目を全部消せば再び0件になり、ここが出る＝復旧の道は残る。 */}
              {items && items.length === 0 && (
                <button style={st.tmplBtn} onClick={importTmpl} disabled={busy}>＋ たたき台を入れる（よくある項目・後で自由に直せます）</button>
              )}
              {items === null ? <div style={st.empty}>読み込み中…</div>
                : items.length === 0 ? <div style={st.empty}>まだ項目がありません。下から追加するか、たたき台を入れてください。</div>
                : items.map((it, i) => (
                  <div key={it.id} style={st.row}>
                    <button style={st.move} disabled={i === 0 || busy} onClick={() => move(items, "/api/inspection-items", i, -1)}>↑</button>
                    <button style={st.move} disabled={i === items.length - 1 || busy} onClick={() => move(items, "/api/inspection-items", i, 1)}>↓</button>
                    <EditName value={it.label} onSave={(v) => guard(async () => { await api(`/api/inspection-items/${it.id}`, { method: "PUT", body: JSON.stringify({ label: v }) }); })()} />
                    <button style={st.tag(!!it.required)} onClick={() => guard(async () => { await api(`/api/inspection-items/${it.id}`, { method: "PUT", body: JSON.stringify({ required: !it.required }) }); })()}>{it.required ? "必須" : "任意"}</button>
                    {/* ★確認なしで即DELETEだったので確認を挟む（現場削除・写真削除と同じ流儀）。対象名を必ず文中に出す。 */}
                    <button style={st.del} onClick={() => { if (!window.confirm("「" + it.label + "」を検査項目から削除しますか？")) return; guard(async () => { await api(`/api/inspection-items/${it.id}`, { method: "DELETE" }); })(); }}>🗑</button>
                  </div>
                ))}
              <div style={st.addRow}>
                <input style={st.inp} placeholder="例：仕上がりの目視確認" value={newItem}
                  disabled={itemFull}
                  onChange={(e) => setNewItem(e.target.value)}
                  onKeyDown={(e) => { if (e.key === "Enter") addItem(); }} />
                <button style={st.addBtn(itemFull || busy)} disabled={itemFull || busy} onClick={addItem}>＋ 項目を追加</button>
              </div>
              {itemFull && <p style={st.note}>上限{ITEM_MAX}に達しました。減らすと追加できます。</p>}
            </section>

            {/* 検査者マスター */}
            <section style={st.card}>
              <div style={st.h2}>検査者 <span style={st.count}>{inspectors ? `${inspectors.length}人` : ""}</span></div>
              <p style={st.note}>検査した人の名簿。完工のときはここから選びます（打ち間違いで記録がバラつかないよう、選択式にしています）。使わなくなった人は「無効」にすれば、過去の記録は残したまま一覧から隠せます。</p>
              {inspectors === null ? <div style={st.empty}>読み込み中…</div>
                : inspectors.length === 0 ? <div style={st.empty}>まだ検査者がいません。下から追加してください。</div>
                : inspectors.map((p, i) => (
                  <div key={p.id} style={st.row}>
                    <button style={st.move} disabled={i === 0 || busy} onClick={() => move(inspectors, "/api/inspectors", i, -1)}>↑</button>
                    <button style={st.move} disabled={i === inspectors.length - 1 || busy} onClick={() => move(inspectors, "/api/inspectors", i, 1)}>↓</button>
                    <EditName value={p.name} onSave={(v) => guard(async () => { await api(`/api/inspectors/${p.id}`, { method: "PUT", body: JSON.stringify({ name: v }) }); })()} />
                    <button style={st.tag(!!p.is_active)} onClick={() => guard(async () => { await api(`/api/inspectors/${p.id}`, { method: "PUT", body: JSON.stringify({ is_active: !p.is_active }) }); })()}>{p.is_active ? "有効" : "無効"}</button>
                    {/* ★同上。説明文が「無効にすれば過去の記録は残る」と案内しているのに🗑が隣にある食い違いを、
                        確認文の中で解消する（削除の前に「無効」という正しい引退手順を示す）。 */}
                    <button style={st.del} onClick={() => { if (!window.confirm("「" + p.name + "」を名簿から削除しますか？\n（過去の検査記録に残った名前は消えません。使わなくなっただけなら「無効」にすれば一覧から隠せます）")) return; guard(async () => { await api(`/api/inspectors/${p.id}`, { method: "DELETE" }); })(); }}>🗑</button>
                  </div>
                ))}
              <div style={st.addRow}>
                <input style={st.inp} placeholder="例：丸投太郎" value={newInsp}
                  onChange={(e) => setNewInsp(e.target.value)}
                  onKeyDown={(e) => { if (e.key === "Enter") addInsp(); }} />
                <button style={st.addBtn(busy)} disabled={busy} onClick={addInsp}>＋ 検査者を追加</button>
              </div>
            </section>

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

  /* ───────────────────────────────────────────────────────────────
     完工検査 実施画面（段2）＝現場に対して✅して検査者を選び「完工」を押す。
     ・検査者は選択式（マスターから）。inspector_id をサーバへ→inspector_name を焼付け。
     ・完工は写真に依存しない（段2は写真なし＝穴2＝写真の成否と証跡の成立を分離）。
     ・金額(calc/単価/amount)には一切触れない。読むのは検査項目/検査者マスターのみ。
     ─────────────────────────────────────────────────────────────── */
  const rst = {
    checkRow: { display: "flex", alignItems: "center", gap: 10, padding: "10px 8px",
      borderTop: "1px solid var(--border)", cursor: "pointer" },
    box: (on) => ({ width: 30, height: 30, borderRadius: 8, flex: "0 0 auto",
      border: "2px solid " + (on ? "var(--success-border)" : "var(--border)"),
      background: on ? "var(--success-border)" : "transparent", color: "var(--on-accent)", fontSize: 18,
      display: "flex", alignItems: "center", justifyContent: "center" }),
    lbl: { flex: 1, fontSize: 15 },
    sel: { width: "100%", padding: "10px 12px", borderRadius: 10, fontSize: 15,
      border: "1px solid var(--border)", background: "var(--bg-sunken)", color: "var(--text)" },
    done: (dis) => ({ width: "100%", padding: "14px", borderRadius: 12, marginTop: 14, fontWeight: 700,
      border: "none", fontSize: 16, cursor: dis ? "default" : "pointer",
      background: dis ? "var(--bg-sunken)" : "var(--success-border)", color: dis ? "var(--text-dimmer)" : "var(--on-accent)" }),
    okBox: { padding: 20, textAlign: "center" },
    okMark: { fontSize: 40, marginBottom: 8 },
    primary: { width: "100%", padding: "13px", borderRadius: 12, fontWeight: 700, fontSize: 15,
      border: "none", cursor: "pointer", background: "var(--blue)", color: "var(--on-accent)" },
    histRow: { padding: "10px 4px", borderTop: "1px solid var(--border)", display: "flex", flexDirection: "column", gap: 3 },
    histTop: { display: "flex", alignItems: "center", gap: 8 },
    doneBadge: { fontSize: 12, fontWeight: 700, color: "var(--success-text)", background: "var(--success-panel)",
      border: "1px solid var(--success-border)", borderRadius: 6, padding: "1px 8px", whiteSpace: "nowrap" },
    // フェーズ2：不合格（直しが要る）＝合格と一目で見分ける琥珀色。色だけに頼らず文言も変える。
    ngBadge: { fontSize: 12, fontWeight: 700, color: "var(--warn-title)", background: "var(--warn-bg)",
      border: "1px solid var(--warn-border)", borderRadius: 6, padding: "1px 8px", whiteSpace: "nowrap" },
    resBtn: (on, col) => ({ flex: 1, padding: "11px 8px", borderRadius: 10, fontSize: 14, fontWeight: 700,
      cursor: "pointer", border: "2px solid " + (on ? col : "var(--border)"),
      background: on ? col : "transparent", color: on ? "var(--on-accent)" : "var(--text-dim)" }),
    histMeta: { fontSize: 12, color: "var(--text-dimmer)" },
  };

  /* ── 検査写真（Supabase Storage・任意）＝1検査(inspId)の写真を 一覧/追加/拡大/削除。
        metaにはStorageパスのみ＝表示URLはサーバ発行の短命署名URL(300秒)を受け取って出すだけ。
        写真ゼロでも記録は成立（従来挙動不変）。estimate/現場/部屋 どのモードでも同じ部品。 ── */
  const pst = {
    wrap: { display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center", marginTop: 8 },
    thumbBox: { position: "relative", width: 64, height: 64 },
    thumb: { width: 64, height: 64, objectFit: "cover", borderRadius: 8, border: "1px solid var(--border)", cursor: "pointer", display: "block" },
    del: { position: "absolute", top: -6, right: -6, width: 20, height: 20, borderRadius: 999, border: "1px solid var(--border)", background: "var(--surface)", color: "var(--danger)", fontSize: 11, lineHeight: 1, cursor: "pointer", padding: 0 },
    addBtn: { width: 64, height: 64, borderRadius: 8, border: "1px dashed var(--border)", background: "transparent", color: "var(--text-dimmer)", fontSize: 11, cursor: "pointer", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 2 },
    zoomBack: { position: "fixed", inset: 0, background: "rgba(8,10,14,0.88)", zIndex: 4600, display: "flex", alignItems: "center", justifyContent: "center", padding: 16, cursor: "pointer" },
    zoomImg: { maxWidth: "100%", maxHeight: "90vh", borderRadius: 10 },
  };

  /* ───────── 送る前に小さくする（長辺1600px / JPEG q0.85）─────────
     ★狙いは2つ。(1)EXIF（GPS15項目・機種名・撮影日時・MakerNote）を落とす
                  (2)通信量と保存容量を減らす（実測＝1.93MB→183KB／11分の1）。
     ★EXIF除去は別処理を書かない＝canvas に描き直した時点で原理的に消える。
     ★向き：Chromium81+ / Safari は image-orientation の既定が from-image なので
       <img>+drawImage なら canvas に正立した絵が入る【はず】。実機で1枚見るまで断定しない。
     ★失敗したら null を返す＝呼び手は「送らない」。原本をそのまま送るフォールバックは作らない
       （GPS入りの原本が本番Storageへ抜ける経路を1本も残さないため）。 */
  const PHOTO_MAX_EDGE = 1600;
  const PHOTO_QUALITY = 0.85;

  function shrinkImage(file) {
    return new Promise((resolve) => {
      let url = "";
      let done = false;
      const finish = (v) => { if (done) return; done = true; if (url) URL.revokeObjectURL(url); resolve(v); };
      try {
        url = URL.createObjectURL(file);
        const img = new Image();
        img.onerror = () => finish(null);
        img.onload = () => {
          try {
            const w0 = img.naturalWidth || img.width;
            const h0 = img.naturalHeight || img.height;
            if (!w0 || !h0) return finish(null);
            // ★長辺が1600px以下なら拡大しない（縮小率は1.0止まり）＝小さい写真を引き伸ばさない。
            const k = Math.min(1, PHOTO_MAX_EDGE / Math.max(w0, h0));
            const w = Math.max(1, Math.round(w0 * k));
            const h = Math.max(1, Math.round(h0 * k));
            const cv = document.createElement("canvas");
            cv.width = w; cv.height = h;
            const cx = cv.getContext("2d");
            if (!cx) return finish(null);
            // ★透過PNGをJPEGにすると透明部分が黒くなる＝先に白で塗ってから描く。
            cx.fillStyle = "#ffffff";
            cx.fillRect(0, 0, w, h);
            cx.drawImage(img, 0, 0, w, h);
            cv.toBlob((b) => finish(b || null), "image/jpeg", PHOTO_QUALITY);
          } catch (_e) { finish(null); }
        };
        img.src = url;
      } catch (_e) { finish(null); }
    });
  }

  /* 保存に失敗した時の文言＝客に見せる日本語にする。★err は内部語（bad_type 等）なので
     そのまま画面に出さない。未知のコードは最後の汎用文に落とす。 */
  function photoErrMsg(err) {
    if (err === "bad_type") return "この形式の写真は保存できません。写真アプリの画像でもう一度お試しください。";
    if (err === "too_large") return "写真が大きすぎて保存できませんでした。もう一度撮り直してお試しください。";
    if (err === "empty" || err === "photo_required") return "写真が選ばれていません。もう一度お試しください。";
    if (err === "storage_not_configured" || err === "storage_error") return "写真の保管先に届きませんでした。時間をおいてもう一度お試しください。";
    if (err === "not_found") return "この検査記録が見つかりませんでした。画面を開き直してからお試しください。";
    return "写真を保存できませんでした。時間をおいてもう一度お試しください。";
  }

  function PhotoStrip({ inspId }) {
    const [photos, setPhotos] = useState(null);   // [{path,url}]（url=短命署名URL）
    const [busy, setBusy] = useState(false);
    const [zoom, setZoom] = useState(null);       // 拡大表示中のurl
    const load = () => api("/api/inspections/" + inspId + "/photos").then((j) => setPhotos(j.ok ? (j.photos || []) : []));
    useEffect(() => { load(); }, [inspId]);
    const up = async (e) => {
      const f = e.target.files && e.target.files[0];
      e.target.value = "";
      if (!f || busy) return;
      setBusy(true);
      try {
        // ★送る前に必ず縮小する＝原本（GPS・機種名・撮影日時入り）はサーバーへ出さない。
        //   ★弾く判定は縮小【後】のサイズで行われる＝10MB超の原本でも縮めれば通る。
        const blob = await shrinkImage(f);
        if (!blob) { alert("写真を小さくできませんでした。もう一度撮り直すか、別の写真でお試しください。"); return; }
        const fd = new FormData();
        // ★backend は「ブラウザが申告したMIME」だけで拡張子を決める（app.py の _PHOTO_EXT）＝
        //   ここを image/jpeg にそろえないと bad_type で弾かれる。ファイル名も .jpg にそろえる。
        fd.append("photo", blob, "photo.jpg");
        // api()はJSONヘッダ固定＝multipartは素のfetchで送る（Content-Typeはブラウザ任せ）
        const r = await fetch("/api/inspections/" + inspId + "/photos", { method: "POST", credentials: "same-origin", body: fd }).then((x) => x.json());
        if (!r.ok) {
          // 無料枠超過はサイレント失敗にしない（✕で1枚消せば枠が空くのは従来どおり）
          if (r.err === "photo_limit_reached" && r.plan === "standard") alert("無料プランは1見積書" + r.limit + "枚まで。課金プランで無制限になります。");
          else alert(photoErrMsg(r.err));
        }
        await load();
      } finally { setBusy(false); }
    };
    const del = async (p) => {
      if (busy || !confirm("この写真を削除しますか？")) return;
      setBusy(true);
      try {
        const r = await fetch("/api/inspections/" + inspId + "/photos?path=" + encodeURIComponent(p), { method: "DELETE", credentials: "same-origin" }).then((x) => x.json());
        if (!r.ok) alert("削除に失敗しました：" + (r.err || ""));
        await load();
      } finally { setBusy(false); }
    };
    return (
      <div style={pst.wrap}>
        {(photos || []).map((p) => (
          <div key={p.path} style={pst.thumbBox}>
            <img src={p.url} alt="検査写真" style={pst.thumb} onClick={() => setZoom(p.url)} />
            <button style={pst.del} onClick={() => del(p.path)} title="この写真を削除">✕</button>
          </div>
        ))}
        <label style={pst.addBtn}>
          <span>📷</span><span>{busy ? "送信中…" : "写真を追加"}</span>
          <input type="file" accept="image/*" style={{ display: "none" }} onChange={up} disabled={busy} />
        </label>
        {zoom && (
          <div style={pst.zoomBack} onClick={() => setZoom(null)}>
            <img src={zoom} alt="検査写真（拡大）" style={pst.zoomImg} />
          </div>
        )}
      </div>
    );
  }

  function GenbaInspectionRun({ site, room, estimate, onClose, onDone }) {
    // room を渡すと【部屋単位】（フェーズ1＝職人の自己検査）。渡さなければ従来どおり現場単位。
    // estimate を渡すと【見積もり案件単位】＝site/room は一切参照しない（site と排他・YC向け）。
    //   estimate={id(=estimates.id), no, title}。サーバは estimate_id 単独でCHECK可（案C）＝backend変更なし。
    const roomId = room ? room.dbId : null;          // site_rooms.id（DBの実id）
    const roomLabel = room ? room.label : null;      // 画面表示用（部屋番号）
    const estLabel = estimate ? (estimate.no || estimate.title || "") : null;  // 案件表示＝見積番号を優先
    const [items, setItems] = useState(null);
    const [inspectors, setInspectors] = useState(null);
    const [checked, setChecked] = useState({});   // item_id -> bool
    const [inspectorId, setInspectorId] = useState("");
    const [result, setResult] = useState("passed");  // フェーズ2：passed=合格 / failed=不合格（要直し）
    const [note, setNote] = useState("");            // 不合格の理由・直す場所
    const [busy, setBusy] = useState(false);
    const [done, setDone] = useState(null);
    const [history, setHistory] = useState(null);   // 対象の完工検査 証跡（新しい順）
    const [mode, setMode] = useState("list");        // list=証跡・履歴 / run=新規検査
    const [masterOpen, setMasterOpen] = useState(false);  // ⚙ 検査者・項目の編集（既存GenbaInspectionを重ねて開く）

    const histUrl = estimate ? ("/api/inspections?estimate_id=" + estimate.id)
      : roomId ? ("/api/inspections?site_room_id=" + roomId) : ("/api/inspections?site_id=" + site.id);
    const loadHistory = () => api(histUrl).then((j) => setHistory(j.inspections || []));
    // マスター（項目・検査者）取得。⚙編集を閉じた直後にも呼び、追加した検査者を即「検査した人」へ反映する。
    const loadMasters = () =>
      Promise.all([api("/api/inspection-items?active=1"), api("/api/inspectors?active=1")]).then(([a, b]) => {
        setItems((a.items || []).slice().sort((x, y) => x.sort - y.sort || x.id - y.id));
        setInspectors((b.inspectors || []).slice().sort((x, y) => x.sort - y.sort || x.id - y.id));
      });
    useEffect(() => { loadMasters(); loadHistory(); }, []);
    const startNew = () => { setChecked({}); setInspectorId(""); setResult("passed"); setNote(""); setDone(null); setMode("run"); };
    const backToList = () => { setDone(null); setMode("list"); loadHistory(); };

    const toggle = (id) => setChecked((c) => ({ ...c, [id]: !c[id] }));
    const requiredLeft = items ? items.filter((i) => i.required && !checked[i.id]).length : 1;
    // 合格＝必須を全部✅／不合格＝✅が不足でも記録できる（直す所を残すのが目的）。検査者はどちらも必須。
    const canFinish = !!inspectorId && !busy && (result === "failed" || requiredLeft === 0);

    const finish = async () => {
      if (!canFinish) return;
      setBusy(true);
      try {
        const checks = items.map((i, idx) => ({ item_id: i.id, label: i.label, required: i.required, checked: !!checked[i.id], sort: i.sort ?? idx }));
        // 案件単位＝estimate_id のみ送る（site_id なし＝サーバCHECKはどちらか必須で通る）。従来2経路は不変。
        const body = estimate
          ? { estimate_id: estimate.id, inspector_id: Number(inspectorId), checks, result, note: note.trim() }
          : { site_id: site.id, inspector_id: Number(inspectorId), checks, result, note: note.trim() };
        if (roomId) body.site_room_id = roomId;      // 部屋単位＝現場IDと併せて部屋IDも送る
        const r = await api("/api/inspections", { method: "POST", body: JSON.stringify(body) });
        if (r.ok) { setDone(r.inspection); if (onDone) onDone(r.inspection); loadHistory(); }
        else alert("完工検査の記録に失敗しました：" + (r.err || ""));
      } finally { setBusy(false); }
    };

    return (
      <div style={st.backdrop} onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
        <div style={st.panel} className="screen-ui">
          <div style={st.bar}>
            <h1 style={st.h1}>{mode === "run" ? "✅ 検査する" : "✅ 完工検査"}{roomLabel ? "（" + roomLabel + "）" : estLabel ? "（" + estLabel + "）" : ""}</h1>
            <button style={st.closeBtn} onClick={onClose}>閉じる</button>
          </div>
          <div style={st.scroll}>
            {done ? (
              <div style={rst.okBox}>
                <div style={rst.okMark}>{done.result === "failed" ? "⚠️" : "✅"}</div>
                <div style={{ fontSize: 17, fontWeight: 700, marginBottom: 6 }}>
                  {done.result === "failed" ? "「直しが要る」として記録しました" : "検査を記録しました"}
                </div>
                <div style={st.note}>検査者：{done.inspector_name} ／ {done.checks.filter((c) => c.checked).length}/{done.checks.length} 項目を確認 ／ {fmtJST(done.inspected_at)}</div>
                {done.note ? <div style={st.note}>メモ：{done.note}</div> : null}
                {/* 写真は任意＝無くても記録は成立（上の記録は既に保存済み）。ここで今の記録に足せる。 */}
                <div style={{ display: "flex", justifyContent: "center" }}><PhotoStrip inspId={done.id} /></div>
                <button style={{ ...rst.primary, marginTop: 14 }} onClick={backToList}>証跡・履歴を見る</button>
              </div>
            ) : mode === "list" ? (
              <React.Fragment>
                <p style={st.note}>
                  {estLabel
                    ? <React.Fragment>対象の案件：<b>{estLabel}</b>。この案件の完工検査の記録（誰が・いつ・何項目を確認したか）です。</React.Fragment>
                    : roomLabel
                    ? <React.Fragment>対象の部屋：<b>{roomLabel}</b>（{site.name}）。この部屋の検査の記録です。</React.Fragment>
                    : <React.Fragment>対象の現場：<b>{site.name}</b>。この現場の完工検査の記録（誰が・いつ・何項目を確認したか）です。</React.Fragment>}
                </p>
                <section style={st.card}>
                  <div style={st.h2}>検査 履歴 {history && <span style={st.count}>{history.length}件</span>}</div>
                  {history === null ? <div style={st.empty}>読み込み中…</div>
                    : history.length === 0 ? <div style={st.empty}>まだ検査の記録がありません。下の「新しく検査する」から記録できます。</div>
                    : history.map((h) => (
                      <div key={h.id} style={rst.histRow}>
                        <div style={rst.histTop}>
                          <span style={h.result === "failed" ? rst.ngBadge : rst.doneBadge}>
                            {h.result === "failed" ? "⚠️ 直しが要る" : "✅ 合格"}
                          </span>
                          <span style={{ fontWeight: 700 }}>{h.inspector_name}</span>
                        </div>
                        <div style={rst.histMeta}>{fmtJST(h.inspected_at)} ／ {(h.checks || []).filter((c) => c.checked).length}/{(h.checks || []).length} 項目を確認</div>
                        {h.note ? <div style={rst.histMeta}>メモ：{h.note}</div> : null}
                        <PhotoStrip inspId={h.id} />
                      </div>
                    ))}
                </section>
                <button style={rst.primary} onClick={startNew}>＋ 新しく検査する</button>
              </React.Fragment>
            ) : (
              <React.Fragment>
                <button style={st.tmplBtn} onClick={backToList}>← 履歴に戻る</button>
                <p style={st.note}>
                  {estLabel
                    ? <React.Fragment>対象の案件：<b>{estLabel}</b>。項目を確認して✅し、検査した人を選んで下のボタンを押してください。</React.Fragment>
                    : roomLabel
                    ? <React.Fragment>対象の部屋：<b>{roomLabel}</b>（{site.name}）。項目を確認して✅し、検査した人を選んで下のボタンを押してください。</React.Fragment>
                    : <React.Fragment>対象の現場：<b>{site.name}</b>。項目を確認して✅し、検査した人を選んで下のボタンを押してください。</React.Fragment>}
                </p>

                <section style={st.card}>
                  <div style={st.h2}>結果</div>
                  <p style={st.note}>直す所が見つかったら「直しが要る」を選んでください。✅が揃っていなくても記録できます（何が残っているかを残すため）。</p>
                  <div style={{ display: "flex", gap: 8 }}>
                    <button style={rst.resBtn(result === "passed", "var(--success-border)")} onClick={() => setResult("passed")}>✅ 合格</button>
                    <button style={rst.resBtn(result === "failed", "var(--warn-border)")} onClick={() => setResult("failed")}>⚠️ 直しが要る</button>
                  </div>
                  <input style={{ ...rst.sel, marginTop: 10 }} type="text" value={note}
                    onChange={(e) => setNote(e.target.value)}
                    placeholder={result === "failed" ? "どこを直すか（例：入口の巾木に浮き）" : "メモ（任意）"} />
                </section>

                <section style={st.card}>
                  <div style={st.h2}>検査した人</div>
                  {/* ★案件（見積）単位のときは「設定 ＞ 完工検査マスター」が存在しない（見積モードに導線が無い）。
                      その場合はこの直下に出る実物のボタン名をそのまま案内する＝存在しない場所へ誘導しない。 */}
                  {inspectors === null ? <div style={st.empty}>読み込み中…</div>
                    : inspectors.length === 0 ? <div style={st.empty}>検査者が未登録です。{estimate ? "下の「⚙ 検査者・項目を編集」から登録してください。" : "設定 ＞ 完工検査マスター で登録してください。"}</div>
                    : <select style={rst.sel} value={inspectorId} onChange={(e) => setInspectorId(e.target.value)}>
                        <option value="">— 選んでください —</option>
                        {inspectors.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
                      </select>}
                  {/* 案件（見積もり）モード限定＝現場に潜らずここから検査者・項目を登録できる導線。
                      現場/部屋モードは従来どおり（設定＞完工検査マスター）＝見た目不変。 */}
                  {estimate && (
                    <button style={{ ...st.tmplBtn, marginBottom: 0, marginTop: 10 }} onClick={() => setMasterOpen(true)}>⚙ 検査者・項目を編集</button>
                  )}
                </section>

                <section style={st.card}>
                  <div style={st.h2}>検査項目 {items && <span style={st.count}>{items.filter((i) => checked[i.id]).length}/{items.length} 確認</span>}</div>
                  {/* ★同上。ただしこのセクションは⚙ボタンより【後】に出るので「上の」と案内する
                      （⚙は直前の「検査した人」セクション内＝この文より上に描画される）。 */}
                  {items === null ? <div style={st.empty}>読み込み中…</div>
                    : items.length === 0 ? <div style={st.empty}>検査項目が未設定です。{estimate ? "上の「⚙ 検査者・項目を編集」から登録してください。" : "設定 ＞ 完工検査マスター で登録してください。"}</div>
                    : items.map((it) => (
                      <div key={it.id} style={rst.checkRow} onClick={() => toggle(it.id)}>
                        <div style={rst.box(!!checked[it.id])}>{checked[it.id] ? "✓" : ""}</div>
                        <span style={rst.lbl}>{it.label}</span>
                        <span style={st.tag(!!it.required)}>{it.required ? "必須" : "任意"}</span>
                      </div>
                    ))}
                </section>

                <button style={rst.done(!canFinish)} disabled={!canFinish} onClick={finish}>
                  {!inspectorId ? "検査した人を選んでください"
                    : result === "failed" ? "⚠️ 直しが要るとして記録する"
                    : requiredLeft > 0 ? `必須があと${requiredLeft}項目`
                    : roomLabel ? "この部屋を合格にする" : estLabel ? "この案件を完工にする" : "完工にする"}
                </button>
                <p style={st.note}>※写真は後から足せます（この記録は写真がなくても成立します）。</p>
              </React.Fragment>
            )}
          </div>
        </div>
        {/* ⚙ 検査者・項目の編集＝既存マスター（同IIFE内 GenbaInspection・site非依存）を重ねて開く。
            閉じたら loadMasters() で検査者/項目を取り直し＝追加した人が即「検査した人」に出る。 */}
        {masterOpen && (
          <GenbaInspection onClose={() => { setMasterOpen(false); loadMasters(); }} />
        )}
      </div>
    );
  }

  window.GenbaInspectionRun = GenbaInspectionRun;
  return GenbaInspection;
})();
