/* compact-list-rows applied */
const DOMAIN_CONFIG = (typeof window !== "undefined" && window.SYSTEM_CONFIG && window.SYSTEM_CONFIG.domains) || [];

// ページネーション状態
// enum コード → 日本語ラベル（class やAPI送信値はraw、表示テキストのみ翻訳）
const ENUM_LABELS = {
  status: {
    active: "有効", inactive: "無効",
    pending: "申請中", approved: "承認済", rejected: "却下",
    certified: "認定済", expired: "失効", suspended: "停止中",
    draft: "下書き", previewed: "プレビュー済", submitted: "提出済",
    DONE: "完了", QUEUED: "承認待ち", FAILED: "却下",
    completed: "完了", processing: "処理中", canceled: "取消",
    paid: "支払済", unpaid: "未払い", scheduled: "予定",
  },
  certificationStatus: {
    pending: "申請中", certified: "認定済", rejected: "却下", expired: "失効", suspended: "停止中",
  },
  applicationType: {
    new_certification: "新規認定", renewal: "更新", amount_change: "額改定",
    child_addition: "児童追加", status_change: "状況変更", termination: "終了",
    new: "新規",
  },
  checkupType: {
    specific_health: "特定健診", cancer_screening: "がん検診", dental: "歯科",
    mental_health: "メンタル", infant: "乳幼児", pregnant: "妊婦",
  },
  exemptionType: {
    full: "全額免除", three_quarters: "3/4免除", half: "半額免除",
    one_quarter: "1/4免除", student: "学生納付特例",
  },
  requestType: {
    tohon: "戸籍謄本", shohon: "戸籍抄本",
    jyuki_tohon: "住民票謄本", jyuki_shohon: "住民票抄本",
    gaijin_tohon: "外国人住民票", identity_proof: "身分証明",
  },
  eventType: {
    birth: "出生", death: "死亡", marriage: "婚姻", divorce: "離婚",
    adoption: "養子縁組", address_change: "住所変更", name_change: "氏名変更",
    transfer: "転出入", stillbirth: "死産", fetal_death: "胎児死亡",
  },
  supportLevel: { none: "対象外", motivational: "動機付け支援", active: "積極的支援" },
  metabolicSyndromeJudgment: { normal: "該当なし", predisposition: "予備群", positive: "該当" },
  disabilityType: { physical: "身体", intellectual: "知的", mental: "精神", developmental: "発達" },
  gender: { male: "男", female: "女", other: "その他", unknown: "不明", M: "男", F: "女" },
  result: { normal: "正常", abnormal: "異常", reexam: "要再検査", pending: "保留" },
  overallJudgment: { A: "A判定", B: "B判定", C: "C判定", D: "D判定" },
  certifiedLevel: { support1: "要支援1", support2: "要支援2", care1: "要介護1", care2: "要介護2", care3: "要介護3", care4: "要介護4", care5: "要介護5" },
  type: {
    physical: "身体", intellectual: "知的", mental: "精神",
    individual: "個人", corporate: "法人",
    receipt: "受領", issuance: "発行",
    birth: "出生", death: "死亡", marriage: "婚姻", divorce: "離婚",
    stillbirth: "死産", fetal_death: "胎児死亡",
  },
};
function enumLabel(key, val) {
  if (val == null || val === "") return val;
  const m = ENUM_LABELS[key];
  return m && Object.prototype.hasOwnProperty.call(m, val) ? m[val] : val;
}
// 詳細表示で隠す内部フィールド（DB管理用）
const HIDDEN_KEYS = new Set(["id", "createdBy", "updatedBy", "createdAt", "updatedAt"]);

const PAGE_LIMIT = 50;
const state = {
  catalog: null, me: null,
  records: [], recordsTotal: 0, recordsOffset: 0, recordsSearch: "", recordsFilterDomain: "", recordsFilterStatus: "",
  audit: [], auditTotal: 0, auditSearch: "",
  jobs: [], jobsTotal: 0, jobsOffset: 0, jobsSearch: "",
  eucJobs: [], eucJobsTotal: 0, eucJobsOffset: 0, eucSearch: "",
  restrictions: [], domainData: {},
};
const views = ["overview", "domain", "work", "reports", "admin", "online", "calc"];

// ── 多言語(i18n): 日本語 / English（職員画面・共通core）──
const STAFF_I18N = {
  ja: {
    "nav.overview":"概要","nav.work":"汎用業務","nav.reports":"帳票","nav.admin":"管理","nav.online":"オンライン申請","nav.calc":"算定",
    "logout":"ログアウト","offline":"⚠️ オフラインです。キャッシュされたデータを表示中。",
    "login.eyebrow":"Standard-compliant web — 職員用","login.title":"職員ログイン","login.id":"職員ID","login.pw":"パスワード","login.role":"役割",
    "role.admin":"管理者（全権限）","role.clerk":"窓口担当（CLERK）","role.auditor":"監査担当（AUDITOR）",
    "login.btn":"ログイン","login.demo":"デモ用: 任意の職員ID・パスワードでログインできます（参照実装のフロント側デモ認証）。","login.required":"職員IDとパスワードを入力してください。",
    "common.search":"検索","common.csv":"CSV出力","common.print":"🖨️ 印刷","common.delete":"削除","common.register":"登録","common.ops":"操作",
    "tbl.domain":"領域","tbl.status":"状態","tbl.subject":"件名","tbl.updated":"更新日時",
    "dash.viz":"可視化","dash.statusComp":"登録データの状態別構成","dash.trend":"直近14日の登録推移",
    "work.create":"業務レコード作成","work.domain":"業務領域","work.subject":"件名","work.content":"内容",
    "calc.run":"算定する","calc.note":"入力して「算定する」を押すと、即座に結果を表示します。","calc.yes":"○ 該当","calc.no":"× 非該当","calc.apply":"この算定結果で受付登録へ →",
    "calc.linkTitle":"中間サーバ照会",
    "calc.linkNote":"宛名番号で中間サーバへ情報照会し、取得値を算定フォームに反映します（別表で許可された範囲のみ）。",
    "calc.linkAtena":"宛名番号",
    "calc.linkFetch":"中間サーバから照会",
    "calc.linkDone":"取得値を算定フォームに反映しました。",
    "calc.linkFail":"中間サーバへの照会に失敗しました。",
  },
  en: {
    "nav.overview":"Overview","nav.work":"General","nav.reports":"Reports","nav.admin":"Admin","nav.online":"Online applications","nav.calc":"Estimate",
    "logout":"Log out","offline":"⚠️ You are offline. Showing cached data.",
    "login.eyebrow":"Standard-compliant web — Staff","login.title":"Staff login","login.id":"Staff ID","login.pw":"Password","login.role":"Role",
    "role.admin":"Administrator (all permissions)","role.clerk":"Counter clerk (CLERK)","role.auditor":"Auditor (AUDITOR)",
    "login.btn":"Sign in","login.demo":"Demo: sign in with any staff ID and password (front-end demo auth of this reference implementation).","login.required":"Please enter staff ID and password.",
    "common.search":"Search","common.csv":"Export CSV","common.print":"🖨️ Print","common.delete":"Delete","common.register":"Register","common.ops":"Actions",
    "tbl.domain":"Domain","tbl.status":"Status","tbl.subject":"Subject","tbl.updated":"Updated",
    "dash.viz":"Visualization","dash.statusComp":"Records by status","dash.trend":"Records (last 14 days)",
    "work.create":"Create work record","work.domain":"Domain","work.subject":"Subject","work.content":"Content",
    "calc.run":"Calculate","calc.note":"Enter values and press Calculate to see results instantly.","calc.yes":"○ Yes","calc.no":"× No","calc.apply":"Register from this result →",
    "calc.linkTitle":"Intermediary-server query",
    "calc.linkNote":"Query the intermediary server by reference number; permitted items are filled into the form (per the linkage table).",
    "calc.linkAtena":"Reference number",
    "calc.linkFetch":"Query intermediary server",
    "calc.linkDone":"Fetched values were applied to the form.",
    "calc.linkFail":"Query to the intermediary server failed.",
  },
  easy: {
    "nav.overview":"ぜんたいの ようす","nav.work":"ふつうの しごと","nav.reports":"ちょうひょう（しょるい）","nav.admin":"かんり","nav.online":"オンライン しんせい","nav.calc":"けいさん",
    "logout":"ログアウト（おわる）","offline":"⚠️ いま インターネットが つながって いません。ほぞんした データを みせて います。",
    "login.eyebrow":"ひょうじゅんの Web — しょくいんよう","login.title":"しょくいん ログイン","login.id":"しょくいん ID","login.pw":"パスワード","login.role":"やくわり",
    "role.admin":"かんりしゃ（ぜんぶ できる）","role.clerk":"まどぐち たんとう（CLERK）","role.auditor":"かんさ たんとう（AUDITOR）",
    "login.btn":"ログイン（はいる）","login.demo":"デモです。すきな ID と パスワードで ログインできます。","login.required":"しょくいん ID と パスワードを いれて ください。",
    "common.search":"さがす","common.csv":"CSVで だす","common.print":"🖨️ いんさつ","common.delete":"けす","common.register":"とうろく","common.ops":"そうさ",
    "tbl.domain":"りょういき","tbl.status":"じょうたい","tbl.subject":"けんめい","tbl.updated":"こうしんした ひにち",
    "dash.viz":"グラフ","dash.statusComp":"とうろくデータの じょうたいべつ","dash.trend":"さいきん14にちの とうろく",
    "work.create":"しごとの データを つくる","work.domain":"しごとの りょういき","work.subject":"けんめい","work.content":"ないよう",
    "calc.run":"けいさんする","calc.note":"いれて「けいさんする」を おすと、すぐに けっかが でます。","calc.yes":"○ あてはまる","calc.no":"× あてはまらない","calc.apply":"この けっかで うけつけ とうろくへ →",
    "calc.linkTitle":"ちゅうかんサーバへの といあわせ",
    "calc.linkNote":"あてな ばんごうで ちゅうかんサーバに きいて、けっかを フォームに いれます（きまった はんいだけ）。",
    "calc.linkAtena":"あてな ばんごう",
    "calc.linkFetch":"ちゅうかんサーバに きく",
    "calc.linkDone":"けっかを フォームに いれました。",
    "calc.linkFail":"といあわせが できませんでした。",
  },
  zh: {
    "nav.overview":"概览","nav.work":"通用业务","nav.reports":"报表","nav.admin":"管理","nav.online":"在线申请","nav.calc":"试算",
    "logout":"登出","offline":"⚠️ 当前处于离线状态，显示已缓存的数据。",
    "login.eyebrow":"标准合规 Web — 工作人员用","login.title":"工作人员登录","login.id":"工作人员ID","login.pw":"密码","login.role":"角色",
    "role.admin":"管理员（全部权限）","role.clerk":"窗口办理（CLERK）","role.auditor":"审计（AUDITOR）",
    "login.btn":"登录","login.demo":"演示用：可使用任意工作人员ID和密码登录（本参考实现的前端演示认证）。","login.required":"请输入工作人员ID和密码。",
    "common.search":"搜索","common.csv":"导出CSV","common.print":"🖨️ 打印","common.delete":"删除","common.register":"登记","common.ops":"操作",
    "tbl.domain":"领域","tbl.status":"状态","tbl.subject":"主题","tbl.updated":"更新时间",
    "dash.viz":"可视化","dash.statusComp":"登记数据的状态构成","dash.trend":"近14天的登记趋势",
    "work.create":"创建业务记录","work.domain":"业务领域","work.subject":"主题","work.content":"内容",
    "calc.run":"试算","calc.note":"输入后点击「试算」即可立即显示结果。","calc.yes":"○ 符合","calc.no":"× 不符合","calc.apply":"以此试算结果进行受理登记 →",
    "calc.linkTitle":"中间服务器查询",
    "calc.linkNote":"通过宛名编号向中间服务器查询，并将允许范围内的结果填入表单。",
    "calc.linkAtena":"宛名编号",
    "calc.linkFetch":"向中间服务器查询",
    "calc.linkDone":"已将查询结果填入表单。",
    "calc.linkFail":"查询中间服务器失败。",
  },
  ko: {
    "nav.overview":"개요","nav.work":"일반 업무","nav.reports":"장표","nav.admin":"관리","nav.online":"온라인 신청","nav.calc":"산정",
    "logout":"로그아웃","offline":"⚠️ 오프라인 상태입니다. 캐시된 데이터를 표시 중입니다.",
    "login.eyebrow":"표준 준수 Web — 직원용","login.title":"직원 로그인","login.id":"직원 ID","login.pw":"비밀번호","login.role":"역할",
    "role.admin":"관리자(모든 권한)","role.clerk":"창구 담당(CLERK)","role.auditor":"감사 담당(AUDITOR)",
    "login.btn":"로그인","login.demo":"데모용: 임의의 직원 ID와 비밀번호로 로그인할 수 있습니다(참조 구현의 프런트엔드 데모 인증).","login.required":"직원 ID와 비밀번호를 입력하세요.",
    "common.search":"검색","common.csv":"CSV 내보내기","common.print":"🖨️ 인쇄","common.delete":"삭제","common.register":"등록","common.ops":"작업",
    "tbl.domain":"영역","tbl.status":"상태","tbl.subject":"제목","tbl.updated":"갱신 일시",
    "dash.viz":"시각화","dash.statusComp":"등록 데이터의 상태별 구성","dash.trend":"최근 14일 등록 추이",
    "work.create":"업무 레코드 작성","work.domain":"업무 영역","work.subject":"제목","work.content":"내용",
    "calc.run":"산정","calc.note":"값을 입력하고 「산정」을 누르면 즉시 결과가 표시됩니다.","calc.yes":"○ 해당","calc.no":"× 비해당","calc.apply":"이 산정 결과로 접수 등록 →",
    "calc.linkTitle":"중간서버 조회",
    "calc.linkNote":"아테나 번호로 중간서버에 조회하여 허용된 항목을 폼에 반영합니다.",
    "calc.linkAtena":"아테나 번호",
    "calc.linkFetch":"중간서버 조회",
    "calc.linkDone":"조회 값을 폼에 반영했습니다.",
    "calc.linkFail":"중간서버 조회에 실패했습니다.",
  },
  vi: {
    "nav.overview":"Tổng quan","nav.work":"Nghiệp vụ chung","nav.reports":"Báo biểu","nav.admin":"Quản trị","nav.online":"Nộp trực tuyến","nav.calc":"Ước tính",
    "logout":"Đăng xuất","offline":"⚠️ Đang ngoại tuyến. Hiển thị dữ liệu đã lưu trong bộ nhớ đệm.",
    "login.eyebrow":"Web tuân thủ chuẩn — Dành cho cán bộ","login.title":"Đăng nhập cán bộ","login.id":"ID cán bộ","login.pw":"Mật khẩu","login.role":"Vai trò",
    "role.admin":"Quản trị viên (toàn quyền)","role.clerk":"Cán bộ quầy (CLERK)","role.auditor":"Kiểm toán (AUDITOR)",
    "login.btn":"Đăng nhập","login.demo":"Bản demo: có thể đăng nhập bằng bất kỳ ID và mật khẩu nào (xác thực demo phía giao diện của bản tham chiếu này).","login.required":"Vui lòng nhập ID cán bộ và mật khẩu.",
    "common.search":"Tìm kiếm","common.csv":"Xuất CSV","common.print":"🖨️ In","common.delete":"Xóa","common.register":"Đăng ký","common.ops":"Thao tác",
    "tbl.domain":"Lĩnh vực","tbl.status":"Trạng thái","tbl.subject":"Tiêu đề","tbl.updated":"Cập nhật",
    "dash.viz":"Trực quan hóa","dash.statusComp":"Cấu thành theo trạng thái","dash.trend":"Đăng ký trong 14 ngày gần đây",
    "work.create":"Tạo bản ghi nghiệp vụ","work.domain":"Lĩnh vực nghiệp vụ","work.subject":"Tiêu đề","work.content":"Nội dung",
    "calc.run":"Ước tính","calc.note":"Nhập giá trị và nhấn「Ước tính」để xem kết quả ngay lập tức.","calc.yes":"○ Thuộc diện","calc.no":"× Không thuộc","calc.apply":"Đăng ký tiếp nhận từ kết quả này →",
    "calc.linkTitle":"Truy vấn máy chủ trung gian",
    "calc.linkNote":"Truy vấn máy chủ trung gian bằng mã số định danh; các mục được phép sẽ được điền vào biểu mẫu.",
    "calc.linkAtena":"Mã số định danh",
    "calc.linkFetch":"Truy vấn máy chủ trung gian",
    "calc.linkDone":"Đã điền kết quả vào biểu mẫu.",
    "calc.linkFail":"Truy vấn máy chủ trung gian thất bại.",
  },
};
let _lang = localStorage.getItem("gov-staff-lang") || "ja";
function tr(key) { return (STAFF_I18N[_lang] && STAFF_I18N[_lang][key]) || STAFF_I18N.ja[key] || key; }
function applyStaffI18n() {
  const navMap = { overview:"nav.overview", work:"nav.work", reports:"nav.reports", admin:"nav.admin", online:"nav.online", calc:"nav.calc" };
  document.querySelectorAll("nav button[data-view]").forEach((b) => { const k = navMap[b.dataset.view]; if (k) b.textContent = tr(k); });
  const lo = document.getElementById("staff-logout"); if (lo) { lo.textContent = tr("logout"); lo.title = tr("logout"); }
  const ob = document.getElementById("offline-banner"); if (ob) ob.textContent = tr("offline");
  const setText = (sel, k) => { const e = document.querySelector(sel); if (e) e.textContent = tr(k); };
  setText(".staff-login__eyebrow", "login.eyebrow"); setText(".staff-login__title", "login.title"); setText(".staff-login__demo", "login.demo");
  setText("#staff-login-form button.primary", "login.btn");
  const labs = document.querySelectorAll("#staff-login-form > label"); const lk = ["login.id","login.pw","login.role"];
  labs.forEach((l, i) => { if (lk[i] && l.childNodes[0] && l.childNodes[0].nodeType === 3) l.childNodes[0].nodeValue = tr(lk[i]); });
  const opts = document.querySelectorAll("#staff-role option"); const ok = ["role.admin","role.clerk","role.auditor"];
  opts.forEach((o, i) => { if (ok[i]) o.textContent = tr(ok[i]); });
  if (!document.getElementById("lang-select")) {
    const nav = document.querySelector("header.topbar nav"); const logout = document.getElementById("staff-logout");
    if (nav) { const sel = document.createElement("select"); sel.id = "lang-select"; sel.style.marginLeft = "8px"; sel.title = "言語 / Language";
      sel.innerHTML = '<option value="ja">日本語</option><option value="en">English</option><option value="easy">やさしい日本語</option><option value="zh">中文</option><option value="ko">한국어</option><option value="vi">Tiếng Việt</option>';
      sel.addEventListener("change", (e) => setLang(e.target.value)); nav.insertBefore(sel, logout || null); }
  }
  const ls = document.getElementById("lang-select"); if (ls) ls.value = _lang;
  document.documentElement.lang = _lang;
}
function setLang(l) { _lang = l; localStorage.setItem("gov-staff-lang", l); applyStaffI18n();
  try { refreshAndRender(document.querySelector("nav button.active")?.dataset.view || "overview"); } catch (e) {} }

const _APP_TITLE = document.title; // ベースタイトルを保存

// ISO日付を日本語形式に変換
function formatVal(v) {
  if (v == null) return "";
  const s = String(v);
  const dtm = s.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
  if (dtm) return `${dtm[1]}年${dtm[2]}月${dtm[3]}日 ${dtm[4]}:${dtm[5]}`;
  const dt = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (dt) return `${dt[1]}年${dt[2]}月${dt[3]}日`;
  return s;
}

// トーストUI (alert()の代替)
function toast(message, type = "info") {
  const container = document.getElementById("toast-container");
  if (!container) { console.log("[toast]", type, message); return; }
  const div = document.createElement("div");
  div.className = "toast toast-" + type;
  div.textContent = message;
  container.append(div);
  const dismiss = () => {
    div.classList.add("toast-fade");
    div.addEventListener("animationend", () => div.remove(), { once: true });
  };
  div.addEventListener("click", dismiss);
  setTimeout(dismiss, type === "error" ? 5000 : 3000);
}

// カスタム確認ダイアログ (confirm()の代替)
function showConfirm(message, okLabel = "実行する") {
  return new Promise((resolve) => {
    const modal = document.getElementById("confirm-modal");
    const msgEl = document.getElementById("confirm-modal-msg");
    const okBtn = document.getElementById("confirm-modal-ok");
    const cancelBtn = document.getElementById("confirm-modal-cancel");
    if (!modal) { resolve(window.confirm(message)); return; }
    msgEl.textContent = message;
    okBtn.textContent = okLabel;
    modal.classList.add("visible");
    cancelBtn.focus(); // フォーカスをキャンセルボタンに設定
    // フォーカストラップ (Tab キーをモーダル内のボタン間で循環)
    const onTrap = (e) => { if (e.key !== "Tab") return; e.preventDefault(); if (document.activeElement === okBtn) cancelBtn.focus(); else okBtn.focus(); };
    window.addEventListener("keydown", onTrap);
    const cleanup = (result) => {
      modal.classList.remove("visible");
      window.removeEventListener("keydown", onTrap); // フォーカストラップ解除
      resolve(result);
    };
    okBtn.addEventListener("click", () => cleanup(true), { once: true });
    cancelBtn.addEventListener("click", () => cleanup(false), { once: true });
    // Escapeキーでキャンセル
    const onKey = (e) => { if (e.key === "Escape") { window.removeEventListener("keydown", onKey); cleanup(false); } };
    window.addEventListener("keydown", onKey);
  });
}

const el = (tag, props = {}, children = []) => {
  const node = document.createElement(tag);
  Object.entries(props).forEach(([key, value]) => {
    if (key === "class") node.className = value;
    else if (key === "html") node.innerHTML = value;
    else node.setAttribute(key, value);
  });
  children.forEach((child) => node.append(child));
  return node;
};

const api = async (path, options = {}) => {
  const res = await fetch(path, {
    ...options,
    headers: {
      "content-type": "application/json",
      "x-user-roles": "ADMIN,CLERK,AUDITOR,SUPPORT_ACCESS,REPORT_VIEWER,REPORT_REQUESTER,REPORT_APPROVER",
      ...(options.headers ?? {}),
    },
  });
  const body = await res.json();
  if (!res.ok) throw new Error(body.error ?? "API request failed");
  return body;
};

const list = (items, emptyText) => {
  const box = el("div", { class: "list" });
  if (!items?.length) box.append(el("p", { class: "muted" }, [emptyText]));
  (items ?? []).slice(0, 12).forEach((item) => {
    box.append(el("div", { class: "item" }, [
      el("code", {}, [item.id ?? "ID未設定"]),
      el("h3", {}, [item.name ?? "名称未設定"]),
      el("p", { class: "muted" }, [item.description ?? ""]),
    ]));
  });
  return box;
};

async function refreshRuntimeData() {
  const domainFetches = DOMAIN_CONFIG.map((cfg) => api(cfg.endpoint).catch(() => []));
  const [me, records, audit, jobs, restrictions, eucJobs, ...domainResults] = await Promise.all([
    api("/api/me"),
    api(`/api/records?limit=${PAGE_LIMIT}&offset=${state.recordsOffset}${state.recordsSearch ? "&search=" + encodeURIComponent(state.recordsSearch) : ""}${state.recordsFilterDomain ? "&domain=" + encodeURIComponent(state.recordsFilterDomain) : ""}${state.recordsFilterStatus ? "&status=" + encodeURIComponent(state.recordsFilterStatus) : ""}${state.recordsSort ? "&sort=" + encodeURIComponent(state.recordsSort) : ""}`),
    // filters applied
    api(`/api/audit?limit=200${state.auditSearch ? "&search=" + encodeURIComponent(state.auditSearch) : ""}`),
    api(`/api/reports/jobs?limit=${PAGE_LIMIT}&offset=${state.jobsOffset}${state.jobsSearch ? "&search=" + encodeURIComponent(state.jobsSearch) : ""}`),
    api("/api/restrictions"),
    api(`/api/euc/jobs?limit=${PAGE_LIMIT}&offset=${state.eucJobsOffset}${state.eucSearch ? "&search=" + encodeURIComponent(state.eucSearch) : ""}`),
    ...domainFetches,
  ]);
  const domainData = {};
  DOMAIN_CONFIG.forEach((cfg, i) => { domainData[cfg.id] = domainResults[i]; });
  // ページネーション対応 — {total,data} または配列の両方を受け付ける
  Object.assign(state, {
    me,
    records: records.data ?? records,
    recordsTotal: records.total ?? (records.data ?? records).length,
    audit: audit.data ?? [],
    auditTotal: audit.total ?? 0,
    jobs: jobs.data ?? jobs,
    jobsTotal: jobs.total ?? (jobs.data ?? jobs).length,
    restrictions,
    eucJobs: eucJobs.data ?? eucJobs,
    eucJobsTotal: eucJobs.total ?? (eucJobs.data ?? eucJobs).length,
    domainData,
  });
}

// ── ダッシュボード可視化（登録データの状態別構成・登録推移）外部ライブラリ非依存 ──
const _STATUS_LABELS = { draft: "下書き", submitted: "提出済", approved: "承認済", pending: "申請中", rejected: "却下", RECEIVED: "受付", PROCESSING: "処理中", COMPLETED: "完了", REJECTED: "却下", restricted: "抑止" };
// CSP(default-src self)はstyle属性をブロックするため、寸法はCSSOM(.style)で設定する
function _styled(node, prop, val) { node.style.setProperty(prop, val); return node; }
function _barChartH(title, rows) {
  const max = Math.max(1, ...rows.map((r) => r.value));
  return el("div", { class: "panel chart-panel" }, [
    el("h2", {}, [title]),
    rows.length === 0
      ? el("p", { class: "muted" }, ["データがありません。"])
      : el("div", { class: "chart-bars" }, rows.map((r) =>
          el("div", { class: "chart-row" }, [
            el("span", { class: "chart-label", title: r.label }, [r.label]),
            el("div", { class: "chart-track" }, [_styled(el("div", { class: "chart-fill" }, []), "width", Math.round(r.value / max * 100) + "%")]),
            el("span", { class: "chart-value" }, [String(r.value)]),
          ]))),
  ]);
}
function _trendChartV(title, points) {
  const max = Math.max(1, ...points.map((p) => p.value));
  return el("div", { class: "panel chart-panel" }, [
    el("h2", {}, [title]),
    el("div", { class: "chart-trend" }, points.map((p) =>
      el("div", { class: "chart-col", title: p.label + ": " + p.value + "件" }, [
        el("div", { class: "chart-col-bar" }, [_styled(el("div", { class: "chart-col-fill" }, []), "height", Math.round(p.value / max * 92) + "px")]),
        el("span", { class: "chart-col-label" }, [p.label]),
      ]))),
  ]);
}
function renderDashboardCharts() {
  const recs = state.records || [];
  const byStatus = {};
  recs.forEach((r) => { const k = r.restricted ? "restricted" : (r.status || "未設定"); byStatus[k] = (byStatus[k] || 0) + 1; });
  const statusRows = Object.entries(byStatus).sort((a, b) => b[1] - a[1]).map(([k, v]) => ({ label: _STATUS_LABELS[k] || k, value: v }));
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const days = [];
  for (let i = 13; i >= 0; i--) { const d = new Date(today); d.setDate(d.getDate() - i); days.push(d); }
  const pad = (n) => String(n).padStart(2, "0");
  const counts = {};
  recs.forEach((r) => { const t = String(r.updatedAt || r.createdAt || "").slice(0, 10); if (t) counts[t] = (counts[t] || 0) + 1; });
  const trend = days.map((d) => ({ label: (d.getMonth() + 1) + "/" + d.getDate(), value: counts[d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate())] || 0 }));
  return el("div", { style: "margin-top:16px" }, [
    el("p", { style: "margin:0 0 6px;font-size:0.8rem;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.05em" }, [tr("dash.viz")]),
    el("div", { class: "chart-grid" }, [
      _barChartH(tr("dash.statusComp"), statusRows),
      _trendChartV(tr("dash.trend"), trend),
    ]),
  ]);
}
function renderOverview() {
  const c = state.catalog;
  const section = document.querySelector("#overview");
  section.replaceChildren(
    el("div", { class: "grid" }, [
      metric("機能", c.functions.length),
      metric("画面", c.screens.length),
      metric("帳票", c.reports.length),
      metric("API", c.apis.length),
      metric("登録データ", state.records.length),
      metric("EUC", state.eucJobs.length),
    ]),
    renderDashboardCharts(),
    // ドメインエンティティ統計カード
    (() => {
      const _dStats = DOMAIN_CONFIG.map((cfg) => {
        const _items = state.domainData[cfg.id] ?? [];
        const _badgeCol = cfg.cols.find((c) => c.badge);
        const _statText = _badgeCol
          ? [...new Set(_items.map((x) => x[_badgeCol.key]).filter(Boolean))]
              .map((s) => `${s}: ${_items.filter((x) => x[_badgeCol.key] === s).length}`)
              .join(" / ") || "—"
          : `${_items.length}件`;
        return el("div", { class: "panel", style: "min-width:140px;flex:0 0 auto;padding:10px 14px" }, [
          el("h3", { style: "font-size:0.8rem;color:#6b7280;margin:0 0 2px;white-space:nowrap" }, [cfg.label]),
          el("div", { style: "font-size:0.9rem;font-weight:600" }, [_items.length === 0 ? "0件" : _statText]),
        ]);
      });
      return el("div", { style: "margin-top:16px" }, [
        el("p", { style: "margin:0 0 6px;font-size:0.8rem;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.05em" }, ["ドメインデータ"]),
        el("div", { style: "display:flex;flex-wrap:wrap;gap:8px" }, _dStats),
      ]);
    })(),
    el("div", { class: "panel", style: "margin-top:16px" }, [
      el("h2", {}, ["利用者"]),
      el("p", {}, [`${state.me.name} / ${state.me.department}`]),
      el("p", { class: "muted" }, [`roles: ${state.me.roles.join(", ")}`]),
    ]),
    el("div", { class: "panel", style: "margin-top:16px" }, [
      el("h2", {}, ["設計書取り込み状況"]),
      el("p", { class: "muted" }, [`参照元: ${c.system.workbook || "設計書Excel未検出"}`]),
      catalogSearch(),
      list(c.functions, "機能一覧は設計書更新後に表示されます。"),
    ]),
  );
}

function catalogSearch() {
  const form = el("form", { class: "inline-form" });
  const input = el("input", { name: "q", placeholder: "機能・画面・帳票を検索" });
  const output = el("div", { class: "list" });
  form.append(input, el("button", { type: "submit" }, [tr("common.search")]));
  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const q = new FormData(form).get("q");
    const [functions, screens, reports] = await Promise.all([
      api(`/api/catalog/functions?q=${encodeURIComponent(q)}`),
      api(`/api/catalog/screens?q=${encodeURIComponent(q)}`),
      api(`/api/catalog/reports?q=${encodeURIComponent(q)}`),
    ]);
    output.replaceChildren(...[...functions, ...screens, ...reports].slice(0, 15).map((item) => (
      el("div", { class: "item" }, [el("code", {}, [item.id]), el("h3", {}, [item.name]), el("p", { class: "muted" }, [item.sheet ?? ""])])
    )));
  });
  return el("div", {}, [form, output]);
}

function metric(label, value) {
  return el("div", { class: "panel" }, [
    el("h3", {}, [label]),
    el("div", { class: "metric" }, [String(value)]),
  ]);
}


// ── ドメインタブ描画 ─────────────────────────────────────────────────────
function renderDomain() {
  const panels = DOMAIN_CONFIG.map((cfg) => {
    const items = state.domainData[cfg.id] ?? [];
    return el("div", { class: "panel", style: "margin-bottom:16px" }, [
      el("h2", {}, [cfg.label + " (" + items.length + " 件)"]),
      domainTable(cfg, items),
      el("details", {}, [
        el("summary", { style: "cursor:pointer;margin:8px 0;font-weight:bold" }, ["＋ 新規登録"]),
        domainCreateForm(cfg),
      ]),
    ]);
  });
  document.querySelector("#domain").replaceChildren(...panels);
}

function domainTable(cfg, items) {
  const container = el("div");
  if (!items.length) { container.append(el("p", { class: "muted" }, ["データがまだありません。"])); return container; }
  // 全アイテムで空のカラムを非表示
  const _visibleCols = cfg.cols.filter((col) => items.some((item) => { const v = item[col.key]; return v != null && v !== ""; }));
  // フィルタ状態を localStorage から復元
  const _filterStorage = "filter:" + cfg.id;
  const _filterSaved = localStorage.getItem(_filterStorage) ?? "";
  const filterInput = el("input", { type: "text", placeholder: "絞り込み検索…", style: "width:100%;max-width:400px;margin-bottom:8px", "aria-label": cfg.label + "を絞り込み検索", value: _filterSaved });
  const csvBtn = el("button", { style: "margin-left:8px;font-size:0.85rem" }, [tr("common.csv")]);
  csvBtn.addEventListener("click", () => {
    const q = filterInput.value.toLowerCase().trim();
    const rows = q ? items.filter((item) => JSON.stringify(item).toLowerCase().includes(q)) : items;
    if (!rows.length) { toast("出力するデータがありません。", "info"); return; }
    const keys = Object.keys(rows[0]);
    const csv = [keys.join(","), ...rows.map((r) => keys.map((k) => { const v = formatVal(r[k] ?? ""); return '"' + String(v).replace(/"/g, '""') + '"'; }).join(","))].join("\n");
    const a = document.createElement("a");
    a.href = URL.createObjectURL(new Blob(["﻿" + csv], { type: "text/csv;charset=utf-8" }));
    a.download = cfg.id + "_" + new Date().toISOString().slice(0, 10) + ".csv";
    a.click();
    URL.revokeObjectURL(a.href);
  });
  const printBtn = el("button", { style: "margin-left:8px;font-size:0.85rem", class: "print-hide" }, [tr("common.print")]);
  printBtn.addEventListener("click", () => window.print());
  // 一括削除
  const _selectedIds = new Set();
  const _bulkDelBtn = el("button", { style: "margin-left:8px;font-size:0.85rem;color:#991b1b;border-color:#fca5a5;display:none" }, ["選択削除"]);
  _bulkDelBtn.addEventListener("click", async () => {
    if (!_selectedIds.size) return;
    if (!await showConfirm(_selectedIds.size + " 件を削除しますか？", "削除する")) return;
    try {
      await Promise.all([..._selectedIds].map((id) => api(cfg.endpoint + "/" + id, { method: "DELETE" })));
      _selectedIds.clear();
      _bulkDelBtn.style.display = "none"; // 即時非表示
      _bulkDelBtn.textContent = "選択削除"; // テキストリセット
      await refreshAndRender("domain");
    } catch (e) { toast("削除エラー: " + e.message, "error"); }
  });
  // フィルタ件数バッジ
  const _countBadge = el("span", { style: "font-size:0.82rem;color:#6b7280;white-space:nowrap" });
  const filterRow = el("div", { style: "display:flex;align-items:center;gap:4px;margin-bottom:8px" }, [filterInput, _countBadge, csvBtn, printBtn, _bulkDelBtn]);
  const tableWrapper = el("div");
  const buildRow = (item) => {
    const _chk = el("input", { type: "checkbox", style: "margin:0 4px;cursor:pointer" });
    _chk.addEventListener("change", () => {
      if (_chk.checked) _selectedIds.add(item.id); else _selectedIds.delete(item.id);
      _bulkDelBtn.style.display = _selectedIds.size ? "" : "none";
    });
    const cells = _visibleCols.map((col) => {
      const val = item[col.key] ?? "";
      // バッジクリックでフィルタ
      if (col.badge) { const _lbl = enumLabel(col.key, val); const _bs = el("span", { class: "badge badge-" + String(val), style: "cursor:pointer", title: "クリックで絞り込み", role: "button", "aria-label": `${String(_lbl)} で絞り込む` }, [String(_lbl)]); _bs.addEventListener("click", () => { filterInput.value = String(val); _page = 0; renderFiltered(); }); return el("td", {}, [_bs]); }
      const _fv = formatVal(enumLabel(col.key, val)); const text = _fv.length > 40 ? _fv.slice(0, 40) + "…" : _fv;
      return el("td", {}, [text]);
    });
    const btn = el("button", { style: "color:#991b1b;border-color:#fca5a5" }, [tr("common.delete")]);
    btn.addEventListener("click", async () => {
      if (!await showConfirm("削除しますか？", "削除する")) return;
      try { await api(cfg.endpoint + "/" + item.id, { method: "DELETE" }); await refreshAndRender("domain"); }
      catch (e) { toast("削除エラー: " + e.message, "error"); }
    });
    const _linkBtn = el("button", { style: "margin-right:4px;font-size:0.85rem", title: "詳細ページを開く" }, ["🔗"]);
    _linkBtn.addEventListener("click", () => {
      location.hash = "/domain/" + cfg.id + "/" + item.id;
    });
    cells.push(el("td", {}, [_linkBtn, btn]));
    cells.unshift(el("td", {}, [_chk]));
    // tabindex + Enterキーで詳細ページへ
    const _tr = el("tr", { tabindex: "0", style: "cursor:pointer" }, cells);
    _tr.addEventListener("click", (e) => { if (e.target.closest("button,input,a,select")) return; location.hash = "/domain/" + cfg.id + "/" + item.id; });
    _tr.addEventListener("keydown", (e) => { if (e.key === "Enter") location.hash = "/domain/" + cfg.id + "/" + item.id; });
    return [_tr];
  };
  // ソート状態を localStorage から復元
  const _sortStorage = "sort:" + cfg.id;
  const _sortSaved = (() => { try { return JSON.parse(localStorage.getItem(_sortStorage) ?? "null"); } catch { return null; } })();
  let _sortKey = _sortSaved?.key ?? null, _sortDir = _sortSaved?.dir ?? 1, _page = 0;
  const renderFiltered = () => {
    const q = filterInput.value.toLowerCase().trim();
    let filtered = q ? items.filter((item) => JSON.stringify(item).toLowerCase().includes(q)) : items;
    // 件数バッジ更新
    _countBadge.textContent = q ? `${items.length}件中 ${filtered.length}件` : "";
    if (_sortKey) {
      filtered = [...filtered].sort((a, b) => {
        const av = String(a[_sortKey] ?? ""), bv = String(b[_sortKey] ?? "");
        return _sortDir * (av < bv ? -1 : av > bv ? 1 : 0);
      });
    }
    const table = el("table", { role: "grid", "aria-label": cfg.label + "一覧" });
    const headers = _visibleCols.map((col) => {
      const isSorted = _sortKey === col.key;
      const indicator = isSorted ? (_sortDir === 1 ? " ▲" : " ▼") : " ⇅";
      const _ariaSort = isSorted ? (_sortDir === 1 ? "ascending" : "descending") : "none";
      const th = el("th", { style: "cursor:pointer;user-select:none", "aria-sort": _ariaSort }, [col.label + indicator]);
      th.addEventListener("click", () => {
        if (_sortKey === col.key) { _sortDir = _sortDir === 1 ? -1 : (_sortDir === -1 ? 0 : 1); if (_sortDir === 0) _sortKey = null; }
        else { _sortKey = col.key; _sortDir = 1; }
        // ソート状態を localStorage に保存
        if (_sortKey) localStorage.setItem(_sortStorage, JSON.stringify({ key: _sortKey, dir: _sortDir }));
        else localStorage.removeItem(_sortStorage);
        renderFiltered();
      });
      return th;
    });
    headers.push(el("th", {}, [tr("common.ops")]));
    // 全選択チェックボックス
    const _allChk = el("input", { type: "checkbox", title: "全選択", style: "margin:0 4px;cursor:pointer" });
    _allChk.addEventListener("change", () => {
      const _pItems = filtered.slice(_page * _pageSize, (_page + 1) * _pageSize);
      _pItems.forEach((item) => { if (_allChk.checked) _selectedIds.add(item.id); else _selectedIds.delete(item.id); });
      body.querySelectorAll("input[type=checkbox]").forEach((c) => { c.checked = _allChk.checked; });
      _bulkDelBtn.style.display = _selectedIds.size ? "" : "none";
    });
    table.append(el("thead", {}, [el("tr", {}, [el("th", {}, [_allChk]), ...headers])]));
    const body = el("tbody");
    const _pageSize = 50;
    const _totalPages = Math.max(1, Math.ceil(filtered.length / _pageSize));
    if (_page >= _totalPages) _page = _totalPages - 1;
    const _pageItems = filtered.slice(_page * _pageSize, (_page + 1) * _pageSize);
    if (!filtered.length) { body.append(el("tr", {}, [el("td", { colspan: String(_visibleCols.length + 1), class: "muted" }, [q ? "条件に一致するデータがありません。" : "データがまだありません。"])])); }
    _pageItems.forEach((item) => { buildRow(item).forEach((r) => body.append(r)); });
    table.append(body);
    const _pageBar = el("div", { style: "display:flex;align-items:center;gap:8px;margin-top:6px;font-size:0.82rem;color:#6b7280" });
    if (_totalPages > 1) {
      const _prevBtn = el("button", { style: "font-size:0.82rem;padding:2px 8px" }, ["← 前"]);
      if (_page === 0) _prevBtn.disabled = true;
      _prevBtn.addEventListener("click", () => { _page--; renderFiltered(); });
      const _nextBtn = el("button", { style: "font-size:0.82rem;padding:2px 8px" }, ["次 →"]);
      if (_page >= _totalPages - 1) _nextBtn.disabled = true;
      _nextBtn.addEventListener("click", () => { _page++; renderFiltered(); });
      _pageBar.append(_prevBtn, `${_page + 1} / ${_totalPages} ページ（全 ${filtered.length} 件）`, _nextBtn);
    } else if (filtered.length > 0) {
      _pageBar.append(`全 ${filtered.length} 件`);
    }
    tableWrapper.replaceChildren(table, _pageBar);
  };
  filterInput.addEventListener("input", () => {
    _page = 0;
    // フィルタ状態を保存
    if (filterInput.value) localStorage.setItem(_filterStorage, filterInput.value);
    else localStorage.removeItem(_filterStorage);
    renderFiltered();
  });
  renderFiltered();
  container.append(filterRow, tableWrapper);
  return container;
}

function domainCreateForm(cfg) {
  const form = el("form", { style: "margin-top:8px" });
  cfg.createFields.forEach((field) => {
    let control;
    if (field.type === "select") {
      control = el("select", { name: field.name }, field.options.map((opt) => option(opt, enumLabel(field.name, opt))));
    } else if (field.type === "textarea") {
      control = el("textarea", { name: field.name, placeholder: field.placeholder ?? "" });
    } else {
      const attrs = { name: field.name, type: field.type ?? "text", placeholder: field.placeholder ?? "" };
      if (field.required) attrs.required = "required";
      control = el("input", attrs);
    }
    form.append(label(field.label + (field.required ? " *" : ""), control));
  });
  const _submitBtn = el("button", { class: "primary", type: "submit" }, [tr("common.register")]);
  form.append(_submitBtn);
  // 入力時にハイライト解除
  form.addEventListener("input", (ev) => {
    if (ev.target.value.trim()) { ev.target.classList.remove("field-error"); }
  });
  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    // ダブル送信防止
    if (_submitBtn.disabled) return;
    // カスタムバリデーション
    const _requiredEls = [...form.querySelectorAll("[required]")];
    const _invalids = _requiredEls.filter((el) => !el.value.trim());
    _requiredEls.forEach((el) => {
      if (_invalids.includes(el)) {
        el.classList.add("field-error");
      } else {
        el.classList.remove("field-error");
      }
    });
    if (_invalids.length) { _invalids[0].focus(); toast("必須項目を入力してください。", "error"); return; }
    const data = Object.fromEntries(new FormData(form).entries());
    Object.keys(data).forEach((k) => { if (data[k] === "") delete data[k]; });
    _submitBtn.disabled = true; _submitBtn.textContent = "送信中…";
    try {
      await api(cfg.endpoint, { method: "POST", body: JSON.stringify(data) });
      await refreshAndRender("domain");
      form.reset();
      _requiredEls.forEach((el) => { el.classList.remove("field-error"); });
    } catch (err) {
      toast("登録エラー: " + err.message, "error");
    } finally {
      _submitBtn.disabled = false; _submitBtn.textContent = tr("common.register");
    }
  });
  return form;
}
// ────────────────────────────────────────────────────────────────────────────

// レコード検索UI
let _searchTimer = null;
function recordsSearchBar() {
  const bar = el("div", { style: "display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap" });
  const searchInput = el("input", { type: "search", placeholder: "キーワード検索...", style: "flex:1;min-width:160px", value: state.recordsSearch ?? "" });
  const domainSel = el("select", { style: "min-width:100px" });
  [["", "全領域"], ["main", "主要業務"], ["support", "支援措置"], ["integration", "連携取込"]].forEach(([v, t]) => {
    const opt = el("option", { value: v }, [t]);
    if ((state.recordsFilterDomain ?? "") === v) opt.setAttribute("selected", "selected");
    domainSel.append(opt);
  });
  const statusSel = el("select", { style: "min-width:100px" });
  [["", "全状態"], ["draft", "下書き"], ["submitted", "申請済み"], ["approved", "承認済み"]].forEach(([v, t]) => {
    const opt = el("option", { value: v }, [t]);
    if ((state.recordsFilterStatus ?? "") === v) opt.setAttribute("selected", "selected");
    statusSel.append(opt);
  });
  const sortSel = el("select", { style: "min-width:120px", "aria-label": "並び替え" });
  [["updated_desc", "更新が新しい順"], ["updated_asc", "更新が古い順"], ["subject", "件名順"]].forEach(([v, t]) => {
    const opt = el("option", { value: v }, [t]);
    if ((state.recordsSort ?? "updated_desc") === v) opt.setAttribute("selected", "selected");
    sortSel.append(opt);
  });
  const applyFilter = async () => {
    state.recordsSearch = searchInput.value;
    state.recordsFilterDomain = domainSel.value;
    state.recordsFilterStatus = statusSel.value;
    state.recordsSort = sortSel.value;
    state.recordsOffset = 0;
    await refreshAndRender("work");
  };
  searchInput.addEventListener("input", () => { clearTimeout(_searchTimer); _searchTimer = setTimeout(applyFilter, 300); });
  domainSel.addEventListener("change", applyFilter);
  statusSel.addEventListener("change", applyFilter);
  sortSel.addEventListener("change", applyFilter);
  bar.append(searchInput, domainSel, statusSel, sortSel);
  return bar;
}

function renderWork() {
  const c = state.catalog;
  document.querySelector("#work").replaceChildren(
    el("div", { class: "grid" }, [
      el("div", { class: "panel" }, [el("h2", {}, ["画面一覧"]), list(c.screens, "画面定義が未登録です。")]),
      el("div", { class: "panel" }, [el("h2", {}, ["外部連携"]), list(c.interfaces, "外部連携定義が未登録です。")]),
    ]),
    el("div", { class: "panel", style: "margin-top:16px" }, [
      el("h2", {}, [tr("work.create")]),
      recordForm(),
    ]),
    el("div", { class: "panel", style: "margin-top:16px" }, [
      el("h2", {}, ["登録済みレコード"]),
      recordsSearchBar(),
      recordsTable(),
    ]),
  );
}

function recordForm() {
  const form = el("form");
  form.append(
    label(tr("work.domain"), el("select", { name: "domain" }, [
      option("main", "主要業務"),
      option("support", "支援措置・例外"),
      option("integration", "連携取込"),
    ])),
    label("状態", el("select", { name: "status" }, [
      option("draft", "下書き"),
      option("submitted", "申請済み"),
      option("approved", "承認済み"),
    ])),
    label(tr("work.subject"), el("input", { name: "subject", required: "required", placeholder: "例: 申請受付テスト" })),
    label(tr("work.content"), el("textarea", { name: "description", placeholder: "入力項目・状態・補足を記録" })),
    el("button", { class: "primary", type: "submit" }, [tr("common.register")]),
  );
  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const data = Object.fromEntries(new FormData(form));
    const post = (confirm) => api(`/api/records/${data.domain}${confirm ? "?confirm=1" : ""}`, {
      method: "POST",
      body: JSON.stringify({ status: data.status, payload: data }),
    });
    try {
      await post(false);
    } catch (error) {
      // 入力チェックのワーニング: 利用者が確認すれば続行できる
      if (!String(error.message).startsWith("WARNING_CONFIRM")) throw error;
      if (!window.confirm(String(error.message).replace("WARNING_CONFIRM: ", ""))) return;
      await post(true);
    }
    await refreshAndRender("work");
    form.reset();
  });
  return form;
}

function recordsTable() {
  const table = el("table");
  table.append(el("thead", {}, [el("tr", {}, [el("th", {}, [tr("tbl.domain")]), el("th", {}, [tr("tbl.status")]), el("th", {}, [tr("tbl.subject")]), el("th", {}, [tr("tbl.updated")]), el("th", {}, [tr("common.ops")])])]));
  const body = el("tbody");
  state.records.forEach((record) => {
    const deleteButton = el("button", { type: "button" }, [tr("common.delete")]);
    deleteButton.addEventListener("click", async () => {
      await api(`/api/records/${record.domain}/${record.id}`, { method: "DELETE" });
      await refreshAndRender("work");
    });
    const restrictButton = el("button", { type: "button" }, ["抑止"]);
    restrictButton.addEventListener("click", async () => {
      await api("/api/restrictions", { method: "POST", body: JSON.stringify({ recordId: record.id, category: "SUPPORT_MEASURE", scope: "record" }) });
      await refreshAndRender("admin");
    });
    // 電子決裁: 申請済みのみ承認/差戻しを表示
    const opCell = el("td", {}, [restrictButton, " ", deleteButton]);
    if (record.status === "submitted") {
      const approveButton = el("button", { type: "button", class: "primary" }, ["決裁"]);
      approveButton.addEventListener("click", async () => {
        const ok = window.confirm("このレコードを承認します（キャンセルで差戻し）。よろしいですか？");
        await api(`/api/records/${record.domain}/${record.id}/approval`, {
          method: "POST",
          body: JSON.stringify({ decision: ok ? "approve" : "reject" }),
        });
        toast(ok ? "承認しました" : "差戻しました", ok ? "success" : "info");
        await refreshAndRender("work");
      });
      opCell.prepend(approveButton, " ");
    }
    // 決定・変更WF: 種別を入力して決定を記録（遡及適用日は今日。詳細はAPIで指定可能）
    const decideButton = el("button", { type: "button" }, ["決定"]);
    decideButton.addEventListener("click", async () => {
      const kind = window.prompt("決定種別を入力してください（認定/変更/停止/再開/廃止/却下）", "認定");
      if (!kind) return;
      const reason = window.prompt("決定理由（任意）", "") ?? "";
      await api(`/api/records/${record.domain}/${record.id}/decision`, {
        method: "POST",
        body: JSON.stringify({ kind, reason }),
      });
      toast(`「${kind}」を記録しました`, "success");
      await refreshAndRender("work");
    });
    opCell.append(" ", decideButton);
    const subjectCell = el("td", {}, [record.payload?.subject ?? ""]);
    if (record.receiptNumber) {
      subjectCell.append(el("div", { class: "muted", style: "font-size:0.75rem" }, [record.receiptNumber]));
    }
    body.append(el("tr", {}, [
      el("td", {}, [record.domain]),
      el("td", {}, [record.restricted ? "restricted" : record.status]),
      subjectCell,
      el("td", {}, [record.updatedAt ?? record.createdAt]),
      opCell,
    ]));
  });
  if (!state.records.length) body.append(el("tr", {}, [el("td", { colspan: "5", class: "muted" }, ["登録データはまだありません。"])]));
  table.append(body);
  // ページナビゲーション
  const _container = document.createElement("div");
  _container.append(table);
  const _total = state.recordsTotal ?? 0;
  const _offset = state.recordsOffset ?? 0;
  if (_total > PAGE_LIMIT || _offset > 0) {
    const _nav = el("div", { style: "display:flex;align-items:center;gap:8px;margin-top:8px" });
    const _prev = el("button", { type: "button" }, ["← 前へ"]);
    if (_offset <= 0) _prev.setAttribute("disabled", "true"), _prev.style.opacity = "0.4";
    else _prev.addEventListener("click", async () => { state.recordsOffset = Math.max(0, _offset - PAGE_LIMIT); await refreshAndRender("work"); });
    const _next = el("button", { type: "button" }, ["次へ →"]);
    if (_offset + PAGE_LIMIT >= _total) _next.setAttribute("disabled", "true"), _next.style.opacity = "0.4";
    else _next.addEventListener("click", async () => { state.recordsOffset = _offset + PAGE_LIMIT; await refreshAndRender("work"); });
    _nav.append(_prev, el("span", { class: "muted", style: "font-size:0.85rem" }, [(_offset + 1) + "–" + Math.min(_offset + state.records.length, _total) + " 件 / 全 " + _total + " 件"]), _next);
    _container.append(_nav);
  }
  return _container;
}

function label(text, control) {
  return el("label", {}, [text, control]);
}

function option(value, text) {
  return el("option", { value }, [text]);
}

function renderReports() {
  const c = state.catalog;
  document.querySelector("#reports").replaceChildren(
    el("div", { class: "grid" }, [
      el("div", { class: "panel" }, [el("h2", {}, ["帳票"]), list(c.reports, "帳票定義が未登録です。")]),
      el("div", { class: "panel" }, [el("h2", {}, ["帳票プレビュー"]), reportForm()]),
      el("div", { class: "panel" }, [el("h2", {}, ["EUC抽出"]), eucForm()]),
    ]),
    el("div", { class: "panel", style: "margin-top:16px" }, [el("h2", {}, ["一括通知・案内"]), noticesPanel()]),
    el("div", { class: "panel", style: "margin-top:16px" }, [el("h2", {}, ["プレビュー履歴"]), jobsSearchBar(), jobsTable()]),
    el("div", { class: "panel", style: "margin-top:16px" }, [el("h2", {}, ["EUC承認キュー"]), eucSearchBar(), eucTable()]),
  );
}

// 通知書・案内文書の一括出力: 対象（領域×状態×年度）を選び、件名/本文に
// {subject} {receiptNumber} {fiscalYear} を差し込んで一括発行する。
function noticesPanel() {
  const box = el("div");
  const listBox = el("div", { class: "list" });
  async function reload() {
    const notices = await api("/api/notices").catch(() => []);
    listBox.replaceChildren();
    if (!notices.length) listBox.append(el("p", { class: "muted" }, ["発行済みの通知・案内はまだありません。"]));
    notices.slice(0, 8).forEach((notice) => listBox.append(el("div", { class: "item" }, [
      el("code", {}, [notice.kind === "guide" ? "案内" : "通知書"]),
      el("h3", {}, [notice.title]),
      el("p", { class: "muted" }, [`${notice.receiptNumber || notice.recordId} ／ ${notice.issuedAt}`]),
    ])));
  }
  const form = el("form");
  form.append(
    label("種別", el("select", { name: "kind" }, [option("notice", "通知書"), option("guide", "案内文書")])),
    label("対象領域", el("select", { name: "domain" }, [option("", "すべて"), option("main", "主要業務"), option("support", "支援措置・例外"), option("integration", "連携取込")])),
    label("対象状態", el("select", { name: "status" }, [option("", "すべて"), option("draft", "下書き"), option("submitted", "申請済み"), option("approved", "承認済み")])),
    label("件名（{subject} {receiptNumber} {fiscalYear} 差し込み可）",
      el("input", { name: "title", required: "required", placeholder: "例: 【{fiscalYear}年度】{subject} に関するお知らせ" })),
    label("本文", el("textarea", { name: "body", placeholder: "受付番号 {receiptNumber} の件についてご案内します。" })),
    el("button", { class: "primary", type: "submit" }, ["一括発行"]),
  );
  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const data = Object.fromEntries(new FormData(form));
    const payload = { kind: data.kind, title: data.title, body: data.body };
    if (data.domain) payload.domain = data.domain;
    if (data.status) payload.status = data.status;
    const result = await api("/api/notices/bulk", { method: "POST", body: JSON.stringify(payload) });
    toast(`${result.count}件を一括発行しました`, "success");
    await reload();
  });
  const exportButton = el("button", { type: "button" }, ["発行一覧CSV"]);
  exportButton.addEventListener("click", async () => {
    const res = await fetch("/api/notices-export", { headers: { "x-user-roles": "ADMIN,CLERK" } });
    const blob = await res.blob();
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = "notices.csv";
    a.click();
    URL.revokeObjectURL(a.href);
  });
  box.append(form, el("div", { style: "margin-top:8px" }, [exportButton]), listBox);
  reload();
  return box;
}

function reportForm() {
  const form = el("form");
  const reportOptions = (state.catalog.reports.length ? state.catalog.reports : [{ id: "RPT-DEMO", name: "デモ帳票" }])
    .slice(0, 80)
    .map((report) => option(report.id, `${report.id} ${report.name}`));
  form.append(
    label("帳票", el("select", { name: "reportId" }, reportOptions)),
    label("宛名", el("input", { name: "recipient", placeholder: "山田 太郎" })),
    label("摘要", el("textarea", { name: "note", placeholder: "帳票へ差し込む検証用データ" })),
    el("button", { class: "primary", type: "submit" }, ["プレビュー生成"]),
  );
  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const data = Object.fromEntries(new FormData(form));
    const job = await api(`/api/reports/${encodeURIComponent(data.reportId)}/preview`, {
      method: "POST",
      body: JSON.stringify(data),
    });
    // 生成した帳票HTMLを即座に新規タブで表示（resultUrl=/api/reports/jobs/{id}/download）
    if (job && job.resultUrl) window.open(job.resultUrl, "_blank", "noopener");
    toast(`${data.reportId} のプレビューを生成しました（新規タブに表示／履歴からも再表示可）`, "success");
    await refreshAndRender("reports");
  });
  return form;
}

function eucForm() {
  const form = el("form");
  form.append(
    label("出力項目", el("input", { name: "outputFields", value: "id,domain,status" })),
    label("機微情報", el("select", { name: "includeSensitive" }, [option("false", "含めない"), option("true", "含める（二人承認）")])),
    el("button", { class: "primary", type: "submit" }, ["抽出要求"]),
  );
  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const data = Object.fromEntries(new FormData(form));
    await api("/api/euc/query", {
      method: "POST",
      body: JSON.stringify({
        outputFields: data.outputFields.split(",").map((item) => item.trim()).filter(Boolean),
        includeSensitive: data.includeSensitive === "true",
      }),
    });
    await refreshAndRender("reports");
  });
  return form;
}

// 帳票プレビュー履歴 検索バー
let _jobsSearchTimer = null;
function jobsSearchBar() {
  const bar = el("div", { style: "display:flex;gap:8px;margin-bottom:8px" });
  const inp = el("input", { type: "search", placeholder: "帳票ID・状態・メモで検索...", style: "flex:1;min-width:160px", value: state.jobsSearch ?? "" });
  inp.addEventListener("input", () => {
    clearTimeout(_jobsSearchTimer);
    _jobsSearchTimer = setTimeout(async () => {
      state.jobsSearch = inp.value;
      state.jobsOffset = 0;
      await refreshAndRender("reports");
    }, 300);
  });
  bar.append(inp);
  return bar;
}

// EUC承認キュー 検索バー
let _eucSearchTimer = null;
function eucSearchBar() {
  const bar = el("div", { style: "display:flex;gap:8px;margin-bottom:8px" });
  const inp = el("input", { type: "search", placeholder: "申請者・状態・出力項目で検索...", style: "flex:1;min-width:160px", value: state.eucSearch ?? "" });
  inp.addEventListener("input", () => {
    clearTimeout(_eucSearchTimer);
    _eucSearchTimer = setTimeout(async () => {
      state.eucSearch = inp.value;
      state.eucJobsOffset = 0;
      await refreshAndRender("reports");
    }, 300);
  });
  bar.append(inp);
  return bar;
}

function jobsTable() {
  const table = el("table");
  table.append(el("thead", {}, [el("tr", {}, [el("th", {}, ["帳票ID"]), el("th", {}, ["状態"]), el("th", {}, ["生成日時"]), el("th", {}, ["メモ"]), el("th", {}, ["帳票"])])]));
  const body = el("tbody");
  state.jobs.slice(0, 30).forEach((job) => body.append(el("tr", {}, [
    el("td", {}, [job.reportId]),
    el("td", {}, [job.status]),
    el("td", {}, [job.generatedAt]),
    el("td", { class: "muted" }, [job.note]),
    el("td", {}, [job.resultUrl ? el("a", { href: job.resultUrl, target: "_blank", rel: "noopener" }, ["表示"]) : ""]),
  ])));
  if (!state.jobs.length) body.append(el("tr", {}, [el("td", { colspan: "5", class: "muted" }, ["履歴はまだありません。"])]));
  table.append(body);
  return table;
}

function eucStatusBadge(status) {
  const MAP = { DONE: ["badge-active", "完了"], QUEUED: ["badge-pending", "承認待ち"], FAILED: ["badge-rejected", "却下"] };
  const [cls, label] = MAP[status] ?? ["badge-pending", status];
  return el("span", { class: cls }, [label]);
}

function eucTable() {
  const container = el("div");
  if (!state.eucJobs.length) {
    container.append(el("p", { class: "muted" }, ["EUC要求はまだありません。"]));
    return container;
  }
  const table = el("table");
  table.append(el("thead", {}, [el("tr", {}, [
    el("th", {}, ["申請者"]),
    el("th", {}, ["申請日時"]),
    el("th", {}, ["状態"]),
    el("th", {}, ["承認数"]),
    el("th", {}, [tr("common.ops")]),
  ])]));
  const body = el("tbody");
  const myId = state.me?.id ?? "";
  state.eucJobs.slice(0, 50).forEach((job) => {
    const approvedCount = job.approvals?.filter((a) => a.action === "APPROVE").length ?? 0;
    const isSelf = myId && myId === job.requesterUserId;
    const alreadyApproved = job.approvals?.some((a) => a.approverUserId === myId);
    const ops = [];

    if (job.status === "QUEUED") {
      const canApprove = !isSelf && !alreadyApproved;
      const approveBtn = el("button", {
        type: "button",
        title: isSelf ? "申請者本人は承認できません" : alreadyApproved ? "承認済み" : "承認する",
        style: canApprove ? "" : "opacity:0.4;cursor:not-allowed",
        disabled: canApprove ? null : "true",
      }, ["✅ 承認"]);
      if (canApprove) {
        approveBtn.addEventListener("click", async () => {
          await api(`/api/euc/${job.id}/approve`, {
            method: "POST",
            body: JSON.stringify({ action: "APPROVE" }),
          });
          await refreshAndRender("reports");
        });
      }
      const rejectBtn = el("button", { type: "button", style: canApprove ? "margin-left:4px" : "display:none" }, ["❌ 却下"]);
      if (canApprove) {
        rejectBtn.addEventListener("click", async () => {
          if (!await showConfirm("このEUC要求を却下しますか？", "却下する")) return;
          await api(`/api/euc/${job.id}/approve`, {
            method: "POST",
            body: JSON.stringify({ action: "REJECT" }),
          });
          await refreshAndRender("reports");
        });
      }
      ops.push(approveBtn, rejectBtn);
    } else if (job.status === "DONE" && job.resultUrl) {
      const dlBtn = el("a", { href: job.resultUrl, download: `euc-${job.id}.csv`, class: "button", style: "font-size:0.85rem" }, ["📥 CSV"]);
      ops.push(dlBtn);
    }

    body.append(el("tr", {}, [
      el("td", {}, [job.requesterUserId ?? "-"]),
      el("td", {}, [job.requestedAt ? new Date(job.requestedAt).toLocaleString("ja-JP") : "-"]),
      el("td", {}, [eucStatusBadge(job.status)]),
      el("td", {}, [`${approvedCount} / ${job.requiredApprovals ?? 1}`]),
      el("td", {}, ops),
    ]));
  });
  table.append(body);
  container.append(table);
  return container;
}

function renderAdmin() {
  const c = state.catalog;
  const rows = [...c.tables, ...c.roles, ...c.batches].slice(0, 80);
  const table = el("table");
  table.append(el("thead", {}, [el("tr", {}, [el("th", {}, ["ID"]), el("th", {}, ["名称"]), el("th", {}, ["種別"])])]));
  const body = el("tbody");
  rows.forEach((item) => body.append(el("tr", {}, [el("td", {}, [item.id]), el("td", {}, [item.name]), el("td", {}, [item.sheet ?? ""]) ])));
  table.append(body);
  document.querySelector("#admin").replaceChildren(
    el("div", { class: "grid" }, [
      el("div", { class: "panel" }, [el("h2", {}, ["管理定義"]), table]),
      el("div", { class: "panel" }, [el("h2", {}, ["支援措置・抑止"]), restrictionList()]),
      el("div", { class: "panel" }, [el("h2", {}, ["監査ログ"]), auditList()]),
    ]),
    el("div", { class: "panel", style: "margin-top:16px" }, [el("h2", {}, ["公金受取口座"]), bankAccountsPanel()]),
  );
}

// 公金受取口座: マスク表示の一覧＋登録。全桁は「提供」操作（ADMIN・監査記録）でのみ取得。
function bankAccountsPanel() {
  const box = el("div");
  const listBox = el("div", { class: "list" });
  async function reload() {
    const accounts = await api("/api/bank-accounts").catch(() => []);
    listBox.replaceChildren();
    if (!accounts.length) listBox.append(el("p", { class: "muted" }, ["登録された口座はありません。"]));
    accounts.slice(0, 12).forEach((account) => {
      const provideButton = el("button", { type: "button" }, ["提供(全桁)"]);
      provideButton.addEventListener("click", async () => {
        const full = await api(`/api/bank-accounts/${account.id}/provide`);
        window.alert(`他システム提供（監査記録済み）\n${full.bankName} ${full.branchName} ${full.accountType === "checking" ? "当座" : "普通"} ${full.accountNumber}`);
      });
      const deleteButton = el("button", { type: "button" }, ["削除"]);
      deleteButton.addEventListener("click", async () => {
        await api(`/api/bank-accounts/${account.id}`, { method: "DELETE" });
        await reload();
      });
      listBox.append(el("div", { class: "item" }, [
        el("code", {}, [account.ownerId]),
        el("h3", {}, [`${account.ownerName || account.ownerId} ／ ${account.bankName} ${account.branchName}`]),
        el("p", { class: "muted" }, [`${account.accountType === "checking" ? "当座" : "普通"} ${account.accountNumber} ${account.purpose ? "（" + account.purpose + "）" : ""}`]),
        el("div", {}, [provideButton, " ", deleteButton]),
      ]));
    });
  }
  const form = el("form");
  form.append(
    label("対象者ID", el("input", { name: "ownerId", required: "required", placeholder: "例: 宛名番号" })),
    label("氏名", el("input", { name: "ownerName" })),
    label("金融機関", el("input", { name: "bankName", required: "required" })),
    label("支店", el("input", { name: "branchName" })),
    label("口座番号", el("input", { name: "accountNumber", required: "required", inputmode: "numeric" })),
    label("用途", el("input", { name: "purpose", placeholder: "例: 手当の振込" })),
    el("button", { class: "primary", type: "submit" }, ["登録"]),
  );
  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    await api("/api/bank-accounts", { method: "POST", body: JSON.stringify(Object.fromEntries(new FormData(form))) });
    form.reset();
    await reload();
  });
  box.append(listBox, form);
  reload();
  return box;
}

function restrictionList() {
  const box = el("div", { class: "list" });
  state.restrictions.slice(0, 20).forEach((row) => box.append(el("div", { class: "item" }, [
    el("code", {}, [row.category]),
    el("p", {}, [`record=${row.recordId ?? "-"} domain=${row.domain ?? "-"}`]),
    el("p", { class: "muted" }, [`${row.status} ${row.startDate}`]),
  ])));
  if (!state.restrictions.length) box.append(el("p", { class: "muted" }, ["抑止設定はまだありません。"]));
  return box;
}

// 監査ログ検索バー (debounce)
let _auditSearchTimer = null;
function auditList() {
  const container = el("div");
  const filterInput = el("input", { type: "search", placeholder: "操作名・ユーザー・詳細で検索…", style: "width:100%;max-width:420px;margin-bottom:8px", value: state.auditSearch ?? "" });
  const box = el("div", { class: "list" });
  const renderItems = () => {
    box.replaceChildren();
    if (!state.audit.length) { box.append(el("p", { class: "muted" }, [state.auditSearch ? "条件に一致するログがありません。" : "監査ログはまだありません。"])); return; }
    state.audit.forEach((row) => box.append(el("div", { class: "item" }, [
      el("code", {}, [row.action ?? ""]),
      el("p", {}, [(row.at ?? "") + " / " + (row.actor ?? "")]),
      el("p", { class: "muted" }, [JSON.stringify(row.detail ?? {})]),
    ])));
    const _remaining = (state.auditTotal ?? state.audit.length) - state.audit.length;
    if (_remaining > 0) {
      const _moreBtn = el("button", { style: "margin-top:8px" }, ["さらに読み込む (" + _remaining + " 件)"]);
      _moreBtn.addEventListener("click", async () => {
        _moreBtn.disabled = true;
        const _searchQs = state.auditSearch ? "&search=" + encodeURIComponent(state.auditSearch) : "";
        const more = await api("/api/audit?limit=200&offset=" + state.audit.length + _searchQs).catch(() => ({ data: [], total: state.auditTotal }));
        state.audit = [...state.audit, ...(more.data ?? [])];
        state.auditTotal = more.total ?? state.auditTotal;
        renderAdmin();
      });
      box.append(_moreBtn);
    }
  };
  filterInput.addEventListener("input", () => {
    clearTimeout(_auditSearchTimer);
    _auditSearchTimer = setTimeout(async () => {
      state.auditSearch = filterInput.value;
      state.audit = [];
      state.auditTotal = 0;
      await refreshAndRender("admin");
    }, 300);
  });
  renderItems();
  container.append(filterInput, box);
  return container;
}

// ドメインエンティティ詳細ページ (hash router)
function renderDetailPage(cfg, item) {
  // ページタイトル更新
  const _itemLabel = item[cfg.cols[0]?.key] ?? item.id ?? "";
  document.title = `${cfg.label} – ${String(_itemLabel).slice(0, 30)} | ${_APP_TITLE}`;
  // 前のEscapeハンドラを解除
  if (renderDetailPage._escHandler) { window.removeEventListener("keydown", renderDetailPage._escHandler); renderDetailPage._escHandler = null; }
  const section = document.querySelector("#domain");
  const backBtn = el("button", { style: "margin-bottom:16px", class: "print-hide" }, ["← 一覧へ戻る"]);
  backBtn.addEventListener("click", () => {
    if (renderDetailPage._escHandler) { window.removeEventListener("keydown", renderDetailPage._escHandler); renderDetailPage._escHandler = null; }
    history.pushState(null, "", location.pathname + location.search);
    renderDomain();
    switchViewRaw("domain");
  });
  // Escapeキーで一覧へ戻る
  renderDetailPage._escHandler = (e) => { if (e.key === "Escape") backBtn.click(); };
  window.addEventListener("keydown", renderDetailPage._escHandler);
  const dl = el("dl", { style: "display:grid;grid-template-columns:200px 1fr;gap:4px 16px;margin:0 0 16px" });
  Object.entries(item).filter(([k]) => !HIDDEN_KEYS.has(k)).forEach(([k, v]) => {
    dl.append(el("dt", { style: "font-weight:600;color:#374151;padding:4px 0" }, [k]));
    dl.append(el("dd", { style: "margin:0;padding:4px 0;word-break:break-all;border-bottom:1px solid #e5e7eb" }, [v == null ? "—" : String(enumLabel(k, v))]));
  });
  const editForm = el("form", { style: "display:flex;flex-wrap:wrap;gap:8px 16px;align-items:flex-end;margin-top:8px;max-width:none" });
  cfg.cols.forEach((col) => {
    const wrap = el("div", { style: "display:flex;flex-direction:column;gap:2px;flex:1 1 200px;min-width:160px;max-width:320px" });
    wrap.append(el("label", { style: "font-size:0.8rem;color:#6b7280" }, [col.label]));
    const _enumOpts = col.options ?? (col.badge ? [...new Set((state.domainData[cfg.id] ?? []).map((x) => x[col.key]).filter(Boolean))].sort() : null);
    if (_enumOpts?.length) {
      const sel = el("select", { name: col.key, style: "font-size:0.85rem" });
      _enumOpts.forEach((opt) => { const o = el("option", { value: String(opt) }, [String(enumLabel(col.key, opt))]); if (String(opt) === String(item[col.key] ?? "")) o.selected = true; sel.append(o); });
      wrap.append(sel);
    } else {
      wrap.append(el("input", { name: col.key, value: String(item[col.key] ?? ""), style: "font-size:0.85rem" }));
    }
    editForm.append(wrap);
  });
  const saveBtn = el("button", { type: "submit", class: "primary" }, ["保存"]);
  editForm.append(saveBtn);
  editForm.addEventListener("submit", async (ev) => {
    ev.preventDefault();
    if (saveBtn.disabled) return;
    const fd = new FormData(editForm);
    const patch = {};
    cfg.cols.forEach((col) => { patch[col.key] = fd.get(col.key); });
    saveBtn.disabled = true; saveBtn.textContent = "保存中…";
    try {
      await api(cfg.endpoint + "/" + item.id, { method: "PUT", body: JSON.stringify(patch) });
      toast("更新しました。", "success");
      await refreshRuntimeData();
      const updated = (state.domainData[cfg.id] ?? []).find((x) => x.id === item.id);
      if (updated) renderDetailPage(cfg, updated);
    } catch (e) { toast("更新エラー: " + e.message, "error"); }
    finally { if (!saveBtn.isConnected) return; saveBtn.disabled = false; saveBtn.textContent = "保存"; }
  });
  // URLコピーボタン
  const _copyUrlBtn = el("button", { style: "margin-left:8px;font-size:0.85rem", title: "詳細ページURLをコピー", class: "print-hide" }, ["🔗 URLをコピー"]);
  _copyUrlBtn.addEventListener("click", async () => {
    try { await navigator.clipboard.writeText(location.href); toast("URLをコピーしました。", "info"); }
    catch { toast("コピーできませんでした。", "error"); }
  });
  // 前の件・次の件ナビゲーション
  const _allItems = state.domainData[cfg.id] ?? [];
  const _idx = _allItems.findIndex((x) => x.id === item.id);
  const _prevBtn82 = el("button", { style: "margin-left:8px;font-size:0.85rem", title: "前の件", class: "print-hide" }, ["‹ 前"]);
  const _nextBtn82 = el("button", { style: "margin-left:8px;font-size:0.85rem", title: "次の件", class: "print-hide" }, ["次 ›"]);
  if (_idx <= 0) _prevBtn82.disabled = true;
  if (_idx >= _allItems.length - 1) _nextBtn82.disabled = true;
  _prevBtn82.addEventListener("click", () => { if (_idx > 0) location.hash = "/domain/" + cfg.id + "/" + _allItems[_idx - 1].id; });
  _nextBtn82.addEventListener("click", () => { if (_idx < _allItems.length - 1) location.hash = "/domain/" + cfg.id + "/" + _allItems[_idx + 1].id; });
  section.replaceChildren(
    backBtn,
    _copyUrlBtn,
    _prevBtn82,
    _nextBtn82,
    el("div", { class: "panel" }, [
      el("h2", {}, [cfg.label + " 詳細"]),
      el("p", { class: "muted", style: "font-size:0.85rem" }, ["ID: " + (item.id ?? "")]),
      dl,
      el("details", {}, [
        el("summary", { style: "cursor:pointer;font-weight:700;margin-bottom:8px" }, ["フィールド編集"]),
        editForm,
      ]),
    ])
  );
}

async function renderOnline() {
  const sec = document.querySelector("#online");
  if (!sec) return;
  const STL = { RECEIVED: "受付", PROCESSING: "処理中", COMPLETED: "完了", REJECTED: "却下" };
  const flow = { RECEIVED: "PROCESSING", PROCESSING: "COMPLETED" };
  let apps = [];
  try { apps = await api("/api/link/applications"); }
  catch (e) { sec.replaceChildren(el("div", { class: "panel" }, [el("h2", {}, ["オンライン申請 受付簿"]), el("p", { class: "muted" }, ["取得できませんでした: " + e.message])])); return; }
  const advance = async (id, status) => {
    try { await api("/api/link/applications/" + id + "/status", { method: "POST", body: JSON.stringify({ status }) }); toast("更新しました。", "success"); renderOnline(); }
    catch (e) { toast("更新エラー: " + e.message, "error"); }
  };
  const rows = apps.map((a) => {
    const ops = el("td", {});
    const nx = flow[a.status];
    if (nx) { const b = el("button", {}, ["「" + STL[nx] + "」へ"]); b.addEventListener("click", () => advance(a.id, nx)); ops.append(b); }
    if (a.status !== "COMPLETED" && a.status !== "REJECTED") { const r = el("button", { style: "margin-left:6px" }, ["却下"]); r.addEventListener("click", () => advance(a.id, "REJECTED")); ops.append(r); }
    return el("tr", { "data-status": a.status }, [
      el("td", {}, [a.receivedAt ? new Date(a.receivedAt).toLocaleString("ja-JP") : ""]),
      el("td", {}, [a.procedureType ?? ""]),
      el("td", {}, [(a.applicant && a.applicant.name) ?? ""]),
      el("td", {}, [a.source ?? ""]),
      el("td", {}, [el("span", { class: "badge badge-" + String(a.status).toLowerCase() }, [STL[a.status] ?? a.status])]),
      ops,
    ]);
  });
  const reloadBtn = el("button", {}, ["再読み込み"]); reloadBtn.addEventListener("click", () => renderOnline());
  const csvBtn = el("button", { class: "print-hide" }, [tr("common.csv")]);
  csvBtn.addEventListener("click", async () => {
    try {
      const res = await fetch("/api/link/applications/export", { headers: { "x-user-roles": "ADMIN,CLERK,AUDITOR" } });
      if (!res.ok) throw new Error("HTTP " + res.status);
      const blob = await res.blob(); const url = URL.createObjectURL(blob);
      const dl = el("a", { href: url, download: "link-applications.csv" }); document.body.append(dl); dl.click(); dl.remove(); URL.revokeObjectURL(url);
    } catch (e) { toast("CSV出力エラー: " + e.message, "error"); }
  });
  const table = el("table", {}, [
    el("thead", {}, [el("tr", {}, ["受付日時", "手続き", "申請者", "連携元", "状態", "操作"].map((h) => el("th", {}, [h])))]),
    el("tbody", {}, rows),
  ]);
  // 状態フィルタ（チップ）
  let onlineFilter = "ALL";
  const emptyMsg = el("p", { class: "muted" }, ["該当する申請はありません。"]); emptyMsg.hidden = true;
  const chipRow = el("div", { style: "display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px" });
  const applyOnlineFilter = () => {
    rows.forEach((tr) => { tr.hidden = onlineFilter !== "ALL" && tr.getAttribute("data-status") !== onlineFilter; });
    chipRow.querySelectorAll("button").forEach((c) => c.classList.toggle("active", c.dataset.f === onlineFilter));
    emptyMsg.hidden = rows.some((tr) => !tr.hidden);
  };
  const mkChip = (val, label, n) => { const b = el("button", {}, [label + "（" + n + "）"]); b.dataset.f = val; b.addEventListener("click", () => { onlineFilter = val; applyOnlineFilter(); }); return b; };
  chipRow.append(mkChip("ALL", "すべて", apps.length));
  [...new Set(apps.map((a) => a.status))].forEach((s) => chipRow.append(mkChip(s, STL[s] ?? s, apps.filter((a) => a.status === s).length)));
  sec.replaceChildren(el("div", { class: "panel" }, [
    el("h2", {}, ["オンライン申請 受付簿"]),
    el("p", { class: "muted" }, ["市民ポータル等から受信したオンライン申請です。状態を進めると申請元（市民）へ自動で通知されます。"]),
    el("div", { style: "display:flex;gap:10px;margin-bottom:12px" }, [reloadBtn, csvBtn]),
    ...(apps.length ? [chipRow, table, emptyMsg] : [el("p", { class: "muted" }, ["受信した申請はありません。"])]),
  ]));
  if (apps.length) applyOnlineFilter();
}

const CALC_CONFIG = (typeof window !== "undefined" && window.SYSTEM_CONFIG && window.SYSTEM_CONFIG.calc) || null;

function renderCalc() {
  const sec = document.querySelector("#calc");
  if (!sec) return;
  const cfg = CALC_CONFIG;
  const form = el("form", { class: "calc-form" });
  cfg.fields.forEach((f) => {
    let input;
    if (f.type === "select") {
      input = el("select", { name: f.key }, (f.options || []).map((o) => el("option", { value: String(o && o.value !== undefined ? o.value : o) }, [String(o && o.label !== undefined ? o.label : o)])));
      if (f.default !== undefined) input.value = String(f.default);
    } else if (f.type === "members" || f.type === "ages" || f.type === "multiline") {
      input = el("textarea", { name: f.key, rows: "3", placeholder: f.placeholder || "" });
      if (f.default !== undefined) input.value = String(f.default);
    } else {
      input = el("input", { name: f.key, type: f.type === "number" ? "number" : f.type === "date" ? "date" : "text", placeholder: f.placeholder || "" });
      if (f.default !== undefined) input.value = String(f.default);
    }
    form.append(el("label", {}, [el("span", {}, [f.label]), input]));
  });
  const resultBox = el("div", { class: "calc-result" });
  form.append(el("button", { class: "primary", type: "submit" }, [tr("calc.run")]));
  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const body = {};
    cfg.fields.forEach((f) => {
      const elx = form.elements[f.key];
      const v = elx ? elx.value : "";
      if (f.type === "number") body[f.key] = v === "" ? undefined : Number(v);
      else if (f.type === "select") body[f.key] = v === "true" ? true : v === "false" ? false : v;
      else if (f.type === "members") body[f.key] = v.split(/\n/).map((l) => l.trim()).filter(Boolean).map((l) => { const p = l.split(/[:：]/); return { income: Number(p[0]), age: Number(p[1]) }; });
      else if (f.type === "ages") body[f.key] = v.split(/\n/).map((l) => l.trim()).filter(Boolean).map((a) => ({ age: Number(a) }));
      else body[f.key] = v;
    });
    try {
      const res = await api(cfg.endpoint, { method: "POST", body: JSON.stringify(body) });
      renderCalcResult(resultBox, cfg, res);
    } catch (err) { toast("算定エラー: " + err.message, "error"); }
  });
  // ── 中間サーバ照会パネル（config.js の linkage 設定があるシステムのみ表示）──
  const LINKAGE = (window.SYSTEM_CONFIG && window.SYSTEM_CONFIG.linkage) || null;
  let linkBox = null;
  if (LINKAGE) {
    const atenaInput = el("input", { name: "linkage-atena", placeholder: "10000001" });
    const fetchBtn = el("button", { type: "button" }, [tr("calc.linkFetch")]);
    const out = el("div", { class: "linkage-results" });
    fetchBtn.addEventListener("click", async () => {
      const atena = atenaInput.value.trim();
      if (!atena) { atenaInput.focus(); return; }
      fetchBtn.disabled = true;
      try {
        const res = await api("/api/linkage/query", {
          method: "POST",
          body: JSON.stringify({ agency: LINKAGE.agency, procedure: LINKAGE.procedure, atena, queries: LINKAGE.queries }),
        });
        const values = {};
        out.replaceChildren(...res.results.map((r) => {
          const ok = r.status === 200;
          if (ok) values[r.item] = r.value;
          const text = ok
            ? `✅ ${r.item}（${r.holder}）: ${Number(r.value).toLocaleString("ja-JP")}${r.fiscalYear ? `（${r.fiscalYear}年度）` : ""}`
            : `⛔ ${r.item}（${r.holder}）: ${r.error || r.status}`;
          return el("div", { class: "muted" }, [text]);
        }));
        const setField = (k, v) => { const i = form.elements[k]; if (i) i.value = v; };
        if (typeof LINKAGE.apply === "function" && LINKAGE.apply(values, setField)) toast(tr("calc.linkDone"));
      } catch (err) {
        toast(tr("calc.linkFail") + " " + err.message, "error");
      } finally {
        fetchBtn.disabled = false;
      }
    });
    linkBox = el("div", { class: "linkage-box" }, [
      el("h3", {}, [tr("calc.linkTitle")]),
      el("p", { class: "muted" }, [tr("calc.linkNote")]),
      el("div", { class: "linkage-row" }, [
        el("label", {}, [el("span", {}, [tr("calc.linkAtena")]), atenaInput]),
        fetchBtn,
      ]),
      out,
    ]);
  }
  sec.replaceChildren(el("div", { class: "panel" }, [
    el("h2", {}, [cfg.title]),
    el("p", { class: "muted" }, [tr("calc.note")]),
    ...(linkBox ? [linkBox] : []),
    form,
    resultBox,
  ]));
}
function renderCalcResult(box, cfg, res) {
  const fmt = (val, format) => {
    if (val === undefined || val === null) return "—";
    if (format === "yen") return Number(val).toLocaleString("ja-JP") + " 円";
    if (format === "bool") return val ? tr("calc.yes") : tr("calc.no");
    if (format === "percent") return val + " %";
    return String(val);
  };
  const p = cfg.primary;
  const flat = {};
  Object.entries(res).forEach(([k, v]) => {
    if (v && typeof v === "object" && !Array.isArray(v)) Object.entries(v).forEach(([k2, v2]) => { if (typeof v2 !== "object") flat[k + "." + k2] = v2; });
    else if (!Array.isArray(v)) flat[k] = v;
  });
  const rows = Object.entries(flat).filter(([k]) => k !== p.key).map(([k, v]) =>
    el("tr", {}, [el("th", {}, [k]), el("td", {}, [typeof v === "boolean" ? (v ? "はい" : "いいえ") : String(v)])]));
  const _applyBtn = el("button", { class: "primary calc-apply", type: "button" }, [tr("calc.apply")]);
  _applyBtn.addEventListener("click", () => {
    const _subject = cfg.title + "：" + fmt(res[p.key], p.format);
    const _desc = "クイック算定の結果から受付登録。\n" + Object.entries(flat).filter(([k]) => k !== p.key).map(([k, v]) => k + ": " + (typeof v === "boolean" ? (v ? "はい" : "いいえ") : String(v))).join("\n");
    switchView("work");
    const _wf = document.querySelector("#work form");
    if (_wf) {
      if (_wf.elements["subject"]) _wf.elements["subject"].value = _subject;
      if (_wf.elements["description"]) _wf.elements["description"].value = _desc;
      _wf.scrollIntoView({ behavior: "smooth", block: "center" });
      if (_wf.elements["subject"]) _wf.elements["subject"].focus();
    }
    toast("算定結果を受付登録フォームに転記しました。内容を確認して登録してください。");
  });
  box.replaceChildren(
    el("div", { class: "calc-primary" + (p.format === "bool" ? (res[p.key] ? " ok" : " ng") : "") }, [
      el("span", { class: "calc-primary__label" }, [p.label]),
      el("span", { class: "calc-primary__value" }, [fmt(res[p.key], p.format)]),
    ]),
    rows.length ? el("table", { class: "calc-detail" }, [el("tbody", {}, rows)]) : el("span", {}),
    _applyBtn,
  );
}

// ── 仕様機能タブ（実装済みAPIカタログ＋実行コンソール）──
// nav/section は JS で注入する（index.html 改修不要で全システムへ一斉波及）
views.push("spec");
(() => {
  if (typeof document === "undefined") return;
  if (!document.querySelector("#spec")) {
    document.querySelector("main")?.append(el("section", { id: "spec", class: "view hidden" }, []));
  }
  if (!document.querySelector('nav button[data-view="spec"]')) {
    const btn = el("button", { "data-view": "spec" }, ["仕様機能"]);
    btn.addEventListener("click", () => { switchView("spec"); renderSpec(); });
    const nav = document.querySelector("header nav");
    if (nav) nav.insertBefore(btn, document.getElementById("staff-logout") ?? null);
  }
})();

let _specCatalog = null;
async function renderSpec() {
  const sec = document.querySelector("#spec");
  if (!sec) return;
  if (!_specCatalog) {
    try { _specCatalog = await api("/api/catalog"); } catch { _specCatalog = { apis: [] }; }
  }
  const apis = (_specCatalog.apis ?? []).slice();
  const sysApis = apis.filter((a) => a.source === "system");
  const coreApis = apis.filter((a) => a.source !== "system");
  const consoleOut = el("pre", { class: "spec-out" }, ["（ここに応答が表示されます）"]);
  const mSel = el("select", { class: "spec-method" }, ["GET", "POST", "PUT", "DELETE"].map((m) => el("option", { value: m }, [m])));
  const pIn = el("input", { class: "spec-path", placeholder: "/api/..." });
  const bIn = el("textarea", { name: "spec-body", class: "spec-body", rows: "4", placeholder: '{ "key": "value" }（GET/DELETEは空でOK）' });
  const runBtn = el("button", { class: "primary", type: "button" }, ["実行"]);
  runBtn.addEventListener("click", async () => {
    consoleOut.textContent = "実行中…";
    try {
      const opts = { method: mSel.value, headers: { "content-type": "application/json",
        "x-user-roles": "ADMIN,CLERK,AUDITOR" } };
      if (mSel.value !== "GET" && bIn.value.trim()) opts.body = JSON.stringify(JSON.parse(bIn.value));
      const res = await fetch(pIn.value, opts);
      const text = await res.text();
      let pretty = text;
      try { pretty = JSON.stringify(JSON.parse(text), null, 2); } catch { /* 非JSONはそのまま表示 */ }
      consoleOut.textContent = `HTTP ${res.status}\n${pretty}`;
    } catch (e) {
      consoleOut.textContent = `エラー: ${e.message}`;
    }
  });
  const specRow = (a) => {
    const tr2 = el("tr", { class: "spec-row" }, [
      el("td", {}, [el("code", {}, [a.method])]),
      el("td", {}, [el("code", {}, [a.path])]),
      el("td", {}, [a.description ?? ""]),
    ]);
    tr2.addEventListener("click", () => {
      mSel.value = ["GET", "POST", "PUT", "DELETE"].includes(a.method) ? a.method : "GET";
      pIn.value = a.path;
      window.scrollTo({ top: 0, behavior: "smooth" });
    });
    return tr2;
  };
  const specTable = (list2) => el("table", { class: "spec-table" }, [
    el("thead", {}, [el("tr", {}, [el("th", {}, ["メソッド"]), el("th", {}, ["パス"]), el("th", {}, ["機能（実装コメント由来）"])])]),
    el("tbody", {}, list2.map(specRow)),
  ]);
  sec.replaceChildren(
    el("div", { class: "panel" }, [
      el("h2", {}, [`標準仕様機能カタログ（業務固有 ${sysApis.length} ＋ 共通基盤 ${coreApis.length} = ${apis.length} API）`]),
      el("p", { class: "muted" }, ["公式標準仕様書の機能網羅キャンペーンで実装したエンドポイントの実体一覧です。行をクリックするとコンソールに転記され、その場で実行して応答を確認できます。"]),
      el("div", { class: "spec-console" }, [mSel, pIn, runBtn]),
      bIn, consoleOut,
    ]),
    el("div", { class: "panel" }, [el("h3", {}, [`業務固有API（${sysApis.length}）`]), specTable(sysApis)]),
    el("details", { class: "panel" }, [el("summary", {}, [`共通基盤API（${coreApis.length}）`]), specTable(coreApis)]),
  );
}

function switchViewRaw(view) {
  views.forEach((name) => document.querySelector(`#${name}`).classList.toggle("hidden", name !== view));
  document.querySelectorAll("nav button").forEach((button) => button.classList.toggle("active", button.dataset.view === view));
  // ページタイトル更新
  const _navBtn = document.querySelector(`nav button[data-view="${view}"]`);
  document.title = _navBtn ? `${_navBtn.textContent.trim()} | ${_APP_TITLE}` : _APP_TITLE;
}

function switchView(view) {
  if (location.hash) history.pushState(null, "", location.pathname + location.search);
  switchViewRaw(view);
}

// ローディングスピナー
const _loadingOverlay = document.getElementById("loading-overlay");
async function refreshAndRender(view = "overview") {
  if (_loadingOverlay) _loadingOverlay.classList.add("visible");
  try {
    await refreshRuntimeData();
    renderOverview();
    renderDomain();
    renderWork();
    renderReports();
    renderAdmin();
    renderOnline();
    renderCalc();
    renderSpec();
    switchView(view);
  } finally {
    if (_loadingOverlay) _loadingOverlay.classList.remove("visible");
  }
}

document.querySelectorAll("nav button").forEach((button) => button.addEventListener("click", () => switchView(button.dataset.view)));

// hash router
function handleHashChange() {
  const hash = location.hash;
  const m = hash.match(/^#\/domain\/([^\/]+)\/([^\/]+)$/);
  if (!m) return false;
  const [, cfgId, itemId] = m;
  const cfg = DOMAIN_CONFIG.find((c) => c.id === cfgId);
  if (!cfg) return false;
  const items = state.domainData[cfgId] ?? [];
  const item = items.find((x) => x.id === itemId);
  if (!item) return false;
  renderDetailPage(cfg, item);
  switchViewRaw("domain");
  return true;
}
window.addEventListener("hashchange", () => handleHashChange());

state.catalog = await api("/api/catalog");
// 職員ログイン（参照実装のフロント側デモ認証ゲート）
(function(){ const e = document.getElementById("staff-login-sys"), h = document.querySelector(".topbar h1"); if (e && h) e.textContent = h.textContent; })();
{
  const _logout = document.getElementById("staff-logout");
  if (_logout) _logout.addEventListener("click", () => { sessionStorage.removeItem("staff-session"); location.reload(); });
}
async function _bootApp() { await refreshAndRender("overview"); }
if (sessionStorage.getItem("staff-session")) {
  document.getElementById("staff-login")?.remove();
  await _bootApp();
} else {
  const _lf = document.getElementById("staff-login-form");
  if (_lf) _lf.addEventListener("submit", async (ev) => {
    ev.preventDefault();
    const id = document.getElementById("staff-id").value.trim();
    const pw = document.getElementById("staff-pw").value;
    const err = document.getElementById("staff-login-err");
    if (!id || !pw) { err.textContent = tr("login.required"); err.hidden = false; return; }
    sessionStorage.setItem("staff-session", JSON.stringify({ id, roles: document.getElementById("staff-role").value, at: Date.now() }));
    document.getElementById("staff-login")?.remove();
    await _bootApp();
  });
}
// Handle any hash present on initial load
handleHashChange();
applyStaffI18n();

// ダークモード切替
(function initTheme() {
  const root = document.documentElement;
  const btn = document.getElementById("theme-toggle");
  if (!btn) return;
  const saved = localStorage.getItem("theme");
  if (saved) root.setAttribute("data-theme", saved);
  const update = () => {
    const isDark = root.getAttribute("data-theme") === "dark" ||
      (!root.getAttribute("data-theme") && window.matchMedia("(prefers-color-scheme: dark)").matches);
    btn.textContent = isDark ? "☀️" : "🌙";
    btn.title = isDark ? "ライトモードに切替" : "ダークモードに切替";
  };
  update();
  btn.addEventListener("click", () => {
    const cur = root.getAttribute("data-theme");
    const next = (cur === "dark") ? "light" : "dark";
    root.setAttribute("data-theme", next);
    localStorage.setItem("theme", next);
    update();
  });
  window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", update);
}());

// オフラインバナー
(function initOfflineBanner() {
  const banner = document.getElementById("offline-banner");
  if (!banner) return;
  const setOffline = (offline) => {
    banner.classList.toggle("visible", offline);
    document.body.classList.toggle("offline-mode", offline);
  };
  setOffline(!navigator.onLine);
  window.addEventListener("offline", () => setOffline(true));
  window.addEventListener("online", async () => {
    setOffline(false);
    await refreshAndRender(document.querySelector("nav button.active")?.dataset.view || "overview");
  });
}());

// SSE リアルタイム通知
(function initSSE() {
  let retryDelay = 2000;
  let _wasDisconnected = false;
  function connect() {
    const es = new EventSource("/api/events");
    es.addEventListener("euc.approval", async (ev) => {
      // EUC承認トースト通知
      try { const d = JSON.parse(ev.data ?? "{}"); toast(`EUC ${d.id?.slice(0, 8) ?? ""} が ${d.status === "DONE" ? "承認完了" : d.status === "QUEUED" ? "承認待ち" : "更新"}されました`, "info"); } catch {}
      await refreshAndRender(document.querySelector("nav button.active")?.dataset.view || "reports");
    });
    es.addEventListener("report.preview", async (ev) => {
      // 帳票プレビュートースト通知
      try { const d = JSON.parse(ev.data ?? "{}"); toast(`帳票 ${d.reportId ?? ""} のプレビューが完了しました`, "success"); } catch {}
      await refreshAndRender(document.querySelector("nav button.active")?.dataset.view || "reports");
    });
    es.addEventListener("error", () => {
      es.close();
      _wasDisconnected = true;
      setTimeout(connect, retryDelay);
      retryDelay = Math.min(retryDelay * 2, 30000);
    });
    es.addEventListener("open", () => {
      retryDelay = 2000;
      if (_wasDisconnected) { toast("サーバーに再接続しました", "success"); _wasDisconnected = false; }
    });
  }
  connect();
}());
