/* rfi.jsx — Module RFI (ráp từ thiết kế "RFI Module", gộp 1 IIFE, scope CSS .rfi) */
(function () {

/* ===== file9.js ===== */
/* ============================================================
   Icon set — simple stroke icons (Lucide-style)
   ============================================================ */
const I = ({ d, size = 18, sw = 1.8, fill = "none", children, vb = 24 }) => (
  <svg width={size} height={size} viewBox={`0 0 ${vb} ${vb}`} fill={fill}
    stroke="currentColor" strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">
    {d ? <path d={d} /> : children}
  </svg>
);

const Icon = {
  grid:   (p) => <I {...p}><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></I>,
  layers: (p) => <I {...p}><path d="M12 3 3 8l9 5 9-5-9-5Z"/><path d="m3 13 9 5 9-5"/></I>,
  cube:   (p) => <I {...p}><path d="M21 8.5 12 13 3 8.5 12 4l9 4.5Z"/><path d="M3 8.5v7L12 20l9-4.5v-7"/><path d="M12 13v7"/></I>,
  coverage:(p)=> <I {...p}><path d="M3 12h4l2-7 4 14 2-7h6"/></I>,
  users:  (p) => <I {...p}><circle cx="9" cy="8" r="3.2"/><path d="M3.5 19a5.5 5.5 0 0 1 11 0"/><path d="M16 5.2a3.2 3.2 0 0 1 0 5.6"/><path d="M17.5 19a5.5 5.5 0 0 0-2.3-4.5"/></I>,
  alert:  (p) => <I {...p}><path d="M10.3 4.3 2.6 17.5A2 2 0 0 0 4.3 20.5h15.4a2 2 0 0 0 1.7-3L13.7 4.3a2 2 0 0 0-3.4 0Z"/><path d="M12 9v4"/><path d="M12 16.5h.01"/></I>,
  doc:    (p) => <I {...p}><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z"/><path d="M14 3v5h5"/><path d="M9 13h6M9 17h6"/></I>,
  flow:   (p) => <I {...p}><rect x="3" y="4" width="7" height="5" rx="1.5"/><rect x="14" y="15" width="7" height="5" rx="1.5"/><path d="M6.5 9v4.5a2 2 0 0 0 2 2H14"/></I>,
  calendar:(p)=> <I {...p}><rect x="3" y="4.5" width="18" height="16" rx="2.5"/><path d="M3 9h18M8 3v3M16 3v3"/></I>,
  chart:  (p) => <I {...p}><path d="M4 20V10M10 20V4M16 20v-7M22 20H2" /></I>,
  sync:   (p) => <I {...p}><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 21v-5h5"/></I>,
  bolt:   (p) => <I {...p} fill="currentColor" stroke="none"><path d="M13 2 4.5 13.5H11l-1 8.5L19.5 10H13l0-8Z"/></I>,
  search: (p) => <I {...p}><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></I>,
  bell:   (p) => <I {...p}><path d="M18 9a6 6 0 0 0-12 0c0 6-2.5 7-2.5 7h17S18 15 18 9Z"/><path d="M10.5 20a2 2 0 0 0 3 0"/></I>,
  gear:   (p) => <I {...p}><circle cx="12" cy="12" r="3"/><path d="M19.4 13.5a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-2.9 1.2V21a2 2 0 1 1-4 0v-.1A1.7 1.7 0 0 0 6.8 19.2a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0-1.2-2.9H2.8a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.2 6.8a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3 1.7 1.7 0 0 0 1-1.5V2.8a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 2.9 1.2 2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/></I>,
  sun:    (p) => <I {...p}><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></I>,
  moon:   (p) => <I {...p}><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"/></I>,
  check:  (p) => <I {...p}><path d="m4 12.5 5 5 11-11"/></I>,
  checkCircle:(p)=> <I {...p}><circle cx="12" cy="12" r="9"/><path d="m8.5 12 2.5 2.5 4.5-5"/></I>,
  clock:  (p) => <I {...p}><circle cx="12" cy="12" r="9"/><path d="M12 7.5V12l3 2"/></I>,
  plus:   (p) => <I {...p}><path d="M12 5v14M5 12h14"/></I>,
  chevD:  (p) => <I {...p}><path d="m6 9 6 6 6-6"/></I>,
  chevR:  (p) => <I {...p}><path d="m9 6 6 6-6 6"/></I>,
  chevUD: (p) => <I {...p}><path d="m8 9 4-4 4 4M8 15l4 4 4-4"/></I>,
  filter: (p) => <I {...p}><path d="M3 5h18l-7 8v6l-4-2v-4L3 5Z"/></I>,
  download:(p)=> <I {...p}><path d="M12 3v12m0 0 4-4m-4 4-4-4M4 21h16"/></I>,
  eye:    (p) => <I {...p}><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="2.8"/></I>,
  eyeOff: (p) => <I {...p}><path d="M10.7 6.2A9.7 9.7 0 0 1 12 6c6.5 0 10 6 10 6a16 16 0 0 1-2.4 3M6.5 7.5A15.8 15.8 0 0 0 2 12s3.5 6 10 6a9.4 9.4 0 0 0 4-.9M3 3l18 18M9.9 9.9a3 3 0 0 0 4.2 4.2"/></I>,
  ruler:  (p) => <I {...p}><path d="m15.5 2.5 6 6L8.5 21.5l-6-6L15.5 2.5Z"/><path d="m7 11 1.5 1.5M10 8l1.5 1.5M13 5l1.5 1.5M4 14l1.5 1.5"/></I>,
  scissors:(p)=> <I {...p}><circle cx="6" cy="6" r="2.6"/><circle cx="6" cy="18" r="2.6"/><path d="M8.2 7.6 20 18M8.2 16.4 20 6"/></I>,
  cursor: (p) => <I {...p}><path d="m4 3 7 17 2.5-6.5L20 11 4 3Z"/></I>,
  home3:  (p) => <I {...p}><path d="m3 10 9-7 9 7"/><path d="M5 9v11h14V9"/><path d="M10 20v-6h4v6"/></I>,
  maximize:(p)=> <I {...p}><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></I>,
  hash:   (p) => <I {...p}><path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18"/></I>,
  link:   (p) => <I {...p}><path d="M9 15 15 9M10.5 6.5 12 5a4 4 0 0 1 6 6l-1.5 1.5M13.5 17.5 12 19a4 4 0 0 1-6-6l1.5-1.5"/></I>,
  pin:    (p) => <I {...p}><path d="M12 21s7-6.2 7-11a7 7 0 1 0-14 0c0 4.8 7 11 7 11Z"/><circle cx="12" cy="10" r="2.5"/></I>,
  box:    (p) => <I {...p}><path d="M3 7.5 12 3l9 4.5v9L12 21l-9-4.5v-9Z"/><path d="M3 7.5 12 12l9-4.5M12 12v9"/></I>,
  upload: (p) => <I {...p}><path d="M12 16V4m0 0 4 4m-4-4-4 4M4 20h16"/></I>,
  dots:   (p) => <I {...p}><circle cx="5" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none"/><circle cx="19" cy="12" r="1.4" fill="currentColor" stroke="none"/></I>,
  arrowUp:(p)=> <I {...p}><path d="M12 19V5m0 0-6 6m6-6 6 6"/></I>,
  arrowDn:(p)=> <I {...p}><path d="M12 5v14m0 0 6-6m-6 6-6-6"/></I>,
  shield: (p) => <I {...p}><path d="M12 3 5 6v5c0 4.5 3 7.6 7 9 4-1.4 7-4.5 7-9V6l-7-3Z"/><path d="m9 12 2 2 4-4"/></I>,
  send:   (p) => <I {...p}><path d="M21 3 10.5 13.5M21 3l-6.6 18-3.9-8.1L2.4 9 21 3Z"/></I>,
  flag:   (p) => <I {...p}><path d="M5 21V4M5 4h11l-1.5 3.5L16 11H5"/></I>,
  chevL:  (p) => <I {...p}><path d="m15 6-6 6 6 6"/></I>,
  reply:  (p) => <I {...p}><path d="M9 17H7a4 4 0 0 1 0-8h12M9 13l-4-4 4-4"/></I>,
  message:(p) => <I {...p}><path d="M21 11.5a8.5 8.5 0 0 1-11.8 7.8L3 21l1.7-6.2A8.5 8.5 0 1 1 21 11.5Z"/></I>,
  ban:    (p) => <I {...p}><circle cx="12" cy="12" r="9"/><path d="m5.6 5.6 12.8 12.8"/></I>,
  user:   (p) => <I {...p}><circle cx="12" cy="8" r="4"/><path d="M4.5 20a7.5 7.5 0 0 1 15 0"/></I>,
  money:  (p) => <I {...p}><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9.5 9.2a2.2 2 0 0 1 2.5-1.2c1.4 0 2.3.8 2.3 1.8 0 2.4-4.8 1.4-4.8 3.8 0 1 1 1.8 2.5 1.8a2.3 2 0 0 0 2.5-1.2"/></I>,
};


/* ===== file7.js ===== */
/* ============================================================
   RFI standalone — data model (RFI + Issues + viewpoints + SLA)
   ============================================================ */

/* Disciplines — nguồn động từ sys-fields.jsx (admin sửa được) */
const DISC_NAME = window.DISC_NAME || (window.DISC_NAME = {});
const DISC_COL  = window.DISC_COL  || (window.DISC_COL  = {});

/* People */
const RFI_PEOPLE = {
  duc:  { name: "Trần Đức",    role: "RFI Manager",   company: "Ventus BIM",  disc: "pm",  init: "TĐ" },
  minh: { name: "Nguyễn Minh", role: "KS Hạ tầng",    company: "Ventus BIM",  disc: "inf", init: "NM" },
  hong: { name: "Lê Hồng",     role: "KS Kết cấu",    company: "Ventus BIM",  disc: "str", init: "LH" },
  lan:  { name: "Vũ Lan",      role: "Kiến trúc sư",  company: "Ventus BIM",  disc: "arc", init: "VL" },
  an:   { name: "Bùi An",      role: "KS MEP",        company: "Ventus BIM",  disc: "mep", init: "BA" },
  tuan: { name: "Phạm Tuấn",   role: "Quản lý dự án", company: "Ban QLDA",    disc: "pm",  init: "PT" },
  hai:  { name: "Đỗ Hải",      role: "Tư vấn GS",     company: "TVGS ABC",    disc: "inf", init: "ĐH" },
  son:  { name: "Đặng V. Sơn", role: "GĐ thiết kế",   company: "Tư vấn TKXD", disc: "pm",  init: "ĐS" },
};

/* Lifecycle */
const RFI_STATUS = {
  draft:     { label: "Bản nháp",  cls: "idle",   bic: "Người tạo",   flowIdx: 0 },
  submitted: { label: "Đã gửi",    cls: "str",    bic: "RFI Manager", flowIdx: 1 },
  open:      { label: "Đang xử lý", cls: "warn",   bic: "Reviewer",    flowIdx: 2 },
  answered:  { label: "Đã trả lời", cls: "inf",    bic: "RFI Manager", flowIdx: 3 },
  closed:    { label: "Đã đóng",    cls: "ok",     bic: "—",           flowIdx: 4 },
  rejected:  { label: "Bị từ chối", cls: "danger", bic: "Người tạo",   flowIdx: 2 },
  void:      { label: "Đã huỷ",     cls: "idle",   bic: "—",           flowIdx: -1 },
  reopened:  { label: "Mở lại",     cls: "warn",   bic: "Reviewer",    flowIdx: 2 },
};
const RFI_FLOW = [
  { k: "draft",     label: "Nháp",      ic: "doc" },
  { k: "submitted", label: "Đã gửi",    ic: "send" },
  { k: "open",      label: "Đang xử lý", ic: "clock" },
  { k: "answered",  label: "Đã trả lời", ic: "reply" },
  { k: "closed",    label: "Đã đóng",   ic: "check" },
];
const RFI_TYPE = { clarify: "Làm rõ thiết kế", coord: "Phối hợp (Coordination)", site: "Điều kiện hiện trường", spec: "Sai khác Spec", subst: "Thay thế vật liệu" };
const RFI_PRIO = {
  urgent: { label: "Khẩn cấp", c: "var(--danger)", cls: "danger", sla: 3 },
  high:   { label: "Cao",      c: "var(--mep)",    cls: "warn",   sla: 5 },
  normal: { label: "Bình thường", c: "var(--str)", cls: "str",   sla: 7 },
  low:    { label: "Thấp",     c: "var(--ink-3)",  cls: "idle",   sla: 10 },
};

/* SLA computation (default by priority; ISO/AIA practice 7 working days) */
function slaInfo(rfi) {
  const resolved = ["answered", "closed", "void"].includes(rfi.status);
  const total = rfi.slaDays || RFI_PRIO[rfi.prio].sla;
  const used = rfi.daysOpen;
  const pct = Math.max(0, Math.min(1, used / total));
  let state = "ok";
  if (!resolved) { if (used > total) state = "overdue"; else if (used / total >= 0.75) state = "warn"; }
  return { resolved, total, used, pct, over: used - total, state };
}

/* RFIs */
const RFIS = [
  {
    id: "rfi42", num: "RFI-0042", subject: "Xung đột ống HVAC với dầm bê tông trục B",
    disc: "mep", type: "coord", prio: "high", status: "open", daysOpen: 11,
    question: "Tại trục B cao độ +18.40m, tuyến ống gió HVAC chính (ø630) giao cắt với dầm bê tông B-04 (cao 700mm). Khoảng thông thuỷ còn lại không đủ cho lớp bảo ôn. Đề nghị làm rõ phương án xử lý: hạ trần kỹ thuật hay định tuyến lại ống gió?",
    proposed: "Đề xuất định tuyến ống gió vòng qua mặt dưới dầm, bù lại bằng cách giảm tiết diện cục bộ ø630→ø500 trong đoạn 4m và tăng tốc độ gió trong giới hạn cho phép.",
    drawing: "MEP-HVAC-B2 · Sheet M-204", spec: "Mục 23 31 00 — Ống gió", suit: "S3",
    cost: { on: true, amount: "~85 triệu" }, schedule: { on: true, days: 5 },
    originator: "an", manager: "duc", reviewers: ["hong", "lan"], bic: "hong",
    created: "03/06/2026", due: "08/06/2026",
    linkedIssue: "iss42",
    viewpoint: { element: "Dầm B-04 ↔ Ống gió ø630", ifc: "1xS3hKp00B04zQv9mE", rot: { x: -26, y: -40 }, disc: "mep" },
    refs: [{ t: "model", label: "STR-Frame-Station-C", ifc: "1xS3...B04" }, { t: "drawing", label: "M-204" }, { t: "issue", label: "ISS-042", id: "iss42" }],
    responses: [{ author: "hong", company: "Ventus BIM", date: "06/06 14:20", official: false, body: "Dầm B-04 là dầm chính chịu lực, không thể tạo lỗ xuyên ø630. Phương án định tuyến vòng là hợp lý nhưng cần kiểm tra lại tổn thất áp suất." }],
    activity: [
      { actor: "an", action: "create", detail: "Tạo RFI từ va chạm ISS-042", time: "03/06 09:10" },
      { actor: "an", action: "submit", detail: "Gửi tới RFI Manager", time: "03/06 09:12" },
      { actor: "duc", action: "open", detail: "Mở & gán cho Lê Hồng, Vũ Lan", time: "03/06 11:30" },
      { actor: "hong", action: "comment", detail: "Thêm phản hồi kỹ thuật", time: "06/06 14:20" },
    ],
  },
  {
    id: "rfi41", num: "RFI-0041", subject: "Làm rõ cao độ hoàn thiện ke ga tầng L3",
    disc: "inf", type: "clarify", prio: "normal", status: "answered", daysOpen: 3,
    question: "Bản vẽ kiến trúc và bản vẽ hạ tầng thể hiện cao độ hoàn thiện ke ga khác nhau (+18.40 vs +18.45). Đề nghị xác nhận cao độ chuẩn để thi công lớp lát.",
    proposed: "Lấy theo bản vẽ kiến trúc +18.45m (đã tính lớp lát granite 30mm).",
    drawing: "INF-Drainage-L3 · Sheet C-110", spec: "Mục 03 30 00", suit: "S2",
    cost: { on: false }, schedule: { on: false }, originator: "minh", manager: "duc",
    reviewers: ["lan"], bic: "duc", created: "01/06/2026", due: "06/06/2026",
    refs: [{ t: "drawing", label: "C-110" }, { t: "drawing", label: "A-220" }],
    responses: [{ author: "lan", company: "Ventus BIM", date: "04/06 10:00", official: true, body: "Xác nhận cao độ hoàn thiện chuẩn là +18.45m theo bản vẽ kiến trúc A-220. Bản vẽ hạ tầng sẽ được cập nhật ở phiên bản kế tiếp." }],
    activity: [
      { actor: "minh", action: "create", detail: "Tạo RFI", time: "01/06 09:00" },
      { actor: "minh", action: "submit", detail: "Gửi tới Manager", time: "01/06 09:05" },
      { actor: "duc", action: "open", detail: "Gán cho Vũ Lan", time: "01/06 14:00" },
      { actor: "lan", action: "answer", detail: "Trả lời chính thức", time: "04/06 10:00" },
    ],
  },
  {
    id: "rfi40", num: "RFI-0040", subject: "Thiếu chi tiết neo thép nút cột-dầm C-12",
    disc: "str", type: "clarify", prio: "high", status: "open", daysOpen: 9,
    question: "Hồ sơ thiếu chi tiết cấu tạo neo thép tại nút giao cột C-12 và dầm ngang. Đề nghị bổ sung bản vẽ chi tiết neo.",
    proposed: "", drawing: "STR-Beam-Detail-B04", spec: "Mục 03 20 00", suit: "S3",
    cost: { on: false }, schedule: { on: true, days: 3 }, originator: "tuan", manager: "duc",
    reviewers: ["hong"], bic: "hong", created: "05/06/2026", due: "09/06/2026",
    linkedIssue: "iss41",
    viewpoint: { element: "Nút cột C-12 / dầm ngang", ifc: "0aZqL77x0C12mNp4", rot: { x: -22, y: -34 }, disc: "str" },
    refs: [{ t: "model", label: "STR-Column-C12", ifc: "0aZ...C12" }, { t: "issue", label: "ISS-041", id: "iss41" }],
    responses: [],
    activity: [
      { actor: "tuan", action: "create", detail: "Tạo RFI", time: "05/06 09:10" },
      { actor: "tuan", action: "submit", detail: "Gửi tới Manager", time: "05/06 09:11" },
      { actor: "duc", action: "open", detail: "Gán cho Lê Hồng", time: "05/06 10:00" },
    ],
  },
  {
    id: "rfi39", num: "RFI-0039", subject: "Đề xuất thay thế vật liệu ốp mặt đứng ga C",
    disc: "arc", type: "subst", prio: "normal", status: "submitted", daysOpen: 6,
    question: "Nhà thầu đề xuất thay tấm ốp GRC bằng tấm nhôm composite chống cháy do thời gian cung ứng GRC kéo dài. Đề nghị duyệt mẫu thay thế.",
    proposed: "Dùng tấm Alu A2 fire-rated, màu & vân theo mẫu đính kèm, tương đương thẩm mỹ.",
    drawing: "ARC-Facade-GaC", spec: "Mục 07 42 00", suit: "S4",
    cost: { on: true, amount: "−120 triệu" }, schedule: { on: true, days: -10 }, originator: "lan", manager: "duc",
    reviewers: [], bic: "duc", created: "06/06/2026", due: "11/06/2026",
    refs: [{ t: "document", label: "Mẫu vật liệu.pdf" }], responses: [],
    activity: [
      { actor: "lan", action: "create", detail: "Tạo RFI", time: "06/06 15:00" },
      { actor: "lan", action: "submit", detail: "Gửi tới Manager", time: "06/06 15:05" },
    ],
  },
  {
    id: "rfi38", num: "RFI-0038", subject: "Điều kiện địa chất khác hồ sơ tại hố ga D",
    disc: "inf", type: "site", prio: "urgent", status: "open", daysOpen: 7,
    question: "Khi đào hố ga D phát hiện lớp đất yếu dày hơn 2.5m so với hồ sơ khảo sát. Đề nghị phương án xử lý nền móng khẩn.",
    proposed: "", drawing: "INF-Foundation-D", spec: "Mục 31 00 00", suit: "S3",
    cost: { on: true, amount: "~340 triệu" }, schedule: { on: true, days: 12 }, originator: "hai", manager: "duc",
    reviewers: ["hong", "minh"], bic: "hai", created: "07/06/2026", due: "09/06/2026",
    refs: [{ t: "document", label: "Báo cáo địa chất.pdf" }, { t: "drawing", label: "C-310" }], responses: [],
    activity: [
      { actor: "hai", action: "create", detail: "Tạo RFI khẩn", time: "07/06 08:00" },
      { actor: "hai", action: "submit", detail: "Gửi tới Manager", time: "07/06 08:02" },
      { actor: "duc", action: "open", detail: "Gán cho Lê Hồng, Nguyễn Minh", time: "07/06 08:30" },
    ],
  },
  {
    id: "rfi37", num: "RFI-0037", subject: "Sai khác spec lớp chống thấm tầng hầm",
    disc: "str", type: "spec", prio: "normal", status: "closed", daysOpen: 5,
    question: "Spec ghi màng chống thấm HDPE 2mm nhưng bản vẽ ghi 1.5mm. Đề nghị xác nhận chiều dày áp dụng.",
    proposed: "Áp dụng 2mm theo spec (yêu cầu cao hơn).", drawing: "STR-Basement-WP", spec: "Mục 07 13 00", suit: "A1",
    cost: { on: false }, schedule: { on: false }, originator: "hong", manager: "duc",
    reviewers: ["son"], bic: "—", created: "28/05/2026", due: "02/06/2026", closed: "02/06/2026",
    refs: [{ t: "drawing", label: "S-410" }],
    responses: [{ author: "son", company: "Tư vấn TKXD", date: "01/06 09:00", official: true, body: "Xác nhận áp dụng màng HDPE 2mm theo spec. Bản vẽ sẽ được hiệu chỉnh. Đây là yêu cầu bắt buộc cho khu vực tiếp xúc nước ngầm." }],
    activity: [
      { actor: "hong", action: "create", detail: "Tạo RFI", time: "28/05 09:00" },
      { actor: "duc", action: "open", detail: "Gán cho Đặng V. Sơn", time: "28/05 11:00" },
      { actor: "son", action: "answer", detail: "Trả lời chính thức", time: "01/06 09:00" },
      { actor: "duc", action: "close", detail: "Đóng & phân phối", time: "02/06 16:00" },
    ],
  },
  {
    id: "rfi36", num: "RFI-0036", subject: "Phối hợp tuyến máng cáp điện tầng L2",
    disc: "mep", type: "coord", prio: "low", status: "closed", daysOpen: 4,
    question: "Tuyến máng cáp điện L2 chồng lấn ống cấp nước DN150. Đề nghị thống nhất cao độ lắp đặt.",
    proposed: "Máng cáp đi trên, ống nước đi dưới, cách nhau 150mm.", drawing: "MEP-Electrical-L2", spec: "Mục 26 05 00", suit: "A1",
    cost: { on: false }, schedule: { on: false }, originator: "an", manager: "duc",
    reviewers: ["an"], bic: "—", created: "26/05/2026", due: "31/05/2026", closed: "30/05/2026",
    linkedIssue: "iss28",
    refs: [{ t: "model", label: "MEP-Tray-L2", ifc: "2bC...L2" }, { t: "issue", label: "ISS-028", id: "iss28" }],
    responses: [{ author: "an", company: "Ventus BIM", date: "29/05 14:00", official: true, body: "Thống nhất máng cáp ở cao độ +3.20m, ống nước +3.05m. Đã cập nhật mô hình phối hợp." }],
    activity: [
      { actor: "an", action: "create", detail: "Tạo RFI", time: "26/05 10:00" },
      { actor: "duc", action: "open", detail: "Tự xử lý phối hợp", time: "26/05 11:00" },
      { actor: "an", action: "answer", detail: "Trả lời chính thức", time: "29/05 14:00" },
      { actor: "duc", action: "close", detail: "Đóng & phân phối", time: "30/05 09:00" },
    ],
  },
  {
    id: "rfi35", num: "RFI-0035", subject: "Làm rõ kích thước cửa kỹ thuật phòng sảnh",
    disc: "arc", type: "clarify", prio: "normal", status: "draft", daysOpen: 0,
    question: "Cần xác nhận kích thước thông thuỷ cửa phòng kỹ thuật sảnh ga để bố trí thiết bị.",
    proposed: "", drawing: "ARC-Door-Schedule", spec: "Mục 08 11 00", suit: "S0",
    cost: { on: false }, schedule: { on: false }, originator: "duc", manager: "duc",
    reviewers: [], bic: "duc", created: "08/06/2026", due: "13/06/2026", refs: [], responses: [],
    activity: [{ actor: "duc", action: "create", detail: "Tạo bản nháp", time: "08/06 09:00" }],
  },
];

/* Issues (coordination) — two-way linked to RFI */
const ISSUE_STATUS = { open: { label: "Mở", cls: "danger" }, progress: { label: "Đang xử lý", cls: "warn" }, resolved: { label: "Đã giải quyết", cls: "str" }, closed: { label: "Đã đóng", cls: "ok" } };
const ISSUES = [
  { id: "iss42", num: "ISS-042", title: "Ống HVAC đâm xuyên dầm bê tông B-04", status: "progress", pri: "high", disc: "mep",
    desc: "Va chạm cứng giữa ống gió HVAC ø630 và dầm chính B-04 tại trục B, cao độ +18.4m. Cần phương án định tuyến.",
    assignee: "an", reporter: "duc", due: "10/06", created: "07/06 11:30", loc: "Trục B · +18.4m",
    bcf: true, linked: "STR-Frame-Station-C", ifc: "1xS3hKp00B04zQv9mE", coord: [62, 44], linkedRfi: "rfi42" },
  { id: "iss41", num: "ISS-041", title: "Thiếu chi tiết neo thép tại nút cột-dầm", status: "progress", pri: "high", disc: "str",
    desc: "Nút giao cột C-12 và dầm ngang chưa có chi tiết neo thép trong mô hình lẫn bản vẽ.",
    assignee: "hong", reporter: "tuan", due: "09/06", created: "07/06 09:10", loc: "Cột C-12",
    bcf: true, linked: "STR-Beam-Detail-B04", ifc: "0aZqL77x0C12mNp4", coord: [38, 60], linkedRfi: "rfi40" },
  { id: "iss39", num: "ISS-039", title: "Cao độ trần kỹ thuật chưa khớp mặt đứng", status: "open", pri: "normal", disc: "arc",
    desc: "Trần kỹ thuật sảnh ga C lệch cao độ so với mặt đứng kiến trúc, cần phối hợp lại.",
    assignee: "lan", reporter: "duc", due: "12/06", created: "06/06 15:20", loc: "Sảnh ga C",
    bcf: false, linked: "ARC-Facade-GaC", ifc: "3cD...GaC", coord: [55, 28], linkedRfi: null },
  { id: "iss35", num: "ISS-035", title: "Sai lệch gốc toạ độ mô hình INF và STR", status: "open", pri: "high", disc: "inf",
    desc: "Mô hình hạ tầng và kết cấu lệch gốc toạ độ 0.4m, ảnh hưởng phối hợp toàn ga.",
    assignee: "hai", reporter: "minh", due: "08/06", created: "05/06 10:00", loc: "Toàn ga",
    bcf: true, linked: "INF-Drainage-L3", ifc: "4dE...INF", coord: [20, 70], linkedRfi: null },
  { id: "iss28", num: "ISS-028", title: "Va chạm máng cáp điện với ống cấp nước L2", status: "closed", pri: "normal", disc: "mep",
    desc: "Máng cáp điện L2 chồng lấn ống cấp nước DN150 — đã giải quyết qua RFI-0036.",
    assignee: "an", reporter: "an", due: "04/06", created: "02/06 14:00", loc: "Tầng L2",
    bcf: true, linked: "MEP-Electrical-L2", ifc: "2bC...L2", coord: [70, 55], linkedRfi: "rfi36" },
];

/* Mini BIM viewer — selectable elements (federated metro station) */
const VIEWER_ELEMENTS = [
  { id: "col", name: "Hệ cột chính C-12", disc: "str", ifc: "0aZqL77x0C12mNp4", drawing: "STR-Column-Schedule", count: "8 đối tượng", mat: "BTCT C40" },
  { id: "beam", name: "Dầm ngang B-04", disc: "str", ifc: "1xS3hKp00B04zQv9mE", drawing: "STR-Beam-Detail-B04", count: "12 đối tượng", mat: "BTCT C40" },
  { id: "duct", name: "Ống gió HVAC trục B", disc: "mep", ifc: "5fG9hvacB00ductX", drawing: "MEP-HVAC-B2", count: "1 tuyến ø630", mat: "Tôn tráng kẽm" },
  { id: "slab", name: "Sàn ke ga L3", disc: "inf", ifc: "6gH0slabL3keGa00", drawing: "INF-Drainage-L3", count: "4 tấm", mat: "BTCT C35" },
  { id: "fac", name: "Mặt đứng ốp ga C", disc: "arc", ifc: "7hJ1facadeGaC000", drawing: "ARC-Facade-GaC", count: "Tổ hợp", mat: "Tấm GRC" },
];

const REF_META = {
  drawing:  { ic: "doc",  label: "Bản vẽ",   c: "var(--str)" },
  spec:     { ic: "doc",  label: "Spec",     c: "var(--ink-3)" },
  document: { ic: "doc",  label: "Tài liệu", c: "var(--arc)" },
  model:    { ic: "cube", label: "Mô hình",  c: "var(--inf)" },
  issue:    { ic: "flag", label: "Issue",    c: "var(--danger)" },
};
const RFI_ACTION_META = {
  create:  { label: "tạo RFI",           ic: "plus",    c: "var(--accent)" },
  submit:  { label: "gửi đi",            ic: "send",    c: "var(--str)" },
  open:    { label: "mở & phân công",     ic: "flow",    c: "var(--warn)" },
  comment: { label: "thêm phản hồi",      ic: "message", c: "var(--ink-3)" },
  answer:  { label: "trả lời chính thức", ic: "reply",   c: "var(--inf)" },
  close:   { label: "đóng & phân phối",    ic: "check",   c: "var(--ok)" },
  reject:  { label: "từ chối",            ic: "ban",     c: "var(--danger)" },
  reopen:  { label: "mở lại",             ic: "sync",    c: "var(--warn)" },
  escalate:{ label: "escalate quá hạn",   ic: "alert",   c: "var(--danger)" },
};

/* prefill builders */
function prefillFromElement(el, rot) {
  return { fromModel: true, subject: `Làm rõ — ${el.name}`, disc: el.disc, drawing: el.drawing,
    viewpoint: { element: el.name, ifc: el.ifc, rot: rot || { x: -24, y: -38 }, disc: el.disc } };
}
function prefillFromIssue(iss) {
  return { subject: iss.title, question: iss.desc, disc: iss.disc, drawing: iss.linked, linkedIssue: iss.id, prio: iss.pri,
    viewpoint: iss.ifc ? { element: iss.linked, ifc: iss.ifc, rot: { x: -24, y: -38 }, disc: iss.disc } : null };
}




/* ===== file3.js ===== */
/* ============================================================
   RFI standalone — people · register · analytics · escalation
   ============================================================ */
const { useState: useStateReg, useMemo: useMemoReg } = React;

function PersonAv({ id, size = 28 }) {
  const p = RFI_PEOPLE[id]; if (!p) return null;
  const grad = { inf: "#1ec7b6,#0d9488", str: "#60a5fa,#2563eb", arc: "#a78bfa,#7c3aed", mep: "#f6b54e,#e0820a", pm: "#334155,#0f1f24" }[p.disc] || "#334155,#0f1f24";
  return <span title={p.name} style={{ width: size, height: size, borderRadius: Math.round(size*0.28), flex: "none",
    display: "grid", placeItems: "center", color: "#fff", fontWeight: 800, fontSize: size*0.38, background: `linear-gradient(150deg, ${grad})` }}>{p.init}</span>;
}
function PersonInline({ id }) {
  const p = RFI_PEOPLE[id]; if (!p) return <span style={{ color: "var(--ink-4)", fontWeight: 700 }}>—</span>;
  return <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}><PersonAv id={id} size={26} /><span style={{ fontSize: 13, fontWeight: 700, whiteSpace: "nowrap" }}>{p.name}</span></span>;
}
function StatusChip({ status, big }) { const s = RFI_STATUS[status]; return <span className={`chip ${s.cls}`} style={big ? { height: 28, fontSize: 12.5 } : {}}>{s.label}</span>; }
function PrioChip({ prio }) { const p = RFI_PRIO[prio]; return <span className={`chip ${p.cls}`} style={{ height: 24, fontSize: 11.5 }}>{prio === "urgent" && <Icon.flag size={11} />}{p.label}</span>; }

function SLABar({ rfi, showLabel = true }) {
  const s = slaInfo(rfi);
  if (s.resolved) return <span style={{ fontSize: 12, color: "var(--ink-4)", fontWeight: 700 }}>—</span>;
  return (
    <div className={`sla sla-${s.state}`}>
      <div className="sla-track"><div className="sla-fill" style={{ width: `${Math.max(8, s.pct*100)}%` }} /></div>
      {showLabel && <span className="sla-lbl">{s.state === "overdue" ? `Quá ${s.over}n` : `${s.used}/${s.total}n`}</span>}
    </div>
  );
}

/* ---------- Escalation banner ---------- */
function EscalationBanner({ onOpen }) {
  const overdue = RFIS.filter(r => slaInfo(r).state === "overdue");
  if (!overdue.length) return null;
  return (
    <div className="esc-banner">
      <div className="esc-ico"><Icon.alert size={20} /></div>
      <div style={{ flex: "1 1 300px", minWidth: 240 }}>
        <div style={{ fontSize: 13.5, fontWeight: 800, color: "var(--ink)" }}>{overdue.length} RFI quá hạn SLA — đã tự động escalate tới RFI Manager</div>
        <div style={{ fontSize: 12, color: "var(--ink-2)", fontWeight: 500, marginTop: 2 }}>
          Hệ thống gửi nhắc nhở hằng ngày cho người giữ ball-in-court. Vượt 75% thời hạn sẽ cảnh báo, quá hạn sẽ escalate (dừng sau 45 ngày).
        </div>
      </div>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" }}>
        {overdue.slice(0, 3).map(r => (
          <span key={r.id} onClick={() => onOpen(r)} className="linkchip" style={{ borderColor: "rgba(239,68,68,.3)", color: "var(--danger)" }}>
            <Icon.flag size={12} /> {r.num} · quá {slaInfo(r).over}n
          </span>
        ))}
      </div>
    </div>
  );
}

/* ---------- Register ---------- */
function RFIRegister({ onOpen, onCreate }) {
  window.useSysCfg && window.useSysCfg();   // re-render khi admin sửa danh sách bộ môn
  const [filter, setFilter] = useStateReg("all");
  const [disc, setDisc] = useStateReg("all");
  const [q, setQ] = useStateReg("");
  const visible = RFIS.filter(r => r.status !== "void");
  const isOpenState = (r) => ["open", "reopened", "submitted"].includes(r.status);
  const counts = {
    all: visible.length,
    open: visible.filter(isOpenState).length,
    overdue: visible.filter(r => slaInfo(r).state === "overdue").length,
    answered: visible.filter(r => r.status === "answered").length,
    closed: visible.filter(r => r.status === "closed").length,
  };
  const summary = [
    { k: "all", label: "Tất cả RFI", v: counts.all, c: "var(--accent)", ic: "flow" },
    { k: "open", label: "Đang mở", v: counts.open, c: "var(--warn)", ic: "clock" },
    { k: "overdue", label: "Quá hạn", v: counts.overdue, c: "var(--danger)", ic: "alert" },
    { k: "answered", label: "Đã trả lời", v: counts.answered, c: "var(--info)", ic: "reply" },
    { k: "closed", label: "Đã đóng", v: counts.closed, c: "var(--ok)", ic: "checkCircle" },
  ];
  const rows = visible.filter(r => {
    const mf = filter === "all" || (filter === "open" && isOpenState(r)) || (filter === "overdue" && slaInfo(r).state === "overdue")
      || (filter === "answered" && r.status === "answered") || (filter === "closed" && r.status === "closed");
    const md = disc === "all" || r.disc === disc;
    const mq = !q || r.subject.toLowerCase().includes(q.toLowerCase()) || r.num.toLowerCase().includes(q.toLowerCase());
    return mf && md && mq;
  });

  return (
    <div>
      <EscalationBanner onOpen={onOpen} />

      <div className="grid" style={{ gridTemplateColumns: "repeat(5,1fr)", gap: 14, marginBottom: 18 }}>
        {summary.map(s => { const Ico = Icon[s.ic]; const on = filter === s.k;
          return (
            <div key={s.k} onClick={() => setFilter(s.k)} className="card" style={{ padding: "16px 18px", cursor: "pointer", border: on ? `1.5px solid ${s.c}` : "1px solid var(--line)", background: on ? `color-mix(in srgb, ${s.c} 7%, var(--surface))` : "var(--surface)" }}>
              <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                <span style={{ width: 32, height: 32, borderRadius: 9, display: "grid", placeItems: "center", background: `${s.c}1c`, color: s.c, flex: "none" }}><Ico size={17} /></span>
                <span className="tnum" style={{ fontSize: 26, fontWeight: 800, letterSpacing: "-.02em" }}>{s.v}</span>
              </div>
              <div style={{ fontSize: 12.5, color: "var(--ink-3)", fontWeight: 700, marginTop: 10 }}>{s.label}</div>
            </div>
          ); })}
      </div>

      <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16, flexWrap: "wrap" }}>
        <div style={{ display: "flex", gap: 7 }}>
          {[["all", "Tất cả"]].concat((window.discOptions ? window.discOptions() : []).map(o => [o.id, o.label])).map(([k, l]) => (
            <span key={k} onClick={() => setDisc(k)} style={{ height: 32, padding: "0 13px", borderRadius: 9, display: "inline-flex", alignItems: "center", gap: 7, fontSize: 12.5, fontWeight: 700, cursor: "pointer", background: disc === k ? "var(--accent)" : "var(--sunken)", color: disc === k ? "#fff" : "var(--ink-3)" }}>
              {k !== "all" && <span style={{ width: 7, height: 7, borderRadius: 99, background: disc === k ? "#fff" : DISC_COL[k] }} />}{l}
            </span>
          ))}
        </div>
        <div className="search" style={{ width: 240, height: 38, marginLeft: "auto" }}>
          <Icon.search size={15} /><input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Tìm số RFI, chủ đề…" style={{ flex: 1, border: 0, background: "transparent", outline: "none", font: "inherit", fontSize: 13, color: "var(--ink)", minWidth: 0 }} />
        </div>
        <button className="btn sm" onClick={() => downloadCSV("rfi-register.csv",
          ["RFI số", "Chủ đề", "Loại", "Bộ môn", "Ưu tiên", "Trạng thái", "Ball-in-court", "SLA"],
          rows.map(r => [r.num, r.subject, RFI_TYPE[r.type] || r.type, DISC_NAME[r.disc] || r.disc,
            (RFI_PRIO[r.prio] || {}).label || r.prio, (RFI_STATUS[r.status] || {}).label || r.status,
            (RFI_PEOPLE[r.bic] || {}).name || r.bic || "—", slaInfo(r).state]))}>
          <Icon.download size={14} /> Xuất Excel</button>
        <button className="btn sm primary" onClick={onCreate}><Icon.plus size={14} /> Tạo RFI</button>
      </div>

      <div className="card" style={{ padding: "8px 10px", overflowX: "auto" }}>
        <table className="tbl" style={{ width: "100%", minWidth: 980 }}>
          <thead><tr>
            <th style={{ paddingLeft: 14 }}>RFI</th><th>Chủ đề</th><th>Bộ môn</th><th>Ưu tiên</th>
            <th>Trạng thái</th><th>Ball-in-court</th><th style={{ minWidth: 140 }}>SLA</th><th>Tác động</th>
          </tr></thead>
          <tbody>
            {rows.map(r => (
              <tr key={r.id} onClick={() => onOpen(r)} style={{ cursor: "pointer" }}>
                <td style={{ paddingLeft: 14 }}><span className="tag-mono" style={{ fontWeight: 700 }}>{r.num}</span></td>
                <td className="t-strong" style={{ maxWidth: 260 }}>
                  <div style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", display: "flex", alignItems: "center", gap: 7 }}>
                    {r.linkedIssue && <span title="Liên kết Issue" style={{ color: "var(--danger)", display: "inline-flex", flex: "none" }}><Icon.flag size={13} /></span>}
                    {r.subject}
                  </div>
                  <div style={{ fontSize: 11.5, color: "var(--ink-3)", fontWeight: 500 }}>{RFI_TYPE[r.type]}</div>
                </td>
                <td><span className={`chip ${r.disc}`}>{DISC_NAME[r.disc]}</span></td>
                <td><PrioChip prio={r.prio} /></td>
                <td><StatusChip status={r.status} /></td>
                <td>{r.status === "closed" ? <span style={{ color: "var(--ink-4)", fontWeight: 700 }}>—</span> : <PersonInline id={r.bic} />}</td>
                <td><SLABar rfi={r} /></td>
                <td><div style={{ display: "flex", gap: 6 }}>
                  {r.cost.on && <span title="Ảnh hưởng chi phí" style={{ color: "var(--mep)" }}><Icon.money size={16} /></span>}
                  {r.schedule.on && <span title="Ảnh hưởng tiến độ" style={{ color: "var(--danger)" }}><Icon.clock size={16} /></span>}
                  {!r.cost.on && !r.schedule.on && <span style={{ color: "var(--ink-4)", fontWeight: 700 }}>—</span>}
                </div></td>
              </tr>
            ))}
          </tbody>
        </table>
        {rows.length === 0 && <div style={{ padding: 50, textAlign: "center", color: "var(--ink-3)", fontWeight: 600 }}>Không có RFI phù hợp bộ lọc</div>}
      </div>
    </div>
  );
}

/* ---------- Analytics ---------- */
function RFIAnalytics() {
  const v = RFIS.filter(r => r.status !== "void");
  const byStatus = [
    { label: "Đang xử lý", c: "var(--warn)", n: v.filter(r => ["open", "reopened", "submitted"].includes(r.status)).length },
    { label: "Đã trả lời", c: "var(--info)", n: v.filter(r => r.status === "answered").length },
    { label: "Đã đóng", c: "var(--ok)", n: v.filter(r => r.status === "closed").length },
    { label: "Nháp", c: "var(--ink-3)", n: v.filter(r => r.status === "draft").length },
  ];
  const maxS = Math.max(...byStatus.map(s => s.n), 1);
  const byDisc = ["inf", "str", "arc", "mep"].map(d => ({ d, n: v.filter(r => r.disc === d).length }));
  const maxD = Math.max(...byDisc.map(s => s.n), 1);
  const closedR = v.filter(r => r.status === "closed");
  const avgDays = (closedR.reduce((a, r) => a + r.daysOpen, 0) / Math.max(1, closedR.length)).toFixed(1);
  const overdue = v.filter(r => slaInfo(r).state === "overdue").length;
  const trend = [12, 9, 14, 11, 16, 8, 13];
  return (
    <div>
      <div className="grid g-4" style={{ marginBottom: 18 }}>
        {[["Tổng RFI", v.length, "var(--accent)", "flow"], ["Thời gian phản hồi TB", avgDays + " ngày", "var(--info)", "clock"], ["Quá hạn SLA", overdue, "var(--danger)", "alert"], ["Tỷ lệ đóng", Math.round(closedR.length / v.length * 100) + "%", "var(--ok)", "checkCircle"]].map(([l, val, c, ic], i) => {
          const Ico = Icon[ic];
          return <div key={i} className="card stat" style={{ flexDirection: "row", alignItems: "center", gap: 14 }}>
            <div className="stat-ico" style={{ background: `${c}1f`, color: c }}><Ico size={20} /></div>
            <div><div className="tnum" style={{ fontSize: 24, fontWeight: 800 }}>{val}</div><div style={{ fontSize: 12.5, color: "var(--ink-3)", fontWeight: 600 }}>{l}</div></div>
          </div>;
        })}
      </div>
      <div className="grid g-2" style={{ marginBottom: 18 }}>
        <div className="card card-pad">
          <div className="card-title" style={{ marginBottom: 18 }}>RFI theo trạng thái</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
            {byStatus.map((s, i) => (
              <div key={i} className="bar-row"><span className="nm" style={{ width: 96 }}><span className="sw" style={{ background: s.c }} />{s.label}</span>
                <span className="bar"><span style={{ width: `${s.n / maxS * 100}%`, background: s.c }} /></span><span className="pc tnum">{s.n}</span></div>
            ))}
          </div>
        </div>
        <div className="card card-pad">
          <div className="card-title" style={{ marginBottom: 18 }}>RFI theo bộ môn</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
            {byDisc.map(s => (
              <div key={s.d} className="bar-row"><span className="nm" style={{ width: 96 }}><span className="sw" style={{ background: DISC_COL[s.d] }} />{DISC_NAME[s.d]}</span>
                <span className="bar"><span style={{ width: `${s.n / maxD * 100}%`, background: DISC_COL[s.d] }} /></span><span className="pc tnum">{s.n}</span></div>
            ))}
          </div>
        </div>
      </div>
      <div className="card card-pad">
        <div className="card-head" style={{ marginBottom: 8 }}>
          <div><div className="card-title">Xu hướng RFI mới theo tuần</div><div className="card-sub">7 tuần gần nhất</div></div>
          <span className="chip warn"><Icon.alert size={12} /> Benchmark ngành: ~22% không phản hồi</span>
        </div>
        <div style={{ display: "flex", alignItems: "flex-end", gap: 12, height: 150, paddingTop: 14 }}>
          {trend.map((n, i) => (
            <div key={i} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 7 }}>
              <div style={{ width: "100%", maxWidth: 54, height: `${n / 16 * 120}px`, borderRadius: "7px 7px 3px 3px", background: i === trend.length - 1 ? "var(--accent)" : "color-mix(in srgb, var(--accent) 32%, transparent)" }} />
              <span style={{ fontSize: 10.5, color: "var(--ink-4)", fontWeight: 700 }}>T{i + 1}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}




/* ===== file5.js ===== */
/* ============================================================
   RFI standalone — Detail panel + Create modal (with prefill)
   ============================================================ */
const { useState: useStateDt } = React;

function RFIStepper({ status }) {
  const cur = RFI_STATUS[status].flowIdx;
  const rejected = status === "rejected";
  return (
    <div style={{ display: "flex", alignItems: "flex-start" }}>
      {RFI_FLOW.map((s, i) => {
        const done = i < cur, active = i === cur; const Ico = Icon[s.ic];
        const col = rejected && i >= 2 ? "var(--danger)" : "var(--accent)";
        return (
          <React.Fragment key={s.k}>
            <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 7, flex: "none", width: 92, textAlign: "center" }}>
              <span style={{ width: 36, height: 36, borderRadius: 99, display: "grid", placeItems: "center", flex: "none", background: done || active ? col : "var(--sunken)", color: done || active ? "#fff" : "var(--ink-4)", boxShadow: active ? `0 0 0 4px color-mix(in srgb, ${col} 16%, transparent)` : "none" }}>{done ? <Icon.check size={16} /> : <Ico size={16} />}</span>
              <span style={{ fontSize: 11.5, fontWeight: 700, color: active ? "var(--ink)" : "var(--ink-4)", whiteSpace: "nowrap" }}>{s.label}</span>
            </div>
            {i < RFI_FLOW.length - 1 && <div style={{ flex: 1, height: 2, background: i < cur ? "var(--accent)" : "var(--line)", marginTop: 17 }} />}
          </React.Fragment>
        );
      })}
    </div>
  );
}

function RFIDetail({ rfi, onBack, go, reload }) {
  const [draft, setDraft] = useStateDt("");
  const [responses, setResponses] = useStateDt(rfi.responses);
  const [busy, setBusy] = useStateDt(false);
  const st = RFI_STATUS[rfi.status];
  const isClosed = rfi.status === "closed";
  const official = responses.find(r => r.official);
  const sla = slaInfo(rfi);
  const linkedIssue = rfi.linkedIssue ? ISSUES.find(i => i.id === rfi.linkedIssue) : null;

  const addResp = async (asOfficial) => {
    const body = draft.trim(); if (!body) return;
    setResponses(rs => [...rs.map(r => asOfficial ? { ...r, official: false } : r), { author: "duc", company: "Ventus BIM", date: "Vừa xong", official: !!asOfficial, body }]);
    setDraft("");
    try {
      if (window.cdeRfi) {
        await window.cdeRfi.addRfiResponse(rfi.id, body, !!asOfficial);
        if (asOfficial && rfi.status === "open") { await window.cdeRfi.setRfiStatus(rfi.id, "answered"); if (reload) reload(); }
      }
    } catch (e) { console.warn("rfi resp", e.message); }
  };

  // nhãn nút → trạng thái đích (đổi trạng thái lưu thật)
  const STATUS_NEXT = {
    "Gửi RFI": "open", "Mở & phân công": "open", "Trả lại": "draft",
    "Trả lời chính thức": "answered", "Từ chối": "rejected",
    "Đóng & phân phối": "closed", "Trả về Reviewer": "open",
    "Mở lại": "open", "Sửa lại": "draft",
  };
  const doAction = async (label) => {
    const next = STATUS_NEXT[label];
    if (!next || !window.cdeRfi) return;
    setBusy(true);
    try { await window.cdeRfi.setRfiStatus(rfi.id, next); if (reload) await reload(); onBack(); }
    catch (e) { alert("Đổi trạng thái thất bại: " + (e.message || e)); }
    finally { setBusy(false); }
  };
  const actions = {
    draft: [["Gửi RFI", "send", "primary"]],
    submitted: [["Mở & phân công", "flow", "primary"], ["Trả lại", "reply", ""]],
    open: [["Trả lời chính thức", "reply", "primary"], ["Từ chối", "ban", ""]],
    answered: [["Đóng & phân phối", "check", "primary"], ["Trả về Reviewer", "reply", ""]],
    closed: [["Mở lại", "sync", ""]],
    rejected: [["Sửa lại", "doc", "primary"], ["Huỷ", "ban", ""]],
  }[rfi.status] || [];

  return (
    <div className="content" style={{ paddingTop: 18 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 14, marginBottom: 18, flexWrap: "wrap" }}>
        <button className="btn sm" onClick={onBack}><Icon.chevL size={15} /> Sổ RFI</button>
        <span className="tag-mono" style={{ fontWeight: 700, height: 28, display: "inline-flex", alignItems: "center" }}>{rfi.num}</span>
        <StatusChip status={rfi.status} big />
        <div style={{ marginLeft: "auto", display: "flex", gap: 9, flexWrap: "wrap" }}>
          {actions.map(([l, ic, v], i) => { const Ico = Icon[ic]; return <button key={i} disabled={busy} className={`btn sm ${v === "primary" ? "primary" : ""}`} onClick={() => doAction(l)}><Ico size={14} /> {l}</button>; })}
          <button className="icon-btn" style={{ width: 34, height: 34, borderRadius: 9 }}><Icon.dots size={16} /></button>
        </div>
      </div>

      <div style={{ fontSize: 22, fontWeight: 800, letterSpacing: "-.02em", marginBottom: 4 }}>{rfi.subject}</div>
      <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 22, flexWrap: "wrap" }}>
        <span className={`chip ${rfi.disc}`}>{DISC_NAME[rfi.disc]}</span>
        <span className="chip idle">{RFI_TYPE[rfi.type]}</span>
        <PrioChip prio={rfi.prio} />
        {rfi.suit && <span className="tag-mono" style={{ height: 24, display: "inline-flex", alignItems: "center", fontWeight: 700 }}>{rfi.suit}</span>}
        {linkedIssue && <span className="linkchip" onClick={() => go && go("issues", { issueId: linkedIssue.id })} style={{ borderColor: "rgba(239,68,68,.3)", color: "var(--danger)" }}><Icon.flag size={12} /> {linkedIssue.num}<Icon.chevR size={12} /></span>}
      </div>

      {/* stepper + BIC + SLA */}
      <div className="card card-pad" style={{ marginBottom: 18 }}>
        <RFIStepper status={rfi.status} />
        <div style={{ display: "flex", gap: 12, marginTop: 18, flexWrap: "wrap" }}>
          <div style={{ flex: "1 1 280px", padding: "12px 15px", borderRadius: 11, background: isClosed ? "rgba(16,185,129,.10)" : "var(--sunken)", display: "flex", alignItems: "center", gap: 12 }}>
            <Icon.user size={16} color={isClosed ? "var(--ok)" : "var(--accent)"} />
            <span style={{ fontSize: 13, fontWeight: 600, color: "var(--ink-2)" }}>{isClosed ? <>Đã đóng & phân phối — lưu hồ sơ.</> : <>Ball-in-court: <b style={{ color: "var(--ink)" }}>{st.bic}</b></>}</span>
            {!isClosed && rfi.bic !== "—" && <span style={{ marginLeft: "auto" }}><PersonInline id={rfi.bic} /></span>}
          </div>
          {/* SLA widget */}
          <div style={{ flex: "1 1 280px", padding: "12px 15px", borderRadius: 11,
            background: sla.state === "overdue" ? "rgba(239,68,68,.08)" : sla.state === "warn" ? "rgba(245,158,11,.10)" : "var(--sunken)" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 9 }}>
              <Icon.clock size={15} color={sla.state === "overdue" ? "var(--danger)" : sla.state === "warn" ? "var(--warn)" : "var(--ink-3)"} />
              <span style={{ fontSize: 12.5, fontWeight: 800 }}>SLA · {sla.total} ngày</span>
              <span style={{ marginLeft: "auto", fontSize: 12, fontWeight: 800, color: sla.state === "overdue" ? "var(--danger)" : sla.state === "warn" ? "#b87503" : "var(--ink-3)" }}>
                {sla.resolved ? "Đã xử lý" : sla.state === "overdue" ? `Quá hạn ${sla.over} ngày` : sla.state === "warn" ? `Còn ${sla.total - sla.used} ngày` : `Đã dùng ${sla.used}/${sla.total} ngày`}
              </span>
            </div>
            <div className={`sla sla-${sla.state}`}><div className="sla-track" style={{ height: 7 }}><div className="sla-fill" style={{ width: `${Math.max(8, sla.pct*100)}%` }} /></div></div>
            {sla.state === "overdue" && !sla.resolved && <div style={{ fontSize: 11.5, color: "var(--danger)", fontWeight: 700, marginTop: 8, display: "flex", alignItems: "center", gap: 6 }}><Icon.alert size={13} /> Đã escalate tới RFI Manager · nhắc nhở hằng ngày</div>}
          </div>
        </div>
      </div>

      <div className="grid g-2u" style={{ alignItems: "start" }}>
        {/* LEFT */}
        <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
          <div className="card card-pad">
            <div className="card-title" style={{ marginBottom: 10 }}>Câu hỏi</div>
            <div style={{ fontSize: 14, color: "var(--ink-2)", fontWeight: 500, lineHeight: 1.65 }}>{rfi.question}</div>
            {rfi.proposed && <>
              <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: ".06em", color: "var(--ink-4)", margin: "18px 0 8px" }}>GIẢI PHÁP ĐỀ XUẤT</div>
              <div style={{ fontSize: 13.5, color: "var(--ink-2)", fontWeight: 500, lineHeight: 1.6, padding: "12px 14px", background: "var(--sunken)", borderRadius: 10 }}>{rfi.proposed}</div>
            </>}
            {rfi.refs.length > 0 && <>
              <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: ".06em", color: "var(--ink-4)", margin: "18px 0 10px" }}>THAM CHIẾU</div>
              <div style={{ display: "flex", gap: 9, flexWrap: "wrap" }}>
                {rfi.refs.map((rf, i) => { const m = REF_META[rf.t]; const Ico = Icon[m.ic]; const clickable = rf.t === "issue" && rf.id;
                  return <span key={i} onClick={() => clickable && go && go("issues", { issueId: rf.id })} className="linkchip" style={{ height: 32, cursor: clickable ? "pointer" : "default" }}>
                    <span style={{ color: m.c }}><Ico size={14} /></span>{rf.label}{rf.ifc && <span className="tag-mono" style={{ fontSize: 10, marginLeft: 2 }}>IFC</span>}{clickable && <Icon.chevR size={12} />}</span>; })}
              </div>
            </>}
          </div>

          {/* responses */}
          <div className="card card-pad">
            <div className="card-head" style={{ marginBottom: 16 }}><div className="card-title">Phản hồi & trao đổi</div><span style={{ fontSize: 12.5, color: "var(--ink-3)", fontWeight: 700 }}>{responses.length} phản hồi</span></div>
            {official && (
              <div style={{ border: "1.5px solid var(--ok)", background: "rgba(16,185,129,.06)", borderRadius: 12, padding: "14px 16px", marginBottom: 16 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}><span className="chip ok" style={{ height: 22 }}><Icon.checkCircle size={12} /> Phản hồi chính thức</span><span style={{ marginLeft: "auto", fontSize: 11.5, color: "var(--ink-3)", fontWeight: 600 }}>{official.date}</span></div>
                <div style={{ fontSize: 13.5, color: "var(--ink)", fontWeight: 500, lineHeight: 1.6 }}>{official.body}</div>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 10 }}><PersonAv id={official.author} size={24} /><span style={{ fontSize: 12.5, fontWeight: 700, whiteSpace: "nowrap" }}>{RFI_PEOPLE[official.author].name}</span><span style={{ fontSize: 11.5, color: "var(--ink-3)", fontWeight: 600 }}>· {official.company}</span></div>
              </div>
            )}
            <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
              {responses.filter(r => !r.official).map((r, i) => (
                <div key={i} style={{ display: "flex", gap: 12 }}>
                  <PersonAv id={r.author} size={34} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 8 }}><span style={{ fontSize: 13, fontWeight: 800, whiteSpace: "nowrap" }}>{RFI_PEOPLE[r.author]?.name || "Bạn"}</span><span style={{ fontSize: 11.5, color: "var(--ink-3)", fontWeight: 600, whiteSpace: "nowrap" }}>· {r.company}</span><span style={{ marginLeft: "auto", fontSize: 11.5, color: "var(--ink-4)", fontWeight: 600 }}>{r.date}</span></div>
                    <div style={{ fontSize: 13.5, color: "var(--ink-2)", fontWeight: 500, lineHeight: 1.55, marginTop: 4 }}>{r.body}</div>
                  </div>
                </div>
              ))}
              {responses.length === 0 && <div style={{ fontSize: 13, color: "var(--ink-3)", fontWeight: 600, padding: "8px 0" }}>Chưa có phản hồi nào.</div>}
            </div>
            {!isClosed && (
              <div style={{ marginTop: 16, paddingTop: 16, borderTop: "1px solid var(--line-soft)" }}>
                <textarea value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Viết phản hồi…" rows={2} style={{ width: "100%", borderRadius: 11, border: "1px solid var(--line)", background: "var(--surface)", padding: "11px 13px", font: "inherit", fontSize: 13.5, color: "var(--ink)", outline: "none", resize: "vertical" }} />
                <div style={{ display: "flex", gap: 9, marginTop: 10, justifyContent: "flex-end" }}>
                  <button className="btn sm" onClick={() => addResp(false)}><Icon.message size={14} /> Bình luận</button>
                  <button className="btn sm primary" onClick={() => addResp(true)}><Icon.checkCircle size={14} /> Phản hồi chính thức</button>
                </div>
              </div>
            )}
          </div>
        </div>

        {/* RIGHT */}
        <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
          {/* linked viewpoint */}
          {rfi.viewpoint && (
            <div className="card card-pad">
              <div className="card-head" style={{ marginBottom: 12 }}><div className="card-title">Viewpoint mô hình</div><span className="chip inf" style={{ height: 22, fontSize: 10.5 }}>BCF</span></div>
              <div className="vp-thumb" style={{ height: 140 }} onClick={() => go && go("viewer", {})}>
                <div className="grid" />
                <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center" }}><Icon.cube size={40} color={DISC_COL[rfi.viewpoint.disc]} /></div>
                <div className="pin" style={{ left: "58%", top: "52%" }}><div className="pin-head" style={{ background: "var(--danger)", color: "var(--danger)" }}><span className="pin-num">!</span></div></div>
                <span className="tag-mono" style={{ position: "absolute", bottom: 8, left: 8, background: "rgba(255,255,255,.1)", color: "#cfe9e5", fontSize: 10 }}>{rfi.viewpoint.ifc}</span>
              </div>
              <div style={{ fontSize: 13, fontWeight: 700, marginTop: 10 }}>{rfi.viewpoint.element}</div>
              <button className="btn sm" style={{ width: "100%", justifyContent: "center", marginTop: 10 }} onClick={() => go && go("viewer", {})}><Icon.cube size={14} /> Mở trong BIM Viewer</button>
            </div>
          )}

          <div className="card card-pad">
            <div className="card-title" style={{ marginBottom: 14 }}>Chi tiết</div>
            {[["Loại RFI", RFI_TYPE[rfi.type]], ["Bản vẽ", rfi.drawing], ["Mục Spec", rfi.spec], ["Ngày tạo", rfi.created], ["Hạn phản hồi", rfi.due], ["Số ngày mở", rfi.daysOpen + " ngày"], ["Suitability", rfi.suit]].map(([k, val], i) => (
              <div key={i} style={{ display: "flex", justifyContent: "space-between", gap: 14, padding: "9px 0", borderBottom: "1px solid var(--line-soft)", fontSize: 13 }}><span style={{ color: "var(--ink-3)", fontWeight: 600, flex: "none" }}>{k}</span><span style={{ fontWeight: 700, textAlign: "right" }}>{val}</span></div>
            ))}
            <div style={{ display: "flex", gap: 10, marginTop: 14 }}>
              <div style={{ flex: 1, padding: "11px 13px", borderRadius: 10, background: rfi.cost.on ? "rgba(245,158,11,.10)" : "var(--sunken)" }}><div style={{ fontSize: 11, fontWeight: 700, color: "var(--ink-4)", letterSpacing: ".04em" }}>CHI PHÍ</div><div style={{ fontSize: 13, fontWeight: 800, color: rfi.cost.on ? "var(--mep)" : "var(--ink-3)", marginTop: 3 }}>{rfi.cost.on ? rfi.cost.amount : "Không"}</div></div>
              <div style={{ flex: 1, padding: "11px 13px", borderRadius: 10, background: rfi.schedule.on ? "rgba(239,68,68,.08)" : "var(--sunken)" }}><div style={{ fontSize: 11, fontWeight: 700, color: "var(--ink-4)", letterSpacing: ".04em" }}>TIẾN ĐỘ</div><div style={{ fontSize: 13, fontWeight: 800, color: rfi.schedule.on ? "var(--danger)" : "var(--ink-3)", marginTop: 3 }}>{rfi.schedule.on ? (rfi.schedule.days > 0 ? "+" : "") + rfi.schedule.days + " ngày" : "Không"}</div></div>
            </div>
          </div>

          <div className="card card-pad">
            <div className="card-title" style={{ marginBottom: 14 }}>Liên quan</div>
            {[["Người tạo", [rfi.originator]], ["RFI Manager", [rfi.manager]], ["Reviewer", rfi.reviewers]].map(([role, ids], i) => (
              <div key={i} style={{ padding: "9px 0", borderBottom: i < 2 ? "1px solid var(--line-soft)" : "none" }}>
                <div style={{ fontSize: 11, fontWeight: 700, color: "var(--ink-4)", letterSpacing: ".04em", marginBottom: 8 }}>{role.toUpperCase()}</div>
                {ids.length ? <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>{ids.map(id => (<div key={id} style={{ display: "flex", alignItems: "center", gap: 9 }}><PersonAv id={id} size={28} /><div><div style={{ fontSize: 13, fontWeight: 700 }}>{RFI_PEOPLE[id].name}</div><div style={{ fontSize: 11, color: "var(--ink-3)", fontWeight: 600 }}>{RFI_PEOPLE[id].company}</div></div></div>))}</div> : <span style={{ fontSize: 12.5, color: "var(--ink-4)", fontWeight: 600 }}>Chưa phân công</span>}
              </div>
            ))}
          </div>

          <div className="card card-pad">
            <div className="card-title" style={{ marginBottom: 14 }}>Nhật ký (Audit trail)</div>
            <div style={{ display: "flex", flexDirection: "column" }}>
              {rfi.activity.map((a, i) => { const m = RFI_ACTION_META[a.action]; const Ico = Icon[m.ic];
                return (<div key={i} style={{ display: "flex", gap: 11, paddingBottom: i < rfi.activity.length - 1 ? 14 : 0 }}>
                  <div style={{ display: "flex", flexDirection: "column", alignItems: "center", flex: "none" }}><span style={{ width: 28, height: 28, borderRadius: 8, display: "grid", placeItems: "center", background: `${m.c}1c`, color: m.c }}><Ico size={14} /></span>{i < rfi.activity.length - 1 && <span style={{ width: 2, flex: 1, background: "var(--line)", marginTop: 2 }} />}</div>
                  <div style={{ flex: 1, minWidth: 0, paddingTop: 2 }}><div style={{ fontSize: 13, color: "var(--ink)", fontWeight: 600 }}><b style={{ fontWeight: 800 }}>{RFI_PEOPLE[a.actor]?.name}</b> {m.label}</div><div style={{ fontSize: 11.5, color: "var(--ink-3)", fontWeight: 500, marginTop: 1 }}>{a.detail} · {a.time}</div></div>
                </div>); })}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ---------- Create modal (with optional prefill) ---------- */
function RFICreate({ onClose, prefill, onCreated }) {
  const pf = prefill || {};
  const [type, setType] = useStateDt("clarify");
  const [disc, setDisc] = useStateDt(pf.disc || "mep");
  const [prio, setPrio] = useStateDt(pf.prio || "normal");
  const [cost, setCost] = useStateDt(false);
  const [sched, setSched] = useStateDt(false);
  const [busy, setBusy] = useStateDt(false);
  const subjRef = React.useRef(null), qRef = React.useRef(null);
  const submit = async () => {
    const subject = (subjRef.current && subjRef.current.value || "").trim();
    const question = (qRef.current && qRef.current.value || "").trim();
    if (!subject) { alert("Vui lòng nhập chủ đề RFI."); return; }
    if (!window.cdeRfi) { onClose(); return; }
    setBusy(true);
    try {
      await window.cdeRfi.createRfi({ subject, question, disc, type, prio, cost: { on: cost }, schedule: { on: sched }, viewpoint: pf.viewpoint || null });
      if (onCreated) await onCreated();
      onClose();
    } catch (e) { alert("Lưu RFI thất bại: " + (e.message || e)); }
    finally { setBusy(false); }
  };
  const fld = { width: "100%", height: 40, padding: "0 12px", borderRadius: 10, border: "1px solid var(--line)", background: "var(--surface)", color: "var(--ink)", font: "inherit", fontSize: 13.5, fontWeight: 600, outline: "none" };
  const lb = { display: "block", fontSize: 11.5, fontWeight: 700, color: "var(--ink-3)", marginBottom: 6 };
  const vp = pf.viewpoint;
  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(8,19,23,.5)", backdropFilter: "blur(3px)", display: "grid", placeItems: "center", padding: 24 }}>
      <div onClick={(e) => e.stopPropagation()} className="card" style={{ width: 600, maxHeight: "90vh", overflowY: "auto", boxShadow: "var(--shadow-lg)" }}>
        <div style={{ padding: "18px 24px", borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", justifyContent: "space-between", position: "sticky", top: 0, background: "var(--surface)", zIndex: 1 }}>
          <div><div style={{ fontWeight: 800, fontSize: 16 }}>Tạo RFI mới</div><div style={{ fontSize: 12.5, color: "var(--ink-3)", fontWeight: 500, marginTop: 2 }}>Số tự sinh · <span className="mono">RFI-0043</span>{pf.fromModel && <> · từ <b style={{ color: "var(--accent)" }}>BIM Viewer</b></>}{pf.linkedIssue && <> · từ <b style={{ color: "var(--danger)" }}>Issue</b></>}</div></div>
          <button className="icon-btn" style={{ width: 30, height: 30, borderRadius: 8 }} onClick={onClose}><span style={{ fontSize: 18, lineHeight: 0 }}>✕</span></button>
        </div>
        <div style={{ padding: 24 }}>
          {vp && (
            <div style={{ marginBottom: 18, padding: 14, borderRadius: 12, background: "var(--sunken)", display: "flex", gap: 14, alignItems: "center" }}>
              <div className="vp-thumb" style={{ width: 96, height: 64, flex: "none" }}>
                <div className="grid" /><div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center" }}><Icon.cube size={26} color={DISC_COL[vp.disc]} /></div>
                <div className="pin" style={{ left: "60%", top: "55%" }}><div className="pin-head" style={{ background: "var(--accent)", color: "var(--accent)", width: 18, height: 18 }}><span className="pin-num" style={{ fontSize: 9 }}>1</span></div></div>
              </div>
              <div style={{ minWidth: 0 }}>
                <div style={{ fontSize: 11, fontWeight: 700, color: "var(--ink-4)", letterSpacing: ".04em" }}>VIEWPOINT ĐÃ GẮN</div>
                <div style={{ fontSize: 13.5, fontWeight: 700, marginTop: 3 }}>{vp.element}</div>
                <div className="mono" style={{ fontSize: 11, color: "var(--ink-3)", marginTop: 2 }}>IFC GUID · {vp.ifc}</div>
              </div>
            </div>
          )}
          {pf.linkedIssue && <div style={{ marginBottom: 16, padding: "11px 14px", borderRadius: 10, background: "rgba(239,68,68,.07)", display: "flex", alignItems: "center", gap: 10 }}><Icon.flag size={15} color="var(--danger)" /><span style={{ fontSize: 12.5, fontWeight: 600, color: "var(--ink-2)" }}>Nâng cấp từ issue <b className="mono">{ISSUES.find(i=>i.id===pf.linkedIssue)?.num}</b> — liên kết hai chiều sẽ được tạo tự động.</span></div>}
          <label style={lb}>Chủ đề (subject) *</label>
          <input ref={subjRef} style={fld} defaultValue={pf.subject || ""} placeholder="VD: Làm rõ cao độ đầu cọc khu A" />
          <div style={{ marginTop: 14 }}><label style={lb}>Câu hỏi (question) *</label><textarea ref={qRef} rows={3} style={{ ...fld, height: "auto", padding: "11px 12px", resize: "vertical" }} defaultValue={pf.question || ""} placeholder="Mô tả thông tin cần làm rõ…" /></div>
          <div className="grid g-2" style={{ marginTop: 14, gap: 14 }}>
            <div><label style={lb}>Loại RFI</label><select value={type} onChange={e => setType(e.target.value)} style={fld}>{Object.entries(RFI_TYPE).map(([k, v]) => <option key={k} value={k}>{v}</option>)}</select></div>
            <div><label style={lb}>Bộ môn *</label><select value={disc} onChange={e => setDisc(e.target.value)} style={fld}>{Object.entries(DISC_NAME).map(([k, v]) => <option key={k} value={k}>{v}</option>)}</select></div>
            <div><label style={lb}>Ưu tiên</label><select value={prio} onChange={e => setPrio(e.target.value)} style={fld}>{Object.entries(RFI_PRIO).map(([k, v]) => <option key={k} value={k}>{v.label} · SLA {v.sla}n</option>)}</select></div>
            <div><label style={lb}>Hạn phản hồi</label><input type="text" style={fld} defaultValue="15/06/2026" /></div>
          </div>
          <div style={{ display: "flex", gap: 12, marginTop: 16 }}>
            {[["Ảnh hưởng chi phí", cost, () => setCost(!cost)], ["Ảnh hưởng tiến độ", sched, () => setSched(!sched)]].map(([l, on, fn], i) => (
              <div key={i} onClick={fn} style={{ flex: 1, display: "flex", alignItems: "center", gap: 11, padding: "12px 14px", borderRadius: 11, border: `1.5px solid ${on ? "var(--accent)" : "var(--line)"}`, background: on ? "var(--accent-soft)" : "var(--surface)", cursor: "pointer" }}>
                <span style={{ width: 20, height: 20, borderRadius: 6, border: `1.5px solid ${on ? "var(--accent)" : "var(--line)"}`, background: on ? "var(--accent)" : "transparent", display: "grid", placeItems: "center", color: "#fff", flex: "none" }}>{on && <Icon.check size={13} />}</span>
                <span style={{ fontSize: 13, fontWeight: 700, color: on ? "var(--accent)" : "var(--ink-2)" }}>{l}</span>
              </div>
            ))}
          </div>
          {!vp && <div style={{ marginTop: 16, padding: "12px 14px", background: "var(--sunken)", borderRadius: 10, display: "flex", alignItems: "center", gap: 10 }}><Icon.cube size={16} color="var(--accent)" /><span style={{ fontSize: 12.5, color: "var(--ink-2)", fontWeight: 600 }}>Gắn phần tử mô hình & lưu viewpoint (camera + IFC GUID)</span><button className="btn sm" style={{ marginLeft: "auto" }}>Thêm từ mô hình</button></div>}
        </div>
        <div style={{ padding: "0 24px 22px", display: "flex", gap: 10, justifyContent: "flex-end" }}>
          <button className="btn" onClick={onClose}>Huỷ</button>
          <button className="btn primary" disabled={busy} onClick={submit}><Icon.send size={14} /> {busy ? "Đang gửi…" : "Gửi RFI"}</button>
        </div>
      </div>
    </div>
  );
}




/* ===== file8.js ===== */
/* ============================================================
   RFI standalone — Issues panel (two-way Issue ↔ RFI)
   ============================================================ */
const { useState: useStateIs } = React;

function IssueDrawing({ issue }) {
  return (
    <div style={{ position: "relative", width: "100%", aspectRatio: "1.5", background: "#fff", borderRadius: 12, border: "1px solid var(--line)", overflow: "hidden" }}>
      <div style={{ position: "absolute", inset: 16, border: "1.5px solid #d4dcde" }} />
      <svg viewBox="0 0 600 400" style={{ position: "absolute", inset: 0, width: "100%", height: "100%" }}>
        <g fill="none" stroke="#9fb6bb" strokeWidth="1.6" strokeLinejoin="round">
          <rect x="80" y="90" width="300" height="200" /><rect x="120" y="150" width="90" height="140" />
          <line x1="80" y1="200" x2="380" y2="200" /><rect x="260" y="120" width="80" height="60" />
          <circle cx="480" cy="200" r="60" /><line x1="480" y1="140" x2="480" y2="260" /><line x1="420" y1="200" x2="540" y2="200" />
        </g>
      </svg>
      <div className="pin" style={{ left: `${issue.coord[0]}%`, top: `${issue.coord[1]}%` }}>
        <span className="pin-pulse" style={{ color: "var(--danger)" }} />
        <div className="pin-head" style={{ background: "var(--danger)", color: "var(--danger)" }}><span className="pin-num">!</span></div>
      </div>
    </div>
  );
}

function IssuesPanel({ go, initialIssueId }) {
  const [sel, setSel] = useStateIs(() => ISSUES.find(i => i.id === initialIssueId) || ISSUES[0]);
  const rfi = sel.linkedRfi ? RFIS.find(r => r.id === sel.linkedRfi) : null;
  const st = ISSUE_STATUS[sel.status];

  return (
    <div className="content flush" style={{ display: "grid", gridTemplateColumns: "340px 1fr", height: "100%", minHeight: 0 }}>
      {/* list */}
      <div style={{ borderRight: "1px solid var(--line)", background: "var(--surface)", display: "flex", flexDirection: "column", minHeight: 0 }}>
        <div style={{ padding: "16px 18px 13px", borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
          <div><div style={{ fontSize: 14.5, fontWeight: 800 }}>Issues phối hợp</div><div style={{ fontSize: 12, color: "var(--ink-3)", fontWeight: 500, marginTop: 3 }}>{ISSUES.length} vấn đề · BCF</div></div>
          <button className="btn sm"><Icon.plus size={14} /></button>
        </div>
        <div style={{ flex: 1, overflowY: "auto", padding: 12 }}>
          {ISSUES.map(it => {
            const s = ISSUE_STATUS[it.status]; const on = sel.id === it.id;
            return (
              <div key={it.id} onClick={() => setSel(it)} className="card" style={{ padding: 13, marginBottom: 10, cursor: "pointer", border: on ? "1.5px solid var(--accent)" : "1px solid var(--line)" }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 7 }}>
                  <span className="tag-mono" style={{ fontWeight: 700 }}>{it.num}</span>
                  <span className={`chip ${s.cls}`} style={{ height: 21, fontSize: 10.5 }}>{s.label}</span>
                  {it.linkedRfi && <span title="Đã liên kết RFI" style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 4, fontSize: 9.5, fontWeight: 800, color: "var(--accent)", border: "1px solid var(--accent)", borderRadius: 5, padding: "1px 5px" }}><Icon.flow size={10} /> RFI</span>}
                </div>
                <div style={{ fontSize: 13, fontWeight: 700, lineHeight: 1.4 }}>{it.title}</div>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
                  <span className={`chip ${it.disc}`} style={{ height: 21, fontSize: 10.5 }}>{DISC_NAME[it.disc]}</span>
                  <span style={{ fontSize: 11.5, color: "var(--ink-3)", fontWeight: 600, display: "inline-flex", alignItems: "center", gap: 5 }}><PersonAv id={it.assignee} size={18} /> {RFI_PEOPLE[it.assignee].name}</span>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {/* detail */}
      <div style={{ overflowY: "auto", minHeight: 0, padding: "22px 26px 40px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16, flexWrap: "wrap" }}>
          <span className="tag-mono" style={{ fontWeight: 700, height: 28, display: "inline-flex", alignItems: "center" }}>{sel.num}</span>
          <span className={`chip ${st.cls}`} style={{ height: 28, fontSize: 12.5 }}>{st.label}</span>
          {sel.bcf && <span className="chip inf" style={{ height: 28 }}>BCF</span>}
          <div style={{ marginLeft: "auto" }}>
            {rfi
              ? <button className="btn sm" onClick={() => go("register", { rfiId: rfi.id })} style={{ borderColor: "var(--accent)", color: "var(--accent)" }}><Icon.flow size={14} /> Xem {rfi.num}<Icon.chevR size={13} /></button>
              : <button className="btn sm primary" onClick={() => go("register", { prefill: prefillFromIssue(sel) })}><Icon.send size={14} /> Nâng cấp thành RFI</button>}
          </div>
        </div>

        <div style={{ fontSize: 21, fontWeight: 800, letterSpacing: "-.02em", marginBottom: 16 }}>{sel.title}</div>

        <div className="grid g-2v" style={{ alignItems: "start", gap: 22 }}>
          <IssueDrawing issue={sel} />
          <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
            <div className="card card-pad">
              <div className="card-title" style={{ marginBottom: 10 }}>Mô tả</div>
              <div style={{ fontSize: 13.5, color: "var(--ink-2)", fontWeight: 500, lineHeight: 1.6 }}>{sel.desc}</div>
            </div>

            {/* link status */}
            <div className="card card-pad" style={{ background: rfi ? "rgba(13,148,136,.05)" : "var(--surface)" }}>
              <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: ".06em", color: "var(--ink-4)", marginBottom: 10 }}>LIÊN KẾT RFI</div>
              {rfi ? (
                <div onClick={() => go("register", { rfiId: rfi.id })} style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 13px", borderRadius: 11, background: "var(--surface)", border: "1px solid var(--line)", cursor: "pointer" }}>
                  <span style={{ width: 34, height: 34, borderRadius: 9, background: "var(--accent-soft)", color: "var(--accent)", display: "grid", placeItems: "center", flex: "none" }}><Icon.flow size={17} /></span>
                  <div style={{ flex: 1, minWidth: 0 }}><div style={{ display: "flex", alignItems: "center", gap: 8 }}><span className="tag-mono" style={{ fontWeight: 700 }}>{rfi.num}</span><StatusChip status={rfi.status} /></div><div style={{ fontSize: 12.5, color: "var(--ink-3)", fontWeight: 600, marginTop: 3, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{rfi.subject}</div></div>
                  <Icon.chevR size={16} color="var(--ink-4)" />
                </div>
              ) : (
                <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
                  <span style={{ fontSize: 13, color: "var(--ink-3)", fontWeight: 600, flex: 1 }}>Chưa có RFI. Nâng cấp issue này thành RFI chính thức khi cần làm rõ hợp đồng.</span>
                  <button className="btn sm primary" onClick={() => go("register", { prefill: prefillFromIssue(sel) })}><Icon.send size={14} /> Nâng cấp</button>
                </div>
              )}
            </div>

            <div className="card card-pad">
              <div className="card-title" style={{ marginBottom: 12 }}>Chi tiết</div>
              {[["Người phụ trách", RFI_PEOPLE[sel.assignee].name], ["Người báo cáo", RFI_PEOPLE[sel.reporter].name], ["Hạn xử lý", sel.due], ["Vị trí 3D", sel.loc], ["Hồ sơ liên kết", sel.linked], ["IFC GUID", sel.ifc]].map(([k, v], i) => (
                <div key={i} style={{ display: "flex", justifyContent: "space-between", gap: 14, padding: "9px 0", borderBottom: i < 5 ? "1px solid var(--line-soft)" : "none", fontSize: 13 }}><span style={{ color: "var(--ink-3)", fontWeight: 600, flex: "none", whiteSpace: "nowrap" }}>{k}</span><span style={{ fontWeight: 700, textAlign: "right", fontFamily: i === 5 ? "var(--mono)" : "inherit", fontSize: i === 5 ? 11.5 : 13, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{v}</span></div>
              ))}
              <button className="btn sm" style={{ width: "100%", justifyContent: "center", marginTop: 14 }} onClick={() => go("viewer", {})}><Icon.cube size={14} /> Mở trong BIM Viewer</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}


/* ===== RFI wrapper (thay outer shell bằng sub-nav, nhúng trong workspace) ===== */
function RegisterScreen({ pending, go, reload }) {
  const [sel, setSel] = React.useState(() => pending.rfiId ? RFIS.find(r => r.id === pending.rfiId) : null);
  const [creating, setCreating] = React.useState(() => !!pending.prefill);
  const prefill = pending.prefill || null;
  if (sel) return <RFIDetail rfi={sel} onBack={() => setSel(null)} go={go} reload={reload} />;
  return (
    <div className="content">
      <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", marginBottom: 20, gap: 16 }}>
        <div>
          <div style={{ fontSize: 16, fontWeight: 800 }}>RFI · Yêu cầu thông tin</div>
          <div style={{ fontSize: 12.5, color: "var(--ink-3)", marginTop: 3, fontWeight: 500 }}>Quy trình hỏi–đáp chính thức theo ISO 19650 · liên kết BCF/IFC · SLA tự động</div>
        </div>
      </div>
      <RFIRegister onOpen={setSel} onCreate={() => setCreating(true)} />
      {creating && <RFICreate prefill={prefill} onClose={() => setCreating(false)} onCreated={reload} />}
    </div>
  );
}

// nạp RFI thật vào mảng RFIS (giữ cùng tham chiếu để các component đọc được)
async function loadRfis() {
  if (!window.cdeRfi) return;
  try { const real = await window.cdeRfi.rfis(); RFIS.length = 0; real.forEach(r => RFIS.push(r)); } catch (e) { console.warn("rfi load", e.message); }
}

function Workflow() {
  const [pending, setPending] = React.useState({ nonce: 0 });
  const [tick, setTick] = React.useState(0);
  React.useEffect(() => { loadRfis().then(() => setTick(t => t + 1)); }, []);
  const reload = async () => { await loadRfis(); setTick(t => t + 1); };
  const go = (r, payload = {}) => {
    if (r === "viewer") { if (window.cdeSetRoute) window.cdeSetRoute("viewer"); return; }
    setPending({ ...payload, nonce: (pending.nonce || 0) + 1 });
  };
  return (
    <div className="rfi rfi-screen">
      <div className="rfi-body"><RegisterScreen key={tick + "-" + pending.nonce} pending={pending} go={go} reload={reload} /></div>
    </div>
  );
}
window.Workflow = Workflow;

})();
