// screen_approvals.jsx — merged Approval Desk + 蓋章 stamp (office DeskPage ported to JSX)
// 5.3 defines the subcomponents; 5.4 inserts the Approvals screen + registration before the IIFE close.
(function () {
  const { useState, useEffect } = React;
  const h = React.createElement;
  window.Screens = window.Screens || {};
  const { Icon, Card, Button, Badge, Modal, PageHeader, RoleDot, Chip, InlineIcon, AUDIT_TONE } = window;

  // C3 DISPLAY maps: logic keys on 繁中 data; we render English so i18n localizes it.
  const ACTION_EN = { "核准": "Approved", "自動執行": "Repeat work", "修改後核准": "Approved with edits", "退回": "Returned", "政策封鎖": "Rule blocked", "撤回": "Undone", "聘用請求": "Hire requested", "核准聘用": "Hire approved", "退回聘用": "Hire declined", "建立任務": "Task created", "建立工作單": "Task created", "完成工作單": "Task completed", "重開工作單": "Task reopened", "指派工具": "App assigned", "移除工具": "App removed", "上傳檔案": "File added", "歸檔檔案": "File saved", "發布設定": "Settings applied", "還原設定": "Settings undone", "匯出": "Downloaded", "連接器連線": "App connected", "重新授權": "App reconnected", "中斷連接": "App disconnected", "健康檢查": "Connection checked", "整合需求": "App requested", "更新設定": "Settings updated", "更新公司": "Company updated", "邀請用戶": "Team member invited", "更新用戶": "Team member updated", "更新用戶權限": "Team member access changed", "重寄邀請": "Invite resent", "停用用戶": "Team member suspended", "恢復用戶": "Team member reactivated", "移除用戶": "Team member removed" };
  const BASIS_EN = { rule: "By the rules", match: "Matched a file", memory: "Reused a habit", prediction: "Predicted — verify" };
  // risk display reuses the existing High/Medium/Low dict keys (PC-6: 高/中/低 already keyed there).
  const RISK_EN = { "高": "High", "中": "Medium", "低": "Low" };
	  const RISK_TONE = { "高": "red", "中": "amber", "低": "green" };
	  const RISK_RANK = { "高": 3, "中": 2, "低": 1 };
	  // English source names for the agent-group headers / roster (i18n dict localizes them).
	  const SHORT_EN = { clerk: "Admin Assistant", cs: "Customer Support Assistant", sales: "Sales Assistant", social: "Marketing Assistant", acct: "Accounting Assistant" };
	  function shortEnName(office, rid) {
	    const r = office.roleById && office.roleById[rid];
	    return r && window.roleDisplayName ? window.roleDisplayName(r, rid, { name: SHORT_EN[rid] || rid }) : (SHORT_EN[rid] || (r && r.short) || rid);
	  }
	  const WARNING_EN = {
	    "觸發規則2：內容含賠償字眼——高風險，不可批次核准，蓋章需二次確認": "Rule 2: This draft mentions compensation. It is high risk, must be approved alone, and needs a second confirmation.",
	    "由客服個案轉入——只草擬內部處理單；對客戶的賠償回覆仍是客服那張草稿，須由你蓋章才外發": "Routed from Customer Support. This is an internal form draft. The customer reply still needs your approval before sending.",
	    "觸發規則1：金額 HK$48,200 ≥ HK$20,000 審批線（規則另含：折扣 ≥ 10%、清單外品項）": "Rule 1: HK$48,200 is above the HK$20,000 approval limit. Discounts of 10% or more and off-list items also need approval.",
	    "內容含法律字眼——依規則2逐件審批，不可批次核准；標註僅供內部參考，並非法律意見": "Rule 2: This draft contains legal wording. Approve it alone. It is for internal reference, not legal advice.",
	    "觸發規則2：涉及款項往來——必經你審批": "Rule 2: This draft involves a payment and needs your approval.",
	    "已等候 21 小時——款項提醒久候會失準，建議今天處理": "Waiting 21 hours. Payment reminders can become outdated, so review this today.",
	    "已遮蔽姓名和電話，但發現未遮蔽身份證號。內容未有外發，請你覆核。": "A name and phone number were hidden, but an ID number was not. Nothing was sent. Please review it.",
	  };
	  const QUOTED_GUIDANCE_WARNING = "草稿提及具體賠償金額 HK$500，與守則（2026-05-12）『賠償金額不寫具體數字，留待你決定』牴觸——建議修改或退回";
	  const QUOTED_GUIDANCE_TEXT = "『賠償金額不寫具體數字，留待你決定』";
	  const PROVENANCE_EN = {
	    "引用檔案：《產品FAQ.docx》第 3 條": "File used: 《產品FAQ.docx》 · item 3",
	    "來源：會議錄音 32 分鐘": "Source: 32-minute meeting recording",
	    "來源：收發室轉入郵件 · 分類：物流／包裝 · 嚴重度：中": "Source: Email from Apps · Type: delivery / packaging · Priority: Medium",
	    "依據：客服轉入的個案摘要（ap7）": "Based on: Customer Support case summary (ap7)",
	    "引用檔案：《價目表_2026H1.xlsx》《報價與付款條款.pdf》": "Files used: 《價目表_2026H1.xlsx》 and 《報價與付款條款.pdf》",
	    "依據檔案：《標準合約範本.docx》和公司審查守則（12 條規則）": "Files used: 《標準合約範本.docx》 and the company review rules (12 rules)",
	    "來源：收發室轉入郵件｜依據：訂單表（今早 06:00 同步）": "Source: Email from Apps · Based on: order table (updated today at 06:00)",
	    "依據：應收清單 INV-0173": "Based on: receivables list INV-0173",
	    "依據：6 月內容日曆 · 第 4 週": "Based on: June content calendar · Week 4",
	    "示範遮罩規則（依客戶設定）· 經人手覆核，未自動外發": "Sample privacy rules (company settings) · Reviewed by a person, not sent automatically",
	    "已檢索：檔案中的全部 7 份文件，未找到退款政策相關條款": "Searched all 7 files in Files. No refund policy was found.",
	  };
	  const UNDO_NOTE_EN = {
	    "郵件發出後無法收回——緩衝期是最後的撤回機會": "This email cannot be recalled after sending. The wait is your last chance to undo.",
	    "內部表格——可隨時還原": "Internal table. You can undo anytime.",
	  };
	  const SKILL_EN = { "客訴轉入：賠償／補發處理單草擬": "Draft a compensation or replacement form" };
	  const MASK_KIND_EN = { "姓名": "Person name", "電話": "Phone", "身份證": "ID number" };
	  const MASK_VALUE_EN = { "[姓名1]": "[Name 1]", "[電話1]": "[Phone 1]", "(未遮蔽 · 已擋下)": "Not hidden · blocked" };
	  const HISTORY_RULE_EN = { "規則1：金額 ≥ HK$20,000": "Rule 1: Amount ≥ HK$20,000" };
	  const APPROVER_EN = { "東主": "Owner" };
	  const RULE_SYSTEM_EN = {
	    r1: {
	      action: { raw: "必經審批 · 標記中風險", en: "Approval required · Medium risk" },
	    },
	    r2: {
	      label: { raw: "內容含款項／賠償／法律字眼", en: "Payment, compensation or legal wording" },
	      action: { raw: "必經審批 · 標記高風險 · 不可批次核准", en: "Approval required · High risk · Approve one at a time" },
	    },
	    r3: {
	      label: { raw: "首次往來的新收件人", en: "New recipient" },
	      action: { raw: "必經審批", en: "Approval required" },
	    },
	    r4: {
	      label: { raw: "所有對外發出（郵件／貼文／訊息）", en: "Anything sent outside (email, post or message)" },
	      action: { raw: "預設必經審批；等級 2 技能僅限低風險類別", en: "Approval required by default · Only low-risk repeat work can skip this step" },
	    },
	  };

	  function systemText(value) {
	    return WARNING_EN[value] || PROVENANCE_EN[value] || UNDO_NOTE_EN[value] || value;
	  }
	  function detailsAria(open, id) {
	    const key = open ? "Hide details {id}" : "View details {id}";
	    return window.I18N && window.I18N.format ? window.I18N.format(key, { id: id || "" }) : (open ? "Hide details " : "View details ") + (id || "");
	  }
	  function warningText(value) {
	    if (value !== QUOTED_GUIDANCE_WARNING) return systemText(value);
	    return h(React.Fragment, null,
	      h("span", null, "The draft includes HK$500 and conflicts with your rule (2026-05-12):"), " ",
	      h("span", { translate: "no", "data-i18n-ignore": "true" }, QUOTED_GUIDANCE_TEXT),
	      h("span", null, ". Edit or return it."));
	  }
	  function skillText(value) { return SKILL_EN[value] || value; }
	  function ruleSystemText(rule, field) {
	    const entry = RULE_SYSTEM_EN[rule && rule.id] && RULE_SYSTEM_EN[rule.id][field];
	    return entry && rule[field] === entry.raw ? entry.en : rule[field];
	  }
	  function historyRuleText(value) { return HISTORY_RULE_EN[value] || value; }
	  function approverText(value) { return APPROVER_EN[value] || value; }

  // PII mask demo (per customer masking rules). ok:false = not on list, blocked by policy.
  function MaskPanel({ map, blocked }) {
    return h("div", { className: "mask-panel" },
      h("div", { className: "mask-head" }, h(Icon, { name: "search", size: 13 }), h("span", null, "Privacy check")),
      map.map((m, i) => h("div", { className: "mask-row" + (m.ok === false ? " bad" : ""), key: i },
        h("span", { className: "mask-kind" }, MASK_KIND_EN[m.kind] || m.kind),
        h("span", { className: "mask-raw" }, m.raw),
        h(Icon, { name: "arrowRight", size: 12 }),
        h("span", { className: "mask-masked" }, MASK_VALUE_EN[m.masked] || m.masked),
        m.ok === false && h(Badge, { tone: "red" }, "No match · blocked"))),
      h("div", { style: { fontSize: 11.5, marginTop: 6, color: "var(--text-faint)" } },
        blocked
          ? "A sensitive field was found. It was not sent out and needs your review."
          : "Sensitive details are hidden before drafting. You still see the original."));
  }

  function AlertNote({ children, high }) {
    return h("div", { className: "risk-note " + (high ? "high" : "") },
      h(Icon, { name: "alert", size: 14 }), h("span", null, children));
  }

  function AutoRow({ ax, ctx }) {
    const [left, setLeft] = useState(typeof ax.bufferLeft === "number" ? ax.bufferLeft : 0);
    useEffect(() => {
      if (ax.status !== "pending") return;
      const t = setInterval(() => setLeft((s) => s - 1), 1000);
      return () => clearInterval(t);
    }, [ax.status]);
    useEffect(() => { if (ax.status === "pending" && left <= 0) ctx.autoSent(ax.id); }, [left, ax.status]);
    const m = Math.max(0, Math.floor(left / 60)), s = Math.max(0, left % 60);
    const [showRun, setShowRun] = useState(false);
    const run = (ctx.office.runs || []).find((r) => r.autoExecId === ax.id);
    return h("div", { "data-auto-exec-id": ax.id, "data-auto-exec-status": ax.status },
      h("div", { className: "auto-row" + (ax.status === "sent" ? " sent" : ax.status === "recalled" || ax.status === "blocked" ? " recalled" : "") },
        h(InlineIcon, { name: "zap", tone: "amber" }),
        h("div", { className: "grow" },
          h("b", { style: { fontSize: 13 } }, ax.title),
          ax.irrevNote && h("div", { style: { fontSize: 12, color: "var(--text-faint)" } }, systemText(ax.irrevNote))),
        h("span", { style: { fontSize: 11.5, color: "var(--text-faint)" } }, window.officeTimeDisplay ? window.officeTimeDisplay(ax.time) : ax.time),
        // C11-iii: dynamic countdown isolated in its own span so the static suffix stays matchable.
        ax.status === "pending" && h("span", { className: "countdown" },
          h("span", null, m + "m " + s + "s "), h("span", null, "left before sending")),
        run && h(Button, { sm: true, variant: "ghost", icon: "activity", ariaLabel: detailsAria(showRun, run.id), onClick: () => setShowRun((v) => !v) }, showRun ? "Hide details" : "View details"),
        ax.status === "pending" && h(Button, { sm: true, "data-approval-action": "recall-auto", "data-auto-exec-id": ax.id, disabled: !ctx.can("recallAuto"),
          title: ctx.can("recallAuto") ? "" : ctx.denyReason("recallAuto"), onClick: () => ctx.recallAuto(ax.id) }, "Recall"),
        ax.status === "sent" && h(Badge, { tone: "green" }, "Sent"),
        ax.status === "blocked" && h(Badge, { tone: "red" }, "Rule blocked"),
        ax.status === "recalled" && h(Badge, { tone: "neutral" }, "Recalled · routed to human approval")),
      showRun && run && h("div", { style: { marginTop: 8 } }, h(window.RunTrace, { run: run,
        onOpenSource: (src) => { if (ctx.setHighlight && src && src.fileId) ctx.setHighlight(src.fileId); ctx.openPage("files", src && src.fileId); },
        onOpenInbox: (id) => { if (ctx.setHighlight && id) ctx.setHighlight(id); ctx.openPage("approvals"); } })));
  }

  function approvalRunFor(office, item) {
    const runs = (office && office.runs) || [];
    return runs.find((r) =>
      (item.runId && r.id === item.runId) ||
      r.inboxId === item.id ||
      r.originalInboxId === item.id) || null;
  }

  function fileBaseName(name) {
    return String(name || "").replace(/\.[^.]+$/, "");
  }

  function fileNameFromText(text) {
    const m = /《([^》]+)》/.exec(String(text || ""));
    return m ? m[1] : "";
  }

  function fileRoomFileFromRef(files, ref) {
    if (!ref || !files || !files.length) return null;
    if (typeof ref === "object") {
      const id = ref.fileId || ref.sourceFileId || (String(ref.id || "").indexOf("f-") === 0 ? ref.id : "");
      if (id) {
        const byId = files.find((f) => f.id === id);
        if (byId) return byId;
      }
      return fileRoomFileFromRef(files, ref.file || ref.fileName || ref.sourceName || ref.name || ref.title || ref.source);
    }
    const text = String(ref || "");
    const quoted = fileNameFromText(text);
    const names = quoted ? [quoted] : [];
    names.push(text);
    for (let i = 0; i < names.length; i += 1) {
      const name = String(names[i] || "").trim();
      if (!name) continue;
      const exact = files.find((f) => f.name === name);
      if (exact) return exact;
      const byMention = files.find((f) => text.indexOf(f.name) >= 0);
      if (byMention) return byMention;
      const base = fileBaseName(name);
      if (base.length >= 4) {
        const byBase = files.find((f) => fileBaseName(f.name) === base);
        if (byBase) return byBase;
      }
    }
    return null;
  }

  function sourceSnippetFromRef(ref) {
    if (!ref || typeof ref !== "object") return "";
    return ref.snippet || ref.sourceSnippet || ref.summary || ref.outcome || "";
  }

  function resolveApprovalFileRoomSource(ctx, item, run) {
    const office = (ctx && ctx.office) || {};
    const files = office.files || [];
    const candidates = [];
    const add = (ref) => { if (ref) candidates.push(ref); };
    add({ fileId: item.sourceFileId, sourceName: item.source, snippet: item.sourceSnippet });
    add(item.sourceMetadata || item.sourceMeta || item.sourceRef);
    add(item.setup && { fileId: item.setup.sourceFileId, sourceName: item.setup.sourceName });
    (item.sources || []).forEach(add);
    (item.evidence || []).forEach((ev) => {
      add({ fileId: ev.fileId || ev.sourceFileId, sourceName: ev.source, snippet: ev.sourceSnippet });
      add(ev.sourceMetadata || ev.sourceMeta || ev.sourceRef);
      add(ev.sourceGapResolution || ev.resolution);
      add(ev.source);
      add(ev.sourceSnippet);
    });
    add(item.source);
    add(item.sourceSnippet);
    if (run) {
      add({ fileId: run.fileId });
      add(run.setup && { fileId: run.setup.sourceFileId, sourceName: run.setup.sourceName });
      (run.sources || []).forEach(add);
      (run.steps || []).forEach((step) => {
        add(step);
        add(step.source);
      });
      (run.scenarioSteps || []).forEach((step) => {
        add(step);
        add(step.evidence);
      });
    }
    for (let i = 0; i < candidates.length; i += 1) {
      const file = fileRoomFileFromRef(files, candidates[i]);
      if (file) return { fileId: file.id, file: file.name, snippet: sourceSnippetFromRef(candidates[i]) || item.sourceSnippet || "" };
    }
    return null;
  }

  function openApprovalFileRoomSource(ctx, src) {
    if (!src || !src.fileId) return false;
    if (ctx.setHighlight) ctx.setHighlight(src.fileId);
    ctx.openPage("files", src.fileId);
    return true;
  }

  function ApprovalCard({ item, ctx, onReject, selected, onToggle, highlighted, inGroup }) {
    const [stamping, setStamping] = useState(false);
    const [showSrc, setShowSrc] = useState(false);
    const [showMask, setShowMask] = useState(false);
    const [editOpen, setEditOpen] = useState(false);
    const [editVal, setEditVal] = useState("");
    const [hrConfirm, setHrConfirm] = useState(null);
    const [hrChecked, setHrChecked] = useState(false);
    const [showRun, setShowRun] = useState(false);
    const [openEvidenceKey, setOpenEvidenceKey] = useState(null);
    const lang = window.I18N ? window.I18N.getLang() : "en";
    const localZh = (text) => (window.scenarioZhText ? window.scenarioZhText(text, lang) : text);
    const localUi = (text) => (window.scenarioUiText ? window.scenarioUiText(text, lang) : (lang === "en" ? text : ((window.I18N && window.I18N.t) ? window.I18N.t(text) : text)));
    const localLabel = (en, zh) => lang === "en" ? en : localZh(zh);
    const checkpointLabel = (count) => lang === "en" ? count + " file-checked steps" : localZh(count + " 個已檢查檔案的步驟");
    // Findings 1+2: when this card becomes the highlighted one, scroll it into focus,
    // then clear the global highlight so the recency cue is transient (not sticky).
    const cardRef = React.useRef(null);
    React.useEffect(() => {
      if (!highlighted) return;
      const el = cardRef.current;
      if (el && el.scrollIntoView) el.scrollIntoView({ block: "center", behavior: "smooth" });
      const t = setTimeout(() => { if (ctx.setHighlight) ctx.setHighlight(null); }, 4000);
      return () => clearTimeout(t);
    }, [highlighted]);
    React.useEffect(() => {
      setOpenEvidenceKey(null);
    }, [item.id]);
    const role = ctx.office.roleById[item.role];
    const run = approvalRunFor(ctx.office, item);
    const isHireRequest = item.type === "hire";
    const eligible = window.isBatchApprovalEligible ? window.isBatchApprovalEligible(item) : item.risk === "低" && !item.type && !item.guidanceWarning && !item.stale && !((item.evidence || []).length);
    const approveAction = isHireRequest ? "hire" : ctx.approveActionForRisk(item.risk);
    const canApprove = ctx.can(approveAction);
    const canEdit = !isHireRequest && canApprove;
    const canReject = isHireRequest ? ctx.can("hire") : ctx.can("rejectWithRule");
    const canFix = ctx.can("applyFix");
    const evidenceRows = ((item.evidence || []).length ? item.evidence : []).map((ev, i) =>
      Object.assign({ _key: "evidence-" + i + "-" + (typeof ev.index === "number" ? ev.index : i), _pos: i }, ev));
    const reviewedEvidenceKeys = Array.isArray(item.reviewedEvidenceKeys) ? item.reviewedEvidenceKeys : [];
    const provenanceLines = (function () {
      const rows = [];
      if (item.sourceSnippet) rows.push(item.sourceSnippet);
      const runSource = run && run.sources && run.sources[0];
      if (runSource && runSource.snippet && rows.indexOf(runSource.snippet) < 0) rows.push(runSource.snippet);
      evidenceRows.forEach((ev) => {
        const line = ev.sourceSnippet || ev.summary || ev.outcome || ev.source;
        if (line && rows.indexOf(line) < 0) rows.push(line);
      });
      if (!rows.length && item.source) rows.push(item.source);
      return rows;
    })();
    const fileRoomSource = resolveApprovalFileRoomSource(ctx, item, run);
	    const packetNeedsReview = evidenceRows.length > 0;
	    const reviewedCount = evidenceRows.filter((ev) => reviewedEvidenceKeys.indexOf(ev._key) >= 0).length;
	    const packetReviewComplete = !packetNeedsReview || reviewedCount >= evidenceRows.length;
	    const sourceGapBlock = ctx.sourceGapBlockForApproval ? ctx.sourceGapBlockForApproval(item) : null;
	    const reviewBlockTitle = packetNeedsReview && !packetReviewComplete
	      ? localLabel("Check every file step before approving this item.", "核准此項目之前，請先檢查每個檔案步驟。")
	      : "";
	    const sourceGapBlockTitle = sourceGapBlock ? localLabel("Add the missing file before approving this item.", "核准此項目之前，請先加入缺少的檔案。") : "";
	    const policyBlock = !isHireRequest && ctx.policyBlockForApproval ? ctx.policyBlockForApproval(item) : null;
	    const policyBlockTitle = policyBlock && ctx.policyBlockMessage ? ctx.policyBlockMessage(policyBlock) : "";
	    const currentPolicyNote = sourceGapBlockTitle || policyBlockTitle || (policyBlock ? item.policyNote : "");
	    const canStamp = canApprove && packetReviewComplete && !policyBlock && !sourceGapBlock;
	    const canStampEdit = canEdit && packetReviewComplete && !policyBlock && !sourceGapBlock;
    function setEvidenceReviewed(key, value) {
      if (!ctx.reviewEvidence) return;
      ctx.reviewEvidence(item.id, key, value);
    }
    function evidenceReviewPanel() {
      if (!evidenceRows.length) return null;
      return h("div", { className: "packet-review" },
        h("div", { className: "packet-review-head" },
          h("span", null, localLabel("File checks", "檔案檢查") + " · " + reviewedCount + "/" + evidenceRows.length),
          h("div", { className: "row gap6 wrap" },
            h(Badge, { tone: packetReviewComplete ? "green" : "amber" }, packetReviewComplete ? localLabel("Ready to approve", "可以核准") : localLabel("Check required", "需要檢查")))),
        h("div", { className: "packet-review-list" },
	          evidenceRows.map((ev) => {
	            const open = openEvidenceKey === ev._key;
	            const reviewed = reviewedEvidenceKeys.indexOf(ev._key) >= 0;
	            const rowSourceGap = ctx.sourceGapBlockForEvidence ? ctx.sourceGapBlockForEvidence(item, ev, ev._pos) : null;
	            const resolvedGap = ev.sourceState === "missing" && !rowSourceGap;
	            const toggleOpen = () => setOpenEvidenceKey(open ? null : ev._key);
	            return h("div", { key: ev._key, style: { display: "grid", gap: 6 } },
		              h("div", { role: "button", tabIndex: 0, className: "packet-review-row" + (reviewed ? " reviewed" : ""), onClick: toggleOpen, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleOpen(); } }, "aria-expanded": open ? "true" : "false", "data-approval-evidence-row": "true", "data-evidence-key": ev._key, "data-reviewed": reviewed ? "true" : "false" },
                h("i", null, h(Icon, { name: reviewed ? "check" : "search", size: 13 })),
                h("span", { className: "packet-review-copy" },
	                  h("b", null, (typeof ev.index === "number" ? "Step " + (ev.index + 1) + ": " : "") + (ev.title || localLabel("Task step", "任務步驟"))),
	                  h("span", null, rowSourceGap ? localLabel("Add the missing file before checking", "檢查前必須先加入缺少的檔案") : resolvedGap ? localLabel("File added; check it now", "已加入檔案；請立即檢查") : (ev.outcome || ev.summary || localLabel("File step captured", "已記錄檔案步驟")))),
	                h("span", { className: "packet-review-check" }, reviewed ? localLabel("Checked", "已檢查") : localLabel("Open", "開啟"))),
              open && h("div", { className: "packet-review-detail", "data-approval-evidence-detail": "true", "data-evidence-key": ev._key },
                h("div", { className: "ac-evidence-grid" },
                  h("div", { className: "ac-evidence-card" }, h("b", null, localLabel("App", "應用程式")), h("span", null, ev.tool || localLabel("Assigned app", "指定應用程式"))),
                  h("div", { className: "ac-evidence-card" }, h("b", null, localLabel("File", "檔案")), h("span", null, systemText(ev.source || item.source || localLabel("Attached file", "附加檔案")))),
                  h("div", { className: "ac-evidence-card" }, h("b", null, localLabel("Result", "結果")), h("span", null, ev.outcome || ev.summary || localLabel("Prepared next task step", "已準備下一個任務步驟"))),
                  h("div", { className: "ac-evidence-card" }, h("b", null, localLabel("Approval", "核准")), h("span", null, ev.gate || localLabel("File checked before approval", "核准前已檢查檔案"))),
                  ev.note && h("div", { className: "ac-evidence-card" }, h("b", null, localLabel("Submitted note", "已提交備註")), h("span", null, ev.note)),
                  ev.sourceSnippet && h("div", { className: "ac-evidence-card" }, h("b", null, localLabel("File excerpt", "檔案摘錄")), h("span", null, systemText(ev.sourceSnippet)))),
                h("div", { className: "wo-step-actions" },
	                  h("button", { type: "button", className: "ac-chip", disabled: !canApprove || !!rowSourceGap, title: rowSourceGap ? sourceGapBlockTitle : (canApprove ? "" : ctx.denyReason(approveAction)), "data-approval-action": "review-evidence", "data-evidence-key": ev._key, onClick: () => setEvidenceReviewed(ev._key, true) }, reviewed ? localLabel("Checked", "已檢查") : rowSourceGap ? localLabel("Add file first", "先加入檔案") : localLabel("Mark checked", "標記為已檢查")),
	                  rowSourceGap && item.workOrderId && h("button", { type: "button", className: "ac-chip", disabled: !ctx.attachAgentChatSourceGap, "data-approval-action": "attach-source-gap", "data-work-order-id": item.workOrderId || "", "data-approval-source-file-id": item.sourceFileId || "", onClick: () => ctx.attachAgentChatSourceGap(item.workOrderId, rowSourceGap.key || rowSourceGap.evidenceKey || rowSourceGap.stepIndex, item.sourceFileId) }, localLabel("Attach file", "附加檔案")),
	                  item.workOrderId && h(Button, { sm: true, variant: "ghost", iconRight: "arrowRight", "data-approval-action": "open-workorder", "data-work-order-id": item.workOrderId || "", onClick: () => ctx.openPage("workorder", item.workOrderId) }, localLabel("Task", "任務")),
	                  run && h(Button, { sm: true, variant: "ghost", icon: "activity", "data-approval-action": "show-run-trace", "data-run-id": run.id || "", onClick: () => setShowRun(true) }, localLabel("View details", "查看詳情"))),
	                rowSourceGap ? h("div", { className: "packet-review-note" }, localLabel("This step needs a file or owner approval before checking.", "此步驟需有檔案或東主核准，才可檢查。")) : !reviewed && h("div", { className: "packet-review-note" }, localLabel("Check this step before approving the item.", "核准此項目之前，請先檢查此步驟。"))));
	          })));
    }

    function stamp(conflict) {
      if (stamping) return;
      if (!packetReviewComplete) {
        ctx.showToast(localLabel("Check every file step before approving.", "核准之前，請先檢查每個檔案步驟。"));
        return;
      }
      if (policyBlock) {
        ctx.approve(item.id, conflict ? "牴觸守則仍核准" : null);
        return;
      }
      setStamping(true);
      // verbatim ~1150ms stamp → ctx.approve; conflict note keyed on the 繁中 data contract.
      setTimeout(() => ctx.approve(item.id, conflict ? "牴觸守則仍核准" : null), 1150);
    }
    function startApprove(conflict) {
      if (stamping) return;
      if (isHireRequest) stamp(conflict);
      else if (item.risk === "高") { setHrChecked(false); setHrConfirm({ conflict: !!conflict }); }
      else stamp(conflict);
    }
    function viewOriginal() {
      if (openApprovalFileRoomSource(ctx, fileRoomSource)) {
        setShowSrc(false);
        return;
      }
      if (provenanceLines.length) setShowSrc((v) => !v);
      else ctx.showToast("No original file is attached to this item.");
    }

	    return h(Card, { innerRef: cardRef, className: ((item.type === "nosource" || item.type === "dlp") ? "nosource " : "") + "approval" + (stamping ? " stamping" : "") + (highlighted ? " onboard-hl" : ""), "data-approval-id": item.id, "data-approval-role-id": item.role, "data-approval-risk": item.risk, "data-approval-type": item.type || "draft", "data-work-order-id": item.workOrderId || "", "data-run-id": item.runId || (run && run.id) || "", style: { padding: 18 } },
      h("div", { className: "row gap10", style: { marginBottom: 10, flexWrap: "wrap", alignItems: "center" } },
        !item.type && h("input", { type: "checkbox", className: "chk",
		          "data-approval-action": "batch-select", "data-approval-id": item.id,
		          checked: eligible && !policyBlock && !sourceGapBlock && ctx.can("batchApprove") && selected, disabled: !eligible || !!policyBlock || !!sourceGapBlock || !ctx.can("batchApprove"),
	          title: sourceGapBlockTitle || policyBlockTitle || (!ctx.can("batchApprove") ? ctx.denyReason("batchApprove") : eligible ? "Add to batch approval" : "Medium / high risk must be approved one by one (rule 2)"),
	          onChange: () => { if (ctx.can("batchApprove") && !policyBlock && !sourceGapBlock) onToggle(item.id); } }),
        !inGroup && h(window.Face, { role: role, size: 22, variant: "head" }),
        h("b", { style: { fontSize: 15 } }, item.title),
        h(Chip, { tone: "neutral" }, inGroup
          ? h("span", null, skillText(item.skill))
          : h(React.Fragment, null,
              h("span", null, shortEnName(ctx.office, item.role)),
              h("span", { "aria-hidden": "true" }, " · "),
              h("span", null, skillText(item.skill)))),
        isHireRequest && h(Chip, { tone: "red", title: "Running a hire is Owner-only — Managers can only request a hire" }, "Owner-only"),
        isHireRequest && item.requestedBy && h(Chip, { tone: "blue" }, h("span", null, "Requested by"), " ", h("span", null, item.requestedBy)),
        item.briefed && h(Chip, { tone: "blue", title: "You gave this task. The draft used a named file and now waits in My Desk." }, "Your task"),
        item.briefed && item.id === ctx.highlight && h(Chip, { tone: "green", title: "Just arrived from your briefing — newly delegated." }, "New"),
        h("span", { className: "grow" }),
        // C14/Phase-1: confidence is informational, never gating; rendered as its own chip.
        item.conf && !item.type && h(Chip, { tone: item.conf === "高" ? "green" : item.conf === "中" ? "amber" : "neutral",
          title: "This ready score helps sort review. It never changes the approval rules." },
          h("span", null, "Ready"), " ", h("span", null, item.conf)),
        h(Chip, { tone: RISK_TONE[item.risk] }, h("span", null, "Risk"), " ", h("span", null, RISK_EN[item.risk])),
        h("span", { style: { fontSize: 12.5, color: "var(--text-faint)" } }, window.officeTimeDisplay ? window.officeTimeDisplay(item.time) : item.time)),
      item.handoffFrom && h("div", { className: "handoff-strip" },
        h(Icon, { name: "arrowRight", size: 13 }),
        h("span", { className: "grow" }, h("span", null, "Routed in by"), " ",
          h("span", null, item.handoffFrom.name), " · ", h("span", null, "File:"), " ", h("span", null, skillText(item.handoffFrom.fromTitle))),
        h(Chip, { tone: "blue", title: "Another employee sent this here. You still approve before anything goes out." }, "Employee handoff")),
      item.handoffTo && h("div", { style: { fontSize: 12, marginBottom: 6, color: "var(--text-faint)" } },
        "An Admin Assistant draft is waiting. This reply still needs your approval."),
      item.stale && item.staleNote && h(AlertNote, null, warningText(item.staleNote)),
      currentPolicyNote && h(AlertNote, { high: true }, warningText(currentPolicyNote)),
      item.riskNote && h(AlertNote, { high: item.risk === "高" }, warningText(item.riskNote)),
      item.dlpNote && h(AlertNote, { high: true }, warningText(item.dlpNote)),
      item.guidanceWarning && h(React.Fragment, null,
        h(AlertNote, null, warningText(item.guidanceWarning)),
          h("div", { className: "warn-actions" },
	            h(Button, { sm: true, variant: "primary", "data-approval-action": "apply-fix", "data-approval-id": item.id, disabled: !canFix, title: canFix ? "" : ctx.denyReason("applyFix"), onClick: () => ctx.applyFix(item.id) }, "Fix the draft to the rules"),
	            h(Button, { sm: true, disabled: !canStamp, title: sourceGapBlockTitle || policyBlockTitle || (canApprove ? reviewBlockTitle : ctx.denyReason(approveAction)), "data-approval-action": "approve", onClick: () => startApprove(true) }, "Approve anyway (saved in History)"))),
      item.followedGuidance && h("div", { className: "rule-banner", style: { marginBottom: 10 } },
        h("span", null, "Following your instruction: "),
        h("b", null, "「" + item.followedGuidance.comment + "」")),
      (item.workOrderId || item.runId || (item.evidence && item.evidence.length)) && h("div", { className: "packet-strip" },
        h("div", { className: "packet-strip-head" },
          h("span", null, localLabel("My Desk item", "我的辦公桌項目")),
          h("div", { className: "row gap6 wrap" },
            item.workOrderId && h(Button, { sm: true, variant: "ghost", iconRight: "arrowRight", "data-approval-action": "open-workorder", "data-work-order-id": item.workOrderId || "", onClick: () => ctx.openPage("workorder", item.workOrderId) }, item.workOrderId),
            run && h(Button, { sm: true, variant: "ghost", icon: "activity", "data-approval-action": "toggle-run-trace", "data-run-id": run.id || "", "aria-expanded": showRun ? "true" : "false", ariaLabel: detailsAria(showRun, run.id), onClick: () => setShowRun((v) => !v) }, localLabel(showRun ? "Hide details" : "View details", showRun ? "隱藏詳情" : "查看詳情")))),
        h("div", { className: "packet-strip-grid" },
          h("div", { className: "packet-strip-item" }, h("b", null, localLabel("Details", "詳情")), h("span", null, item.runId || (run && run.id) || localLabel("Details pending", "詳情待定"))),
          h("div", { className: "packet-strip-item" }, h("b", null, localLabel("File checks", "檔案檢查")), h("span", null, checkpointLabel((item.evidence || []).length || 0))),
          h("div", { className: "packet-strip-item" }, h("b", null, localLabel("Assumptions", "假設")), h("span", null, (item.assumptions && item.assumptions.length ? item.assumptions.join("; ") : localLabel("No unresolved assumptions captured", "未擷取未決假設")))),
          h("div", { className: "packet-strip-item" }, h("b", null, localLabel("Approval", "核准")), h("span", null, localLabel("Owner approval required before sending", "傳送前必須由東主核准")))),
        evidenceReviewPanel()),
      h("div", { className: "draft" }, item.lines.map((l, i) => h("p", { key: i }, l))),
      showSrc && provenanceLines.length > 0 && h("div", { className: "draft", style: { marginTop: 8 } },
        h("p", { style: { fontWeight: 700, fontSize: 12.5 } }, "File excerpt"),
        provenanceLines.map((line, i) => h("p", { key: i }, systemText(line)))),
      item.type === "dlp" && item.maskMap && h(MaskPanel, { map: item.maskMap, blocked: true }),
      showMask && item.type !== "dlp" && item.maskMap && h(MaskPanel, { map: item.maskMap, blocked: false }),
      showRun && run && h("div", { style: { marginTop: 10 } },
        h(window.RunTrace, { run: run,
          onOpenSource: (src) => { if (ctx.setHighlight && src && src.fileId) ctx.setHighlight(src.fileId); ctx.openPage("files", src && src.fileId); },
          onOpenInbox: (id) => { if (ctx.setHighlight && id) ctx.setHighlight(id); ctx.openPage("approvals"); } })),
      h("div", { className: "row gap10", style: { marginTop: 12, flexWrap: "wrap", alignItems: "center" } },
        h("span", { style: { fontSize: 12.5, color: "var(--text-faint)" } }, systemText(item.source)),
        item.basis && !item.type && h(Chip, { tone: "blue", title: "Draft basis" }, BASIS_EN[item.basis]),
        run && h(Button, { sm: true, variant: "ghost", icon: "activity", "data-approval-action": "toggle-run-trace", "data-run-id": run.id || "", "aria-expanded": showRun ? "true" : "false", ariaLabel: detailsAria(showRun, run.id), onClick: () => setShowRun((v) => !v) }, showRun ? "Hide details" : "View details"),
        h("span", { className: "grow" }),
        item.type === "nosource"
          ? h(Button, { variant: "primary", sm: true, icon: "upload", "data-approval-action": "stage-missing-source", "data-approval-id": item.id, disabled: !ctx.can("addUpload"), title: ctx.can("addUpload") ? "" : ctx.denyReason("addUpload"),
              onClick: () => ctx.stageMissingSource({ source: "approval", approvalId: item.id, roleId: item.role, skillId: item.skillId, skill: item.skill, title: item.title, sourceStatement: item.source, topic: "退款政策", customerLine: (item.lines || [])[0] }) }, "Add missing file to Files")
          : item.type === "dlp"
          ? h(Button, { variant: "primary", sm: true, icon: "edit", "data-approval-action": "manual-review", "data-approval-id": item.id, disabled: !canStampEdit, title: policyBlockTitle || (canEdit ? reviewBlockTitle : ctx.denyReason(approveAction)),
              onClick: () => { setEditVal("您好：已收到您的退款查詢。為保障你的個人資料，我們會以安全方式核對訂單後盡快回覆，期間毋須提供身份證號碼。"); setEditOpen(true); } }, "Manual review")
          : isHireRequest
          ? h(React.Fragment, null,
	            h(Button, { sm: true, disabled: !canReject, title: canReject ? "" : ctx.denyReason("hire"), "data-approval-action": "reject-open", onClick: () => onReject(item) }, "Decline request"),
	            h(Button, { variant: "primary", sm: true, icon: "stamp", disabled: !canStamp, title: canApprove ? reviewBlockTitle : ctx.denyReason("hire"), "data-approval-action": "approve", onClick: () => startApprove(false) }, "Approve hire"))
          : h(React.Fragment, null,
	            item.maskMap && h(Button, { sm: true, onClick: () => setShowMask((v) => !v) }, showMask ? "Hide privacy check" : "Show privacy check"),
		            h(Button, { sm: true, "data-approval-action": "view-original", "data-approval-source-file-id": fileRoomSource ? fileRoomSource.fileId : "", onClick: viewOriginal }, !fileRoomSource && showSrc ? "Hide original" : "View original"),
		            h(Button, { sm: true, disabled: !canReject, title: canReject ? "" : ctx.denyReason("rejectWithRule"), "data-approval-action": "reject-open", onClick: () => onReject(item) }, "Reject (with comment)"),
	            h(Button, { sm: true, icon: "edit", disabled: !canStampEdit, title: sourceGapBlockTitle || policyBlockTitle || (canEdit ? reviewBlockTitle : ctx.denyReason(approveAction)), "data-approval-action": "edit-open", onClick: () => { setEditVal(item.lines.join("\n")); setEditOpen(true); } }, "Edit and approve"),
	            h(Button, { variant: "primary", sm: true, icon: "stamp", disabled: !canStamp, title: sourceGapBlockTitle || policyBlockTitle || (canApprove ? reviewBlockTitle : ctx.denyReason(approveAction)), "data-approval-action": "approve", onClick: () => startApprove(false) }, sourceGapBlock ? "File blocked" : policyBlock ? "Rule blocked" : packetNeedsReview && !packetReviewComplete ? localLabel("Check files first", "先檢查檔案") : "Approve"))),
      stamping && h("div", { className: "stamp-seal" }, h("span", null, "核准")),
      editOpen && h(Modal, { title: "Edit and approve", sub: "Your changes are saved in history.", width: 640, onClose: () => setEditOpen(false), "data-approval-modal": "edit", "data-approval-id": item.id,
        footer: [
          h(Button, { key: "c", "data-approval-action": "edit-cancel", onClick: () => setEditOpen(false) }, "Cancel"),
	          h(Button, { key: "ok", variant: "primary", disabled: !editVal.trim() || !packetReviewComplete || !!policyBlock || !!sourceGapBlock, title: sourceGapBlockTitle || policyBlockTitle || reviewBlockTitle, "data-approval-action": "edit-confirm", onClick: () => { setEditOpen(false); ctx.editApprove(item.id, editVal.split("\n").filter(Boolean)); } }, "Apply edits and approve"),
        ] },
        h("textarea", { className: "ta", rows: 6, autoFocus: true, style: { width: "100%" }, value: editVal, "data-approval-edit-text": "true", onChange: (e) => setEditVal(e.target.value) })),
      hrConfirm && h(Modal, { title: "High-risk draft · second confirmation", sub: item.title, width: 520, onClose: () => setHrConfirm(null), "data-approval-modal": "high-risk", "data-approval-id": item.id,
        footer: [
          h(Button, { key: "c", "data-approval-action": "high-risk-cancel", onClick: () => setHrConfirm(null) }, "Cancel"),
	          h(Button, { key: "ok", variant: "primary", disabled: !hrChecked || !packetReviewComplete || !!policyBlock || !!sourceGapBlock, title: sourceGapBlockTitle || policyBlockTitle || reviewBlockTitle, "data-approval-action": "confirm-high-risk", onClick: () => { const conflict = hrConfirm.conflict; setHrConfirm(null); stamp(conflict); } }, "Confirm approval"),
        ] },
        item.riskNote && h(AlertNote, { high: true }, warningText(item.riskNote)),
        h("label", { className: "row gap8", style: { fontSize: 13.5, cursor: "pointer", marginTop: 8 } },
	          h("input", { type: "checkbox", className: "chk", checked: hrChecked, "data-approval-high-risk-confirm": "true", onChange: (e) => setHrChecked(e.target.checked) }),
          "I have checked the compensation / payment amount and the recipient")));
  }

  function RuleRow({ rule, ctx }) {
    const canRule = ctx.can("updateRule");
    const isR1 = rule.id === "r1" && rule.editable;
    const amountInputId = "approval-rule-" + rule.id + "-amount";
    const [v, setV] = useState(rule.value);
    useEffect(() => { setV(rule.value); }, [rule.value]);
    function commit() {
      const n = parseInt(v, 10);
      if (!isNaN(n) && n > 0 && n !== rule.value) ctx.updateRule("r1", n);
      else setV(rule.value);
    }
    return h("div", { className: "rule-row" },
      isR1
        ? h(React.Fragment, null,
          h("label", { htmlFor: amountInputId, style: { fontSize: 13, fontWeight: 700 } }, "Amount ≥ HK$"),
          h("input", { id: amountInputId, type: "number", value: v == null ? "" : v, disabled: !canRule,
            title: canRule ? "" : ctx.denyReason("updateRule"),
            onChange: (e) => setV(e.target.value), onBlur: commit,
            onKeyDown: (e) => { if (e.key === "Enter") e.target.blur(); } }),
          !canRule && h(Chip, { tone: "neutral", title: ctx.denyReason("updateRule") }, "Read-only"),
	          h("span", { style: { fontSize: 12, color: "var(--text-faint)" } }, "Also covers discounts of 10% or more and off-list items."))
        : h("b", { style: { fontSize: 13 } }, ruleSystemText(rule, "label")),
      h("span", { style: { fontSize: 12.5, color: "var(--text-faint)" } }, ruleSystemText(rule, "action")),
      h("span", { className: "grow" }),
      rule.id === "r4" && h(Chip, { tone: "violet" }, "Default · cannot be turned off"),
      h(Chip, { tone: "neutral" }, h("span", null, "Hit this month"), " ", h("span", null, rule.hits), h("span", null, "times")));
  }

  Object.assign(window, { MaskPanel, AlertNote });
  window.__ApprovalParts = { AutoRow, ApprovalCard, RuleRow, ACTION_EN, BASIS_EN };

  // AutoRow/ApprovalCard/RuleRow/ACTION_EN are already in this IIFE scope (defined above) —
  // not re-destructured from window.__ApprovalParts (that would duplicate-declare them).
  // Audit filters: VALUE is the 繁中 data key (logic, C3); LABEL is English (display, i18n).
  function isBatchApprovalEligible(item) {
    return !!(item && item.risk === "低" && !item.type && !item.guidanceWarning && !item.stale && !((item.evidence || []).length));
  }
  window.isBatchApprovalEligible = isBatchApprovalEligible;

  const AUDIT_FILTERS = [
    { v: "全部", l: "All" }, { v: "核准", l: "Approved" }, { v: "修改後核准", l: "Approved with edits" },
	  { v: "退回", l: "Returned" }, { v: "政策封鎖", l: "Rule blocked" }, { v: "自動執行", l: "Repeat work" }, { v: "撤回", l: "Undone" },
	    { v: "聘用請求", l: "Hire requested" }, { v: "核准聘用", l: "Hire approved" }, { v: "退回聘用", l: "Hire declined" },
	    { v: "建立任務", l: "Task created" }, { v: "建立工作單", l: "Task created" }, { v: "完成工作單", l: "Task completed" }, { v: "重開工作單", l: "Task reopened" },
	    { v: "指派工具", l: "App assigned" }, { v: "移除工具", l: "App removed" }, { v: "上傳檔案", l: "File added" }, { v: "歸檔檔案", l: "File saved" }, { v: "發布設定", l: "Settings applied" }, { v: "還原設定", l: "Settings undone" }, { v: "匯出", l: "Downloaded" },
	    { v: "連接器連線", l: "App connected" }, { v: "重新授權", l: "App reconnected" }, { v: "中斷連接", l: "App disconnected" }, { v: "健康檢查", l: "Connection checked" }, { v: "整合需求", l: "App requested" },
    { v: "更新設定", l: "Settings updated" }, { v: "更新公司", l: "Company updated" }, { v: "邀請用戶", l: "Team member invited" }, { v: "更新用戶", l: "Team member updated" }, { v: "更新用戶權限", l: "Team member access changed" }, { v: "重寄邀請", l: "Invite resent" }, { v: "停用用戶", l: "Team member suspended" }, { v: "恢復用戶", l: "Team member reactivated" }, { v: "移除用戶", l: "Team member removed" },
  ];

  function Approvals({ ctx }) {
    const { PageHeader, Card, Button, Badge, Chip, Modal, RoleDot, InlineIcon, Icon, AUDIT_TONE } = window;
    const office = ctx.office;
    const [rejectItem, setRejectItem] = useState(null);
    const [comment, setComment] = useState("");
    const [sel, setSel] = useState([]);
    const [auditFilter, setAuditFilter] = useState("全部");

    let suggestHit = null;
    office.roles.forEach((r) => {
      if (!r.hired || suggestHit) return;
      r.skills.forEach((s) => { if (!suggestHit && s.suggest && s.level === 1 && s.enabled) suggestHit = { role: r, skill: s }; });
    });

    function isBatchSelectable(item) {
      return isBatchApprovalEligible(item) && !(ctx.policyBlockForApproval && ctx.policyBlockForApproval(item));
    }
    const staleCount = office.inbox.filter((x) => x.stale).length;
    const validSel = sel.filter((id) => office.inbox.some((x) => x.id === id && isBatchSelectable(x)));
    function toggleSel(id) { setSel((s) => s.indexOf(id) >= 0 ? s.filter((x) => x !== id) : s.concat(id)); }
    const auditRows = office.auditLog.filter((e) => auditFilter === "全部" || e.action === auditFilter);

    // group inbox by role; within a group sort stale → highest risk; order groups by
    // urgency (has-stale → highest risk present → pending count).
    const byRole = {};
    office.inbox.forEach((it) => { (byRole[it.role] = byRole[it.role] || []).push(it); });
    const groups = Object.keys(byRole).map((rid) => {
      const list = byRole[rid].slice().sort((a, b) =>
        (b.stale ? 1 : 0) - (a.stale ? 1 : 0) || (RISK_RANK[b.risk] || 0) - (RISK_RANK[a.risk] || 0));
      const counts = { "高": 0, "中": 0, "低": 0 };
      list.forEach((it) => { counts[it.risk] = (counts[it.risk] || 0) + 1; });
      const eligible = list.filter(isBatchSelectable).map((it) => it.id);
      const maxRisk = list.reduce((m, it) => Math.max(m, RISK_RANK[it.risk] || 0), 0);
      return { rid, role: office.roleById[rid], list, counts, eligible, hasStale: list.some((it) => it.stale), maxRisk };
    }).sort((a, b) =>
      (b.hasStale ? 1 : 0) - (a.hasStale ? 1 : 0) || b.maxRisk - a.maxRisk || b.list.length - a.list.length);

    function scrollToGroup(rid) {
      const el = document.getElementById("agroup-" + rid);
      if (el && el.scrollIntoView) el.scrollIntoView({ block: "start", behavior: "smooth" });
    }

	    return h("div", { className: "approvals-screen", "data-approvals-root": "true" },
      h(PageHeader, { eyebrow: "Control", title: "My Desk",
	        sub: "Review drafts before they are sent.",
        // C11-iii: counts isolated in their own spans.
        right: h(Chip, { tone: "neutral" },
          h("span", null, "On your desk"), " ", h("span", null, office.inbox.length), " ", h("span", null, "items"),
          staleCount ? h(React.Fragment, null, " · ", h("span", null, staleCount), " ", h("span", null, "waiting")) : null) }),

      h("div", { className: "rule-banner" },
	        "Approval is on by default. You decide what can repeat."),

      groups.length > 0 && h(Card, { className: "who-card", "data-approval-group-nav": "true", style: { padding: 14 } },
        h("div", { className: "row gap8", style: { marginBottom: 10, alignItems: "center" } },
          h(Icon, { name: "users", size: 15 }), h("b", { style: { fontSize: 13 } }, "Who needs you")),
        h("div", { className: "who-strip" }, groups.map((g) =>
          h("div", { key: g.rid, className: "who-chip", role: "button", tabIndex: 0,
            "data-approval-action": "jump-group", "data-approval-role-id": g.rid, "data-approval-group-count": String(g.list.length), "data-approval-group-target": "agroup-" + g.rid,
            onClick: () => scrollToGroup(g.rid),
            onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); scrollToGroup(g.rid); } },
            title: shortEnName(office, g.rid) },
            h(window.Face, { role: g.role, size: 26, variant: "head" }),
            h("span", null, shortEnName(office, g.rid)),
            h("span", { className: "who-n" }, g.list.length))))),

      suggestHit && h(Card, { className: "suggest", "data-approval-suggest": "true", "data-approval-role-id": suggestHit.role.id, "data-approval-skill-id": suggestHit.skill.id, "data-approval-skill-level": String(suggestHit.skill.level || 1), style: { padding: 18 } },
        h("div", { className: "row gap10", style: { flexWrap: "wrap" } },
          h(InlineIcon, { name: "graduation", tone: "green" }),
          h("div", { className: "grow" },
            h("b", null, suggestHit.skill.name),
            h("p", { "data-approval-suggest-stats": "true", style: { margin: "3px 0 0", fontSize: 13, color: "var(--text-faint)" } }, suggestHit.skill.suggestStats)),
          h(Button, { sm: true, "data-approval-action": "dismiss-autonomy-suggest", "data-approval-role-id": suggestHit.role.id, "data-approval-skill-id": suggestHit.skill.id, "data-approval-rbac-disabled": ctx.can("superviseRole") ? "false" : "true", "aria-disabled": ctx.can("superviseRole") ? "false" : "true", disabled: !ctx.can("superviseRole"),
            title: ctx.can("superviseRole") ? "" : ctx.denyReason("superviseRole"),
            onClick: () => { ctx.setSkill(suggestHit.role.id, suggestHit.skill.id, { suggest: false }); ctx.showToast("Keeping things as they are."); } }, "Not now"),
          h(Button, { variant: "primary", sm: true, "data-approval-action": "allow-auto-run", "data-approval-role-id": suggestHit.role.id, "data-approval-skill-id": suggestHit.skill.id, "data-approval-rbac-disabled": ctx.can("promoteL2") ? "false" : "true", "aria-disabled": ctx.can("promoteL2") ? "false" : "true", disabled: !ctx.can("promoteL2"),
            title: ctx.can("promoteL2") ? "" : ctx.denyReason("promoteL2"),
            onClick: () => { ctx.setSkill(suggestHit.role.id, suggestHit.skill.id, { level: 2, suggest: false }); ctx.showToast("Low-risk repeat work is now allowed."); } }, "Allow repeat work"))),

      office.autoExec.length > 0 && h(Card, { style: { padding: 18 } },
        h("div", { className: "card-head", style: { marginBottom: 8 } }, h("div", { className: "row gap8" }, h(Icon, { name: "zap", size: 15 }), h("h3", null, "Repeat work today"))),
        office.autoExec.map((ax) => h(AutoRow, { key: ax.id, ax, ctx }))),

	      office.inbox.length === 0
	        ? h(Card, { "data-approval-empty-state": "true", style: { textAlign: "center", padding: 48 } },
          h("div", { className: "row", style: { justifyContent: "center", marginBottom: 10 } }, h(InlineIcon, { name: "check", tone: "green" })),
          h("p", { style: { marginTop: 10, fontWeight: 700 } }, "Your desk is clear!"),
	          h("p", { style: { fontSize: 13.5, color: "var(--text-faint)" } }, "New drafts will appear here."),
	          h("div", { className: "row", style: { justifyContent: "center", marginTop: 16 } },
	            h(Button, { variant: "primary", iconRight: "arrowRight", "data-approval-empty-action": "brief-employee", onClick: () => ctx.openPage("employees") }, "Give another task")))
        : groups.map((g) => h("div", { key: g.rid, id: "agroup-" + g.rid, className: "agent-group", "data-approval-group": "true", "data-approval-group-role-id": g.rid, "data-approval-group-count": String(g.list.length),
            style: { "--ag-accent": g.role.color } },
          h("div", { className: "agent-head" },
            h(window.Face, { role: g.role, size: 40, variant: "portrait" }),
            h("div", { className: "agent-name" }, h("span", null, shortEnName(office, g.rid))),
            h("span", { className: "grow" }),
            [["高", "red"], ["中", "amber"], ["低", "green"]].filter((p) => g.counts[p[0]]).map((p) =>
              h(Chip, { key: p[0], tone: p[1] }, h("span", null, g.counts[p[0]]), " ", h("span", null, RISK_EN[p[0]]))),
            h(Chip, { tone: "neutral" }, h("span", null, g.list.length), " ", h("span", null, "pending")),
            h(Button, { sm: true, icon: "stamp", "data-approval-action": "batch-confirm-group", "data-approval-role-id": g.rid, disabled: !g.eligible.length || !ctx.can("batchApprove"),
              title: !ctx.can("batchApprove") ? ctx.denyReason("batchApprove") : g.eligible.length ? "" : "No low-risk drafts eligible for batch",
              onClick: () => ctx.batchApprove(g.eligible) }, "Approve low-risk batch"),
            h(Button, { sm: true, iconRight: "arrowRight", "data-approval-action": "open-employee", "data-approval-role-id": g.rid, onClick: () => ctx.openPage("employee", g.rid) }, "Open employee")),
          h("div", { className: "agent-body" },
            g.list.map((item) => h(ApprovalCard, { key: item.id, item, ctx, onReject: setRejectItem,
              selected: validSel.indexOf(item.id) >= 0, onToggle: toggleSel,
              highlighted: ctx.highlight === item.id, inGroup: true }))))),

	      h("p", { style: { fontSize: 13, textAlign: "center", color: "var(--text-faint)" } }, "Return notes improve the next draft."),

      h(Card, { style: { padding: 18 }, "data-approval-history": "processed", "data-approval-history-filter-current": auditFilter, "data-approval-history-row-count": String(auditRows.length) },
        h("div", { className: "row gap10", style: { marginBottom: 8, flexWrap: "wrap", alignItems: "center" } },
          h("h3", { style: { margin: 0 } }, "History · processed today"),
          h("span", { className: "grow" }),
          h(Button, { sm: true, icon: "download", "data-approval-action": "export-history-preview", "data-approval-history-filter": auditFilter, "data-approval-history-export-count": String(auditRows.length), onClick: () => ctx.exportCsv("emplix-my-desk-history-preview.csv", auditRows, [
            { key: "id", label: "Event ID" },
            { key: "time", label: "Time" },
            { key: "action", label: "Action", value: (e) => ACTION_EN[e.action] || e.action },
            { key: "roleId", label: "AI Employee" },
            { key: "tool", label: "App / File" },
            { key: "rule", label: "Rule", value: (e) => historyRuleText(e.rule) },
            { key: "approver", label: "Approver", value: (e) => e.approver && approverText(e.approver.name) },
            { key: "note", label: "Note", value: (e) => systemText(e.note) },
          ], "My Desk history") }, "Download preview")),
        h("div", { className: "row gap6", style: { marginBottom: 10, flexWrap: "wrap" } },
          AUDIT_FILTERS.map((f) => h("button", { key: f.v, onClick: () => setAuditFilter(f.v),
            "data-approval-history-filter": f.v, "data-approval-history-filter-active": auditFilter === f.v ? "true" : "false",
            className: "badge badge-" + (auditFilter === f.v ? "blue" : "neutral"),
            style: { cursor: "pointer", fontFamily: "inherit" } }, f.l))),
        h("div", { className: "audit-list" },
          auditRows.length === 0
            ? h("p", { "data-approval-history-empty": "true", style: { fontSize: 13, color: "var(--text-faint)" } }, "No matching records.")
            : auditRows.map((e) => h("div", { key: e.id, className: "audit-row", "data-approval-history-row": e.id, "data-approval-action-key": e.action, "data-approval-history-role-id": e.roleId || "" },
                h("span", { className: "audit-time" }, window.officeTimeDisplay ? window.officeTimeDisplay(e.time) : e.time),
                h(Badge, { tone: AUDIT_TONE[e.action] || "neutral" }, ACTION_EN[e.action] || e.action),
                h(window.Face, { role: office.roleById[e.roleId], size: 20, variant: "head", className: "row-mid" }),
                h("b", { style: { fontSize: 13 } }, e.title),
                e.rule && h(Chip, { tone: "neutral" }, historyRuleText(e.rule)),
                e.approver && h(Chip, { tone: "blue", title: "Approver: " + approverText(e.approver.name) }, h("span", null, "by"), " ", h("span", null, approverText(e.approver.name))),
                e.note && h("div", { className: "audit-note" }, systemText(e.note))))),
	        h("p", { style: { fontSize: 12, marginTop: 10, color: "var(--text-faint)" } }, "Every decision stays in History.")),

      h(Card, { style: { padding: 18 } },
        h("h3", { style: { marginBottom: 4 } }, "Approval rules"),
        office.approvalRules.map((r) => h(RuleRow, { key: r.id, rule: r, ctx }))),

      validSel.length > 0 && h("div", { className: "batch-bar" },
        h("span", null, h("span", null, validSel.length), " ", h("span", null, "low-risk selected")),
        h("span", { className: "grow" }),
        h(Button, { variant: "ghost", sm: true, style: { color: "var(--bg-2)" }, onClick: () => setSel([]) }, "Clear"),
        h(Button, { variant: "primary", sm: true, icon: "stamp", "data-approval-action": "batch-confirm-selected", disabled: !ctx.can("batchApprove"),
          title: ctx.can("batchApprove") ? "" : ctx.denyReason("batchApprove"),
          onClick: () => { ctx.batchApprove(validSel); setSel([]); } }, "Approve batch")),

      rejectItem && h(Modal, { title: "Return work",
        sub: rejectItem.type === "hire" ? "Add a short reason for declining this hire." : "Add one short note so the employee learns what to change.",
	        width: 560, onClose: () => { setRejectItem(null); setComment(""); }, "data-approval-modal": "reject", "data-approval-id": rejectItem.id,
	        footer: [
	          h(Button, { key: "c", "data-approval-action": "reject-cancel", onClick: () => { setRejectItem(null); setComment(""); } }, "Cancel"),
	          h(Button, { key: "ok", variant: "primary", disabled: !comment.trim(), "data-approval-action": "reject-confirm", onClick: () => { ctx.reject(rejectItem.id, comment.trim()); setRejectItem(null); setComment(""); } }, rejectItem.type === "hire" ? "Decline hire request" : "Return with note"),
        ] },
	        rejectItem.type === "hire"
	          ? h("p", { style: { fontSize: 12.5, color: "var(--text-faint)", marginBottom: 10 } },
	              h("span", null, "This note is saved in History. The hire does not start."))
	          : h("p", { style: { fontSize: 12.5, color: "var(--text-faint)", marginBottom: 10 } },
	              h("span", null, "Your note guides future drafts for this task.")),
	        h("textarea", { className: "ta", autoFocus: true, style: { width: "100%" }, rows: 3, value: comment, "data-approval-reject-comment": "true", onChange: (e) => setComment(e.target.value),
          placeholder: rejectItem.type === "hire" ? "e.g. Hold until the next billing cycle." : "e.g. Make the tone more formal; add the company phone number at the end." })));
  }

  window.Screens.approvals = Approvals;
})();
