/* approval.jsx — Approval Engine v2 (Path Builder)
   ① canvas full-bleed (không viền) ② cổng có outcomes TỰ ĐỊNH NGHĨA + kéo nối nhánh
   ③ hiệu ứng ghi thuộc tính LINK doc_properties thật (Quản trị → Thuộc tính).
   Persist qua window.cdeApproval. Dùng window.Icon (bộ app). */
(function () {
  const { useState, useEffect, useRef } = React;
  const Icon = window.Icon;
  const uid = () => "n" + Math.random().toString(36).slice(2, 8);
  const OUT_COLORS = ["#10b981", "#ef4444", "#f59e0b", "#2563eb", "#7c3aed", "#0ea5e9", "#64748b"];

  function defaultConfig() {
    const gates = [
      { id: "g_qa", name: "Kiểm tra QA nội bộ", color: "#0d9488",
        outcomes: [{ key: "pass", label: "Đạt", color: "#10b981" }, { key: "fail", label: "Không đạt", color: "#ef4444" }], effects: {} },
      { id: "g_share", name: "Phê duyệt chia sẻ", color: "#2563eb",
        outcomes: [{ key: "approve", label: "Duyệt", color: "#10b981" }, { key: "revise", label: "Cần sửa", color: "#f59e0b" }, { key: "reject", label: "Từ chối", color: "#ef4444" }], effects: {} },
      { id: "g_pub", name: "Phê duyệt phát hành", color: "#7c3aed",
        outcomes: [{ key: "approve", label: "Duyệt", color: "#10b981" }, { key: "cond", label: "Duyệt kèm ý kiến", color: "#0ea5e9" }, { key: "reject", label: "Từ chối", color: "#ef4444" }], effects: {} },
    ];
    const nodes = [
      { key: "n1", gateId: "g_qa", isStart: true, pos: { x: 60, y: 90 }, next: { pass: "n2", fail: "END" } },
      { key: "n2", gateId: "g_share", isStart: false, pos: { x: 430, y: 70 }, next: { approve: "n3", revise: "n1", reject: "END" } },
      { key: "n3", gateId: "g_pub", isStart: false, pos: { x: 810, y: 90 }, next: { approve: "END", cond: "END", reject: "n2" } },
    ];
    return { name: "Phê duyệt thiết kế tiêu chuẩn", gates, nodes };
  }

  // kiểm tra luồng hợp lệ trước khi kích hoạt: đúng 1 cổng bắt đầu + mọi kết quả đều đã nối
  function validateConfig(cfg) {
    if (!cfg || !Array.isArray(cfg.nodes) || !cfg.nodes.length) return { ok: false, msg: "Sơ đồ chưa có cổng nào." };
    const starts = cfg.nodes.filter(n => n.isStart);
    if (starts.length === 0) return { ok: false, msg: "Chưa chọn cổng bắt đầu." };
    if (starts.length > 1) return { ok: false, msg: "Có nhiều hơn một cổng bắt đầu — chỉ được một." };
    const keys = new Set(cfg.nodes.map(n => n.key));
    const problems = [];
    for (const n of cfg.nodes) {
      const g = (cfg.gates || []).find(x => x.id === n.gateId);
      if (!g) { problems.push("Một cổng chưa gán loại."); continue; }
      for (const o of g.outcomes) {
        const tgt = n.next && n.next[o.key];
        if (!tgt) problems.push(`“${g.name}” → kết quả “${o.label}” chưa nối đi đâu.`);
        else if (tgt !== "END" && !keys.has(tgt)) problems.push(`“${g.name}” → “${o.label}” nối tới cổng không tồn tại.`);
      }
    }
    if (problems.length) return { ok: false, msg: problems.slice(0, 6).join("\n") };
    return { ok: true, msg: "" };
  }

  /* ===================== Path Builder ===================== */
  function PathBuilder({ cfg, setCfg, props, folders }) {
    const innerRef = useRef(null);
    const drag = useRef(null);
    const [sel, setSel] = useState(cfg.nodes[0] ? cfg.nodes[0].key : null);
    const [conn, setConn] = useState(null);
    const [pan, setPan] = useState({ x: 132, y: 44 });   // chừa lề trái cho node Bắt đầu; pan tự do mọi hướng, không giới hạn
    const panRef = useRef(pan); panRef.current = pan;
    const [, force] = useState(0);

    const gateOf = (n) => cfg.gates.find(g => g.id === n.gateId) || cfg.gates[0];
    const nodeOf = (k) => cfg.nodes.find(n => n.key === k);
    // hằng số khớp ĐÚNG layout CSS (đo từ DOM): dải màu+header ~49px, mỗi hàng kết quả 30px,
    // tâm hàng đầu tiên ở y=70, tâm header (điểm vào) ở y=27.5
    const NODE_W = 232, ROW_H = 30, PORT_Y0 = 70, HEAD_MID = 27.5;
    const portXY = (n, oi) => ({ x: n.pos.x + NODE_W, y: n.pos.y + PORT_Y0 + oi * ROW_H });
    const inXY = (n) => ({ x: n.pos.x, y: n.pos.y + HEAD_MID });
    // toạ độ "world" (đã trừ pan) từ con trỏ
    const toWorld = (e) => { const r = innerRef.current.getBoundingClientRect(); return { x: e.clientX - r.left - panRef.current.x, y: e.clientY - r.top - panRef.current.y }; };

    const onMove = (e) => {
      if (!drag.current) return;
      if (drag.current.type === "pan") {
        setPan({ x: drag.current.px + (e.clientX - drag.current.mx), y: drag.current.py + (e.clientY - drag.current.my) });
        return;
      }
      const p = toWorld(e);
      if (drag.current.type === "move") {
        const n = nodeOf(drag.current.key);
        if (n) { n.pos = { x: p.x - drag.current.dx, y: p.y - drag.current.dy }; force(v => v + 1); }  // không kẹp min/max
      } else setConn({ fromKey: drag.current.key, outcome: drag.current.outcome, x: p.x, y: p.y });
    };
    const onUp = (e) => {
      if (drag.current && drag.current.type === "link") {
        const el = document.elementFromPoint(e.clientX, e.clientY);
        const nodeEl = el && el.closest(".ae2-node");
        const endEl = el && el.closest(".ae2-endzone");
        const target = endEl ? "END" : (nodeEl ? nodeEl.getAttribute("data-key") : null);
        if (target && target !== drag.current.key) { const n = nodeOf(drag.current.key); n.next = { ...n.next, [drag.current.outcome]: target }; setCfg({ ...cfg }); }
      }
      drag.current = null; setConn(null);
      window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp);
    };
    const startDrag = (d) => { drag.current = d; window.addEventListener("pointermove", onMove); window.addEventListener("pointerup", onUp); };
    const beginMove = (e, key) => { if (e.target.closest(".ae2-port") || e.target.closest(".ae2-out-tgt")) return; e.stopPropagation(); const n = nodeOf(key); const p = toWorld(e); startDrag({ type: "move", key, dx: p.x - n.pos.x, dy: p.y - n.pos.y }); setSel(key); e.preventDefault(); };
    const beginLink = (e, key, outcome) => { e.stopPropagation(); const p = toWorld(e); startDrag({ type: "link", key, outcome }); setConn({ fromKey: key, outcome, x: p.x, y: p.y }); };
    const clearLink = (key, outcome) => { const n = nodeOf(key); n.next = { ...n.next, [outcome]: null }; setCfg({ ...cfg }); };
    // pan: kéo chuột trái trên nền trống (không trúng node/port/endzone)
    const beginPan = (e) => { if (e.button !== 0 || e.target.closest(".ae2-node") || e.target.closest(".ae2-endzone")) return; startDrag({ type: "pan", px: pan.x, py: pan.y, mx: e.clientX, my: e.clientY }); };

    const edges = [];
    cfg.nodes.forEach(n => { const g = gateOf(n); g.outcomes.forEach((o, oi) => {
      const tgt = n.next && n.next[o.key]; if (!tgt) return;
      const s = portXY(n, oi); let e2;
      if (tgt === "END") e2 = { x: n.pos.x + NODE_W + 52, y: s.y };
      else { const tn = nodeOf(tgt); if (!tn) return; e2 = inXY(tn); }
      const mx = (s.x + e2.x) / 2;
      edges.push({ id: n.key + o.key, d: `M${s.x},${s.y} C${mx},${s.y} ${mx},${e2.y} ${e2.x},${e2.y}`, c: o.color, end: tgt === "END", ex: e2.x, ey: e2.y, sx: s.x, sy: s.y });
    }); });
    let temp = null;
    if (conn) { const fn = nodeOf(conn.fromKey); const g = gateOf(fn); const oi = g.outcomes.findIndex(o => o.key === conn.outcome); const s = portXY(fn, oi); const mx = (s.x + conn.x) / 2; temp = `M${s.x},${s.y} C${mx},${s.y} ${mx},${conn.y} ${conn.x},${conn.y}`; }

    // node BẮT ĐẦU hình tròn — thể hiện người thực thi khởi chạy luồng, nối vào cổng bắt đầu
    const startNode = cfg.nodes.find(n => n.isStart);
    const START_R = 28;
    const startPos = startNode ? { x: startNode.pos.x - 96, y: startNode.pos.y + HEAD_MID - START_R } : null;
    let startEdge = null;
    if (startNode) { const cx = startPos.x + 2 * START_R, cy = startPos.y + START_R; const e2 = inXY(startNode); const mx = (cx + e2.x) / 2; startEdge = `M${cx},${cy} C${mx},${cy} ${mx},${e2.y} ${e2.x},${e2.y}`; }

    const selNode = nodeOf(sel);
    const addNode = (gateId) => { const n = { key: uid(), gateId, isStart: false, pos: { x: 130, y: 360 }, next: {} }; setCfg({ ...cfg, nodes: [...cfg.nodes, n] }); setSel(n.key); };
    const delNode = (key) => { const nodes = cfg.nodes.filter(n => n.key !== key).map(n => { const next = { ...n.next }; Object.keys(next).forEach(k => { if (next[k] === key) next[k] = null; }); return { ...n, next }; }); setCfg({ ...cfg, nodes }); setSel(nodes[0] ? nodes[0].key : null); };

    return (
      <div className="ae2-wrap">
        <div className="ae2-canvas" ref={innerRef} onPointerDown={beginPan}>
          <div className="ae2-pan" style={{ transform: `translate(${pan.x}px, ${pan.y}px)` }}>
          <svg className="ae2-edges">
            {startEdge && <g><path d={startEdge} fill="none" stroke="var(--accent)" strokeWidth="2.4" /><circle cx={inXY(startNode).x} cy={inXY(startNode).y} r="4" fill="var(--accent)" /></g>}
            {edges.map(e => <g key={e.id}>
              <path d={e.d} fill="none" stroke={e.c} strokeWidth="2.4" />
              {e.end
                ? <g><rect x={e.ex} y={e.ey - 9} width="40" height="18" rx="9" fill={e.c} /><text x={e.ex + 20} y={e.ey + 0.5} fill="#fff" textAnchor="middle" dominantBaseline="central" style={{ fontSize: 9.5, fontWeight: 800 }}>END</text></g>
                : <circle cx={e.ex} cy={e.ey} r="4" fill={e.c} />}
            </g>)}
            {temp && <path d={temp} fill="none" stroke="var(--ink-4)" strokeWidth="2.2" strokeDasharray="5 5" />}
          </svg>
          {cfg.nodes.map(n => { const g = gateOf(n); const eff = g.effects || {}; return (
            <div key={n.key} className={"ae2-node" + (sel === n.key ? " sel" : "")} data-key={n.key} style={{ left: n.pos.x, top: n.pos.y, width: NODE_W, ["--g"]: g.color }} onPointerDown={(e) => beginMove(e, n.key)}>
              <div className="ae2-node-strip" />
              <div className="ae2-node-head">
                <span className="ae2-gate-ico"><Icon.shield size={13} /></span>
                <span className="ae2-node-name">{g.name}</span>
                {n.isStart && <span className="ae2-startbadge">BẮT ĐẦU</span>}
              </div>
              <div className="ae2-node-body">
                {g.outcomes.map((o) => { const ec = (eff[o.key] || []).length; const linked = !!(n.next && n.next[o.key]); return (
                  <div className={"ae2-out" + (linked ? " linked" : "")} key={o.key}>
                    <span className="ae2-out-dot" style={{ background: o.color }} /><span className="ae2-out-label">{o.label}</span>
                    {ec > 0 && <span className="ae2-out-eff" title={ec + " hành động"}><Icon.bolt size={9} />{ec}</span>}
                    {linked && <span className="ae2-out-tgt" onPointerDown={(e) => { e.stopPropagation(); clearLink(n.key, o.key); }} title="Bỏ nối">{n.next[o.key] === "END" ? "END ✕" : "✕"}</span>}
                    <span className="ae2-port" style={{ background: o.color }} title="Kéo để nối nhánh" onPointerDown={(e) => beginLink(e, n.key, o.key)} />
                  </div>
                ); })}
              </div>
            </div>
          ); })}
          {startNode && (
            <div className="ae2-startwrap" style={{ left: startPos.x, top: startPos.y }}>
              <div className="ae2-startnode" title="Người thực thi khởi chạy luồng phê duyệt"><Icon.send size={20} /></div>
              <div className="ae2-startlbl">Bắt đầu</div>
            </div>
          )}
          </div>
          <div className="ae2-endzone"><Icon.check size={15} /> KẾT THÚC · kéo nhánh vào đây</div>
        </div>
        <Inspector node={selNode} gate={selNode && gateOf(selNode)} gates={cfg.gates} props={props} folders={folders}
          onGate={(gid) => { selNode.gateId = gid; selNode.next = {}; setCfg({ ...cfg }); }}
          onStart={() => { cfg.nodes.forEach(n => n.isStart = (n.key === selNode.key)); setCfg({ ...cfg }); }}
          onEffects={(eff) => { const g = gateOf(selNode); g.effects = eff; setCfg({ ...cfg }); }}
          onDel={() => delNode(selNode.key)} addNode={addNode} />
      </div>
    );
  }

  function AddGateBtn({ gates, addNode }) {
    const [open, setOpen] = useState(false);
    return (
      <div className="ae2-addnode">
        <button className="btn sm" onClick={() => setOpen(o => !o)}><Icon.plus size={13} /> Thêm cổng vào sơ đồ</button>
        {open && <div className="ae2-addmenu">{gates.map(g => <div key={g.id} className="ae2-addmenu-item" onClick={() => { addNode(g.id); setOpen(false); }}><span className="ae2-dot" style={{ background: g.color }} /> {g.name}</div>)}</div>}
      </div>
    );
  }

  // [{v,label}] — v là giá trị lưu vào container.props (ID option), label để hiển thị
  const optlist = (p) => (p && p.options || []).map(o => (typeof o === "string"
    ? { v: o, label: o }
    : { v: (o.id || o.value || o.name || o.label || ""), label: (o.label || o.name || o.value || "") })).filter(o => o.v !== "");

  // dropdown chọn loại hành động khi thêm
  function EffAdd({ onPick }) {
    const [open, setOpen] = useState(false);
    const items = [
      { t: "prop", icon: "tag", label: "Ghi thuộc tính" },
      { t: "copy", icon: "copy", label: "Sao chép tệp (giữ liên kết)" },
      { t: "delete", icon: "trash", label: "Xoá tệp" },
    ];
    return (
      <div className="ae2-effadd">
        <button className="ae2-addeff" onClick={() => setOpen(o => !o)}><Icon.plus size={12} /> Thêm hành động</button>
        {open && <div className="ae2-effadd-menu">{items.map(it => { const Ico = Icon[it.icon]; return (
          <div key={it.t} className={"ae2-effadd-item" + (it.t === "delete" ? " danger" : "")} onClick={() => { onPick(it.t); setOpen(false); }}><Ico size={13} /> {it.label}</div>
        ); })}</div>}
      </div>
    );
  }

  // 1 dòng hành động — render theo loại (prop / copy / delete)
  function EffRow({ ef, props, folders, propById, onChange, onDel }) {
    const t = ef.type || "prop";
    if (t === "delete") return (
      <div className="ae2-eff-row ae2-eff-danger">
        <span className="ae2-eff-ico danger"><Icon.trash size={13} /></span>
        <span className="ae2-eff-txt">Xoá tệp khỏi thư mục</span>
        <button className="ae2-x" onClick={onDel}>✕</button>
      </div>
    );
    if (t === "copy") return (
      <div className="ae2-eff-row ae2-eff-copy">
        <span className="ae2-eff-ico"><Icon.copy size={13} /></span>
        <span className="ae2-eff-txt">Sao chép tệp (giữ liên kết tới bản gốc)</span>
        <span className="ae2-eff-keep" title="Bản sao luôn giữ liên kết tới tài liệu gốc"><Icon.link size={11} /> giữ liên kết</span>
        <button className="ae2-x" onClick={onDel}>✕</button>
      </div>
    );
    const p = propById(ef.prop); const opts = optlist(p);
    return (
      <div className="ae2-eff-row">
        <span className="ae2-eff-ico"><Icon.tag size={12} /></span>
        <select className="ae2-sel sm" value={ef.prop} onChange={(e) => { const np = propById(e.target.value); onChange({ prop: e.target.value, value: (optlist(np)[0] || {}).v || "" }); }}>{props.map(pp => <option key={pp.id} value={pp.id}>{pp.name}</option>)}</select>
        <span className="ae2-arrow">=</span>
        {opts.length ? <select className="ae2-sel sm" value={ef.value} onChange={(e) => onChange({ value: e.target.value })}>{opts.map(o => <option key={o.v} value={o.v}>{o.label}</option>)}</select>
          : <input className="ae2-inp" value={ef.value} onChange={(e) => onChange({ value: e.target.value })} placeholder="giá trị" />}
        <button className="ae2-x" onClick={onDel}>✕</button>
      </div>
    );
  }

  function Inspector({ node, gate, gates, props, folders, onGate, onStart, onEffects, onDel, addNode }) {
    if (!node) return <aside className="ae2-insp"><div className="ae2-insp-empty"><Icon.cursor size={22} /><div>Chọn một cổng để cấu hình.</div></div><AddGateBtn gates={gates} addNode={addNode} /></aside>;
    const eff = gate.effects || {};
    const propById = (id) => props.find(p => p.id === id);
    const blank = (t) => {
      if (t === "copy") return { type: "copy", toKey: "", to: "", keepLink: true };
      if (t === "delete") return { type: "delete" };
      const p = props[0]; return { type: "prop", prop: p ? p.id : "", value: p ? ((optlist(p)[0] || {}).v || "") : "" };
    };
    const addEffect = (ok, t) => { if (t === "prop" && !props.length) { alert("Chưa có thuộc tính. Tạo ở Quản trị → Thuộc tính."); return; } onEffects({ ...eff, [ok]: [...(eff[ok] || []), blank(t)] }); };
    const setEffect = (ok, i, patch) => onEffects({ ...eff, [ok]: (eff[ok] || []).map((e, j) => j === i ? { ...e, ...patch } : e) });
    const delEffect = (ok, i) => onEffects({ ...eff, [ok]: (eff[ok] || []).filter((_, j) => j !== i) });
    return (
      <aside className="ae2-insp">
        <div className="ae2-insp-head">
          <span className="ae2-dot" style={{ background: gate.color }} />
          <select className="ae2-sel" value={node.gateId} onChange={(e) => onGate(e.target.value)}>{gates.map(g => <option key={g.id} value={g.id}>{g.name}</option>)}</select>
          <button className="icon-btn ae2-trash" title="Xoá cổng khỏi sơ đồ" onClick={onDel}><Icon.trash size={15} /></button>
        </div>
        <label className="ae2-check"><input type="checkbox" checked={!!node.isStart} onChange={onStart} /> Cổng bắt đầu</label>
        <div className="ae2-insp-lbl">Hành động khi đóng cổng (theo từng kết quả)</div>
        <div className="ae2-insp-sub">Mỗi kết quả có thể <b>ghi thuộc tính</b> (lấy từ Quản trị → Thuộc tính), <b>sao chép tệp</b> (bản sao giữ liên kết tới bản gốc) hoặc <b>xoá tệp</b>.</div>
        {gate.outcomes.map(o => (
          <div className="ae2-eff-group" key={o.key}>
            <div className="ae2-eff-head"><span className="ae2-out-dot" style={{ background: o.color }} /> {o.label}</div>
            {(eff[o.key] || []).map((ef, i) => (
              <EffRow key={i} ef={ef} props={props} folders={folders} propById={propById}
                onChange={(patch) => setEffect(o.key, i, patch)} onDel={() => delEffect(o.key, i)} />
            ))}
            <EffAdd onPick={(t) => addEffect(o.key, t)} />
          </div>
        ))}
        <AddGateBtn gates={gates} addNode={addNode} />
      </aside>
    );
  }

  /* ===================== Gate Library ===================== */
  function GateLibrary({ cfg, setCfg, roles }) {
    const setGate = (id, patch) => setCfg({ ...cfg, gates: cfg.gates.map(g => g.id === id ? { ...g, ...patch } : g) });
    const addGate = () => setCfg({ ...cfg, gates: [...cfg.gates, { id: "g_" + uid(), name: "Cổng mới", color: "#0d9488", outcomes: [{ key: "approve", label: "Duyệt", color: "#10b981" }, { key: "reject", label: "Từ chối", color: "#ef4444" }], effects: {} }] });
    const delGate = (id) => { if (cfg.nodes.some(n => n.gateId === id)) { alert("Cổng đang dùng trong sơ đồ — gỡ khỏi sơ đồ trước."); return; } setCfg({ ...cfg, gates: cfg.gates.filter(g => g.id !== id) }); };
    const addOutcome = (g) => setGate(g.id, { outcomes: [...g.outcomes, { key: "o" + uid(), label: "Kết quả", color: OUT_COLORS[g.outcomes.length % OUT_COLORS.length] }] });
    const setOutcome = (g, i, patch) => setGate(g.id, { outcomes: g.outcomes.map((o, j) => j === i ? { ...o, ...patch } : o) });
    const delOutcome = (g, i) => setGate(g.id, { outcomes: g.outcomes.filter((_, j) => j !== i) });
    return (
      <div className="content" style={{ overflowY: "auto" }}>
        <div className="ae2-lib-head">
          <div><div className="card-title">Thư viện cổng</div><div className="card-sub">Mỗi cổng tự định nghĩa các <b>kết quả (outcome)</b> — không chỉ Duyệt/Từ chối.</div></div>
          <button className="btn sm primary" onClick={addGate}><Icon.plus size={14} /> Thêm cổng</button>
        </div>
        <div className="ae2-lib-grid">
          {cfg.gates.map(g => (
            <div className="card card-pad ae2-lib-card" key={g.id}>
              <div className="ae2-lib-card-head">
                <input type="color" className="ae2-color" value={g.color} onChange={(e) => setGate(g.id, { color: e.target.value })} />
                <input className="ae2-inp grow" value={g.name} onChange={(e) => setGate(g.id, { name: e.target.value })} />
                <button className="icon-btn ae2-trash" onClick={() => delGate(g.id)} title="Xoá cổng"><Icon.trash size={14} /></button>
              </div>
              <div className="ae2-lib-out-lbl">Kết quả ({g.outcomes.length})</div>
              {g.outcomes.map((o, i) => (
                <div className="ae2-lib-out" key={i}>
                  <input type="color" className="ae2-color sm" value={o.color} onChange={(e) => setOutcome(g, i, { color: e.target.value })} />
                  <input className="ae2-inp grow" value={o.label} onChange={(e) => setOutcome(g, i, { label: e.target.value })} />
                  <button className="ae2-x" onClick={() => delOutcome(g, i)}>✕</button>
                </div>
              ))}
              <button className="ae2-addeff" onClick={() => addOutcome(g)}><Icon.plus size={12} /> Thêm kết quả</button>

              <div className="ae2-lib-out-lbl" style={{ marginTop: 14 }}>Vai trò được phép duyệt cổng</div>
              {!(roles || []).length
                ? <div className="ae2-noprop">Chưa có vai trò. <a onClick={() => window.cdeSetRoute && window.cdeSetRoute("roles")}>Tạo ở Quản trị → Vai trò →</a></div>
                : <div className="rl-perms">
                    {(roles || []).map(r => { const on = ((g.roles || []).includes(r.id)); return (
                      <button key={r.id} className={"rl-perm" + (on ? " on" : "")} onClick={() => { const cur = g.roles || []; const next = on ? cur.filter(x => x !== r.id) : [...cur, r.id]; setGate(g.id, { roles: next }); }}>
                        {on && <Icon.check size={11} />} {r.name}
                      </button>
                    ); })}
                  </div>}
              <div className="ae2-insp-sub" style={{ marginTop: 6 }}>Bỏ trống = mặc định editor/admin được duyệt.</div>
            </div>
          ))}
        </div>
      </div>
    );
  }

  /* ===================== Shell ===================== */
  function WorkflowList({ list, onOpen, onCreate, onActivate, onDelete }) {
    const [name, setName] = useState("");
    return (
      <div className="content" style={{ overflowY: "auto" }}>
        <div className="ae2-lib-head">
          <div><div className="card-title">Luồng phê duyệt</div><div className="card-sub">Danh sách quy trình duyệt của dự án — chọn để thiết kế, hoặc kích hoạt luồng áp dụng.</div></div>
          <div style={{ display: "flex", gap: 8 }}>
            <input className="ae2-inp" style={{ width: 200, height: 38 }} placeholder="Tên luồng mới…" value={name} onChange={(e) => setName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && name.trim()) { onCreate(name.trim()); setName(""); } }} />
            <button className="btn sm primary" onClick={() => { onCreate(name.trim() || "Luồng mới"); setName(""); }}><Icon.plus size={14} /> Tạo luồng</button>
          </div>
        </div>
        {!list.length && <div className="ae2-wf-empty"><Icon.flow size={28} /><div>Chưa có luồng phê duyệt. Tạo luồng đầu tiên.</div></div>}
        <div className="ae2-wf-list">
          {list.map(w => {
            const n = (w.config && w.config.nodes && w.config.nodes.length) || 0, gt = (w.config && w.config.gates && w.config.gates.length) || 0;
            return (
              <div className="card card-pad ae2-wf-card" key={w.id}>
                <div className="ae2-wf-ico"><Icon.flow size={18} /></div>
                <div className="ae2-wf-info" onClick={() => onOpen(w)}>
                  <div className="ae2-wf-name">{w.name} {w.active && <span className="chip ok" style={{ height: 20, fontSize: 10.5 }}>Đang dùng</span>}</div>
                  <div className="ae2-wf-sub">{n} cổng · {gt} loại cổng · v{w.version}</div>
                </div>
                <button className="btn sm" disabled={w.active} onClick={() => onActivate(w.id)}>{w.active ? "Đang dùng" : "Kích hoạt"}</button>
                <button className="btn sm" onClick={() => onOpen(w)}><Icon.pen size={13} /> Sửa</button>
                <button className="icon-btn ae2-trash" onClick={() => onDelete(w)} title="Xoá"><Icon.trash size={14} /></button>
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  function ApprovalEngine() {
    const [list, setList] = useState([]);
    const [props, setProps] = useState([]);
    const [folders, setFolders] = useState([]);
    const [roles, setRoles] = useState([]);
    const [cur, setCur] = useState(null);
    const [cfg, setCfg] = useState(null);
    const [tab, setTab] = useState("path");
    const [saving, setSaving] = useState(false);

    const reload = () => (window.cdeApproval ? window.cdeApproval.list().then(l => setList(l || [])) : Promise.resolve());
    useEffect(() => { reload();
      if (window.cdeData && window.cdeData.docProperties) window.cdeData.docProperties().then(p => setProps(p || [])).catch(() => {});
      if (window.cdeData && window.cdeData.folders) window.cdeData.folders().then(f => setFolders(f || [])).catch(() => {});
      if (window.cdeRoles) window.cdeRoles.list().then(r => setRoles(r || [])).catch(() => {});
    }, []);

    const open = async (w) => {
      let c = null; try { const full = await window.cdeApproval.get(w.id); c = full && full.config; } catch (e) {}
      if (!c || !Array.isArray(c.gates)) c = { gates: defaultConfig().gates, nodes: [] };
      setCur({ id: w.id }); setCfg({ ...c, name: w.name }); setTab("path");
    };
    const create = async (name) => { try { const w = await window.cdeApproval.create(name); await reload(); open(w); } catch (e) { alert("Tạo thất bại: " + e.message); } };
    const activate = async (id) => {
      const w = list.find(x => x.id === id); const v = validateConfig(w && w.config);
      if (!v.ok) { alert("Chưa thể kích hoạt luồng này:\n\n" + v.msg); return; }
      try { await window.cdeApproval.setActive(id); reload(); } catch (e) { alert(e.message); }
    };
    const del = async (w) => { if (!window.confirm('Xoá luồng "' + w.name + '"?')) return; try { await window.cdeApproval.remove(w.id); reload(); } catch (e) { alert(e.message); } };
    const save = async (alsoActivate) => {
      if (alsoActivate) { const v = validateConfig(cfg); if (!v.ok) { alert("Chưa thể kích hoạt luồng này:\n\n" + v.msg); return; } }
      setSaving(true);
      try { await window.cdeApproval.save(cur.id, cfg.name, cfg); if (alsoActivate) await window.cdeApproval.setActive(cur.id); await reload(); alert(alsoActivate ? "Đã lưu & kích hoạt." : "Đã lưu."); }
      catch (e) { alert("Lưu thất bại: " + (e.message || e)); } finally { setSaving(false); }
    };

    if (!cur) return <div className="approval ae2-screen"><div className="ae2-body"><WorkflowList list={list} onOpen={open} onCreate={create} onActivate={activate} onDelete={del} /></div></div>;

    const NAV = [{ id: "path", label: "Con đường", icon: "flow" }, { id: "gates", label: "Thư viện cổng", icon: "shield" }];
    return (
      <div className="approval ae2-screen">
        <div className="ae2-top">
          <button className="btn sm" onClick={() => { setCur(null); reload(); }}><Icon.chevL size={14} /> Danh sách</button>
          <div className="ae2-tabs">{NAV.map(n => { const Ico = Icon[n.icon]; return <button key={n.id} className={"ae2-tab " + (tab === n.id ? "active" : "")} onClick={() => setTab(n.id)}><Ico size={16} /> {n.label}</button>; })}</div>
          <input className="ae2-wfname" value={cfg.name} onChange={(e) => setCfg({ ...cfg, name: e.target.value })} />
          <button className="btn sm" disabled={saving} onClick={() => save(false)}>{saving ? "…" : "Lưu"}</button>
          <button className="btn sm primary ae2-activate" disabled={saving} onClick={() => save(true)}><Icon.bolt size={14} /> Kích hoạt</button>
        </div>
        <div className="ae2-body">{tab === "gates" ? <GateLibrary cfg={cfg} setCfg={setCfg} roles={roles} /> : <PathBuilder cfg={cfg} setCfg={setCfg} props={props} folders={folders} />}</div>
      </div>
    );
  }

  window.Approvals = ApprovalEngine;
})();
